mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Merge branch 'dev' into swiftyos/open-1920-marketplace-home-components
This commit is contained in:
4
.github/workflows/platform-frontend-ci.yml
vendored
4
.github/workflows/platform-frontend-ci.yml
vendored
@@ -69,6 +69,10 @@ jobs:
|
||||
run: |
|
||||
cp ../supabase/docker/.env.example ../.env
|
||||
|
||||
- name: Copy backend .env
|
||||
run: |
|
||||
cp ../backend/.env.example ../backend/.env
|
||||
|
||||
- name: Run docker compose
|
||||
run: |
|
||||
docker compose -f ../docker-compose.yml up -d
|
||||
|
||||
@@ -63,7 +63,7 @@ class AIMusicGeneratorBlock(Block):
|
||||
placeholder="e.g., 'An upbeat electronic dance track with heavy bass'",
|
||||
title="Prompt",
|
||||
)
|
||||
model_version: MusicGenModelVersion = SchemaField(
|
||||
music_gen_model_version: MusicGenModelVersion = SchemaField(
|
||||
description="Model to use for generation",
|
||||
default=MusicGenModelVersion.STEREO_LARGE,
|
||||
title="Model Version",
|
||||
@@ -118,7 +118,7 @@ class AIMusicGeneratorBlock(Block):
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"prompt": "An upbeat electronic dance track with heavy bass",
|
||||
"model_version": MusicGenModelVersion.STEREO_LARGE,
|
||||
"music_gen_model_version": MusicGenModelVersion.STEREO_LARGE,
|
||||
"duration": 8,
|
||||
"temperature": 1.0,
|
||||
"top_k": 250,
|
||||
@@ -134,7 +134,7 @@ class AIMusicGeneratorBlock(Block):
|
||||
),
|
||||
],
|
||||
test_mock={
|
||||
"run_model": lambda api_key, model_version, prompt, duration, temperature, top_k, top_p, classifier_free_guidance, output_format, normalization_strategy: "https://replicate.com/output/generated-audio-url.wav",
|
||||
"run_model": lambda api_key, music_gen_model_version, prompt, duration, temperature, top_k, top_p, classifier_free_guidance, output_format, normalization_strategy: "https://replicate.com/output/generated-audio-url.wav",
|
||||
},
|
||||
test_credentials=TEST_CREDENTIALS,
|
||||
)
|
||||
@@ -153,7 +153,7 @@ class AIMusicGeneratorBlock(Block):
|
||||
)
|
||||
result = self.run_model(
|
||||
api_key=credentials.api_key,
|
||||
model_version=input_data.model_version,
|
||||
music_gen_model_version=input_data.music_gen_model_version,
|
||||
prompt=input_data.prompt,
|
||||
duration=input_data.duration,
|
||||
temperature=input_data.temperature,
|
||||
@@ -182,7 +182,7 @@ class AIMusicGeneratorBlock(Block):
|
||||
def run_model(
|
||||
self,
|
||||
api_key: SecretStr,
|
||||
model_version: MusicGenModelVersion,
|
||||
music_gen_model_version: MusicGenModelVersion,
|
||||
prompt: str,
|
||||
duration: int,
|
||||
temperature: float,
|
||||
@@ -200,7 +200,7 @@ class AIMusicGeneratorBlock(Block):
|
||||
"meta/musicgen:671ac645ce5e552cc63a54a2bbff63fcf798043055d2dac5fc9e36a837eedcfb",
|
||||
input={
|
||||
"prompt": prompt,
|
||||
"model_version": model_version,
|
||||
"music_gen_model_version": music_gen_model_version,
|
||||
"duration": duration,
|
||||
"temperature": temperature,
|
||||
"top_k": top_k,
|
||||
|
||||
@@ -547,16 +547,14 @@ async def fix_llm_provider_credentials():
|
||||
|
||||
broken_nodes = await prisma.get_client().query_raw(
|
||||
"""
|
||||
SELECT "User".id user_id,
|
||||
SELECT graph."userId" user_id,
|
||||
node.id node_id,
|
||||
node."constantInput" node_preset_input
|
||||
FROM platform."AgentNode" node
|
||||
LEFT JOIN platform."AgentGraph" graph
|
||||
ON node."agentGraphId" = graph.id
|
||||
LEFT JOIN platform."User" "User"
|
||||
ON graph."userId" = "User".id
|
||||
WHERE node."constantInput"::jsonb->'credentials'->>'provider' = 'llm'
|
||||
ORDER BY user_id;
|
||||
ORDER BY graph."userId";
|
||||
"""
|
||||
)
|
||||
logger.info(f"Fixing LLM credential inputs on {len(broken_nodes)} nodes")
|
||||
|
||||
@@ -95,9 +95,7 @@ class AgentServer(backend.util.service.AppProcess):
|
||||
async def test_execute_graph(
|
||||
graph_id: str, node_input: dict[typing.Any, typing.Any], user_id: str
|
||||
):
|
||||
return await backend.server.routers.v1.execute_graph(
|
||||
graph_id, node_input, user_id
|
||||
)
|
||||
return backend.server.routers.v1.execute_graph(graph_id, node_input, user_id)
|
||||
|
||||
@staticmethod
|
||||
async def test_create_graph(
|
||||
|
||||
@@ -275,7 +275,7 @@ async def set_graph_active_version(
|
||||
tags=["graphs"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def execute_graph(
|
||||
def execute_graph(
|
||||
graph_id: str,
|
||||
node_input: dict[Any, Any],
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
@@ -480,7 +480,7 @@ async def create_schedule(
|
||||
tags=["schedules"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def delete_schedule(
|
||||
def delete_schedule(
|
||||
schedule_id: str,
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
) -> dict[Any, Any]:
|
||||
@@ -493,7 +493,7 @@ async def delete_schedule(
|
||||
tags=["schedules"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def get_execution_schedules(
|
||||
def get_execution_schedules(
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
graph_id: str | None = None,
|
||||
) -> list[scheduler.JobInfo]:
|
||||
|
||||
@@ -9,6 +9,7 @@ from backend.util.settings import Config
|
||||
|
||||
# List of IP networks to block
|
||||
BLOCKED_IP_NETWORKS = [
|
||||
# --8<-- [start:BLOCKED_IP_NETWORKS]
|
||||
ipaddress.ip_network("0.0.0.0/8"), # "This" Network
|
||||
ipaddress.ip_network("10.0.0.0/8"), # Private-Use
|
||||
ipaddress.ip_network("127.0.0.0/8"), # Loopback
|
||||
@@ -17,6 +18,7 @@ BLOCKED_IP_NETWORKS = [
|
||||
ipaddress.ip_network("192.168.0.0/16"), # Private-Use
|
||||
ipaddress.ip_network("224.0.0.0/4"), # Multicast
|
||||
ipaddress.ip_network("240.0.0.0/4"), # Reserved for Future Use
|
||||
# --8<-- [end:BLOCKED_IP_NETWORKS]
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "^0.5.5",
|
||||
"@faker-js/faker": "^9.2.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@next/third-parties": "^15.0.3",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
@@ -32,17 +33,17 @@
|
||||
"@radix-ui/react-context-menu": "^2.2.1",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-icons": "^1.3.1",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-toast": "^1.2.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@sentry/nextjs": "^8",
|
||||
"@supabase/ssr": "^0.5.2",
|
||||
"@supabase/supabase-js": "^2.46.1",
|
||||
@@ -60,13 +61,13 @@
|
||||
"framer-motion": "^11.11.9",
|
||||
"geist": "^1.3.1",
|
||||
"elliptic": "6.6.0",
|
||||
"lucide-react": "^0.456.0",
|
||||
"lucide-react": "^0.460.0",
|
||||
"moment": "^2.30.1",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "^14.2.13",
|
||||
"next-themes": "^0.4.3",
|
||||
"react": "^18",
|
||||
"react-day-picker": "^9.3.0",
|
||||
"react-day-picker": "^9.3.2",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-icons": "^5.3.0",
|
||||
|
||||
@@ -4,10 +4,10 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// import path from 'path';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
dotenv.config({ path: path.resolve(__dirname, ".env") });
|
||||
dotenv.config({ path: path.resolve(__dirname, "../backend/.env") });
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
|
||||
@@ -89,7 +89,6 @@ const RunnerUIWrapper = forwardRef<RunnerUIWrapperRef, RunnerUIWrapperProps>(
|
||||
const outputs = outputBlocks.map((node) => ({
|
||||
id: node.id,
|
||||
type: "output" as const,
|
||||
outputSchema: node.data.outputSchema as BlockIORootSchema,
|
||||
hardcodedValues: {
|
||||
name: (node.data.hardcodedValues as any).name || "Output",
|
||||
description:
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Block, BlockUIType } from "@/lib/autogpt-server-api";
|
||||
import { Block, BlockUIType, SpecialBlockID } from "@/lib/autogpt-server-api";
|
||||
import { MagnifyingGlassIcon, PlusIcon } from "@radix-ui/react-icons";
|
||||
import { IconToyBrick } from "@/components/ui/icons";
|
||||
import { getPrimaryCategoryColor } from "@/lib/utils";
|
||||
@@ -57,7 +57,7 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
|
||||
const agentList = flows.map(
|
||||
(flow) =>
|
||||
({
|
||||
id: "e189baac-8c20-45a1-94a7-55177ea42565", // TODO: fetch this programmatically.
|
||||
id: SpecialBlockID.AGENT,
|
||||
name: flow.name,
|
||||
description:
|
||||
`Ver.${flow.version}` +
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import React, { useCallback } from "react";
|
||||
import AutoGPTServerAPI, { GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import AutoGPTServerAPI, {
|
||||
BlockIORootSchema,
|
||||
Graph,
|
||||
GraphMeta,
|
||||
NodeExecutionResult,
|
||||
SpecialBlockID,
|
||||
} 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 { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { IconSquare } from "@/components/ui/icons";
|
||||
import { Pencil2Icon } from "@radix-ui/react-icons";
|
||||
import { ExitIcon, Pencil2Icon } from "@radix-ui/react-icons";
|
||||
import moment from "moment/moment";
|
||||
import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge";
|
||||
import RunnerOutputUI, { BlockOutput } from "../runner-ui/RunnerOutputUI";
|
||||
|
||||
export const FlowRunInfo: React.FC<
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
@@ -15,6 +22,66 @@ export const FlowRunInfo: React.FC<
|
||||
flowRun: FlowRun;
|
||||
}
|
||||
> = ({ flow, flowRun, ...props }) => {
|
||||
const [isOutputOpen, setIsOutputOpen] = useState(false);
|
||||
const [blockOutputs, setBlockOutputs] = useState<BlockOutput[]>([]);
|
||||
const api = useMemo(() => new AutoGPTServerAPI(), []);
|
||||
|
||||
const fetchBlockResults = useCallback(async () => {
|
||||
const executionResults = await api.getGraphExecutionInfo(
|
||||
flow.id,
|
||||
flowRun.id,
|
||||
);
|
||||
|
||||
// Create a map of the latest COMPLETED execution results of output nodes by node_id
|
||||
const latestCompletedResults = executionResults
|
||||
.filter(
|
||||
(result) =>
|
||||
result.status === "COMPLETED" &&
|
||||
result.block_id === SpecialBlockID.OUTPUT,
|
||||
)
|
||||
.reduce((acc, result) => {
|
||||
const existing = acc.get(result.node_id);
|
||||
|
||||
// Compare dates if there's an existing result
|
||||
if (existing) {
|
||||
const existingDate = existing.end_time || existing.add_time;
|
||||
const currentDate = result.end_time || result.add_time;
|
||||
|
||||
if (currentDate > existingDate) {
|
||||
acc.set(result.node_id, result);
|
||||
}
|
||||
} else {
|
||||
acc.set(result.node_id, result);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Map<string, NodeExecutionResult>());
|
||||
|
||||
// Transform results to BlockOutput format
|
||||
setBlockOutputs(
|
||||
Array.from(latestCompletedResults.values()).map((result) => ({
|
||||
id: result.node_id,
|
||||
type: "output" as const,
|
||||
hardcodedValues: {
|
||||
name: result.input_data.name || "Output",
|
||||
description: result.input_data.description || "Output from the agent",
|
||||
value: result.input_data.value,
|
||||
},
|
||||
// Change this line to extract the array directly
|
||||
result: result.output_data?.output || undefined,
|
||||
})),
|
||||
);
|
||||
}, [api, flow.id, flow.version, flowRun.id]);
|
||||
|
||||
// Fetch graph and execution data
|
||||
useEffect(() => {
|
||||
if (!isOutputOpen || blockOutputs.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchBlockResults();
|
||||
}, [isOutputOpen, blockOutputs]);
|
||||
|
||||
if (flowRun.graphID != flow.id) {
|
||||
throw new Error(
|
||||
`FlowRunInfo can't be used with non-matching flowRun.flowID and flow.id`,
|
||||
@@ -22,58 +89,67 @@ export const FlowRunInfo: React.FC<
|
||||
}
|
||||
|
||||
const handleStopRun = useCallback(() => {
|
||||
const api = new AutoGPTServerAPI();
|
||||
api.stopGraphExecution(flow.id, flowRun.id);
|
||||
}, [flow.id, flowRun.id]);
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader className="flex-row items-center justify-between space-x-3 space-y-0">
|
||||
<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>
|
||||
<div className="flex space-x-2">
|
||||
{flowRun.status === "running" && (
|
||||
<Button onClick={handleStopRun} variant="destructive">
|
||||
<IconSquare className="mr-2" /> Stop Run
|
||||
<>
|
||||
<Card {...props}>
|
||||
<CardHeader className="flex-row items-center justify-between space-x-3 space-y-0">
|
||||
<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>
|
||||
<div className="flex space-x-2">
|
||||
{flowRun.status === "running" && (
|
||||
<Button onClick={handleStopRun} variant="destructive">
|
||||
<IconSquare className="mr-2" /> Stop Run
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => setIsOutputOpen(true)} variant="outline">
|
||||
<ExitIcon className="mr-2" /> View Outputs
|
||||
</Button>
|
||||
)}
|
||||
<Link
|
||||
className={buttonVariants({ variant: "default" })}
|
||||
href={`/build?flowID=${flow.id}`}
|
||||
>
|
||||
<Pencil2Icon className="mr-2" /> Open in Builder
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
<strong>Status:</strong>{" "}
|
||||
<FlowRunStatusBadge status={flowRun.status} />
|
||||
</div>
|
||||
<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>
|
||||
<Link
|
||||
className={buttonVariants({ variant: "default" })}
|
||||
href={`/build?flowID=${flow.id}`}
|
||||
>
|
||||
<Pencil2Icon className="mr-2" /> Open in Builder
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
<strong>Status:</strong>{" "}
|
||||
<FlowRunStatusBadge status={flowRun.status} />
|
||||
</div>
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<RunnerOutputUI
|
||||
isOpen={isOutputOpen}
|
||||
onClose={() => setIsOutputOpen(false)}
|
||||
blockOutputs={blockOutputs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlowRunInfo;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -10,10 +10,12 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { BlockIORootSchema } from "@/lib/autogpt-server-api/types";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Clipboard } from "lucide-react";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
interface BlockOutput {
|
||||
export interface BlockOutput {
|
||||
id: string;
|
||||
outputSchema: BlockIORootSchema;
|
||||
hardcodedValues: {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -30,11 +32,20 @@ interface OutputModalProps {
|
||||
const formatOutput = (output: any): string => {
|
||||
if (typeof output === "object") {
|
||||
try {
|
||||
if (
|
||||
Array.isArray(output) &&
|
||||
output.every((item) => typeof item === "string")
|
||||
) {
|
||||
return output.join("\n").replace(/\\n/g, "\n");
|
||||
}
|
||||
return JSON.stringify(output, null, 2);
|
||||
} catch (error) {
|
||||
return `Error formatting output: ${(error as Error).message}`;
|
||||
}
|
||||
}
|
||||
if (typeof output === "string") {
|
||||
return output.replace(/\\n/g, "\n");
|
||||
}
|
||||
return String(output);
|
||||
};
|
||||
|
||||
@@ -43,11 +54,28 @@ export function RunnerOutputUI({
|
||||
onClose,
|
||||
blockOutputs,
|
||||
}: OutputModalProps) {
|
||||
const { toast } = useToast();
|
||||
|
||||
const copyOutput = (name: string, output: any) => {
|
||||
const formattedOutput = formatOutput(output);
|
||||
navigator.clipboard.writeText(formattedOutput).then(() => {
|
||||
toast({
|
||||
title: `"${name}" output copied to clipboard!`,
|
||||
duration: 2000,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const adjustTextareaHeight = (textarea: HTMLTextAreaElement) => {
|
||||
textarea.style.height = "auto";
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={onClose}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="flex h-full w-full flex-col overflow-hidden sm:max-w-[500px]"
|
||||
className="flex h-full w-full flex-col overflow-hidden sm:max-w-[600px]"
|
||||
>
|
||||
<SheetHeader className="px-2 py-2">
|
||||
<SheetTitle className="text-xl">Run Outputs</SheetTitle>
|
||||
@@ -71,11 +99,38 @@ export function RunnerOutputUI({
|
||||
</Label>
|
||||
)}
|
||||
|
||||
<div className="rounded-md bg-gray-100 p-2">
|
||||
<div className="group relative rounded-md bg-gray-100 p-2">
|
||||
<Button
|
||||
className="absolute right-1 top-1 z-10 m-1 hidden p-2 group-hover:block"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
copyOutput(
|
||||
block.hardcodedValues.name || "Unnamed Output",
|
||||
block.result,
|
||||
)
|
||||
}
|
||||
title="Copy Output"
|
||||
>
|
||||
<Clipboard size={18} />
|
||||
</Button>
|
||||
<Textarea
|
||||
readOnly
|
||||
value={formatOutput(block.result ?? "No output yet")}
|
||||
className="resize-none whitespace-pre-wrap break-words border-none bg-transparent text-sm"
|
||||
className="w-full resize-none whitespace-pre-wrap break-words border-none bg-transparent text-sm"
|
||||
style={{
|
||||
height: "auto",
|
||||
minHeight: "2.5rem",
|
||||
maxHeight: "400px",
|
||||
}}
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
adjustTextareaHeight(el);
|
||||
if (el.scrollHeight > 400) {
|
||||
el.style.height = "400px";
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -241,11 +241,12 @@ export type GraphExecuteResponse = {
|
||||
|
||||
/* Mirror of backend/data/execution.py:ExecutionResult */
|
||||
export type NodeExecutionResult = {
|
||||
graph_exec_id: string;
|
||||
node_exec_id: string;
|
||||
graph_id: string;
|
||||
graph_version: number;
|
||||
graph_exec_id: string;
|
||||
node_exec_id: string;
|
||||
node_id: string;
|
||||
block_id: string;
|
||||
status: "INCOMPLETE" | "QUEUED" | "RUNNING" | "COMPLETED" | "FAILED";
|
||||
input_data: { [key: string]: any };
|
||||
output_data: { [key: string]: Array<any> };
|
||||
@@ -319,6 +320,12 @@ export enum BlockUIType {
|
||||
AGENT = "Agent",
|
||||
}
|
||||
|
||||
export enum SpecialBlockID {
|
||||
AGENT = "e189baac-8c20-45a1-94a7-55177ea42565",
|
||||
INPUT = "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b",
|
||||
OUTPUT = "363ae599-353e-4804-937e-b2ee3cef3da4",
|
||||
}
|
||||
|
||||
export type AnalyticsMetrics = {
|
||||
metric_name: string;
|
||||
metric_value: number;
|
||||
|
||||
46
autogpt_platform/frontend/src/tests/auth.spec.ts
Normal file
46
autogpt_platform/frontend/src/tests/auth.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
|
||||
test.describe("Authentication", () => {
|
||||
test("user can login successfully", async ({ page, loginPage, testUser }) => {
|
||||
await page.goto("/login"); // Make sure we're on the login page
|
||||
await loginPage.login(testUser.email, testUser.password);
|
||||
// expect to be redirected to the home page
|
||||
await expect(page).toHaveURL("/");
|
||||
// expect to see the Monitor text
|
||||
await expect(page.getByText("Monitor")).toBeVisible();
|
||||
});
|
||||
|
||||
test("user can logout successfully", async ({
|
||||
page,
|
||||
loginPage,
|
||||
testUser,
|
||||
}) => {
|
||||
await page.goto("/login"); // Make sure we're on the login page
|
||||
await loginPage.login(testUser.email, testUser.password);
|
||||
|
||||
// Expect to be on the home page
|
||||
await expect(page).toHaveURL("/");
|
||||
// Click on the user menu
|
||||
await page.getByRole("button", { name: "CN" }).click();
|
||||
// Click on the logout menu item
|
||||
await page.getByRole("menuitem", { name: "Log out" }).click();
|
||||
// Expect to be redirected to the login page
|
||||
await expect(page).toHaveURL("/login");
|
||||
});
|
||||
|
||||
test("login in, then out, then in again", async ({
|
||||
page,
|
||||
loginPage,
|
||||
testUser,
|
||||
}) => {
|
||||
await page.goto("/login"); // Make sure we're on the login page
|
||||
await loginPage.login(testUser.email, testUser.password);
|
||||
await page.goto("/");
|
||||
await page.getByRole("button", { name: "CN" }).click();
|
||||
await page.getByRole("menuitem", { name: "Log out" }).click();
|
||||
await expect(page).toHaveURL("/login");
|
||||
await loginPage.login(testUser.email, testUser.password);
|
||||
await expect(page).toHaveURL("/");
|
||||
await expect(page.getByText("Monitor")).toBeVisible();
|
||||
});
|
||||
});
|
||||
18
autogpt_platform/frontend/src/tests/fixtures/index.ts
vendored
Normal file
18
autogpt_platform/frontend/src/tests/fixtures/index.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test as base } from "@playwright/test";
|
||||
import { createTestUserFixture } from "./test-user.fixture";
|
||||
import { createLoginPageFixture } from "./login-page.fixture";
|
||||
import type { TestUser } from "./test-user.fixture";
|
||||
import { LoginPage } from "../pages/login.page";
|
||||
|
||||
type Fixtures = {
|
||||
testUser: TestUser;
|
||||
loginPage: LoginPage;
|
||||
};
|
||||
|
||||
// Combine fixtures
|
||||
export const test = base.extend<Fixtures>({
|
||||
testUser: createTestUserFixture,
|
||||
loginPage: createLoginPageFixture,
|
||||
});
|
||||
|
||||
export { expect } from "@playwright/test";
|
||||
14
autogpt_platform/frontend/src/tests/fixtures/login-page.fixture.ts
vendored
Normal file
14
autogpt_platform/frontend/src/tests/fixtures/login-page.fixture.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { test as base } from "@playwright/test";
|
||||
import { LoginPage } from "../pages/login.page";
|
||||
|
||||
export const loginPageFixture = base.extend<{ loginPage: LoginPage }>({
|
||||
loginPage: async ({ page }, use) => {
|
||||
await use(new LoginPage(page));
|
||||
},
|
||||
});
|
||||
|
||||
// Export just the fixture function
|
||||
export const createLoginPageFixture = async ({ page }, use) => {
|
||||
await use(new LoginPage(page));
|
||||
};
|
||||
83
autogpt_platform/frontend/src/tests/fixtures/test-user.fixture.ts
vendored
Normal file
83
autogpt_platform/frontend/src/tests/fixtures/test-user.fixture.ts
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export type TestUser = {
|
||||
email: string;
|
||||
password: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
let supabase: SupabaseClient;
|
||||
|
||||
function getSupabaseAdmin() {
|
||||
if (!supabase) {
|
||||
supabase = createClient(
|
||||
process.env.SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
||||
{
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
return supabase;
|
||||
}
|
||||
|
||||
async function createTestUser(userData: TestUser): Promise<TestUser> {
|
||||
const supabase = getSupabaseAdmin();
|
||||
|
||||
const { data: authUser, error: authError } = await supabase.auth.signUp({
|
||||
email: userData.email,
|
||||
password: userData.password,
|
||||
});
|
||||
|
||||
if (authError) {
|
||||
throw new Error(`Failed to create test user: ${authError.message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
...userData,
|
||||
id: authUser.user?.id,
|
||||
};
|
||||
}
|
||||
|
||||
async function deleteTestUser(userId: string) {
|
||||
const supabase = getSupabaseAdmin();
|
||||
|
||||
try {
|
||||
const { error } = await supabase.auth.admin.deleteUser(userId);
|
||||
|
||||
if (error) {
|
||||
console.warn(`Warning: Failed to delete test user: ${error.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Warning: Error during user cleanup: ${(error as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function generateUserData(): TestUser {
|
||||
return {
|
||||
email: `test.${faker.string.uuid()}@example.com`,
|
||||
password: faker.internet.password({ length: 12 }),
|
||||
};
|
||||
}
|
||||
|
||||
// Export just the fixture function
|
||||
export const createTestUserFixture = async ({}, use) => {
|
||||
let user: TestUser | null = null;
|
||||
|
||||
try {
|
||||
const userData = generateUserData();
|
||||
user = await createTestUser(userData);
|
||||
await use(user);
|
||||
} finally {
|
||||
if (user?.id) {
|
||||
await deleteTestUser(user.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
51
autogpt_platform/frontend/src/tests/pages/login.page.ts
Normal file
51
autogpt_platform/frontend/src/tests/pages/login.page.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
export class LoginPage {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async login(email: string, password: string) {
|
||||
console.log("Attempting login with:", { email, password }); // Debug log
|
||||
|
||||
// Fill email
|
||||
const emailInput = this.page.getByPlaceholder("user@email.com");
|
||||
await emailInput.waitFor({ state: "visible" });
|
||||
await emailInput.fill(email);
|
||||
|
||||
// Fill password
|
||||
const passwordInput = this.page.getByPlaceholder("password");
|
||||
await passwordInput.waitFor({ state: "visible" });
|
||||
await passwordInput.fill(password);
|
||||
|
||||
// Check terms
|
||||
const termsCheckbox = this.page.getByLabel("I agree to the Terms of Use");
|
||||
await termsCheckbox.waitFor({ state: "visible" });
|
||||
await termsCheckbox.click();
|
||||
|
||||
// TODO: This is a workaround to wait for the page to load after filling the email and password
|
||||
const emailInput2 = this.page.getByPlaceholder("user@email.com");
|
||||
await emailInput2.waitFor({ state: "visible" });
|
||||
await emailInput2.fill(email);
|
||||
|
||||
// Fill password
|
||||
const passwordInput2 = this.page.getByPlaceholder("password");
|
||||
await passwordInput2.waitFor({ state: "visible" });
|
||||
await passwordInput2.fill(password);
|
||||
|
||||
// Wait for the button to be ready
|
||||
const loginButton = this.page.getByRole("button", { name: "Log in" });
|
||||
await loginButton.waitFor({ state: "visible" });
|
||||
|
||||
// Start waiting for navigation before clicking
|
||||
const navigationPromise = this.page.waitForURL("/", { timeout: 60000 });
|
||||
|
||||
console.log("About to click login button"); // Debug log
|
||||
await loginButton.click();
|
||||
|
||||
console.log("Waiting for navigation"); // Debug log
|
||||
await navigationPromise;
|
||||
|
||||
console.log("Navigation complete, waiting for network idle"); // Debug log
|
||||
await this.page.waitForLoadState("networkidle", { timeout: 60000 });
|
||||
console.log("Login process complete"); // Debug log
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { test, expect } from "./fixtures";
|
||||
|
||||
test("has title", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { test, expect } from "./fixtures";
|
||||
import { setNestedProperty } from "../lib/utils";
|
||||
|
||||
const testCases = [
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export function generateUser() {
|
||||
return {
|
||||
email: faker.internet.email(),
|
||||
password: faker.internet.password(),
|
||||
name: faker.person.fullName(),
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
8
autogpt_platform/market/poetry.lock
generated
8
autogpt_platform/market/poetry.lock
generated
@@ -247,13 +247,13 @@ test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.4"
|
||||
version = "0.115.5"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "fastapi-0.115.4-py3-none-any.whl", hash = "sha256:0b504a063ffb3cf96a5e27dc1bc32c80ca743a2528574f9cdc77daa2d31b4742"},
|
||||
{file = "fastapi-0.115.4.tar.gz", hash = "sha256:db653475586b091cb8b2fec2ac54a680ac6a158e07406e1abae31679e8826349"},
|
||||
{file = "fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796"},
|
||||
{file = "fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1298,4 +1298,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "4f7e4bbed535e40688e953dd0bfab80f704ba6017fc01360941f5cf858ef5f4c"
|
||||
content-hash = "985f87e9d6e2b7232f880a476c69c626bc4227156d8a57d8f1867236b215f82f"
|
||||
|
||||
@@ -13,7 +13,7 @@ python = "^3.10"
|
||||
prisma = "^0.15.0"
|
||||
python-dotenv = "^1.0.1"
|
||||
uvicorn = "^0.32.0"
|
||||
fastapi = "^0.115.4"
|
||||
fastapi = "^0.115.5"
|
||||
sentry-sdk = { extras = ["fastapi"], version = "^2.18.0" }
|
||||
fuzzywuzzy = "^0.18.0"
|
||||
python-levenshtein = "^0.26.1"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# To boot the app run the following:
|
||||
# docker-compose run auto-gpt
|
||||
# docker compose run auto-gpt
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
|
||||
@@ -14,7 +14,7 @@ To print out debug logs:
|
||||
|
||||
.\autogpt.bat --debug # on Windows
|
||||
|
||||
docker-compose run --rm auto-gpt --debug # in Docker
|
||||
docker compose run --rm auto-gpt --debug # in Docker
|
||||
```
|
||||
|
||||
## Inspect and share logs
|
||||
|
||||
@@ -56,7 +56,7 @@ You can check if you have Docker installed by running the following command:
|
||||
|
||||
```bash
|
||||
docker -v
|
||||
docker-compose -v
|
||||
docker compose -v
|
||||
```
|
||||
|
||||
Once you have Docker and Docker Compose installed, you can proceed to the next step.
|
||||
|
||||
@@ -337,15 +337,73 @@ For the WikipediaSummaryBlock:
|
||||
|
||||
This approach allows us to test the block's logic comprehensively without relying on external services, while also accommodating non-deterministic outputs.
|
||||
|
||||
## Security Best Practices for SSRF Prevention
|
||||
|
||||
When creating blocks that handle external URL inputs or make network requests, it's crucial to use the platform's built-in SSRF protection mechanisms. The `backend.util.request` module provides a secure `Requests` wrapper class that should be used for all HTTP requests.
|
||||
|
||||
### Using the Secure Requests Wrapper
|
||||
|
||||
```python
|
||||
from backend.util.request import requests
|
||||
|
||||
class MyNetworkBlock(Block):
|
||||
def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
||||
try:
|
||||
# The requests wrapper automatically validates URLs and blocks dangerous requests
|
||||
response = requests.get(input_data.url)
|
||||
yield "result", response.text
|
||||
except ValueError as e:
|
||||
# URL validation failed
|
||||
raise RuntimeError(f"Invalid URL provided: {e}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
# Request failed
|
||||
raise RuntimeError(f"Request failed: {e}")
|
||||
```
|
||||
|
||||
The `Requests` wrapper provides these security features:
|
||||
|
||||
1. **URL Validation**:
|
||||
- Blocks requests to private IP ranges (RFC 1918)
|
||||
- Validates URL format and protocol
|
||||
- Resolves DNS and checks IP addresses
|
||||
- Supports whitelisting trusted origins
|
||||
|
||||
2. **Secure Defaults**:
|
||||
- Disables redirects by default
|
||||
- Raises exceptions for non-200 status codes
|
||||
- Supports custom headers and validators
|
||||
|
||||
3. **Protected IP Ranges**:
|
||||
The wrapper denies requests to these networks:
|
||||
|
||||
```python title="backend/util/request.py"
|
||||
--8<-- "autogpt_platform/backend/backend/util/request.py:BLOCKED_IP_NETWORKS"
|
||||
```
|
||||
|
||||
### Custom Request Configuration
|
||||
|
||||
If you need to customize the request behavior:
|
||||
|
||||
```python
|
||||
from backend.util.request import Requests
|
||||
|
||||
# Create a custom requests instance with specific trusted origins
|
||||
custom_requests = Requests(
|
||||
trusted_origins=["api.trusted-service.com"],
|
||||
raise_for_status=True,
|
||||
extra_headers={"User-Agent": "MyBlock/1.0"}
|
||||
)
|
||||
```
|
||||
|
||||
## Tips for Effective Block Testing
|
||||
|
||||
1. **Provide realistic test_input**: Ensure your test input covers typical use cases.
|
||||
|
||||
2. **Define appropriate test_output**:
|
||||
|
||||
- For deterministic outputs, use specific expected values.
|
||||
- For non-deterministic outputs or when only the type matters, use Python types (e.g., `str`, `int`, `dict`).
|
||||
- You can mix specific values and types, e.g., `("key1", str), ("key2", 42)`.
|
||||
- For deterministic outputs, use specific expected values.
|
||||
- For non-deterministic outputs or when only the type matters, use Python types (e.g., `str`, `int`, `dict`).
|
||||
- You can mix specific values and types, e.g., `("key1", str), ("key2", 42)`.
|
||||
|
||||
3. **Use test_mock for network calls**: This prevents tests from failing due to network issues or API changes.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user