From 7ec9830b025755007da2d94cd52b796ea4caf621 Mon Sep 17 00:00:00 2001 From: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:14:35 +0530 Subject: [PATCH 1/3] fix(platform): Add custom fonts and update layout styles (#9195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - resolve #9187 ### Changes 🏗️ Add support for `Inter`, `Poppins`, `Geist-Mono`, `Geist-Neue`, and `Neue` in `layout.tsx` and `tailwind.config.ts`. Screenshot 2025-01-06 at 10 59 35 AM --------- Co-authored-by: Aarushi <50577581+aarushik93@users.noreply.github.com> --- autogpt_platform/frontend/src/app/layout.tsx | 17 ++++++++++++++--- .../frontend/src/components/TallyPopup.tsx | 2 +- autogpt_platform/frontend/tailwind.config.ts | 2 ++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/autogpt_platform/frontend/src/app/layout.tsx b/autogpt_platform/frontend/src/app/layout.tsx index 944e2301d3..60efbde136 100644 --- a/autogpt_platform/frontend/src/app/layout.tsx +++ b/autogpt_platform/frontend/src/app/layout.tsx @@ -1,6 +1,6 @@ import React from "react"; import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import { Inter, Poppins } from "next/font/google"; import { Providers } from "@/app/providers"; import { cn } from "@/lib/utils"; import { Navbar } from "@/components/agptui/Navbar"; @@ -10,8 +10,16 @@ import TallyPopupSimple from "@/components/TallyPopup"; import { GoogleAnalytics } from "@next/third-parties/google"; import { Toaster } from "@/components/ui/toaster"; import { IconType } from "@/components/ui/icons"; +import { GeistSans } from "geist/font/sans"; +import { GeistMono } from "geist/font/mono"; -const inter = Inter({ subsets: ["latin"] }); +// Fonts +const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); +const poppins = Poppins({ + subsets: ["latin"], + weight: ["400", "500", "600", "700"], + variable: "--font-poppins", +}); export const metadata: Metadata = { title: "NextGen AutoGPT", @@ -24,7 +32,10 @@ export default async function RootLayout({ children: React.ReactNode; }>) { return ( - + { diff --git a/autogpt_platform/frontend/tailwind.config.ts b/autogpt_platform/frontend/tailwind.config.ts index 3101cd7cc5..d2616f98a4 100644 --- a/autogpt_platform/frontend/tailwind.config.ts +++ b/autogpt_platform/frontend/tailwind.config.ts @@ -18,6 +18,8 @@ const config = { mono: ["var(--font-geist-mono)"], // Include the custom font family neue: ['"PP Neue Montreal TT"', "sans-serif"], + poppin: ["var(--font-poppins)"], + inter: ["var(--font-inter)"], }, colors: { border: "hsl(var(--border))", From 43a79d063fa23af49d8c6adda50d1353dc35967f Mon Sep 17 00:00:00 2001 From: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:43:08 +0530 Subject: [PATCH 2/3] feat(platform) : Add api key generation frontend (#9212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow users to create API keys for the AutoGPT platform. The backend is already set up, and here I’ve added the frontend for it. ### Changes 1. Fix the `response-model` of the API keys endpoints. 2. Add a new page `/store/api_keys`. 3. Add an `APIKeySection` component to create, delete, and view all your API keys. Screenshot 2025-01-07 at 3 59 25 PM --- .../backend/backend/server/routers/v1.py | 10 +- .../src/app/store/(user)/api_keys/page.tsx | 11 + .../frontend/src/app/store/(user)/layout.tsx | 3 +- .../src/components/agptui/Sidebar.tsx | 20 +- .../agptui/composite/APIKeySection.tsx | 296 ++++++++++++++++++ .../src/lib/autogpt-server-api/client.ts | 33 ++ .../src/lib/autogpt-server-api/types.ts | 33 ++ 7 files changed, 399 insertions(+), 7 deletions(-) create mode 100644 autogpt_platform/frontend/src/app/store/(user)/api_keys/page.tsx create mode 100644 autogpt_platform/frontend/src/components/agptui/composite/APIKeySection.tsx diff --git a/autogpt_platform/backend/backend/server/routers/v1.py b/autogpt_platform/backend/backend/server/routers/v1.py index aca22e5c5d..9e8bf50d6d 100644 --- a/autogpt_platform/backend/backend/server/routers/v1.py +++ b/autogpt_platform/backend/backend/server/routers/v1.py @@ -541,7 +541,7 @@ def get_execution_schedules( @v1_router.post( "/api-keys", - response_model=list[CreateAPIKeyResponse] | dict[str, str], + response_model=CreateAPIKeyResponse, tags=["api-keys"], dependencies=[Depends(auth_middleware)], ) @@ -583,7 +583,7 @@ async def get_api_keys( @v1_router.get( "/api-keys/{key_id}", - response_model=list[APIKeyWithoutHash] | dict[str, str], + response_model=APIKeyWithoutHash, tags=["api-keys"], dependencies=[Depends(auth_middleware)], ) @@ -604,7 +604,7 @@ async def get_api_key( @v1_router.delete( "/api-keys/{key_id}", - response_model=list[APIKeyWithoutHash] | dict[str, str], + response_model=APIKeyWithoutHash, tags=["api-keys"], dependencies=[Depends(auth_middleware)], ) @@ -626,7 +626,7 @@ async def delete_api_key( @v1_router.post( "/api-keys/{key_id}/suspend", - response_model=list[APIKeyWithoutHash] | dict[str, str], + response_model=APIKeyWithoutHash, tags=["api-keys"], dependencies=[Depends(auth_middleware)], ) @@ -648,7 +648,7 @@ async def suspend_key( @v1_router.put( "/api-keys/{key_id}/permissions", - response_model=list[APIKeyWithoutHash] | dict[str, str], + response_model=APIKeyWithoutHash, tags=["api-keys"], dependencies=[Depends(auth_middleware)], ) diff --git a/autogpt_platform/frontend/src/app/store/(user)/api_keys/page.tsx b/autogpt_platform/frontend/src/app/store/(user)/api_keys/page.tsx new file mode 100644 index 0000000000..87f3d58b4e --- /dev/null +++ b/autogpt_platform/frontend/src/app/store/(user)/api_keys/page.tsx @@ -0,0 +1,11 @@ +import { APIKeysSection } from "@/components/agptui/composite/APIKeySection"; + +const ApiKeysPage = () => { + return ( +
+ +
+ ); +}; + +export default ApiKeysPage; diff --git a/autogpt_platform/frontend/src/app/store/(user)/layout.tsx b/autogpt_platform/frontend/src/app/store/(user)/layout.tsx index 0f90e5bd3b..64900562af 100644 --- a/autogpt_platform/frontend/src/app/store/(user)/layout.tsx +++ b/autogpt_platform/frontend/src/app/store/(user)/layout.tsx @@ -8,6 +8,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { { text: "Creator Dashboard", href: "/store/dashboard" }, { text: "Agent dashboard", href: "/store/agent-dashboard" }, { text: "Integrations", href: "/store/integrations" }, + { text: "API Keys", href: "/store/api_keys" }, { text: "Profile", href: "/store/profile" }, { text: "Settings", href: "/store/settings" }, ], @@ -17,7 +18,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { return (
-
{children}
+
{children}
); } diff --git a/autogpt_platform/frontend/src/components/agptui/Sidebar.tsx b/autogpt_platform/frontend/src/components/agptui/Sidebar.tsx index 5cad3fddcc..545d82b76f 100644 --- a/autogpt_platform/frontend/src/components/agptui/Sidebar.tsx +++ b/autogpt_platform/frontend/src/components/agptui/Sidebar.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import Link from "next/link"; import { Button } from "./Button"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; -import { Menu } from "lucide-react"; +import { KeyIcon, Menu } from "lucide-react"; import { IconDashboardLayout, IconIntegrations, @@ -58,6 +58,15 @@ export const Sidebar: React.FC = ({ linkGroups }) => { Integrations + + +
+ API Keys +
+ = ({ linkGroups }) => { Integrations + + +
+ API Keys +
+ ([]); + const [isLoading, setIsLoading] = useState(true); + const [isCreateOpen, setIsCreateOpen] = useState(false); + const [isKeyDialogOpen, setIsKeyDialogOpen] = useState(false); + const [newKeyName, setNewKeyName] = useState(""); + const [newKeyDescription, setNewKeyDescription] = useState(""); + const [newApiKey, setNewApiKey] = useState(""); + const [selectedPermissions, setSelectedPermissions] = useState< + APIKeyPermission[] + >([]); + const { toast } = useToast(); + const api = useBackendAPI(); + + useEffect(() => { + loadAPIKeys(); + }, []); + + const loadAPIKeys = async () => { + setIsLoading(true); + try { + const keys = await api.listAPIKeys(); + setApiKeys(keys.filter((key) => key.status === "ACTIVE")); + } finally { + setIsLoading(false); + } + }; + + const handleCreateKey = async () => { + try { + const response = await api.createAPIKey( + newKeyName, + selectedPermissions, + newKeyDescription, + ); + + setNewApiKey(response.plain_text_key); + setIsCreateOpen(false); + setIsKeyDialogOpen(true); + loadAPIKeys(); + } catch (error) { + toast({ + title: "Error", + description: "Failed to create AutoGPT Platform API key", + variant: "destructive", + }); + } + }; + + const handleCopyKey = () => { + navigator.clipboard.writeText(newApiKey); + toast({ + title: "Copied", + description: "AutoGPT Platform API key copied to clipboard", + }); + }; + + const handleRevokeKey = async (keyId: string) => { + try { + await api.revokeAPIKey(keyId); + toast({ + title: "Success", + description: "AutoGPT Platform API key revoked successfully", + }); + loadAPIKeys(); + } catch (error) { + toast({ + title: "Error", + description: "Failed to revoke AutoGPT Platform API key", + variant: "destructive", + }); + } + }; + + return ( + + + AutoGPT Platform API Keys + + Manage your AutoGPT Platform API keys for programmatic access + + + +
+ + + + + + + Create New API Key + + Create a new AutoGPT Platform API key + + +
+
+ + setNewKeyName(e.target.value)} + placeholder="My AutoGPT Platform API Key" + /> +
+
+ + setNewKeyDescription(e.target.value)} + placeholder="Used for..." + /> +
+
+ + {Object.values(APIKeyPermission).map((permission) => ( +
+ { + setSelectedPermissions( + checked + ? [...selectedPermissions, permission] + : selectedPermissions.filter( + (p) => p !== permission, + ), + ); + }} + /> + +
+ ))} +
+
+ + + + +
+
+ + + + + AutoGPT Platform API Key Created + + Please copy your AutoGPT API key now. You won't be able + to see it again! + + +
+ + {newApiKey} + + +
+ + + +
+
+
+ + {isLoading ? ( +
+ +
+ ) : ( + apiKeys.length > 0 && ( + + + + Name + API Key + Status + Created + Last Used + + + + + {apiKeys.map((key) => ( + + {key.name} + +
+ {`${key.prefix}******************${key.postfix}`} +
+
+ + + {key.status} + + + + {new Date(key.created_at).toLocaleDateString()} + + + {key.last_used_at + ? new Date(key.last_used_at).toLocaleDateString() + : "Never"} + + + + + + + + handleRevokeKey(key.id)} + > + Revoke + + + + +
+ ))} +
+
+ ) + )} +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts index 1f74e67f63..169c17ac86 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts @@ -29,6 +29,9 @@ import { StoreReview, ScheduleCreatable, Schedule, + APIKeyPermission, + CreateAPIKeyResponse, + APIKey, } from "./types"; import { createBrowserClient } from "@supabase/ssr"; import getServerSupabase from "../supabase/getServerSupabase"; @@ -221,6 +224,36 @@ export default class BackendAPI { ); } + // API Key related requests + async createAPIKey( + name: string, + permissions: APIKeyPermission[], + description?: string, + ): Promise { + return this._request("POST", "/api-keys", { + name, + permissions, + description, + }); + } + + async listAPIKeys(): Promise { + return this._get("/api-keys"); + } + + async revokeAPIKey(keyId: string): Promise { + return this._request("DELETE", `/api-keys/${keyId}`); + } + + async updateAPIKeyPermissions( + keyId: string, + permissions: APIKeyPermission[], + ): Promise { + return this._request("PUT", `/api-keys/${keyId}/permissions`, { + permissions, + }); + } + /** * @returns `true` if a ping event was received, `false` if provider doesn't support pinging but the webhook exists. * @throws `Error` if the webhook does not exist. diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts index 2f2b8fb965..d383206668 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts @@ -513,3 +513,36 @@ export type StoreReviewCreate = { score: number; comments?: string; }; + +// API Key Types + +export enum APIKeyPermission { + EXECUTE_GRAPH = "EXECUTE_GRAPH", + READ_GRAPH = "READ_GRAPH", + EXECUTE_BLOCK = "EXECUTE_BLOCK", + READ_BLOCK = "READ_BLOCK", +} + +export enum APIKeyStatus { + ACTIVE = "ACTIVE", + REVOKED = "REVOKED", + SUSPENDED = "SUSPENDED", +} + +export interface APIKey { + id: string; + name: string; + prefix: string; + postfix: string; + status: APIKeyStatus; + permissions: APIKeyPermission[]; + created_at: string; + last_used_at?: string; + revoked_at?: string; + description?: string; +} + +export interface CreateAPIKeyResponse { + api_key: APIKey; + plain_text_key: string; +} From e4d8502729a0a084f5b56e2881953b329d0af1a0 Mon Sep 17 00:00:00 2001 From: Bently Date: Wed, 8 Jan 2025 12:18:42 +0000 Subject: [PATCH 3/3] fix(blocks): improve handling of plain text in send web request block (#9219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Changes 🏗️ This is to improve how we deal with plain text in the send web request block. - Plain text stays as plain text (regardless of JSON toggle) - Valid JSON with JSON toggle enabled sends as JSON - JSON-like data with JSON toggle disabled sends as form data --- autogpt_platform/backend/backend/blocks/http.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/autogpt_platform/backend/backend/blocks/http.py b/autogpt_platform/backend/backend/blocks/http.py index 2adb058309..099e2c3c1e 100644 --- a/autogpt_platform/backend/backend/blocks/http.py +++ b/autogpt_platform/backend/backend/blocks/http.py @@ -56,15 +56,24 @@ class SendWebRequestBlock(Block): ) def run(self, input_data: Input, **kwargs) -> BlockOutput: - if isinstance(input_data.body, str): - input_data.body = json.loads(input_data.body) + body = input_data.body + + if input_data.json_format: + if isinstance(body, str): + try: + # Try to parse as JSON first + body = json.loads(body) + except json.JSONDecodeError: + # If it's not valid JSON and just plain text, + # we should send it as plain text instead + input_data.json_format = False response = requests.request( input_data.method.value, input_data.url, headers=input_data.headers, - json=input_data.body if input_data.json_format else None, - data=input_data.body if not input_data.json_format else None, + json=body if input_data.json_format else None, + data=body if not input_data.json_format else None, ) result = response.json() if input_data.json_format else response.text