mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-22 21:48:12 -05:00
Merge branch 'master' into aarushikansal/open-1646-ci-for-docker
This commit is contained in:
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -6,3 +6,5 @@ docs/_javascript/** linguist-vendored
|
||||
|
||||
# Exclude VCR cassettes from stats
|
||||
forge/tests/vcr_cassettes/**/**.y*ml linguist-generated
|
||||
|
||||
* text=auto
|
||||
10
.github/workflows/autogpt-server-ci.yml
vendored
10
.github/workflows/autogpt-server-ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
<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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -15,13 +15,16 @@ 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 { CustomEdgeData } from "./CustomEdge";
|
||||
import { NodeGenericInputField } from "./node-input-components";
|
||||
|
||||
type ParsedKey = { key: string; index?: number };
|
||||
|
||||
export type CustomNodeData = {
|
||||
blockType: string;
|
||||
@@ -37,8 +40,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 };
|
||||
@@ -55,7 +58,10 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
const [isOutputModalOpen, setIsOutputModalOpen] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const { getNode, setNodes, getEdges, setEdges } = useReactFlow();
|
||||
const { getNode, setNodes, getEdges, setEdges } = useReactFlow<
|
||||
CustomNodeData,
|
||||
CustomEdgeData
|
||||
>();
|
||||
|
||||
const outputDataRef = useRef<HTMLDivElement>(null);
|
||||
const isInitialSetup = useRef(true);
|
||||
@@ -86,14 +92,11 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
setIsAdvancedOpen(checked);
|
||||
};
|
||||
|
||||
const hasOptionalFields = () => {
|
||||
return (
|
||||
data.inputSchema &&
|
||||
Object.keys(data.inputSchema.properties).some((key) => {
|
||||
return !data.inputSchema.required?.includes(key);
|
||||
})
|
||||
);
|
||||
};
|
||||
const hasOptionalFields =
|
||||
data.inputSchema &&
|
||||
Object.keys(data.inputSchema.properties).some((key) => {
|
||||
return !data.inputSchema.required?.includes(key);
|
||||
});
|
||||
|
||||
const generateOutputHandles = (schema: BlockIORootSchema) => {
|
||||
if (!schema?.properties) return null;
|
||||
@@ -110,16 +113,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 +152,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 +258,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 +322,67 @@ 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);
|
||||
const isConnected = isHandleConnected(propKey);
|
||||
return (
|
||||
(isRequired || isAdvancedOpen || isConnected) && (
|
||||
<div key={propKey} onMouseOver={() => {}}>
|
||||
<NodeHandle
|
||||
keyName={propKey}
|
||||
isConnected={isConnected}
|
||||
isRequired={isRequired}
|
||||
schema={propSchema}
|
||||
side="left"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
{!isConnected && (
|
||||
<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 +412,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() && (
|
||||
{hasOptionalFields && (
|
||||
<>
|
||||
<Switch
|
||||
onCheckedChange={toggleAdvancedSettings}
|
||||
className="custom-switch"
|
||||
/>
|
||||
<Switch onCheckedChange={toggleAdvancedSettings} />
|
||||
<span className="m-1">Advanced</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -17,17 +17,16 @@ import ReactFlow, {
|
||||
Connection,
|
||||
EdgeTypes,
|
||||
MarkerType,
|
||||
Controls,
|
||||
} from "reactflow";
|
||||
import "reactflow/dist/style.css";
|
||||
import CustomNode, { CustomNodeData } from "./CustomNode";
|
||||
import "./flow.css";
|
||||
import AutoGPTServerAPI, {
|
||||
Block,
|
||||
BlockIOSchema,
|
||||
Graph,
|
||||
NodeExecutionResult,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { Play, Undo2, Redo2 } from "lucide-react";
|
||||
import {
|
||||
deepEquals,
|
||||
getTypeColor,
|
||||
@@ -41,6 +40,7 @@ import Ajv from "ajv";
|
||||
import { Control, ControlPanel } from "@/components/edit/control/ControlPanel";
|
||||
import { SaveControl } from "@/components/edit/control/SaveControl";
|
||||
import { BlocksControl } from "@/components/edit/control/BlocksControl";
|
||||
import { IconPlay, IconRedo2, IconUndo2 } from "@/components/ui/icons";
|
||||
|
||||
// This is for the history, this is the minimum distance a block must move before it is logged
|
||||
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
||||
@@ -458,7 +458,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 +501,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 +514,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 +575,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 +680,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 +843,17 @@ const FlowEditor: React.FC<{
|
||||
const editorControls: Control[] = [
|
||||
{
|
||||
label: "Undo",
|
||||
icon: <Undo2 />,
|
||||
icon: <IconUndo2 />,
|
||||
onClick: handleUndo,
|
||||
},
|
||||
{
|
||||
label: "Redo",
|
||||
icon: <Redo2 />,
|
||||
icon: <IconRedo2 />,
|
||||
onClick: handleRedo,
|
||||
},
|
||||
{
|
||||
label: "Run",
|
||||
icon: <Play />,
|
||||
icon: <IconPlay />,
|
||||
onClick: runAgent,
|
||||
},
|
||||
];
|
||||
@@ -883,17 +881,16 @@ 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>
|
||||
<Controls />
|
||||
<ControlPanel className="absolute z-10" controls={editorControls}>
|
||||
<BlocksControl blocks={availableNodes} addBlock={addNode} />
|
||||
<SaveControl
|
||||
agentMeta={savedAgent}
|
||||
onSave={saveAgent}
|
||||
onDescriptionChange={setAgentDescription}
|
||||
onNameChange={setAgentName}
|
||||
/>
|
||||
</ControlPanel>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import Link from "next/link";
|
||||
import { CircleUser, Menu, SquareActivity, Workflow } from "lucide-react";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import React from "react";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Pencil1Icon, TimerIcon, ArchiveIcon } from "@radix-ui/react-icons";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import Image from "next/image";
|
||||
import getServerUser from "@/hooks/getServerUser";
|
||||
import ProfileDropdown from "./ProfileDropdown";
|
||||
import {
|
||||
IconCircleUser,
|
||||
IconMenu,
|
||||
IconPackage2,
|
||||
IconSquareActivity,
|
||||
IconWorkFlow,
|
||||
} from "@/components/ui/icons";
|
||||
|
||||
export async function NavBar() {
|
||||
const isAvailable = Boolean(
|
||||
@@ -32,7 +30,7 @@ export async function NavBar() {
|
||||
size="icon"
|
||||
className="shrink-0 md:hidden"
|
||||
>
|
||||
<Menu className="size-5" />
|
||||
<IconMenu />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
@@ -42,19 +40,19 @@ export async function NavBar() {
|
||||
href="/monitor"
|
||||
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 "
|
||||
>
|
||||
<SquareActivity className="size-6" /> Monitor
|
||||
<IconSquareActivity /> Monitor
|
||||
</Link>
|
||||
<Link
|
||||
href="/build"
|
||||
className="text-muted-foreground hover:text-foreground flex flex-row gap-2"
|
||||
>
|
||||
<Workflow className="size-6" /> Build
|
||||
<IconWorkFlow /> Build
|
||||
</Link>
|
||||
<Link
|
||||
href="/marketplace"
|
||||
className="text-muted-foreground hover:text-foreground flex flex-row gap-2"
|
||||
>
|
||||
<ArchiveIcon className="size-6" /> Marketplace
|
||||
<IconPackage2 /> Marketplace
|
||||
</Link>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
@@ -64,19 +62,19 @@ export async function NavBar() {
|
||||
href="/monitor"
|
||||
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
|
||||
>
|
||||
<SquareActivity className="size-4" /> Monitor
|
||||
<IconSquareActivity /> Monitor
|
||||
</Link>
|
||||
<Link
|
||||
href="/build"
|
||||
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
|
||||
>
|
||||
<Workflow className="size-4" /> Build
|
||||
<IconWorkFlow /> Build
|
||||
</Link>
|
||||
<Link
|
||||
href="/marketplace"
|
||||
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
|
||||
>
|
||||
<ArchiveIcon className="size-4" /> Marketplace
|
||||
<IconPackage2 /> Marketplace
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -104,7 +102,7 @@ export async function NavBar() {
|
||||
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
|
||||
>
|
||||
Log In
|
||||
<CircleUser className="size-5" />
|
||||
<IconCircleUser />
|
||||
</Link>
|
||||
)}
|
||||
{isAvailable && user && <ProfileDropdown />}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
×
|
||||
</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;
|
||||
@@ -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 (
|
||||
|
||||
59
rnd/autogpt_builder/src/components/TallyPopup.tsx
Normal file
59
rnd/autogpt_builder/src/components/TallyPopup.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { IconMegaphone } from "@/components/ui/icons";
|
||||
|
||||
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"
|
||||
>
|
||||
<IconMegaphone size="lg" />
|
||||
<span className="sr-only">Reach Out</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TallyPopupSimple;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Represents a control element for the ControlPanel Component.
|
||||
* @type {Object} Control
|
||||
* @property {React.ReactNode} icon - The icon of the control from lucide-react https://lucide.dev/icons/
|
||||
* @property {string} label - The label of the control, to be leveraged by ToolTip.
|
||||
* @property {onclick} onClick - The function to be executed when the control is clicked.
|
||||
*/
|
||||
export type Control = {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
interface ControlPanelProps {
|
||||
controls: Control[];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* ControlPanel component displays a panel with controls as icons with the ability to take in children.
|
||||
* @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.
|
||||
* @returns The rendered control panel component.
|
||||
*/
|
||||
export const ControlPanel = ({ controls, children }: ControlPanelProps) => {
|
||||
return (
|
||||
<aside className="hidden w-14 flex-col sm:flex">
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col items-center gap-4 px-2 sm:py-5 rounded-radius">
|
||||
{controls.map((control, index) => (
|
||||
<Tooltip key={index} delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => control.onClick()}
|
||||
>
|
||||
{control.icon}
|
||||
<span className="sr-only">{control.label}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{control.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
<Separator />
|
||||
{children}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
export default ControlPanel;
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { Block } from "@/lib/autogpt-server-api";
|
||||
import { PlusIcon } from "@radix-ui/react-icons";
|
||||
import { IconToyBrick } from "@/components/ui/icons";
|
||||
|
||||
interface BlocksControlProps {
|
||||
blocks: Block[];
|
||||
@@ -40,12 +41,14 @@ 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 variant="ghost" size="icon">
|
||||
<IconToyBrick />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
sideOffset={15}
|
||||
sideOffset={22}
|
||||
align="start"
|
||||
className="w-80 p-0"
|
||||
>
|
||||
|
||||
@@ -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,42 +25,46 @@ export type Control = {
|
||||
interface ControlPanelProps {
|
||||
controls: Control[];
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ControlPanel component displays a panel with controls as icons with the ability to take in children.
|
||||
* ControlPanel component displays a panel with controls as icons.tsx with the ability to take in children.
|
||||
* @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">
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col items-center gap-4 px-2 sm:py-5 rounded-radius">
|
||||
{children}
|
||||
<Separator />
|
||||
{controls.map((control, index) => (
|
||||
<Tooltip key={index} delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => control.onClick()}
|
||||
>
|
||||
{control.icon}
|
||||
<span className="sr-only">{control.label}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{control.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
<Card className={cn("w-14", className)}>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col items-center gap-8 px-2 sm:py-5 rounded-radius">
|
||||
{children}
|
||||
<Separator />
|
||||
{controls.map((control, index) => (
|
||||
<Tooltip key={index} delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => control.onClick()}
|
||||
>
|
||||
{control.icon}
|
||||
<span className="sr-only">{control.label}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{control.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
export default ControlPanel;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Save } from "lucide-react";
|
||||
import { IconSave } from "@/components/ui/icons";
|
||||
|
||||
interface SaveControlProps {
|
||||
agentMeta: GraphMeta | null;
|
||||
@@ -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">
|
||||
<IconSave />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="right" sideOffset={15} align="start">
|
||||
<Card className="border-none shadow-none">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
187
rnd/autogpt_builder/src/components/monitor/AgentFlowList.tsx
Normal file
187
rnd/autogpt_builder/src/components/monitor/AgentFlowList.tsx
Normal 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;
|
||||
134
rnd/autogpt_builder/src/components/monitor/FlowInfo.tsx
Normal file
134
rnd/autogpt_builder/src/components/monitor/FlowInfo.tsx
Normal 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;
|
||||
66
rnd/autogpt_builder/src/components/monitor/FlowRunInfo.tsx
Normal file
66
rnd/autogpt_builder/src/components/monitor/FlowRunInfo.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
68
rnd/autogpt_builder/src/components/monitor/FlowRunsList.tsx
Normal file
68
rnd/autogpt_builder/src/components/monitor/FlowRunsList.tsx
Normal 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;
|
||||
114
rnd/autogpt_builder/src/components/monitor/FlowRunsStatus.tsx
Normal file
114
rnd/autogpt_builder/src/components/monitor/FlowRunsStatus.tsx
Normal 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;
|
||||
172
rnd/autogpt_builder/src/components/monitor/FlowRunsTimeline.tsx
Normal file
172
rnd/autogpt_builder/src/components/monitor/FlowRunsTimeline.tsx
Normal 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>
|
||||
<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"
|
||||
);
|
||||
}
|
||||
6
rnd/autogpt_builder/src/components/monitor/index.ts
Normal file
6
rnd/autogpt_builder/src/components/monitor/index.ts
Normal 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";
|
||||
616
rnd/autogpt_builder/src/components/node-input-components.tsx
Normal file
616
rnd/autogpt_builder/src/components/node-input-components.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
479
rnd/autogpt_builder/src/components/ui/icons.tsx
Normal file
479
rnd/autogpt_builder/src/components/ui/icons.tsx
Normal file
@@ -0,0 +1,479 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Represents different variants of an icon, based on its size.
|
||||
*/
|
||||
const iconVariants = {
|
||||
size: {
|
||||
default: "size-4",
|
||||
sm: "size-2",
|
||||
lg: "size-6",
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Props for the Icon component.
|
||||
*/
|
||||
export interface IconProps extends React.SVGProps<SVGSVGElement> {
|
||||
size?: keyof typeof iconVariants.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an icon component that wraps a given SVG icon component.
|
||||
* This function applies consistent styling and size variants to the icon.
|
||||
*
|
||||
* @template P - The props type for the icon component
|
||||
* @param {React.FC<P>} IconComponent - The SVG icon component to be wrapped
|
||||
* @returns {React.ForwardRefExoticComponent<IconProps & React.RefAttributes<SVGSVGElement>>}
|
||||
*
|
||||
*/
|
||||
const createIcon = <P extends React.SVGProps<SVGSVGElement>>(
|
||||
IconComponent: React.FC<P>,
|
||||
): React.ForwardRefExoticComponent<
|
||||
IconProps & React.RefAttributes<SVGSVGElement>
|
||||
> => {
|
||||
const Icon = React.forwardRef<SVGSVGElement, IconProps>(
|
||||
({ className, size = "default", ...props }, ref) => {
|
||||
return (
|
||||
<IconComponent
|
||||
className={cn(iconVariants.size[size], className)}
|
||||
ref={ref}
|
||||
{...(props as P)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Icon.displayName = IconComponent.name || "Icon";
|
||||
return Icon;
|
||||
};
|
||||
|
||||
/**
|
||||
* Save icon component.
|
||||
*
|
||||
* @component IconSave
|
||||
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
|
||||
* @returns {JSX.Element} - The save icon.
|
||||
*
|
||||
* @example
|
||||
* // Default usage this is the standard usage
|
||||
* <IconSave />
|
||||
*
|
||||
* @example
|
||||
* // With custom color and size these should be used sparingly and only when necessary
|
||||
* <IconSave className="text-primary" size="lg" />
|
||||
*
|
||||
* @example
|
||||
* // With custom size and onClick handler
|
||||
* <IconSave size="sm" onClick={handleOnClick} />
|
||||
*/
|
||||
export const IconSave = createIcon((props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" />
|
||||
<path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7" />
|
||||
<path d="M7 3v4a1 1 0 0 0 1 1h7" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
/**
|
||||
* Undo icon component.
|
||||
*
|
||||
* @component IconUndo2
|
||||
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
|
||||
* @returns {JSX.Element} - The undo icon.
|
||||
*
|
||||
* @example
|
||||
* // Default usage this is the standard usage
|
||||
* <IconUndo2 />
|
||||
*
|
||||
* @example
|
||||
* // With custom color and size these should be used sparingly and only when necessary
|
||||
* <IconUndo2 className="text-primary" size="lg" />
|
||||
*
|
||||
* @example
|
||||
* // With custom size and onClick handler
|
||||
* <IconUndo2 size="sm" onClick={handleOnClick} />
|
||||
*/
|
||||
export const IconUndo2 = createIcon((props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="M9 14 4 9l5-5" />
|
||||
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
/**
|
||||
* Redo icon component.
|
||||
*
|
||||
* @component IconRedo2
|
||||
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
|
||||
* @returns {JSX.Element} - The redo icon.
|
||||
*
|
||||
* @example
|
||||
* // Default usage this is the standard usage
|
||||
* <IconRedo2 />
|
||||
*
|
||||
* @example
|
||||
* // With custom color and size these should be used sparingly and only when necessary
|
||||
* <IconRedo2 className="text-primary" size="lg" />
|
||||
*
|
||||
* @example
|
||||
* // With custom size and onClick handler
|
||||
* <IconRedo2 size="sm" onClick={handleOnClick} />
|
||||
*/
|
||||
export const IconRedo2 = createIcon((props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="m15 14 5-5-5-5" />
|
||||
<path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
/**
|
||||
* Toy brick icon component.
|
||||
*
|
||||
* @component IconToyBrick
|
||||
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
|
||||
* @returns {JSX.Element} - The toy brick icon.
|
||||
*
|
||||
* @example
|
||||
* // Default usage this is the standard usage
|
||||
* <IconToyBrick />
|
||||
*
|
||||
* @example
|
||||
* // With custom color and size these should be used sparingly and only when necessary
|
||||
* <IconToyBrick className="text-primary" size="lg" />
|
||||
*
|
||||
* @example
|
||||
* // With custom size and onClick handler
|
||||
* <IconToyBrick size="sm" onClick={handleOnClick} />
|
||||
*/
|
||||
export const IconToyBrick = createIcon((props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<rect width="18" height="12" x="3" y="8" rx="1" />
|
||||
<path d="M10 8V5c0-.6-.4-1-1-1H6a1 1 0 0 0-1 1v3" />
|
||||
<path d="M19 8V5c0-.6-.4-1-1-1h-3a1 1 0 0 0-1 1v3" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
/**
|
||||
* A circle alert icon component.
|
||||
*
|
||||
* @component IconCircleAlert
|
||||
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
|
||||
* @returns {JSX.Element} - The circle alert icon.
|
||||
*
|
||||
* @example
|
||||
* // Default usage this is the standard usage
|
||||
* <IconCircleAlert />
|
||||
*
|
||||
* @example
|
||||
* // With custom color and size these should be used sparingly and only when necessary
|
||||
* <IconCircleAlert className="text-primary" size="lg" />
|
||||
*
|
||||
* @example
|
||||
* // With custom size and onClick handler
|
||||
* <IconCircleAlert size="sm" onClick={handleOnClick} />
|
||||
*/
|
||||
export const IconCircleAlert = createIcon((props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" x2="12" y1="8" y2="12" />
|
||||
<line x1="12" x2="12.01" y1="16" y2="16" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
/**
|
||||
* Circle User icon component.
|
||||
*
|
||||
* @component IconCircleUser
|
||||
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
|
||||
* @returns {JSX.Element} - The circle user icon.
|
||||
*
|
||||
* @example
|
||||
* // Default usage this is the standard usage
|
||||
* <IconCircleUser />
|
||||
*
|
||||
* @example
|
||||
* // With custom color and size these should be used sparingly and only when necessary
|
||||
* <IconCircleUser className="text-primary" size="lg" />
|
||||
*
|
||||
* @example
|
||||
* // With custom size and onClick handler
|
||||
* <IconCircleUser size="sm" onClick={handleOnClick} />
|
||||
*/
|
||||
export const IconCircleUser = createIcon((props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
<path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
/**
|
||||
* Menu icon component.
|
||||
*
|
||||
* @component IconMenu
|
||||
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
|
||||
* @returns {JSX.Element} - The menu icon.
|
||||
*
|
||||
* @example
|
||||
* // Default usage this is the standard usage
|
||||
* <IconMenu />
|
||||
*
|
||||
* @example
|
||||
* // With custom color and size these should be used sparingly and only when necessary
|
||||
* <IconMenu className="text-primary" size="lg" />
|
||||
*
|
||||
* @example
|
||||
* // With custom size and onClick handler
|
||||
* <IconMenu size="sm" onClick={handleOnClick} />
|
||||
*/
|
||||
export const IconMenu = createIcon((props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<line x1="4" x2="20" y1="12" y2="12" />
|
||||
<line x1="4" x2="20" y1="6" y2="6" />
|
||||
<line x1="4" x2="20" y1="18" y2="18" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
/**
|
||||
* Square Activity icon component.
|
||||
*
|
||||
* @component IconSquareActivity
|
||||
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
|
||||
* @returns {JSX.Element} - The square activity icon.
|
||||
*
|
||||
* @example
|
||||
* // Default usage this is the standard usage
|
||||
* <IconSquareActivity />
|
||||
*
|
||||
* @example
|
||||
* // With custom color and size these should be used sparingly and only when necessary
|
||||
* <IconSquareActivity className="text-primary" size="lg" />
|
||||
*
|
||||
* @example
|
||||
* // With custom size and onClick handler
|
||||
* <IconSquareActivity size="sm" onClick={handleOnClick} />
|
||||
*/
|
||||
export const IconSquareActivity = createIcon((props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M17 12h-2l-2 5-2-10-2 5H7" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
/**
|
||||
* Workflow icon component.
|
||||
*
|
||||
* @component IconWorkFlow
|
||||
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
|
||||
* @returns {JSX.Element} - The workflow icon.
|
||||
*
|
||||
* @example
|
||||
* // Default usage this is the standard usage
|
||||
* <IconWorkFlow />
|
||||
*
|
||||
* @example
|
||||
* // With custom color and size these should be used sparingly and only when necessary
|
||||
* <IconWorkFlow className="text-primary" size="lg" />
|
||||
*
|
||||
* @example
|
||||
* // With custom size and onClick handler
|
||||
* <IconWorkFlow size="sm" onClick={handleOnClick} />
|
||||
*/
|
||||
export const IconWorkFlow = createIcon((props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<rect width="8" height="8" x="3" y="3" rx="2" />
|
||||
<path d="M7 11v4a2 2 0 0 0 2 2h4" />
|
||||
<rect width="8" height="8" x="13" y="13" rx="2" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
/**
|
||||
* Play icon component.
|
||||
*
|
||||
* @component IconPlay
|
||||
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
|
||||
* @returns {JSX.Element} - The play icon.
|
||||
*
|
||||
* @example
|
||||
* // Default usage this is the standard usage
|
||||
* <IconPlay />
|
||||
*
|
||||
* @example
|
||||
* // With custom color and size these should be used sparingly and only when necessary
|
||||
* <IconPlay className="text-primary" size="lg" />
|
||||
*
|
||||
* @example
|
||||
* // With custom size and onClick handler
|
||||
* <IconPlay size="sm" onClick={handleOnClick} />
|
||||
*/
|
||||
export const IconPlay = createIcon((props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<polygon points="6 3 20 12 6 21 6 3" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
/**
|
||||
* Package2 icon component.
|
||||
*
|
||||
* @component IconPackage2
|
||||
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
|
||||
* @returns {JSX.Element} - The package2 icon.
|
||||
*
|
||||
* @example
|
||||
* // Default usage this is the standard usage
|
||||
* <IconPackage2 />
|
||||
*
|
||||
* @example
|
||||
* // With custom color and size these should be used sparingly and only when necessary
|
||||
* <IconPackage2 className="text-primary" size="lg" />
|
||||
*
|
||||
* @example
|
||||
* // With custom size and onClick handler
|
||||
* <IconPackage2 size="sm" onClick={handleOnClick} />
|
||||
*/
|
||||
export const IconPackage2 = createIcon((props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="M3 9h18v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9Z" />
|
||||
<path d="m3 9 2.45-4.9A2 2 0 0 1 7.24 3h9.52a2 2 0 0 1 1.8 1.1L21 9" />
|
||||
<path d="M12 3v6" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
/**
|
||||
* Megaphone icon component.
|
||||
*
|
||||
* @component IconMegaphone
|
||||
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
|
||||
* @returns {JSX.Element} - The megaphone icon.
|
||||
*
|
||||
* @example
|
||||
* // Default usage this is the standard usage
|
||||
* <IconMegaphone />
|
||||
*
|
||||
* @example
|
||||
* // With custom color and size these should be used sparingly and only when necessary
|
||||
* <IconMegaphone className="text-primary" size="lg" />
|
||||
*
|
||||
* @example
|
||||
* // With custom size and onClick handler
|
||||
* <IconMegaphone size="sm" onClick={handleOnClick} />
|
||||
*/
|
||||
export const IconMegaphone = createIcon((props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="m3 11 18-5v12L3 14v-3z" />
|
||||
<path d="M11.6 16.8a3 3 0 1 1-5.8-1.6" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
export { iconVariants };
|
||||
167
rnd/autogpt_builder/src/components/ui/select.tsx
Normal file
167
rnd/autogpt_builder/src/components/ui/select.tsx
Normal 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,
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createClient } from "../supabase/client";
|
||||
import {
|
||||
Block,
|
||||
Graph,
|
||||
@@ -11,8 +12,10 @@ import {
|
||||
export default class AutoGPTServerAPI {
|
||||
private baseUrl: string;
|
||||
private wsUrl: string;
|
||||
private socket: WebSocket | null = null;
|
||||
private messageHandlers: { [key: string]: (data: any) => void } = {};
|
||||
private webSocket: WebSocket | null = null;
|
||||
private wsConnecting: Promise<void> | null = null;
|
||||
private wsMessageHandlers: { [key: string]: (data: any) => void } = {};
|
||||
private supabaseClient = createClient();
|
||||
|
||||
constructor(
|
||||
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
|
||||
@@ -141,18 +144,23 @@ export default class AutoGPTServerAPI {
|
||||
console.debug(`${method} ${path} payload:`, payload);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
this.baseUrl + path,
|
||||
method != "GET"
|
||||
? {
|
||||
method,
|
||||
headers: {
|
||||
const token =
|
||||
(await this.supabaseClient?.auth.getSession())?.data.session
|
||||
?.access_token || "";
|
||||
|
||||
const response = await fetch(this.baseUrl + path, {
|
||||
method,
|
||||
headers:
|
||||
method != "GET"
|
||||
? {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
}
|
||||
: {
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const response_data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -166,37 +174,48 @@ export default class AutoGPTServerAPI {
|
||||
return response_data;
|
||||
}
|
||||
|
||||
connectWebSocket(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket = new WebSocket(this.wsUrl);
|
||||
async connectWebSocket(): Promise<void> {
|
||||
this.wsConnecting ??= new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const token =
|
||||
(await this.supabaseClient?.auth.getSession())?.data.session
|
||||
?.access_token || "";
|
||||
|
||||
this.socket.onopen = () => {
|
||||
console.log("WebSocket connection established");
|
||||
resolve();
|
||||
};
|
||||
const wsUrlWithToken = `${this.wsUrl}?token=${token}`;
|
||||
this.webSocket = new WebSocket(wsUrlWithToken);
|
||||
|
||||
this.socket.onclose = (event) => {
|
||||
console.log("WebSocket connection closed", event);
|
||||
this.socket = null;
|
||||
};
|
||||
this.webSocket.onopen = () => {
|
||||
console.log("WebSocket connection established");
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.socket.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
this.webSocket.onclose = (event) => {
|
||||
console.log("WebSocket connection closed", event);
|
||||
this.webSocket = null;
|
||||
};
|
||||
|
||||
this.webSocket.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
this.webSocket.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
if (this.wsMessageHandlers[message.method]) {
|
||||
this.wsMessageHandlers[message.method](message.data);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error connecting to WebSocket:", error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
if (this.messageHandlers[message.method]) {
|
||||
this.messageHandlers[message.method](message.data);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
return this.wsConnecting;
|
||||
}
|
||||
|
||||
disconnectWebSocket() {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.close();
|
||||
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
|
||||
this.webSocket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,10 +223,12 @@ export default class AutoGPTServerAPI {
|
||||
method: M,
|
||||
data: WebsocketMessageTypeMap[M],
|
||||
) {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(JSON.stringify({ method, data }));
|
||||
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
|
||||
this.webSocket.send(JSON.stringify({ method, data }));
|
||||
} else {
|
||||
console.error("WebSocket is not connected");
|
||||
this.connectWebSocket().then(() =>
|
||||
this.sendWebSocketMessage(method, data),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +236,7 @@ export default class AutoGPTServerAPI {
|
||||
method: M,
|
||||
handler: (data: WebsocketMessageTypeMap[M]) => void,
|
||||
) {
|
||||
this.messageHandlers[method] = handler;
|
||||
this.wsMessageHandlers[method] = handler;
|
||||
}
|
||||
|
||||
subscribeToExecution(graphId: string) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createClient } from "../supabase/client";
|
||||
import {
|
||||
AddAgentRequest,
|
||||
AgentResponse,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
|
||||
export default class MarketplaceAPI {
|
||||
private baseUrl: string;
|
||||
private supabaseClient = createClient();
|
||||
|
||||
constructor(
|
||||
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL ||
|
||||
@@ -140,18 +142,24 @@ export default class MarketplaceAPI {
|
||||
console.debug(`${method} ${path} payload:`, payload);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
this.baseUrl + path,
|
||||
method != "GET"
|
||||
? {
|
||||
method,
|
||||
headers: {
|
||||
const token =
|
||||
(await this.supabaseClient?.auth.getSession())?.data.session
|
||||
?.access_token || "";
|
||||
|
||||
const response = await fetch(this.baseUrl + path, {
|
||||
method,
|
||||
headers:
|
||||
method != "GET"
|
||||
? {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
}
|
||||
: {
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const response_data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
13
rnd/autogpt_builder/src/lib/types.ts
Normal file
13
rnd/autogpt_builder/src/lib/types.ts
Normal 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[];
|
||||
};
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from .config import Settings
|
||||
from .depends import requires_admin_user, requires_user
|
||||
from .jwt_utils import parse_jwt_token
|
||||
from .middleware import auth_middleware
|
||||
from .models import User
|
||||
|
||||
__all__ = [
|
||||
"Settings",
|
||||
"parse_jwt_token",
|
||||
"requires_user",
|
||||
"requires_admin_user",
|
||||
"auth_middleware",
|
||||
"User",
|
||||
]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Settings:
|
||||
JWT_SECRET_KEY: str = os.getenv("SUPABASE_JWT_SECRET", "")
|
||||
ENABLE_AUTH: bool = os.getenv("ENABLE_AUTH", "false").lower() == "true"
|
||||
|
||||
32
rnd/autogpt_libs/autogpt_libs/auth/depends.py
Normal file
32
rnd/autogpt_libs/autogpt_libs/auth/depends.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import fastapi
|
||||
|
||||
from .middleware import auth_middleware
|
||||
from .models import User
|
||||
|
||||
|
||||
def requires_user(payload: dict = fastapi.Depends(auth_middleware)) -> User:
|
||||
return verify_user(payload, admin_only=False)
|
||||
|
||||
|
||||
def requires_admin_user(
|
||||
payload: dict = fastapi.Depends(auth_middleware),
|
||||
) -> User:
|
||||
return verify_user(payload, admin_only=True)
|
||||
|
||||
|
||||
def verify_user(payload: dict | None, admin_only: bool) -> User:
|
||||
if not payload:
|
||||
# This handles the case when authentication is disabled
|
||||
payload = {"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "admin"}
|
||||
|
||||
user_id = payload.get("sub")
|
||||
|
||||
if not user_id:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=401, detail="User ID not found in token"
|
||||
)
|
||||
|
||||
if admin_only and payload["role"] != "admin":
|
||||
raise fastapi.HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
return User.from_payload(payload)
|
||||
68
rnd/autogpt_libs/autogpt_libs/auth/depends_tests.py
Normal file
68
rnd/autogpt_libs/autogpt_libs/auth/depends_tests.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import pytest
|
||||
|
||||
from .depends import verify_user, requires_admin_user, requires_user
|
||||
|
||||
|
||||
def test_verify_user_no_payload():
|
||||
user = verify_user(None, admin_only=False)
|
||||
assert user.user_id == "3e53486c-cf57-477e-ba2a-cb02dc828e1a"
|
||||
assert user.role == "admin"
|
||||
|
||||
|
||||
def test_verify_user_no_user_id():
|
||||
with pytest.raises(Exception):
|
||||
verify_user({"role": "admin"}, admin_only=False)
|
||||
|
||||
|
||||
def test_verify_user_not_admin():
|
||||
with pytest.raises(Exception):
|
||||
verify_user(
|
||||
{"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "user"},
|
||||
admin_only=True,
|
||||
)
|
||||
|
||||
|
||||
def test_verify_user_with_admin_role():
|
||||
user = verify_user(
|
||||
{"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "admin"},
|
||||
admin_only=True,
|
||||
)
|
||||
assert user.user_id == "3e53486c-cf57-477e-ba2a-cb02dc828e1a"
|
||||
assert user.role == "admin"
|
||||
|
||||
|
||||
def test_verify_user_with_user_role():
|
||||
user = verify_user(
|
||||
{"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "user"},
|
||||
admin_only=False,
|
||||
)
|
||||
assert user.user_id == "3e53486c-cf57-477e-ba2a-cb02dc828e1a"
|
||||
assert user.role == "user"
|
||||
|
||||
|
||||
def test_requires_user():
|
||||
user = requires_user(
|
||||
{"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "user"}
|
||||
)
|
||||
assert user.user_id == "3e53486c-cf57-477e-ba2a-cb02dc828e1a"
|
||||
assert user.role == "user"
|
||||
|
||||
|
||||
def test_requires_user_no_user_id():
|
||||
with pytest.raises(Exception):
|
||||
requires_user({"role": "user"})
|
||||
|
||||
|
||||
def test_requires_admin_user():
|
||||
user = requires_admin_user(
|
||||
{"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "admin"}
|
||||
)
|
||||
assert user.user_id == "3e53486c-cf57-477e-ba2a-cb02dc828e1a"
|
||||
assert user.role == "admin"
|
||||
|
||||
|
||||
def test_requires_admin_user_not_admin():
|
||||
with pytest.raises(Exception):
|
||||
requires_admin_user(
|
||||
{"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "user"}
|
||||
)
|
||||
@@ -1,5 +1,7 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
import jwt
|
||||
from typing import Dict, Any
|
||||
|
||||
from .config import settings
|
||||
|
||||
|
||||
@@ -12,7 +14,12 @@ def parse_jwt_token(token: str) -> Dict[str, Any]:
|
||||
:raises ValueError: If the token is invalid or expired
|
||||
"""
|
||||
try:
|
||||
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.JWT_SECRET_KEY,
|
||||
algorithms=[settings.JWT_ALGORITHM],
|
||||
audience="authenticated",
|
||||
)
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise ValueError("Token has expired")
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import logging
|
||||
|
||||
from fastapi import Request, HTTPException, Depends
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from .jwt_utils import parse_jwt_token
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.security import HTTPBearer
|
||||
|
||||
from .config import settings
|
||||
from .jwt_utils import parse_jwt_token
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
async def auth_middleware(request: Request):
|
||||
if not settings.ENABLE_AUTH:
|
||||
# If authentication is disabled, allow the request to proceed
|
||||
|
||||
19
rnd/autogpt_libs/autogpt_libs/auth/models.py
Normal file
19
rnd/autogpt_libs/autogpt_libs/auth/models.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
# Using dataclass here to avoid adding dependency on pydantic
|
||||
@dataclass(frozen=True)
|
||||
class User:
|
||||
user_id: str
|
||||
email: str
|
||||
phone_number: str
|
||||
role: str
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload):
|
||||
return cls(
|
||||
user_id=payload["sub"],
|
||||
email=payload.get("email", ""),
|
||||
phone_number=payload.get("phone", ""),
|
||||
role=payload["role"],
|
||||
)
|
||||
@@ -10,4 +10,10 @@ REDDIT_USERNAME=
|
||||
REDDIT_PASSWORD=
|
||||
|
||||
# Discord
|
||||
DISCORD_BOT_TOKEN=
|
||||
DISCORD_BOT_TOKEN=
|
||||
|
||||
# SMTP/Email
|
||||
SMTP_SERVER=
|
||||
SMTP_PORT=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
@@ -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:
|
||||
|
||||
101
rnd/autogpt_server/autogpt_server/blocks/email_block.py
Normal file
101
rnd/autogpt_server/autogpt_server/blocks/email_block.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from autogpt_server.data.model import BlockSecret, SchemaField, SecretField
|
||||
|
||||
|
||||
class EmailCredentials(BaseModel):
|
||||
smtp_server: str = Field(
|
||||
default="smtp.gmail.com", description="SMTP server address"
|
||||
)
|
||||
smtp_port: int = Field(default=25, description="SMTP port number")
|
||||
smtp_username: BlockSecret = SecretField(key="smtp_username")
|
||||
smtp_password: BlockSecret = SecretField(key="smtp_password")
|
||||
|
||||
model_config = ConfigDict(title="Email Credentials")
|
||||
|
||||
|
||||
class SendEmailBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
to_email: str = SchemaField(
|
||||
description="Recipient email address", placeholder="recipient@example.com"
|
||||
)
|
||||
subject: str = SchemaField(
|
||||
description="Subject of the email", placeholder="Enter the email subject"
|
||||
)
|
||||
body: str = SchemaField(
|
||||
description="Body of the email", placeholder="Enter the email body"
|
||||
)
|
||||
creds: EmailCredentials = Field(
|
||||
description="SMTP credentials",
|
||||
default=EmailCredentials(),
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
status: str = SchemaField(description="Status of the email sending operation")
|
||||
error: str = SchemaField(
|
||||
description="Error message if the email sending failed"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="a1234567-89ab-cdef-0123-456789abcdef",
|
||||
description="This block sends an email using the provided SMTP credentials.",
|
||||
categories={BlockCategory.TEXT},
|
||||
input_schema=SendEmailBlock.Input,
|
||||
output_schema=SendEmailBlock.Output,
|
||||
test_input={
|
||||
"to_email": "recipient@example.com",
|
||||
"subject": "Test Email",
|
||||
"body": "This is a test email.",
|
||||
"creds": {
|
||||
"smtp_server": "smtp.gmail.com",
|
||||
"smtp_port": 25,
|
||||
"smtp_username": "your-email@gmail.com",
|
||||
"smtp_password": "your-gmail-password",
|
||||
},
|
||||
},
|
||||
test_output=[("status", "Email sent successfully")],
|
||||
test_mock={"send_email": lambda *args, **kwargs: "Email sent successfully"},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def send_email(
|
||||
creds: EmailCredentials, to_email: str, subject: str, body: str
|
||||
) -> str:
|
||||
try:
|
||||
smtp_server = creds.smtp_server
|
||||
smtp_port = creds.smtp_port
|
||||
smtp_username = creds.smtp_username.get_secret_value()
|
||||
smtp_password = creds.smtp_password.get_secret_value()
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = smtp_username
|
||||
msg["To"] = to_email
|
||||
msg["Subject"] = subject
|
||||
msg.attach(MIMEText(body, "plain"))
|
||||
|
||||
with smtplib.SMTP(smtp_server, smtp_port) as server:
|
||||
server.starttls()
|
||||
server.login(smtp_username, smtp_password)
|
||||
server.sendmail(smtp_username, to_email, msg.as_string())
|
||||
|
||||
return "Email sent successfully"
|
||||
except Exception as e:
|
||||
return f"Failed to send email: {str(e)}"
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
status = self.send_email(
|
||||
input_data.creds,
|
||||
input_data.to_email,
|
||||
input_data.subject,
|
||||
input_data.body,
|
||||
)
|
||||
if "successfully" in status:
|
||||
yield "status", status
|
||||
else:
|
||||
yield "error", status
|
||||
@@ -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",
|
||||
|
||||
139
rnd/autogpt_server/autogpt_server/blocks/time_blocks.py
Normal file
139
rnd/autogpt_server/autogpt_server/blocks/time_blocks.py
Normal file
@@ -0,0 +1,139 @@
|
||||
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",
|
||||
lambda t: abs(datetime.now() - datetime.strptime(t, "%Y-%m-%d"))
|
||||
< timedelta(days=8), # 7 days difference + 1 day error margin.
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
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",
|
||||
lambda t: abs(
|
||||
datetime.now() - datetime.strptime(t, "%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
< timedelta(seconds=10), # 10 seconds error margin.
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
current_date_time = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
yield "date_time", current_date_time
|
||||
|
||||
|
||||
class TimerBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
seconds: Union[int, str] = 0
|
||||
minutes: Union[int, str] = 0
|
||||
hours: Union[int, str] = 0
|
||||
days: Union[int, str] = 0
|
||||
|
||||
class Output(BlockSchema):
|
||||
message: str
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="d67a9c52-5e4e-11e2-bcfd-0800200c9a71",
|
||||
description="This block triggers after a specified duration.",
|
||||
categories={BlockCategory.TEXT},
|
||||
input_schema=TimerBlock.Input,
|
||||
output_schema=TimerBlock.Output,
|
||||
test_input=[
|
||||
{"seconds": 1},
|
||||
],
|
||||
test_output=[
|
||||
("message", "timer finished"),
|
||||
],
|
||||
)
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
|
||||
seconds = int(input_data.seconds)
|
||||
minutes = int(input_data.minutes)
|
||||
hours = int(input_data.hours)
|
||||
days = int(input_data.days)
|
||||
|
||||
total_seconds = seconds + minutes * 60 + hours * 3600 + days * 86400
|
||||
|
||||
time.sleep(total_seconds)
|
||||
yield "message", "timer finished"
|
||||
@@ -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"),
|
||||
@@ -272,7 +272,8 @@ async def get_node(node_id: str) -> Node | None:
|
||||
|
||||
|
||||
async def get_graphs_meta(
|
||||
filter_by: Literal["active", "template"] | None = "active"
|
||||
filter_by: Literal["active", "template"] | None = "active",
|
||||
user_id: str | None = None,
|
||||
) -> list[GraphMeta]:
|
||||
"""
|
||||
Retrieves graph metadata objects.
|
||||
@@ -291,6 +292,9 @@ async def get_graphs_meta(
|
||||
elif filter_by == "template":
|
||||
where_clause["isTemplate"] = True
|
||||
|
||||
if user_id and filter_by != "template":
|
||||
where_clause["userId"] = user_id
|
||||
|
||||
graphs = await AgentGraph.prisma().find_many(
|
||||
where=where_clause,
|
||||
distinct=["id"],
|
||||
@@ -304,7 +308,10 @@ async def get_graphs_meta(
|
||||
|
||||
|
||||
async def get_graph(
|
||||
graph_id: str, version: int | None = None, template: bool = False
|
||||
graph_id: str,
|
||||
version: int | None = None,
|
||||
template: bool = False,
|
||||
user_id: str | None = None,
|
||||
) -> Graph | None:
|
||||
"""
|
||||
Retrieves a graph from the DB.
|
||||
@@ -322,6 +329,9 @@ async def get_graph(
|
||||
elif not template:
|
||||
where_clause["isActive"] = True
|
||||
|
||||
if user_id and not template:
|
||||
where_clause["userId"] = user_id
|
||||
|
||||
graph = await AgentGraph.prisma().find_first(
|
||||
where=where_clause,
|
||||
include=AGENT_GRAPH_INCLUDE,
|
||||
@@ -330,10 +340,23 @@ async def get_graph(
|
||||
return Graph.from_db(graph) if graph else None
|
||||
|
||||
|
||||
async def set_graph_active_version(graph_id: str, version: int) -> None:
|
||||
async def set_graph_active_version(graph_id: str, version: int, user_id: str) -> None:
|
||||
# Check if the graph belongs to the user
|
||||
graph = await AgentGraph.prisma().find_first(
|
||||
where={
|
||||
"id": graph_id,
|
||||
"version": version,
|
||||
"userId": user_id,
|
||||
}
|
||||
)
|
||||
if not graph:
|
||||
raise Exception(f"Graph #{graph_id} v{version} not found or not owned by user")
|
||||
|
||||
updated_graph = await AgentGraph.prisma().update(
|
||||
data={"isActive": True},
|
||||
where={"graphVersionId": {"id": graph_id, "version": version}},
|
||||
where={
|
||||
"graphVersionId": {"id": graph_id, "version": version},
|
||||
},
|
||||
)
|
||||
if not updated_graph:
|
||||
raise Exception(f"Graph #{graph_id} v{version} not found")
|
||||
@@ -341,13 +364,15 @@ async def set_graph_active_version(graph_id: str, version: int) -> None:
|
||||
# Deactivate all other versions
|
||||
await AgentGraph.prisma().update_many(
|
||||
data={"isActive": False},
|
||||
where={"id": graph_id, "version": {"not": version}},
|
||||
where={"id": graph_id, "version": {"not": version}, "userId": user_id},
|
||||
)
|
||||
|
||||
|
||||
async def get_graph_all_versions(graph_id: str) -> list[Graph]:
|
||||
async def get_graph_all_versions(
|
||||
graph_id: str, user_id: str | None = None
|
||||
) -> list[Graph]:
|
||||
graph_versions = await AgentGraph.prisma().find_many(
|
||||
where={"id": graph_id},
|
||||
where={"id": graph_id, "userId": user_id},
|
||||
order={"version": "desc"},
|
||||
include=AGENT_GRAPH_INCLUDE,
|
||||
)
|
||||
@@ -358,17 +383,19 @@ async def get_graph_all_versions(graph_id: str) -> list[Graph]:
|
||||
return [Graph.from_db(graph) for graph in graph_versions]
|
||||
|
||||
|
||||
async def create_graph(graph: Graph) -> Graph:
|
||||
async def create_graph(graph: Graph, user_id: str | None) -> Graph:
|
||||
async with transaction() as tx:
|
||||
await __create_graph(tx, graph)
|
||||
await __create_graph(tx, graph, user_id)
|
||||
|
||||
if created_graph := await get_graph(graph.id, graph.version, graph.is_template):
|
||||
if created_graph := await get_graph(
|
||||
graph.id, graph.version, graph.is_template, user_id=user_id
|
||||
):
|
||||
return created_graph
|
||||
|
||||
raise ValueError(f"Created graph {graph.id} v{graph.version} is not in DB")
|
||||
|
||||
|
||||
async def __create_graph(tx, graph: Graph):
|
||||
async def __create_graph(tx, graph: Graph, user_id: str | None):
|
||||
await AgentGraph.prisma(tx).create(
|
||||
data={
|
||||
"id": graph.id,
|
||||
@@ -377,6 +404,7 @@ async def __create_graph(tx, graph: Graph):
|
||||
"description": graph.description,
|
||||
"isTemplate": graph.is_template,
|
||||
"isActive": graph.is_active,
|
||||
"userId": user_id,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -391,6 +419,7 @@ async def __create_graph(tx, graph: Graph):
|
||||
"description": f"Sub-Graph of {graph.id}",
|
||||
"isTemplate": graph.is_template,
|
||||
"isActive": graph.is_active,
|
||||
"userId": user_id,
|
||||
}
|
||||
)
|
||||
for subgraph_id in graph.subgraphs
|
||||
@@ -453,5 +482,5 @@ async def import_packaged_templates() -> None:
|
||||
exists := next((t for t in templates_in_db if t.id == template.id), None)
|
||||
) and exists.version >= template.version:
|
||||
continue
|
||||
await create_graph(template)
|
||||
await create_graph(template, None)
|
||||
print(f"Loaded template '{template.name}' ({template.id})")
|
||||
|
||||
@@ -10,6 +10,7 @@ from autogpt_server.util import json
|
||||
|
||||
class ExecutionSchedule(BaseDbModel):
|
||||
graph_id: str
|
||||
user_id: str
|
||||
graph_version: int
|
||||
schedule: str
|
||||
is_enabled: bool
|
||||
@@ -25,6 +26,7 @@ class ExecutionSchedule(BaseDbModel):
|
||||
return ExecutionSchedule(
|
||||
id=schedule.id,
|
||||
graph_id=schedule.agentGraphId,
|
||||
user_id=schedule.userId,
|
||||
graph_version=schedule.agentGraphVersion,
|
||||
schedule=schedule.schedule,
|
||||
is_enabled=schedule.isEnabled,
|
||||
@@ -47,11 +49,12 @@ async def disable_schedule(schedule_id: str):
|
||||
)
|
||||
|
||||
|
||||
async def get_schedules(graph_id: str) -> list[ExecutionSchedule]:
|
||||
async def get_schedules(graph_id: str, user_id: str) -> list[ExecutionSchedule]:
|
||||
query = AgentGraphExecutionSchedule.prisma().find_many(
|
||||
where={
|
||||
"isEnabled": True,
|
||||
"agentGraphId": graph_id,
|
||||
"userId": user_id,
|
||||
},
|
||||
)
|
||||
return [ExecutionSchedule.from_db(schedule) for schedule in await query]
|
||||
@@ -61,6 +64,7 @@ async def add_schedule(schedule: ExecutionSchedule) -> ExecutionSchedule:
|
||||
obj = await AgentGraphExecutionSchedule.prisma().create(
|
||||
data={
|
||||
"id": schedule.id,
|
||||
"userId": schedule.user_id,
|
||||
"agentGraphId": schedule.graph_id,
|
||||
"agentGraphVersion": schedule.graph_version,
|
||||
"schedule": schedule.schedule,
|
||||
@@ -71,7 +75,7 @@ async def add_schedule(schedule: ExecutionSchedule) -> ExecutionSchedule:
|
||||
return ExecutionSchedule.from_db(obj)
|
||||
|
||||
|
||||
async def update_schedule(schedule_id: str, is_enabled: bool):
|
||||
async def update_schedule(schedule_id: str, is_enabled: bool, user_id: str):
|
||||
await AgentGraphExecutionSchedule.prisma().update(
|
||||
where={"id": schedule_id}, data={"isEnabled": is_enabled}
|
||||
)
|
||||
|
||||
40
rnd/autogpt_server/autogpt_server/data/user.py
Normal file
40
rnd/autogpt_server/autogpt_server/data/user.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from typing import Optional
|
||||
|
||||
from prisma.models import User
|
||||
|
||||
from autogpt_server.data.db import prisma
|
||||
|
||||
|
||||
async def get_or_create_user(user_data: dict) -> User:
|
||||
user = await prisma.user.find_unique(where={"id": user_data["sub"]})
|
||||
if not user:
|
||||
user = await prisma.user.create(
|
||||
data={
|
||||
"id": user_data["sub"],
|
||||
"email": user_data["email"],
|
||||
"name": user_data.get("user_metadata", {}).get("name"),
|
||||
}
|
||||
)
|
||||
return User.model_validate(user)
|
||||
|
||||
|
||||
async def get_user_by_id(user_id: str) -> Optional[User]:
|
||||
user = await prisma.user.find_unique(where={"id": user_id})
|
||||
return User.model_validate(user) if user else None
|
||||
|
||||
|
||||
async def create_default_user(enable_auth: str) -> Optional[User]:
|
||||
if not enable_auth.lower() == "true":
|
||||
user = await prisma.user.find_unique(
|
||||
where={"id": "3e53486c-cf57-477e-ba2a-cb02dc828e1a"}
|
||||
)
|
||||
if not user:
|
||||
user = await prisma.user.create(
|
||||
data={
|
||||
"id": "3e53486c-cf57-477e-ba2a-cb02dc828e1a",
|
||||
"email": "default@example.com",
|
||||
"name": "Default User",
|
||||
}
|
||||
)
|
||||
return User.model_validate(user)
|
||||
return None
|
||||
@@ -416,8 +416,10 @@ class ExecutionManager(AppService):
|
||||
return get_agent_server_client()
|
||||
|
||||
@expose
|
||||
def add_execution(self, graph_id: str, data: BlockInput) -> dict[Any, Any]:
|
||||
graph: Graph | None = self.run_and_wait(get_graph(graph_id))
|
||||
def add_execution(
|
||||
self, graph_id: str, data: BlockInput, user_id: str
|
||||
) -> dict[Any, Any]:
|
||||
graph: Graph | None = self.run_and_wait(get_graph(graph_id, user_id=user_id))
|
||||
if not graph:
|
||||
raise Exception(f"Graph #{graph_id} not found.")
|
||||
graph.validate_graph(for_run=True)
|
||||
|
||||
@@ -62,16 +62,22 @@ class ExecutionScheduler(AppService):
|
||||
logger.exception(f"Error executing graph {graph_id}: {e}")
|
||||
|
||||
@expose
|
||||
def update_schedule(self, schedule_id: str, is_enabled: bool) -> str:
|
||||
self.run_and_wait(model.update_schedule(schedule_id, is_enabled))
|
||||
def update_schedule(self, schedule_id: str, is_enabled: bool, user_id: str) -> str:
|
||||
self.run_and_wait(model.update_schedule(schedule_id, is_enabled, user_id))
|
||||
return schedule_id
|
||||
|
||||
@expose
|
||||
def add_execution_schedule(
|
||||
self, graph_id: str, graph_version: int, cron: str, input_data: BlockInput
|
||||
self,
|
||||
graph_id: str,
|
||||
graph_version: int,
|
||||
cron: str,
|
||||
input_data: BlockInput,
|
||||
user_id: str,
|
||||
) -> str:
|
||||
schedule = model.ExecutionSchedule(
|
||||
graph_id=graph_id,
|
||||
user_id=user_id,
|
||||
graph_version=graph_version,
|
||||
schedule=cron,
|
||||
input_data=input_data,
|
||||
@@ -79,7 +85,7 @@ class ExecutionScheduler(AppService):
|
||||
return self.run_and_wait(model.add_schedule(schedule)).id
|
||||
|
||||
@expose
|
||||
def get_execution_schedules(self, graph_id: str) -> dict[str, str]:
|
||||
query = model.get_schedules(graph_id)
|
||||
def get_execution_schedules(self, graph_id: str, user_id: str) -> dict[str, str]:
|
||||
query = model.get_schedules(graph_id, user_id=user_id)
|
||||
schedules: list[model.ExecutionSchedule] = self.run_and_wait(query)
|
||||
return {v.id: v.schedule for v in schedules}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
import inspect
|
||||
from collections import defaultdict
|
||||
from contextlib import asynccontextmanager
|
||||
from functools import wraps
|
||||
from typing import Annotated, Any, Dict
|
||||
|
||||
import uvicorn
|
||||
from autogpt_libs.auth.jwt_utils import parse_jwt_token
|
||||
from autogpt_libs.auth.middleware import auth_middleware
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
@@ -21,12 +23,14 @@ from fastapi.responses import JSONResponse
|
||||
import autogpt_server.server.ws_api
|
||||
from autogpt_server.data import block, db
|
||||
from autogpt_server.data import graph as graph_db
|
||||
from autogpt_server.data import user as user_db
|
||||
from autogpt_server.data.block import BlockInput, CompletedBlockOutput
|
||||
from autogpt_server.data.execution import (
|
||||
ExecutionResult,
|
||||
get_execution_results,
|
||||
list_executions,
|
||||
)
|
||||
from autogpt_server.data.user import get_or_create_user
|
||||
from autogpt_server.executor import ExecutionManager, ExecutionScheduler
|
||||
from autogpt_server.server.conn_manager import ConnectionManager
|
||||
from autogpt_server.server.model import (
|
||||
@@ -39,12 +43,26 @@ from autogpt_server.util.lock import KeyedMutex
|
||||
from autogpt_server.util.service import AppService, expose, get_service_client
|
||||
from autogpt_server.util.settings import Settings
|
||||
|
||||
settings = Settings()
|
||||
|
||||
|
||||
def get_user_id(payload: dict = Depends(auth_middleware)) -> str:
|
||||
if not payload:
|
||||
# This handles the case when authentication is disabled
|
||||
return "3e53486c-cf57-477e-ba2a-cb02dc828e1a"
|
||||
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User ID not found in token")
|
||||
return user_id
|
||||
|
||||
|
||||
class AgentServer(AppService):
|
||||
event_queue: asyncio.Queue[ExecutionResult] = asyncio.Queue()
|
||||
manager = ConnectionManager()
|
||||
mutex = KeyedMutex()
|
||||
use_db = False
|
||||
_test_dependency_overrides = {}
|
||||
|
||||
async def event_broadcaster(self):
|
||||
while True:
|
||||
@@ -56,6 +74,7 @@ class AgentServer(AppService):
|
||||
await db.connect()
|
||||
await block.initialize_blocks()
|
||||
await graph_db.import_packaged_templates()
|
||||
await user_db.create_default_user(settings.config.enable_auth)
|
||||
asyncio.create_task(self.event_broadcaster())
|
||||
yield
|
||||
await db.disconnect()
|
||||
@@ -72,6 +91,9 @@ class AgentServer(AppService):
|
||||
lifespan=self.lifespan,
|
||||
)
|
||||
|
||||
if self._test_dependency_overrides:
|
||||
app.dependency_overrides.update(self._test_dependency_overrides)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Allows all origins
|
||||
@@ -84,6 +106,12 @@ class AgentServer(AppService):
|
||||
router = APIRouter(prefix="/api")
|
||||
router.dependencies.append(Depends(auth_middleware))
|
||||
|
||||
router.add_api_route(
|
||||
path="/auth/user",
|
||||
endpoint=self.get_or_create_user_route,
|
||||
methods=["POST"],
|
||||
)
|
||||
|
||||
router.add_api_route(
|
||||
path="/blocks",
|
||||
endpoint=self.get_graph_blocks, # type: ignore
|
||||
@@ -201,6 +229,35 @@ class AgentServer(AppService):
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
def set_test_dependency_overrides(self, overrides: dict):
|
||||
self._test_dependency_overrides = overrides
|
||||
|
||||
def _apply_overrides_to_methods(self):
|
||||
for attr_name in dir(self):
|
||||
attr = getattr(self, attr_name)
|
||||
if callable(attr) and hasattr(attr, "__annotations__"):
|
||||
setattr(self, attr_name, self._override_method(attr))
|
||||
|
||||
# TODO: fix this with some proper refactoring of the server
|
||||
def _override_method(self, method):
|
||||
@wraps(method)
|
||||
async def wrapper(*args, **kwargs):
|
||||
sig = inspect.signature(method)
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param.annotation is inspect.Parameter.empty:
|
||||
continue
|
||||
if isinstance(param.annotation, Depends) or ( # type: ignore
|
||||
isinstance(param.annotation, type) and issubclass(param.annotation, Depends) # type: ignore
|
||||
):
|
||||
dependency = param.annotation.dependency if isinstance(param.annotation, Depends) else param.annotation # type: ignore
|
||||
if dependency in self._test_dependency_overrides:
|
||||
kwargs[param_name] = self._test_dependency_overrides[
|
||||
dependency
|
||||
]()
|
||||
return await method(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
@property
|
||||
def execution_manager_client(self) -> ExecutionManager:
|
||||
return get_service_client(ExecutionManager)
|
||||
@@ -219,7 +276,30 @@ class AgentServer(AppService):
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
async def authenticate_websocket(self, websocket: WebSocket) -> str:
|
||||
if settings.config.enable_auth.lower() == "true":
|
||||
token = websocket.query_params.get("token")
|
||||
if not token:
|
||||
await websocket.close(code=4001, reason="Missing authentication token")
|
||||
return ""
|
||||
|
||||
try:
|
||||
payload = parse_jwt_token(token)
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
await websocket.close(code=4002, reason="Invalid token")
|
||||
return ""
|
||||
return user_id
|
||||
except ValueError:
|
||||
await websocket.close(code=4003, reason="Invalid token")
|
||||
return ""
|
||||
else:
|
||||
return "3e53486c-cf57-477e-ba2a-cb02dc828e1a"
|
||||
|
||||
async def websocket_router(self, websocket: WebSocket):
|
||||
user_id = await self.authenticate_websocket(websocket)
|
||||
if not user_id:
|
||||
return
|
||||
await self.manager.connect(websocket)
|
||||
try:
|
||||
while True:
|
||||
@@ -258,7 +338,7 @@ class AgentServer(AppService):
|
||||
).model_dump_json()
|
||||
)
|
||||
elif message.method == Methods.GET_GRAPHS:
|
||||
data = await self.get_graphs()
|
||||
data = await self.get_graphs(user_id=user_id)
|
||||
await websocket.send_text(
|
||||
WsMessage(
|
||||
method=Methods.GET_GRAPHS,
|
||||
@@ -269,7 +349,9 @@ class AgentServer(AppService):
|
||||
print("Get graphs request received")
|
||||
elif message.method == Methods.GET_GRAPH:
|
||||
assert isinstance(message.data, dict), "Data must be a dictionary"
|
||||
data = await self.get_graph(message.data["graph_id"])
|
||||
data = await self.get_graph(
|
||||
message.data["graph_id"], user_id=user_id
|
||||
)
|
||||
await websocket.send_text(
|
||||
WsMessage(
|
||||
method=Methods.GET_GRAPH,
|
||||
@@ -281,7 +363,7 @@ class AgentServer(AppService):
|
||||
elif message.method == Methods.CREATE_GRAPH:
|
||||
assert isinstance(message.data, dict), "Data must be a dictionary"
|
||||
create_graph = CreateGraph.model_validate(message.data)
|
||||
data = await self.create_new_graph(create_graph)
|
||||
data = await self.create_new_graph(create_graph, user_id=user_id)
|
||||
await websocket.send_text(
|
||||
WsMessage(
|
||||
method=Methods.CREATE_GRAPH,
|
||||
@@ -294,7 +376,7 @@ class AgentServer(AppService):
|
||||
elif message.method == Methods.RUN_GRAPH:
|
||||
assert isinstance(message.data, dict), "Data must be a dictionary"
|
||||
data = await self.execute_graph(
|
||||
message.data["graph_id"], message.data["data"]
|
||||
message.data["graph_id"], message.data["data"], user_id=user_id
|
||||
)
|
||||
await websocket.send_text(
|
||||
WsMessage(
|
||||
@@ -307,7 +389,9 @@ class AgentServer(AppService):
|
||||
print("Run graph request received")
|
||||
elif message.method == Methods.GET_GRAPH_RUNS:
|
||||
assert isinstance(message.data, dict), "Data must be a dictionary"
|
||||
data = await self.list_graph_runs(message.data["graph_id"])
|
||||
data = await self.list_graph_runs(
|
||||
message.data["graph_id"], user_id=user_id
|
||||
)
|
||||
await websocket.send_text(
|
||||
WsMessage(
|
||||
method=Methods.GET_GRAPH_RUNS,
|
||||
@@ -323,6 +407,7 @@ class AgentServer(AppService):
|
||||
message.data["graph_id"],
|
||||
message.data["cron"],
|
||||
message.data["data"],
|
||||
user_id=user_id,
|
||||
)
|
||||
await websocket.send_text(
|
||||
WsMessage(
|
||||
@@ -335,7 +420,9 @@ class AgentServer(AppService):
|
||||
print("Create scheduled run request received")
|
||||
elif message.method == Methods.GET_SCHEDULED_RUNS:
|
||||
assert isinstance(message.data, dict), "Data must be a dictionary"
|
||||
data = self.get_execution_schedules(message.data["graph_id"])
|
||||
data = self.get_execution_schedules(
|
||||
message.data["graph_id"], user_id=user_id
|
||||
)
|
||||
await websocket.send_text(
|
||||
WsMessage(
|
||||
method=Methods.GET_SCHEDULED_RUNS,
|
||||
@@ -347,7 +434,7 @@ class AgentServer(AppService):
|
||||
elif message.method == Methods.UPDATE_SCHEDULED_RUN:
|
||||
assert isinstance(message.data, dict), "Data must be a dictionary"
|
||||
data = self.update_schedule(
|
||||
message.data["schedule_id"], message.data
|
||||
message.data["schedule_id"], message.data, user_id=user_id
|
||||
)
|
||||
await websocket.send_text(
|
||||
WsMessage(
|
||||
@@ -386,6 +473,11 @@ class AgentServer(AppService):
|
||||
self.manager.disconnect(websocket)
|
||||
print("Client Disconnected")
|
||||
|
||||
@classmethod
|
||||
async def get_or_create_user_route(cls, user_data: dict = Depends(auth_middleware)):
|
||||
user = await get_or_create_user(user_data)
|
||||
return user.model_dump()
|
||||
|
||||
@classmethod
|
||||
def get_graph_blocks(cls) -> list[dict[Any, Any]]:
|
||||
return [v.to_dict() for v in block.get_blocks().values()] # type: ignore
|
||||
@@ -404,8 +496,10 @@ class AgentServer(AppService):
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
async def get_graphs(cls) -> list[graph_db.GraphMeta]:
|
||||
return await graph_db.get_graphs_meta(filter_by="active")
|
||||
async def get_graphs(
|
||||
cls, user_id: Annotated[str, Depends(get_user_id)]
|
||||
) -> list[graph_db.GraphMeta]:
|
||||
return await graph_db.get_graphs_meta(filter_by="active", user_id=user_id)
|
||||
|
||||
@classmethod
|
||||
async def get_templates(cls) -> list[graph_db.GraphMeta]:
|
||||
@@ -413,9 +507,12 @@ class AgentServer(AppService):
|
||||
|
||||
@classmethod
|
||||
async def get_graph(
|
||||
cls, graph_id: str, version: int | None = None
|
||||
cls,
|
||||
graph_id: str,
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
version: int | None = None,
|
||||
) -> graph_db.Graph:
|
||||
graph = await graph_db.get_graph(graph_id, version)
|
||||
graph = await graph_db.get_graph(graph_id, version, user_id=user_id)
|
||||
if not graph:
|
||||
raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.")
|
||||
return graph
|
||||
@@ -432,30 +529,39 @@ class AgentServer(AppService):
|
||||
return graph
|
||||
|
||||
@classmethod
|
||||
async def get_graph_all_versions(cls, graph_id: str) -> list[graph_db.Graph]:
|
||||
graphs = await graph_db.get_graph_all_versions(graph_id)
|
||||
async def get_graph_all_versions(
|
||||
cls, graph_id: str, user_id: Annotated[str, Depends(get_user_id)]
|
||||
) -> list[graph_db.Graph]:
|
||||
graphs = await graph_db.get_graph_all_versions(graph_id, user_id=user_id)
|
||||
if not graphs:
|
||||
raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.")
|
||||
return graphs
|
||||
|
||||
@classmethod
|
||||
async def create_new_graph(cls, create_graph: CreateGraph) -> graph_db.Graph:
|
||||
return await cls.create_graph(create_graph, is_template=False)
|
||||
async def create_new_graph(
|
||||
cls, create_graph: CreateGraph, user_id: Annotated[str, Depends(get_user_id)]
|
||||
) -> graph_db.Graph:
|
||||
return await cls.create_graph(create_graph, is_template=False, user_id=user_id)
|
||||
|
||||
@classmethod
|
||||
async def create_new_template(cls, create_graph: CreateGraph) -> graph_db.Graph:
|
||||
return await cls.create_graph(create_graph, is_template=True)
|
||||
async def create_new_template(
|
||||
cls, create_graph: CreateGraph, user_id: Annotated[str, Depends(get_user_id)]
|
||||
) -> graph_db.Graph:
|
||||
return await cls.create_graph(create_graph, is_template=True, user_id=user_id)
|
||||
|
||||
@classmethod
|
||||
async def create_graph(
|
||||
cls, create_graph: CreateGraph, is_template: bool
|
||||
cls, create_graph: CreateGraph, is_template: bool, user_id: str
|
||||
) -> graph_db.Graph:
|
||||
if create_graph.graph:
|
||||
graph = create_graph.graph
|
||||
elif create_graph.template_id:
|
||||
# Create a new graph from a template
|
||||
graph = await graph_db.get_graph(
|
||||
create_graph.template_id, create_graph.template_version, template=True
|
||||
create_graph.template_id,
|
||||
create_graph.template_version,
|
||||
template=True,
|
||||
user_id=user_id,
|
||||
)
|
||||
if not graph:
|
||||
raise HTTPException(
|
||||
@@ -471,16 +577,23 @@ class AgentServer(AppService):
|
||||
graph.is_active = not is_template
|
||||
graph.reassign_ids(reassign_graph_id=True)
|
||||
|
||||
return await graph_db.create_graph(graph)
|
||||
return await graph_db.create_graph(graph, user_id=user_id)
|
||||
|
||||
@classmethod
|
||||
async def update_graph(cls, graph_id: str, graph: graph_db.Graph) -> graph_db.Graph:
|
||||
async def update_graph(
|
||||
cls,
|
||||
graph_id: str,
|
||||
graph: graph_db.Graph,
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
) -> graph_db.Graph:
|
||||
# Sanity check
|
||||
if graph.id and graph.id != graph_id:
|
||||
raise HTTPException(400, detail="Graph ID does not match ID in URI")
|
||||
|
||||
# Determine new version
|
||||
existing_versions = await graph_db.get_graph_all_versions(graph_id)
|
||||
existing_versions = await graph_db.get_graph_all_versions(
|
||||
graph_id, user_id=user_id
|
||||
)
|
||||
if not existing_versions:
|
||||
raise HTTPException(404, detail=f"Graph #{graph_id} not found")
|
||||
latest_version_number = max(g.version for g in existing_versions)
|
||||
@@ -496,43 +609,56 @@ class AgentServer(AppService):
|
||||
graph.is_active = not graph.is_template
|
||||
graph.reassign_ids()
|
||||
|
||||
new_graph_version = await graph_db.create_graph(graph)
|
||||
new_graph_version = await graph_db.create_graph(graph, user_id=user_id)
|
||||
|
||||
if new_graph_version.is_active:
|
||||
# Ensure new version is the only active version
|
||||
await graph_db.set_graph_active_version(
|
||||
graph_id=graph_id, version=new_graph_version.version
|
||||
graph_id=graph_id, version=new_graph_version.version, user_id=user_id
|
||||
)
|
||||
|
||||
return new_graph_version
|
||||
|
||||
@classmethod
|
||||
async def set_graph_active_version(
|
||||
cls, graph_id: str, request_body: SetGraphActiveVersion
|
||||
cls,
|
||||
graph_id: str,
|
||||
request_body: SetGraphActiveVersion,
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
):
|
||||
new_active_version = request_body.active_graph_version
|
||||
if not await graph_db.get_graph(graph_id, new_active_version):
|
||||
if not await graph_db.get_graph(graph_id, new_active_version, user_id=user_id):
|
||||
raise HTTPException(
|
||||
404, f"Graph #{graph_id} v{new_active_version} not found"
|
||||
)
|
||||
await graph_db.set_graph_active_version(
|
||||
graph_id=graph_id, version=request_body.active_graph_version
|
||||
graph_id=graph_id,
|
||||
version=request_body.active_graph_version,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
async def execute_graph(
|
||||
self, graph_id: str, node_input: dict[Any, Any]
|
||||
self,
|
||||
graph_id: str,
|
||||
node_input: dict[Any, Any],
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
) -> dict[Any, Any]:
|
||||
try:
|
||||
return self.execution_manager_client.add_execution(graph_id, node_input)
|
||||
return self.execution_manager_client.add_execution(
|
||||
graph_id, node_input, user_id=user_id
|
||||
)
|
||||
except Exception as e:
|
||||
msg = e.__str__().encode().decode("unicode_escape")
|
||||
raise HTTPException(status_code=400, detail=msg)
|
||||
|
||||
@classmethod
|
||||
async def list_graph_runs(
|
||||
cls, graph_id: str, graph_version: int | None = None
|
||||
cls,
|
||||
graph_id: str,
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
graph_version: int | None = None,
|
||||
) -> list[str]:
|
||||
graph = await graph_db.get_graph(graph_id, graph_version)
|
||||
graph = await graph_db.get_graph(graph_id, graph_version, user_id=user_id)
|
||||
if not graph:
|
||||
rev = "" if graph_version is None else f" v{graph_version}"
|
||||
raise HTTPException(
|
||||
@@ -543,38 +669,47 @@ class AgentServer(AppService):
|
||||
|
||||
@classmethod
|
||||
async def get_run_execution_results(
|
||||
cls, graph_id: str, run_id: str
|
||||
cls, graph_id: str, run_id: str, user_id: Annotated[str, Depends(get_user_id)]
|
||||
) -> list[ExecutionResult]:
|
||||
graph = await graph_db.get_graph(graph_id)
|
||||
graph = await graph_db.get_graph(graph_id, user_id=user_id)
|
||||
if not graph:
|
||||
raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.")
|
||||
|
||||
return await get_execution_results(run_id)
|
||||
|
||||
async def create_schedule(
|
||||
self, graph_id: str, cron: str, input_data: dict[Any, Any]
|
||||
self,
|
||||
graph_id: str,
|
||||
cron: str,
|
||||
input_data: dict[Any, Any],
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
) -> dict[Any, Any]:
|
||||
graph = await graph_db.get_graph(graph_id)
|
||||
graph = await graph_db.get_graph(graph_id, user_id=user_id)
|
||||
if not graph:
|
||||
raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.")
|
||||
execution_scheduler = self.execution_scheduler_client
|
||||
return {
|
||||
"id": execution_scheduler.add_execution_schedule(
|
||||
graph_id, graph.version, cron, input_data
|
||||
graph_id, graph.version, cron, input_data, user_id=user_id
|
||||
)
|
||||
}
|
||||
|
||||
def update_schedule(
|
||||
self, schedule_id: str, input_data: dict[Any, Any]
|
||||
self,
|
||||
schedule_id: str,
|
||||
input_data: dict[Any, Any],
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
) -> dict[Any, Any]:
|
||||
execution_scheduler = self.execution_scheduler_client
|
||||
is_enabled = input_data.get("is_enabled", False)
|
||||
execution_scheduler.update_schedule(schedule_id, is_enabled) # type: ignore
|
||||
execution_scheduler.update_schedule(schedule_id, is_enabled, user_id=user_id) # type: ignore
|
||||
return {"id": schedule_id}
|
||||
|
||||
def get_execution_schedules(self, graph_id: str) -> dict[str, str]:
|
||||
def get_execution_schedules(
|
||||
self, graph_id: str, user_id: Annotated[str, Depends(get_user_id)]
|
||||
) -> dict[str, str]:
|
||||
execution_scheduler = self.execution_scheduler_client
|
||||
return execution_scheduler.get_execution_schedules(graph_id) # type: ignore
|
||||
return execution_scheduler.get_execution_schedules(graph_id, user_id) # type: ignore
|
||||
|
||||
@expose
|
||||
def send_execution_update(self, execution_result_dict: dict[Any, Any]):
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from pathlib import Path
|
||||
|
||||
from prisma.models import User
|
||||
|
||||
from autogpt_server.blocks.basic import ValueBlock
|
||||
from autogpt_server.blocks.block import BlockInstallationBlock
|
||||
from autogpt_server.blocks.http import HttpRequestBlock
|
||||
from autogpt_server.blocks.llm import TextLlmCallBlock
|
||||
from autogpt_server.blocks.text import TextFormatterBlock, TextParserBlock
|
||||
from autogpt_server.data.graph import Graph, Link, Node, create_graph
|
||||
from autogpt_server.data.user import get_or_create_user
|
||||
from autogpt_server.util.test import SpinTestServer, wait_execution
|
||||
|
||||
sample_block_modules = {
|
||||
@@ -23,6 +26,16 @@ for module, description in sample_block_modules.items():
|
||||
sample_block_codes[module] = f"[Example: {description}]\n{code}"
|
||||
|
||||
|
||||
async def create_test_user() -> User:
|
||||
test_user_data = {
|
||||
"sub": "ef3b97d7-1161-4eb4-92b2-10c24fb154c1",
|
||||
"email": "testuser@example.com",
|
||||
"name": "Test User",
|
||||
}
|
||||
user = await get_or_create_user(test_user_data)
|
||||
return user
|
||||
|
||||
|
||||
def create_test_graph() -> Graph:
|
||||
"""
|
||||
ValueBlock (input)
|
||||
@@ -237,9 +250,12 @@ Here are a couple of sample of the Block class implementation:
|
||||
async def block_autogen_agent():
|
||||
async with SpinTestServer() as server:
|
||||
test_manager = server.exec_manager
|
||||
test_graph = await create_graph(create_test_graph())
|
||||
test_user = await create_test_user()
|
||||
test_graph = await create_graph(create_test_graph(), user_id=test_user.id)
|
||||
input_data = {"input": "Write me a block that writes a string into a file."}
|
||||
response = await server.agent_server.execute_graph(test_graph.id, input_data)
|
||||
response = await server.agent_server.execute_graph(
|
||||
test_graph.id, input_data, test_user.id
|
||||
)
|
||||
print(response)
|
||||
result = await wait_execution(
|
||||
exec_manager=test_manager,
|
||||
@@ -247,6 +263,7 @@ async def block_autogen_agent():
|
||||
graph_exec_id=response["id"],
|
||||
num_execs=10,
|
||||
timeout=1200,
|
||||
user_id=test_user.id,
|
||||
)
|
||||
print(result)
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from prisma.models import User
|
||||
|
||||
from autogpt_server.blocks.llm import ObjectLlmCallBlock
|
||||
from autogpt_server.blocks.reddit import RedditGetPostsBlock, RedditPostCommentBlock
|
||||
from autogpt_server.blocks.text import TextFormatterBlock, TextMatcherBlock
|
||||
from autogpt_server.data.graph import Graph, Link, Node, create_graph
|
||||
from autogpt_server.data.user import get_or_create_user
|
||||
from autogpt_server.util.test import SpinTestServer, wait_execution
|
||||
|
||||
|
||||
@@ -136,14 +139,29 @@ Make sure to only comment on a relevant post.
|
||||
return test_graph
|
||||
|
||||
|
||||
async def create_test_user() -> User:
|
||||
test_user_data = {
|
||||
"sub": "ef3b97d7-1161-4eb4-92b2-10c24fb154c1",
|
||||
"email": "testuser@example.com",
|
||||
"name": "Test User",
|
||||
}
|
||||
user = await get_or_create_user(test_user_data)
|
||||
return user
|
||||
|
||||
|
||||
async def reddit_marketing_agent():
|
||||
async with SpinTestServer() as server:
|
||||
exec_man = server.exec_manager
|
||||
test_graph = await create_graph(create_test_graph())
|
||||
test_user = await create_test_user()
|
||||
test_graph = await create_graph(create_test_graph(), user_id=test_user.id)
|
||||
input_data = {"subreddit": "AutoGPT"}
|
||||
response = await server.agent_server.execute_graph(test_graph.id, input_data)
|
||||
response = await server.agent_server.execute_graph(
|
||||
test_graph.id, input_data, test_user.id
|
||||
)
|
||||
print(response)
|
||||
result = await wait_execution(exec_man, test_graph.id, response["id"], 13, 120)
|
||||
result = await wait_execution(
|
||||
exec_man, test_user.id, test_graph.id, response["id"], 13, 120
|
||||
)
|
||||
print(result)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
from prisma.models import User
|
||||
|
||||
from autogpt_server.blocks.basic import InputBlock, PrintingBlock
|
||||
from autogpt_server.blocks.text import TextFormatterBlock
|
||||
from autogpt_server.data import graph
|
||||
from autogpt_server.data.graph import create_graph
|
||||
from autogpt_server.data.user import get_or_create_user
|
||||
from autogpt_server.util.test import SpinTestServer, wait_execution
|
||||
|
||||
|
||||
async def create_test_user() -> User:
|
||||
test_user_data = {
|
||||
"sub": "ef3b97d7-1161-4eb4-92b2-10c24fb154c1",
|
||||
"email": "testuser@example.com",
|
||||
"name": "Test User",
|
||||
}
|
||||
user = await get_or_create_user(test_user_data)
|
||||
return user
|
||||
|
||||
|
||||
def create_test_graph() -> graph.Graph:
|
||||
"""
|
||||
ValueBlock
|
||||
@@ -63,11 +76,16 @@ def create_test_graph() -> graph.Graph:
|
||||
async def sample_agent():
|
||||
async with SpinTestServer() as server:
|
||||
exec_man = server.exec_manager
|
||||
test_graph = await create_graph(create_test_graph())
|
||||
test_user = await create_test_user()
|
||||
test_graph = await create_graph(create_test_graph(), test_user.id)
|
||||
input_data = {"input_1": "Hello", "input_2": "World"}
|
||||
response = await server.agent_server.execute_graph(test_graph.id, input_data)
|
||||
response = await server.agent_server.execute_graph(
|
||||
test_graph.id, input_data, test_user.id
|
||||
)
|
||||
print(response)
|
||||
result = await wait_execution(exec_man, test_graph.id, response["id"], 4, 10)
|
||||
result = await wait_execution(
|
||||
exec_man, test_user.id, test_graph.id, response["id"], 4, 10
|
||||
)
|
||||
print(result)
|
||||
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
|
||||
default="localhost",
|
||||
description="The default hostname of the Pyro server.",
|
||||
)
|
||||
enable_auth: str = Field(
|
||||
default="false",
|
||||
description="If authentication is enabled or not",
|
||||
)
|
||||
# Add more configuration fields as needed
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
@@ -107,6 +111,11 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
|
||||
|
||||
discord_bot_token: str = Field(default="", description="Discord bot token")
|
||||
|
||||
smtp_server: str = Field(default="", description="SMTP server IP")
|
||||
smtp_port: str = Field(default="", description="SMTP server port")
|
||||
smtp_username: str = Field(default="", description="SMTP username")
|
||||
smtp_password: str = Field(default="", description="SMTP password")
|
||||
|
||||
# Add more secret fields as needed
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
|
||||
@@ -5,6 +5,7 @@ from autogpt_server.data.block import Block, initialize_blocks
|
||||
from autogpt_server.data.execution import ExecutionStatus
|
||||
from autogpt_server.executor import ExecutionManager, ExecutionScheduler
|
||||
from autogpt_server.server import AgentServer
|
||||
from autogpt_server.server.server import get_user_id
|
||||
from autogpt_server.util.service import PyroNameServer
|
||||
|
||||
log = print
|
||||
@@ -17,14 +18,21 @@ class SpinTestServer:
|
||||
self.agent_server = AgentServer()
|
||||
self.scheduler = ExecutionScheduler()
|
||||
|
||||
@staticmethod
|
||||
def test_get_user_id():
|
||||
return "3e53486c-cf57-477e-ba2a-cb02dc828e1a"
|
||||
|
||||
async def __aenter__(self):
|
||||
|
||||
self.name_server.__enter__()
|
||||
self.setup_dependency_overrides()
|
||||
self.agent_server.__enter__()
|
||||
self.exec_manager.__enter__()
|
||||
self.scheduler.__enter__()
|
||||
|
||||
await db.connect()
|
||||
await initialize_blocks()
|
||||
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
@@ -35,16 +43,25 @@ class SpinTestServer:
|
||||
self.exec_manager.__exit__(exc_type, exc_val, exc_tb)
|
||||
self.scheduler.__exit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
def setup_dependency_overrides(self):
|
||||
# Override get_user_id for testing
|
||||
self.agent_server.set_test_dependency_overrides(
|
||||
{get_user_id: self.test_get_user_id}
|
||||
)
|
||||
|
||||
|
||||
async def wait_execution(
|
||||
exec_manager: ExecutionManager,
|
||||
user_id: str,
|
||||
graph_id: str,
|
||||
graph_exec_id: str,
|
||||
num_execs: int,
|
||||
timeout: int = 20,
|
||||
) -> list:
|
||||
async def is_execution_completed():
|
||||
execs = await AgentServer().get_run_execution_results(graph_id, graph_exec_id)
|
||||
execs = await AgentServer().get_run_execution_results(
|
||||
graph_id, graph_exec_id, user_id
|
||||
)
|
||||
return (
|
||||
exec_manager.queue.empty()
|
||||
and len(execs) == num_execs
|
||||
@@ -58,7 +75,7 @@ async def wait_execution(
|
||||
for i in range(timeout):
|
||||
if await is_execution_completed():
|
||||
return await AgentServer().get_run_execution_results(
|
||||
graph_id, graph_exec_id
|
||||
graph_id, graph_exec_id, user_id
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
@@ -96,10 +113,14 @@ def execute_block_test(block: Block):
|
||||
ex_output_name, ex_output_data = block.test_output[output_index]
|
||||
|
||||
def compare(data, expected_data):
|
||||
if isinstance(expected_data, type):
|
||||
if data == expected_data:
|
||||
is_matching = True
|
||||
elif isinstance(expected_data, type):
|
||||
is_matching = isinstance(data, expected_data)
|
||||
elif callable(expected_data):
|
||||
is_matching = expected_data(data)
|
||||
else:
|
||||
is_matching = data == expected_data
|
||||
is_matching = False
|
||||
|
||||
mark = "✅" if is_matching else "❌"
|
||||
log(f"{prefix} {mark} comparing `{data}` vs `{expected_data}`")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
266
rnd/autogpt_server/graph_templates/Discord Search Bot_v17.json
Normal file
266
rnd/autogpt_server/graph_templates/Discord Search Bot_v17.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_AgentGraph" (
|
||||
"id" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"name" TEXT,
|
||||
"description" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"isTemplate" BOOLEAN NOT NULL DEFAULT false,
|
||||
"userId" TEXT,
|
||||
"agentGraphParentId" TEXT,
|
||||
|
||||
PRIMARY KEY ("id", "version"),
|
||||
CONSTRAINT "AgentGraph_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "AgentGraph_agentGraphParentId_version_fkey" FOREIGN KEY ("agentGraphParentId", "version") REFERENCES "AgentGraph" ("id", "version") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_AgentGraph" ("agentGraphParentId", "description", "id", "isActive", "isTemplate", "name", "version") SELECT "agentGraphParentId", "description", "id", "isActive", "isTemplate", "name", "version" FROM "AgentGraph";
|
||||
DROP TABLE "AgentGraph";
|
||||
ALTER TABLE "new_AgentGraph" RENAME TO "AgentGraph";
|
||||
CREATE TABLE "new_AgentGraphExecution" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"agentGraphId" TEXT NOT NULL,
|
||||
"agentGraphVersion" INTEGER NOT NULL DEFAULT 1,
|
||||
"userId" TEXT,
|
||||
CONSTRAINT "AgentGraphExecution_agentGraphId_agentGraphVersion_fkey" FOREIGN KEY ("agentGraphId", "agentGraphVersion") REFERENCES "AgentGraph" ("id", "version") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "AgentGraphExecution_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_AgentGraphExecution" ("agentGraphId", "agentGraphVersion", "id") SELECT "agentGraphId", "agentGraphVersion", "id" FROM "AgentGraphExecution";
|
||||
DROP TABLE "AgentGraphExecution";
|
||||
ALTER TABLE "new_AgentGraphExecution" RENAME TO "AgentGraphExecution";
|
||||
CREATE TABLE "new_AgentGraphExecutionSchedule" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"agentGraphId" TEXT NOT NULL,
|
||||
"agentGraphVersion" INTEGER NOT NULL DEFAULT 1,
|
||||
"schedule" TEXT NOT NULL,
|
||||
"isEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"inputData" TEXT NOT NULL,
|
||||
"lastUpdated" DATETIME NOT NULL,
|
||||
"userId" TEXT,
|
||||
CONSTRAINT "AgentGraphExecutionSchedule_agentGraphId_agentGraphVersion_fkey" FOREIGN KEY ("agentGraphId", "agentGraphVersion") REFERENCES "AgentGraph" ("id", "version") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "AgentGraphExecutionSchedule_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_AgentGraphExecutionSchedule" ("agentGraphId", "agentGraphVersion", "id", "inputData", "isEnabled", "lastUpdated", "schedule") SELECT "agentGraphId", "agentGraphVersion", "id", "inputData", "isEnabled", "lastUpdated", "schedule" FROM "AgentGraphExecutionSchedule";
|
||||
DROP TABLE "AgentGraphExecutionSchedule";
|
||||
ALTER TABLE "new_AgentGraphExecutionSchedule" RENAME TO "AgentGraphExecutionSchedule";
|
||||
CREATE INDEX "AgentGraphExecutionSchedule_isEnabled_idx" ON "AgentGraphExecutionSchedule"("isEnabled");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
@@ -0,0 +1,5 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_id_idx" ON "User"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_email_idx" ON "User"("email");
|
||||
22
rnd/autogpt_server/poetry.lock
generated
22
rnd/autogpt_server/poetry.lock
generated
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "agpt"
|
||||
@@ -25,7 +25,7 @@ requests = "*"
|
||||
sentry-sdk = "^1.40.4"
|
||||
|
||||
[package.extras]
|
||||
benchmark = ["agbenchmark @ file:///home/bently/Desktop/autogpt-ui/AutoGPT/benchmark"]
|
||||
benchmark = ["agbenchmark"]
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
@@ -329,7 +329,7 @@ watchdog = "4.0.0"
|
||||
webdriver-manager = "^4.0.1"
|
||||
|
||||
[package.extras]
|
||||
benchmark = ["agbenchmark @ file:///home/bently/Desktop/autogpt-ui/AutoGPT/benchmark"]
|
||||
benchmark = ["agbenchmark"]
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
@@ -342,7 +342,7 @@ description = "Shared libraries across NextGen AutoGPT"
|
||||
optional = false
|
||||
python-versions = ">=3.10,<4.0"
|
||||
files = []
|
||||
develop = true
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
pyjwt = "^2.8.0"
|
||||
@@ -4212,19 +4212,19 @@ windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
description = "JSON Web Token implementation in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"},
|
||||
{file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"},
|
||||
{file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"},
|
||||
{file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
crypto = ["cryptography (>=3.4.0)"]
|
||||
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
|
||||
docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
|
||||
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
@@ -6419,4 +6419,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "9991857e7076d3bfcbae7af6c2cec54dc943167a3adceb5a0ebf74d80c05778f"
|
||||
content-hash = "003a4c89682abbf72c67631367f57e56d91d72b44f95e972b2326440199045e7"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "AgentGraph" ADD COLUMN "userId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "AgentGraphExecution" ADD COLUMN "userId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "AgentGraphExecutionSchedule" ADD COLUMN "userId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AgentGraph" ADD CONSTRAINT "AgentGraph_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AgentGraphExecution" ADD CONSTRAINT "AgentGraphExecution_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AgentGraphExecutionSchedule" ADD CONSTRAINT "AgentGraphExecutionSchedule_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_id_idx" ON "User"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_email_idx" ON "User"("email");
|
||||
@@ -10,6 +10,23 @@ generator client {
|
||||
interface = "asyncio"
|
||||
}
|
||||
|
||||
// User model to mirror Auth provider users
|
||||
model User {
|
||||
id String @id // This should match the Supabase user ID
|
||||
email String @unique
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
AgentGraphs AgentGraph[]
|
||||
AgentGraphExecutions AgentGraphExecution[]
|
||||
AgentGraphExecutionSchedules AgentGraphExecutionSchedule[]
|
||||
|
||||
@@index([id])
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
// This model describes the Agent Graph/Flow (Multi Agent System).
|
||||
model AgentGraph {
|
||||
id String @default(uuid())
|
||||
@@ -20,6 +37,10 @@ model AgentGraph {
|
||||
isActive Boolean @default(true)
|
||||
isTemplate Boolean @default(false)
|
||||
|
||||
// Link to User model
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
|
||||
AgentNodes AgentNode[]
|
||||
AgentGraphExecution AgentGraphExecution[]
|
||||
AgentGraphExecutionSchedule AgentGraphExecutionSchedule[]
|
||||
@@ -99,6 +120,10 @@ model AgentGraphExecution {
|
||||
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version])
|
||||
|
||||
AgentNodeExecutions AgentNodeExecution[]
|
||||
|
||||
// Link to User model
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
}
|
||||
|
||||
// This model describes the execution of an AgentNode.
|
||||
@@ -158,5 +183,9 @@ model AgentGraphExecutionSchedule {
|
||||
// default and set the value on each update, lastUpdated field has no time zone.
|
||||
lastUpdated DateTime @updatedAt
|
||||
|
||||
// Link to User model
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
|
||||
@@index([isEnabled])
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ python-dotenv = "^1.0.1"
|
||||
expiringdict = "^1.2.2"
|
||||
discord-py = "^2.4.0"
|
||||
|
||||
autogpt-libs = { path = "../autogpt_libs", develop = true }
|
||||
autogpt-libs = {path = "../autogpt_libs"}
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
cx-freeze = { git = "https://github.com/ntindle/cx_Freeze.git", rev = "main", develop = true }
|
||||
poethepoet = "^0.26.1"
|
||||
|
||||
@@ -9,6 +9,23 @@ generator client {
|
||||
interface = "asyncio"
|
||||
}
|
||||
|
||||
// User model to mirror Auth provider users
|
||||
model User {
|
||||
id String @id // This should match the Supabase user ID
|
||||
email String @unique
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
AgentGraphs AgentGraph[]
|
||||
AgentGraphExecutions AgentGraphExecution[]
|
||||
AgentGraphExecutionSchedules AgentGraphExecutionSchedule[]
|
||||
|
||||
@@index([id])
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
// This model describes the Agent Graph/Flow (Multi Agent System).
|
||||
model AgentGraph {
|
||||
id String @default(uuid())
|
||||
@@ -19,6 +36,10 @@ model AgentGraph {
|
||||
isActive Boolean @default(true)
|
||||
isTemplate Boolean @default(false)
|
||||
|
||||
// Link to User model
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
|
||||
AgentNodes AgentNode[]
|
||||
AgentGraphExecution AgentGraphExecution[]
|
||||
AgentGraphExecutionSchedule AgentGraphExecutionSchedule[]
|
||||
@@ -98,6 +119,10 @@ model AgentGraphExecution {
|
||||
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version])
|
||||
|
||||
AgentNodeExecutions AgentNodeExecution[]
|
||||
|
||||
// Link to User model
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
}
|
||||
|
||||
// This model describes the execution of an AgentNode.
|
||||
@@ -157,5 +182,9 @@ model AgentGraphExecutionSchedule {
|
||||
// default and set the value on each update, lastUpdated field has no time zone.
|
||||
lastUpdated DateTime @updatedAt
|
||||
|
||||
// Link to User model
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
|
||||
@@index([isEnabled])
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import pytest
|
||||
from prisma.models import User
|
||||
|
||||
from autogpt_server.blocks.basic import ObjectLookupBlock, ValueBlock
|
||||
from autogpt_server.blocks.maths import MathsBlock, Operation
|
||||
from autogpt_server.data import execution, graph
|
||||
from autogpt_server.executor import ExecutionManager
|
||||
from autogpt_server.server import AgentServer
|
||||
from autogpt_server.usecases.sample import create_test_graph
|
||||
from autogpt_server.usecases.sample import create_test_graph, create_test_user
|
||||
from autogpt_server.util.test import wait_execution
|
||||
|
||||
|
||||
@@ -13,24 +14,30 @@ async def execute_graph(
|
||||
agent_server: AgentServer,
|
||||
test_manager: ExecutionManager,
|
||||
test_graph: graph.Graph,
|
||||
test_user: User,
|
||||
input_data: dict,
|
||||
num_execs: int = 4,
|
||||
) -> str:
|
||||
# --- Test adding new executions --- #
|
||||
response = await agent_server.execute_graph(test_graph.id, input_data)
|
||||
response = await agent_server.execute_graph(test_graph.id, input_data, test_user.id)
|
||||
graph_exec_id = response["id"]
|
||||
|
||||
# Execution queue should be empty
|
||||
assert await wait_execution(test_manager, test_graph.id, graph_exec_id, num_execs)
|
||||
assert await wait_execution(
|
||||
test_manager, test_user.id, test_graph.id, graph_exec_id, num_execs
|
||||
)
|
||||
return graph_exec_id
|
||||
|
||||
|
||||
async def assert_sample_graph_executions(
|
||||
agent_server: AgentServer, test_graph: graph.Graph, graph_exec_id: str
|
||||
agent_server: AgentServer,
|
||||
test_graph: graph.Graph,
|
||||
test_user: User,
|
||||
graph_exec_id: str,
|
||||
):
|
||||
input = {"input_1": "Hello", "input_2": "World"}
|
||||
executions = await agent_server.get_run_execution_results(
|
||||
test_graph.id, graph_exec_id
|
||||
test_graph.id, graph_exec_id, test_user.id
|
||||
)
|
||||
|
||||
# Executing ValueBlock
|
||||
@@ -75,12 +82,20 @@ async def assert_sample_graph_executions(
|
||||
@pytest.mark.asyncio(scope="session")
|
||||
async def test_agent_execution(server):
|
||||
test_graph = create_test_graph()
|
||||
await graph.create_graph(test_graph)
|
||||
test_user = await create_test_user()
|
||||
await graph.create_graph(test_graph, user_id=test_user.id)
|
||||
data = {"input_1": "Hello", "input_2": "World"}
|
||||
graph_exec_id = await execute_graph(
|
||||
server.agent_server, server.exec_manager, test_graph, data, 4
|
||||
server.agent_server,
|
||||
server.exec_manager,
|
||||
test_graph,
|
||||
test_user,
|
||||
data,
|
||||
4,
|
||||
)
|
||||
await assert_sample_graph_executions(
|
||||
server.agent_server, test_graph, test_user, graph_exec_id
|
||||
)
|
||||
await assert_sample_graph_executions(server.agent_server, test_graph, graph_exec_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio(scope="session")
|
||||
@@ -130,14 +145,14 @@ async def test_input_pin_always_waited(server):
|
||||
nodes=nodes,
|
||||
links=links,
|
||||
)
|
||||
|
||||
test_graph = await graph.create_graph(test_graph)
|
||||
test_user = await create_test_user()
|
||||
test_graph = await graph.create_graph(test_graph, user_id=test_user.id)
|
||||
graph_exec_id = await execute_graph(
|
||||
server.agent_server, server.exec_manager, test_graph, {}, 3
|
||||
server.agent_server, server.exec_manager, test_graph, test_user, {}, 3
|
||||
)
|
||||
|
||||
executions = await server.agent_server.get_run_execution_results(
|
||||
test_graph.id, graph_exec_id
|
||||
test_graph.id, graph_exec_id, test_user.id
|
||||
)
|
||||
assert len(executions) == 3
|
||||
# ObjectLookupBlock should wait for the input pin to be provided,
|
||||
@@ -211,13 +226,13 @@ async def test_static_input_link_on_graph(server):
|
||||
nodes=nodes,
|
||||
links=links,
|
||||
)
|
||||
|
||||
test_graph = await graph.create_graph(test_graph)
|
||||
test_user = await create_test_user()
|
||||
test_graph = await graph.create_graph(test_graph, user_id=test_user.id)
|
||||
graph_exec_id = await execute_graph(
|
||||
server.agent_server, server.exec_manager, test_graph, {}, 8
|
||||
server.agent_server, server.exec_manager, test_graph, test_user, {}, 8
|
||||
)
|
||||
executions = await server.agent_server.get_run_execution_results(
|
||||
test_graph.id, graph_exec_id
|
||||
test_graph.id, graph_exec_id, test_user.id
|
||||
)
|
||||
assert len(executions) == 8
|
||||
# The last 3 executions will be a+b=4+5=9
|
||||
|
||||
@@ -2,32 +2,34 @@ import pytest
|
||||
|
||||
from autogpt_server.data import db, graph
|
||||
from autogpt_server.executor import ExecutionScheduler
|
||||
from autogpt_server.usecases.sample import create_test_graph
|
||||
from autogpt_server.usecases.sample import create_test_graph, create_test_user
|
||||
from autogpt_server.util.service import get_service_client
|
||||
|
||||
|
||||
@pytest.mark.asyncio(scope="session")
|
||||
async def test_agent_schedule(server):
|
||||
await db.connect()
|
||||
test_graph = await graph.create_graph(create_test_graph())
|
||||
test_user = await create_test_user()
|
||||
test_graph = await graph.create_graph(create_test_graph(), user_id=test_user.id)
|
||||
|
||||
scheduler = get_service_client(ExecutionScheduler)
|
||||
|
||||
schedules = scheduler.get_execution_schedules(test_graph.id)
|
||||
schedules = scheduler.get_execution_schedules(test_graph.id, test_user.id)
|
||||
assert len(schedules) == 0
|
||||
|
||||
schedule_id = scheduler.add_execution_schedule(
|
||||
graph_id=test_graph.id,
|
||||
user_id=test_user.id,
|
||||
graph_version=1,
|
||||
cron="0 0 * * *",
|
||||
input_data={"input": "data"},
|
||||
)
|
||||
assert schedule_id
|
||||
|
||||
schedules = scheduler.get_execution_schedules(test_graph.id)
|
||||
schedules = scheduler.get_execution_schedules(test_graph.id, test_user.id)
|
||||
assert len(schedules) == 1
|
||||
assert schedules[schedule_id] == "0 0 * * *"
|
||||
|
||||
scheduler.update_schedule(schedule_id, is_enabled=False)
|
||||
schedules = scheduler.get_execution_schedules(test_graph.id)
|
||||
scheduler.update_schedule(schedule_id, is_enabled=False, user_id=test_user.id)
|
||||
schedules = scheduler.get_execution_schedules(test_graph.id, user_id=test_user.id)
|
||||
assert len(schedules) == 0
|
||||
|
||||
23
rnd/infra/helm/autogpt_builder/.helmignore
Normal file
23
rnd/infra/helm/autogpt_builder/.helmignore
Normal 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/
|
||||
24
rnd/infra/helm/autogpt_builder/Chart.yaml
Normal file
24
rnd/infra/helm/autogpt_builder/Chart.yaml
Normal 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"
|
||||
22
rnd/infra/helm/autogpt_builder/templates/NOTES.txt
Normal file
22
rnd/infra/helm/autogpt_builder/templates/NOTES.txt
Normal 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 }}
|
||||
62
rnd/infra/helm/autogpt_builder/templates/_helpers.tpl
Normal file
62
rnd/infra/helm/autogpt_builder/templates/_helpers.tpl
Normal 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 }}
|
||||
68
rnd/infra/helm/autogpt_builder/templates/deployment.yaml
Normal file
68
rnd/infra/helm/autogpt_builder/templates/deployment.yaml
Normal 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 }}
|
||||
32
rnd/infra/helm/autogpt_builder/templates/hpa.yaml
Normal file
32
rnd/infra/helm/autogpt_builder/templates/hpa.yaml
Normal 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 }}
|
||||
61
rnd/infra/helm/autogpt_builder/templates/ingress.yaml
Normal file
61
rnd/infra/helm/autogpt_builder/templates/ingress.yaml
Normal 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 }}
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: networking.gke.io/v1
|
||||
kind: ManagedCertificate
|
||||
metadata:
|
||||
name: {{ include "autogpt-builder.fullname" . }}-cert
|
||||
spec:
|
||||
domains:
|
||||
- {{ .Values.domain }}
|
||||
15
rnd/infra/helm/autogpt_builder/templates/service.yaml
Normal file
15
rnd/infra/helm/autogpt_builder/templates/service.yaml
Normal 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 }}
|
||||
13
rnd/infra/helm/autogpt_builder/templates/serviceaccount.yaml
Normal file
13
rnd/infra/helm/autogpt_builder/templates/serviceaccount.yaml
Normal 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 }}
|
||||
@@ -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
|
||||
77
rnd/infra/helm/autogpt_builder/values.dev.yaml
Normal file
77
rnd/infra/helm/autogpt_builder/values.dev.yaml
Normal 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: ""
|
||||
76
rnd/infra/helm/autogpt_builder/values.yaml
Normal file
76
rnd/infra/helm/autogpt_builder/values.yaml
Normal 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: ""
|
||||
@@ -4,4 +4,7 @@ DB_PASS=pass123
|
||||
DB_NAME=marketplace
|
||||
DB_PORT=5432
|
||||
DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}
|
||||
SENTRY_DSN=Set correct url or dealete me
|
||||
SENTRY_DSN=Set correct url or dealete me
|
||||
|
||||
ENABLE_AUTH=true
|
||||
SUPABASE_JWT_SECRET=AAAAAAAA
|
||||
@@ -1,4 +1,5 @@
|
||||
import contextlib
|
||||
import logging.config
|
||||
import os
|
||||
|
||||
import dotenv
|
||||
@@ -12,12 +13,15 @@ import sentry_sdk.integrations.asyncio
|
||||
import sentry_sdk.integrations.fastapi
|
||||
import sentry_sdk.integrations.starlette
|
||||
|
||||
import market.config
|
||||
import market.routes.admin
|
||||
import market.routes.agents
|
||||
import market.routes.search
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
logging.config.dictConfig(market.config.LogConfig().model_dump())
|
||||
|
||||
if os.environ.get("SENTRY_DSN"):
|
||||
sentry_sdk.init(
|
||||
dsn=os.environ.get("SENTRY_DSN"),
|
||||
|
||||
30
rnd/market/market/config.py
Normal file
30
rnd/market/market/config.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class LogConfig(BaseModel):
|
||||
"""Logging configuration to be set for the server"""
|
||||
|
||||
LOGGER_NAME: str = "marketplace"
|
||||
LOG_FORMAT: str = "%(levelprefix)s | %(asctime)s | %(message)s"
|
||||
LOG_LEVEL: str = "DEBUG"
|
||||
|
||||
# Logging config
|
||||
version: int = 1
|
||||
disable_existing_loggers: bool = False
|
||||
formatters: dict = {
|
||||
"default": {
|
||||
"()": "uvicorn.logging.DefaultFormatter",
|
||||
"fmt": LOG_FORMAT,
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
}
|
||||
handlers: dict = {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stderr",
|
||||
},
|
||||
}
|
||||
loggers: dict = {
|
||||
LOGGER_NAME: {"handlers": ["default"], "level": LOG_LEVEL},
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
import fuzzywuzzy.fuzz
|
||||
import prisma.enums
|
||||
import prisma.errors
|
||||
import prisma.models
|
||||
import prisma.types
|
||||
@@ -61,6 +63,7 @@ async def create_agent_entry(
|
||||
keywords: typing.List[str],
|
||||
categories: typing.List[str],
|
||||
graph: prisma.Json,
|
||||
submission_state: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.PENDING,
|
||||
):
|
||||
"""
|
||||
Create a new agent entry in the database.
|
||||
@@ -89,6 +92,7 @@ async def create_agent_entry(
|
||||
"categories": categories,
|
||||
"graph": graph,
|
||||
"AnalyticsTracker": {"create": {"downloads": 0, "views": 0}},
|
||||
"submissionStatus": submission_state,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -100,6 +104,39 @@ async def create_agent_entry(
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def update_agent_entry(
|
||||
agent_id: str,
|
||||
version: int,
|
||||
submission_state: prisma.enums.SubmissionStatus,
|
||||
comments: str | None = None,
|
||||
):
|
||||
"""
|
||||
Update an existing agent entry in the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
version (int): The version of the agent.
|
||||
submission_state (prisma.enums.SubmissionStatus): The submission state of the agent.
|
||||
"""
|
||||
|
||||
try:
|
||||
agent = await prisma.models.Agents.prisma().update(
|
||||
where={"id": agent_id},
|
||||
data={
|
||||
"version": version,
|
||||
"submissionStatus": submission_state,
|
||||
"submissionReviewDate": datetime.datetime.now(datetime.timezone.utc),
|
||||
"submissionReviewComments": comments,
|
||||
},
|
||||
)
|
||||
|
||||
return agent
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Agent Update Failed Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_agents(
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
@@ -108,6 +145,7 @@ async def get_agents(
|
||||
category: str | None = None,
|
||||
description: str | None = None,
|
||||
description_threshold: int = 60,
|
||||
submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
|
||||
sort_by: str = "createdAt",
|
||||
sort_order: typing.Literal["desc"] | typing.Literal["asc"] = "desc",
|
||||
):
|
||||
@@ -140,6 +178,8 @@ async def get_agents(
|
||||
if category:
|
||||
query["categories"] = {"has": category}
|
||||
|
||||
query["submissionStatus"] = submission_status
|
||||
|
||||
# Define sorting
|
||||
order = {sort_by: sort_order}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
import prisma.enums
|
||||
import pydantic
|
||||
|
||||
|
||||
@@ -11,6 +12,13 @@ class AddAgentRequest(pydantic.BaseModel):
|
||||
categories: list[str]
|
||||
|
||||
|
||||
class SubmissionReviewRequest(pydantic.BaseModel):
|
||||
agent_id: str
|
||||
version: int
|
||||
status: prisma.enums.SubmissionStatus
|
||||
comments: str | None
|
||||
|
||||
|
||||
class AgentResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents a response from an agent.
|
||||
@@ -36,6 +44,7 @@ class AgentResponse(pydantic.BaseModel):
|
||||
version: int
|
||||
createdAt: datetime.datetime
|
||||
updatedAt: datetime.datetime
|
||||
submission_status: str
|
||||
views: int = 0
|
||||
downloads: int = 0
|
||||
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import logging
|
||||
import typing
|
||||
|
||||
import autogpt_libs.auth
|
||||
import fastapi
|
||||
import prisma
|
||||
import prisma.enums
|
||||
import prisma.models
|
||||
|
||||
import market.db
|
||||
import market.model
|
||||
|
||||
logger = logging.getLogger("marketplace")
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@router.post("/agent", response_model=market.model.AgentResponse)
|
||||
async def create_agent_entry(request: market.model.AddAgentRequest):
|
||||
async def create_agent_entry(
|
||||
request: market.model.AddAgentRequest,
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
):
|
||||
"""
|
||||
A basic endpoint to create a new agent entry in the database.
|
||||
|
||||
TODO: Protect endpoint!
|
||||
"""
|
||||
try:
|
||||
agent = await market.db.create_agent_entry(
|
||||
@@ -32,7 +44,13 @@ async def create_agent_entry(request: market.model.AddAgentRequest):
|
||||
|
||||
|
||||
@router.post("/agent/featured/{agent_id}")
|
||||
async def set_agent_featured(agent_id: str, category: str = "featured"):
|
||||
async def set_agent_featured(
|
||||
agent_id: str,
|
||||
category: str = "featured",
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
):
|
||||
"""
|
||||
A basic endpoint to set an agent as featured in the database.
|
||||
"""
|
||||
@@ -48,7 +66,13 @@ async def set_agent_featured(agent_id: str, category: str = "featured"):
|
||||
|
||||
|
||||
@router.delete("/agent/featured/{agent_id}")
|
||||
async def unset_agent_featured(agent_id: str, category: str = "featured"):
|
||||
async def unset_agent_featured(
|
||||
agent_id: str,
|
||||
category: str = "featured",
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
):
|
||||
"""
|
||||
A basic endpoint to unset an agent as featured in the database.
|
||||
"""
|
||||
@@ -61,3 +85,96 @@ async def unset_agent_featured(agent_id: str, category: str = "featured"):
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/agent/submissions", response_model=market.model.AgentListResponse)
|
||||
async def get_agent_submissions(
|
||||
page: int = fastapi.Query(1, ge=1, description="Page number"),
|
||||
page_size: int = fastapi.Query(
|
||||
10, ge=1, le=100, description="Number of items per page"
|
||||
),
|
||||
name: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Filter by agent name"
|
||||
),
|
||||
keyword: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Filter by keyword"
|
||||
),
|
||||
category: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Filter by category"
|
||||
),
|
||||
description: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Fuzzy search in description"
|
||||
),
|
||||
description_threshold: int = fastapi.Query(
|
||||
60, ge=0, le=100, description="Fuzzy search threshold"
|
||||
),
|
||||
sort_by: str = fastapi.Query("createdAt", description="Field to sort by"),
|
||||
sort_order: typing.Literal["asc", "desc"] = fastapi.Query(
|
||||
"desc", description="Sort order (asc or desc)"
|
||||
),
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
):
|
||||
logger.info("Getting agent submissions")
|
||||
try:
|
||||
result = await market.db.get_agents(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
name=name,
|
||||
keyword=keyword,
|
||||
category=category,
|
||||
description=description,
|
||||
description_threshold=description_threshold,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
submission_status=prisma.enums.SubmissionStatus.PENDING,
|
||||
)
|
||||
|
||||
agents = [
|
||||
market.model.AgentResponse(**agent.dict()) for agent in result["agents"]
|
||||
]
|
||||
|
||||
return market.model.AgentListResponse(
|
||||
agents=agents,
|
||||
total_count=result["total_count"],
|
||||
page=result["page"],
|
||||
page_size=result["page_size"],
|
||||
total_pages=result["total_pages"],
|
||||
)
|
||||
|
||||
except market.db.AgentQueryError as e:
|
||||
logger.error(f"Error getting agent submissions: {e}")
|
||||
raise fastapi.HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting agent submissions: {e}")
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/agent/submissions")
|
||||
async def review_submission(
|
||||
review_request: market.model.SubmissionReviewRequest,
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
):
|
||||
"""
|
||||
A basic endpoint to review a submission in the database.
|
||||
"""
|
||||
logger.info(
|
||||
f"Reviewing submission: {review_request.agent_id}, {review_request.version}"
|
||||
)
|
||||
try:
|
||||
# await market.db.update_agent_entry(
|
||||
# agent_id=review_request.agent_id,
|
||||
# version=review_request.version,
|
||||
# submission_state=review_request.status,
|
||||
# comments=review_request.comments,
|
||||
# )
|
||||
return fastapi.responses.Response(status_code=200)
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
76
rnd/market/market/routes/admin_tests.py
Normal file
76
rnd/market/market/routes/admin_tests.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import datetime
|
||||
from unittest import mock
|
||||
|
||||
import autogpt_libs.auth.middleware
|
||||
import fastapi
|
||||
import fastapi.testclient
|
||||
import prisma.enums
|
||||
import prisma.models
|
||||
|
||||
import market.app
|
||||
|
||||
client = fastapi.testclient.TestClient(market.app.app)
|
||||
|
||||
|
||||
async def override_auth_middleware(request: fastapi.Request):
|
||||
return {"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "admin"}
|
||||
|
||||
|
||||
market.app.app.dependency_overrides[autogpt_libs.auth.middleware.auth_middleware] = (
|
||||
override_auth_middleware
|
||||
)
|
||||
|
||||
|
||||
def test_get_submissions():
|
||||
with mock.patch("market.db.get_agents") as mock_get_agents:
|
||||
mock_get_agents.return_value = {
|
||||
"agents": [],
|
||||
"total_count": 0,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total_pages": 0,
|
||||
}
|
||||
response = client.get(
|
||||
"/api/v1/market/admin/agent/submissions?page=1&page_size=10&description_threshold=60&sort_by=createdAt&sort_order=desc",
|
||||
headers={"Bearer": ""},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"agents": [],
|
||||
"total_count": 0,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total_pages": 0,
|
||||
}
|
||||
|
||||
|
||||
def test_review_submission():
|
||||
with mock.patch("market.db.update_agent_entry") as mock_update_agent_entry:
|
||||
mock_update_agent_entry.return_value = prisma.models.Agents(
|
||||
id="aaa-bbb-ccc",
|
||||
version=1,
|
||||
createdAt=datetime.datetime.fromisoformat("2021-10-01T00:00:00+00:00"),
|
||||
updatedAt=datetime.datetime.fromisoformat("2021-10-01T00:00:00+00:00"),
|
||||
submissionStatus=prisma.enums.SubmissionStatus.APPROVED,
|
||||
submissionDate=datetime.datetime.fromisoformat("2021-10-01T00:00:00+00:00"),
|
||||
submissionReviewComments="Looks good",
|
||||
submissionReviewDate=datetime.datetime.fromisoformat(
|
||||
"2021-10-01T00:00:00+00:00"
|
||||
),
|
||||
keywords=["test"],
|
||||
categories=["test"],
|
||||
graph='{"name": "test", "description": "test"}', # type: ignore
|
||||
)
|
||||
response = client.post(
|
||||
"/api/v1/market/admin/agent/submissions",
|
||||
headers={
|
||||
"Authorization": "Bearer token"
|
||||
}, # Assuming you need an authorization token
|
||||
json={
|
||||
"agent_id": "aaa-bbb-ccc",
|
||||
"version": 1,
|
||||
"status": "APPROVED",
|
||||
"comments": "Looks good",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -248,6 +248,7 @@ async def top_agents_by_downloads(
|
||||
updatedAt=item.agent.updatedAt,
|
||||
views=item.views,
|
||||
downloads=item.downloads,
|
||||
submission_status=item.agent.submissionStatus,
|
||||
)
|
||||
for item in result.analytics
|
||||
if item.agent is not None
|
||||
@@ -323,6 +324,7 @@ async def get_featured_agents(
|
||||
and len(item.agent.AnalyticsTracker) > 0
|
||||
else 0
|
||||
),
|
||||
submission_status=item.agent.submissionStatus,
|
||||
)
|
||||
for item in result.featured_agents
|
||||
if item.agent is not None
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user