From cf2ca63d528452699e58387a5ee84e0d76cb20a9 Mon Sep 17 00:00:00 2001 From: Nicholas Tindle Date: Fri, 18 Jul 2025 16:12:02 -0500 Subject: [PATCH 001/200] fix(backend): use admin route for store downloads (#10406) --- .vscode/launch.json | 4 ++-- .../backend/backend/server/v2/admin/store_admin_routes.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6740265f3391..c869e33c1e22 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "node-terminal", "request": "launch", "cwd": "${workspaceFolder}/autogpt_platform/frontend", - "command": "yarn dev" + "command": "pnpm dev" }, { "name": "Frontend: Client Side", @@ -19,7 +19,7 @@ "type": "node-terminal", "request": "launch", - "command": "yarn dev", + "command": "pnpm dev", "cwd": "${workspaceFolder}/autogpt_platform/frontend", "serverReadyAction": { "pattern": "- Local:.+(https?://.+)", diff --git a/autogpt_platform/backend/backend/server/v2/admin/store_admin_routes.py b/autogpt_platform/backend/backend/server/v2/admin/store_admin_routes.py index 88f69360a40d..f37f83294805 100644 --- a/autogpt_platform/backend/backend/server/v2/admin/store_admin_routes.py +++ b/autogpt_platform/backend/backend/server/v2/admin/store_admin_routes.py @@ -131,7 +131,7 @@ async def admin_download_agent_file( Raises: HTTPException: If the agent is not found or an unexpected error occurs. """ - graph_data = await backend.server.v2.store.db.get_agent( + graph_data = await backend.server.v2.store.db.get_agent_as_admin( user_id=user.user_id, store_listing_version_id=store_listing_version_id, ) From 6dba6ec3e674e266ca4afce8bd01060785ab4391 Mon Sep 17 00:00:00 2001 From: Swifty Date: Mon, 21 Jul 2025 10:19:37 +0200 Subject: [PATCH 002/200] feat(backend): Add database index for improved query performance (#10411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a database index to improve query performance based on Supabase query performance insights and index recommendations. These indexes target frequently queried columns and relationships to reduce query execution time. ### Changes 🏗️ - Added index on `AgentNodeExecutionInputOutput.agentPresetId` ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Verified schema changes are valid Prisma syntax - [x] Confirmed indexes target frequently queried columns based on Supabase recommendations - [x] Ensured no duplicate indexes are created #### For configuration changes: - [x] `.env.example` is updated or already compatible with my changes - [x] `docker-compose.yml` is updated or already compatible with my changes - [x] I have included a list of my configuration changes in the PR description (under **Changes**) --- .../migrations/20250721073830_add_preset_index/migration.sql | 2 ++ autogpt_platform/backend/schema.prisma | 1 + 2 files changed, 3 insertions(+) create mode 100644 autogpt_platform/backend/migrations/20250721073830_add_preset_index/migration.sql diff --git a/autogpt_platform/backend/migrations/20250721073830_add_preset_index/migration.sql b/autogpt_platform/backend/migrations/20250721073830_add_preset_index/migration.sql new file mode 100644 index 000000000000..1bc3b5293693 --- /dev/null +++ b/autogpt_platform/backend/migrations/20250721073830_add_preset_index/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "AgentNodeExecutionInputOutput_agentPresetId_idx" ON "AgentNodeExecutionInputOutput"("agentPresetId"); diff --git a/autogpt_platform/backend/schema.prisma b/autogpt_platform/backend/schema.prisma index 7e8b53bdc769..1e8c17f753d6 100644 --- a/autogpt_platform/backend/schema.prisma +++ b/autogpt_platform/backend/schema.prisma @@ -411,6 +411,7 @@ model AgentNodeExecutionInputOutput { @@index([referencedByOutputExecId]) // Composite index for `upsert_execution_input`. @@index([name, time]) + @@index([agentPresetId]) } model AgentNodeExecutionKeyValueData { From f81d7e6a56f2aded8e22974c9eadc829e11f124f Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Mon, 21 Jul 2025 17:26:46 +0800 Subject: [PATCH 003/200] feat(backend): Add Missing FK indexes and remove unused & redundant indexes (#10412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Changes 🏗️ This PR optimizes database performance by adding missing foreign key indexes, removing unused/redundant indexes, cleaning up all legacy untracked indexes, and adding performance indexes for materialized views to achieve 100% optimized database indexing. **Foreign Key Indexes Added:** - `AgentGraph`: `[forkedFromId, forkedFromVersion]` - For fork relationship queries - `AgentGraphExecution`: `[agentPresetId]` - For preset-based execution filtering - `AgentNodeExecution`: `[agentNodeId]` - For node execution lookups - `AgentNodeExecutionInputOutput`: `[agentPresetId]` - For preset input/output queries - `AgentPreset`: `[agentGraphId, agentGraphVersion]` & `[webhookId]` - For graph and webhook lookups - `LibraryAgent`: `[agentGraphId, agentGraphVersion]` & `[creatorId]` - For agent and creator queries - `StoreListing`: `[agentGraphId, agentGraphVersion]` - For marketplace agent lookups - `StoreListingReview`: `[reviewByUserId]` - For user review queries **Unused/Redundant Indexes Removed:** - `User.email` - Unused index identified by linter - `AnalyticsMetrics.userId` - Unused index causing write overhead - `AnalyticsDetails.type` - Redundant (covered by composite `[userId, type]`) - `APIKey.key`, `APIKey.status` - Unused indexes - Named index `"analyticsDetails"` - Converted to standard composite index **All Legacy Untracked Indexes Removed:** - `idx_store_listing_version_status` - Redundant with Prisma composite index - `idx_slv_agent` - Redundant with Prisma-managed `[agentGraphId, agentGraphVersion]` - `idx_store_listing_version_approved_listing` - Redundant with unique constraint - `StoreListing_agentId_owningUserId_idx` - Legacy index superseded by current strategy - `StoreListing_isDeleted_isApproved_idx` - Replaced by optimized composite index - `StoreListing_isDeleted_idx` - Redundant with composite index - `StoreListingVersion_agentId_agentVersion_isDeleted_idx` - Legacy index replaced - `idx_store_listing_approved` - Redundant with existing `[owningUserId, slug]` unique constraint and `[isDeleted, hasApprovedVersion]` index - `idx_slv_categories_gin` - Specialized array search index removed (can be re-added if category filtering is implemented) - `idx_profile_user` - Duplicate of Prisma-managed `Profile_userId_idx` **Materialized View Performance Indexes Added:** - `idx_mv_review_stats_rating` on `mv_review_stats(avg_rating DESC)` - Optimizes sorting agents by rating - `idx_mv_review_stats_count` on `mv_review_stats(review_count DESC)` - Optimizes sorting agents by review count **Result: 100% Optimized Database Indexing** - All database indexes are now defined and managed through Prisma schema - No more untracked indexes requiring manual SQL maintenance - Added performance indexes for materialized views used by marketplace views - Improved query performance for agent sorting and filtering - Enhanced maintainability and consistency across environments **Schema Comments Updated:** - Removed all references to dropped untracked indexes - Simplified documentation to reflect Prisma-only approach for regular tables - Added comprehensive documentation for materialized view indexes and their purposes - Maintained documentation for materialized view refresh strategy ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Schema changes compile successfully with Prisma - [x] Migration adds required FK indexes and materialized view performance indexes - [x] Migration drops all legacy indexes and redundant untracked indexes - [x] All pre-commit hooks pass (linting, formatting, type checking) - [x] No breaking changes to existing foreign key relationships - [x] Verified existing Prisma indexes cover all query patterns - [x] Schema comments comprehensively document all indexing strategy - [x] Materialized view performance indexes optimize marketplace sorting 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Swifty Co-authored-by: Claude --- .../migration.sql | 109 ++++++++++++++++++ autogpt_platform/backend/schema.prisma | 78 +++++-------- 2 files changed, 135 insertions(+), 52 deletions(-) create mode 100644 autogpt_platform/backend/migrations/20250721081856_add_missing_fk_indexes_remove_unused_indexes/migration.sql diff --git a/autogpt_platform/backend/migrations/20250721081856_add_missing_fk_indexes_remove_unused_indexes/migration.sql b/autogpt_platform/backend/migrations/20250721081856_add_missing_fk_indexes_remove_unused_indexes/migration.sql new file mode 100644 index 000000000000..86f443b56d3b --- /dev/null +++ b/autogpt_platform/backend/migrations/20250721081856_add_missing_fk_indexes_remove_unused_indexes/migration.sql @@ -0,0 +1,109 @@ +-- DropIndex +DROP INDEX IF EXISTS "APIKey_key_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "APIKey_prefix_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "APIKey_status_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_agent_graph_execution_agent"; + +-- DropIndex +DROP INDEX IF EXISTS "AnalyticsDetails_type_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "AnalyticsMetrics_userId_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "IntegrationWebhook_userId_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "Profile_username_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_store_listing_review_version"; + +-- DropIndex +DROP INDEX IF EXISTS "User_email_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "User_id_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "UserOnboarding_userId_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_store_listing_version_status"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_slv_agent"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_store_listing_version_approved_listing"; + +-- DropIndex +DROP INDEX IF EXISTS "StoreListing_agentId_owningUserId_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "StoreListing_isDeleted_isApproved_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "StoreListing_isDeleted_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "StoreListingVersion_agentId_agentVersion_isDeleted_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_store_listing_approved"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_slv_categories_gin"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_profile_user"; + +-- CreateIndex +CREATE INDEX "APIKey_prefix_name_idx" ON "APIKey"("prefix", "name"); + +-- CreateIndex +CREATE INDEX "AgentGraph_forkedFromId_forkedFromVersion_idx" ON "AgentGraph"("forkedFromId", "forkedFromVersion"); + +-- CreateIndex +CREATE INDEX "AgentGraphExecution_agentPresetId_idx" ON "AgentGraphExecution"("agentPresetId"); + +-- CreateIndex +CREATE INDEX "AgentNodeExecution_agentNodeId_executionStatus_idx" ON "AgentNodeExecution"("agentNodeId", "executionStatus"); + +-- CreateIndex +CREATE INDEX "AgentPreset_agentGraphId_agentGraphVersion_idx" ON "AgentPreset"("agentGraphId", "agentGraphVersion"); + +-- CreateIndex +CREATE INDEX "AgentPreset_webhookId_idx" ON "AgentPreset"("webhookId"); + +-- CreateIndex +CREATE INDEX "LibraryAgent_agentGraphId_agentGraphVersion_idx" ON "LibraryAgent"("agentGraphId", "agentGraphVersion"); + +-- CreateIndex +CREATE INDEX "LibraryAgent_creatorId_idx" ON "LibraryAgent"("creatorId"); + +-- CreateIndex +CREATE INDEX "StoreListing_agentGraphId_agentGraphVersion_idx" ON "StoreListing"("agentGraphId", "agentGraphVersion"); + +-- CreateIndex +CREATE INDEX "StoreListingReview_reviewByUserId_idx" ON "StoreListingReview"("reviewByUserId"); + +-- CreateIndex (Materialized View Performance Indexes) +CREATE INDEX IF NOT EXISTS "idx_mv_review_stats_rating" ON "mv_review_stats" ("avg_rating" DESC); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "idx_mv_review_stats_count" ON "mv_review_stats" ("review_count" DESC); + +-- RenameIndex (only if exists) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'analyticsDetails') THEN + ALTER INDEX "analyticsDetails" RENAME TO "AnalyticsDetails_userId_type_idx"; + END IF; +END $$; diff --git a/autogpt_platform/backend/schema.prisma b/autogpt_platform/backend/schema.prisma index 1e8c17f753d6..bcc56b541a72 100644 --- a/autogpt_platform/backend/schema.prisma +++ b/autogpt_platform/backend/schema.prisma @@ -53,9 +53,6 @@ model User { APIKeys APIKey[] IntegrationWebhooks IntegrationWebhook[] NotificationBatches UserNotificationBatch[] - - @@index([id]) - @@index([email]) } enum OnboardingStep { @@ -98,8 +95,6 @@ model UserOnboarding { userId String @unique User User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@index([userId]) } // This model describes the Agent Graph/Flow (Multi Agent System). @@ -135,6 +130,7 @@ model AgentGraph { @@id(name: "graphVersionId", [id, version]) @@index([userId, isActive]) + @@index([forkedFromId, forkedFromVersion]) } //////////////////////////////////////////////////////////// @@ -176,6 +172,8 @@ model AgentPreset { isDeleted Boolean @default(false) @@index([userId]) + @@index([agentGraphId, agentGraphVersion]) + @@index([webhookId]) } enum NotificationType { @@ -248,6 +246,8 @@ model LibraryAgent { isDeleted Boolean @default(false) @@unique([userId, agentGraphId, agentGraphVersion]) + @@index([agentGraphId, agentGraphVersion]) + @@index([creatorId]) } //////////////////////////////////////////////////////////// @@ -361,6 +361,7 @@ model AgentGraphExecution { @@index([agentGraphId, agentGraphVersion]) @@index([userId]) @@index([createdAt]) + @@index([agentPresetId]) } // This model describes the execution of an AgentNode. @@ -386,6 +387,7 @@ model AgentNodeExecution { stats Json? @@index([agentGraphExecutionId, agentNodeId, executionStatus]) + @@index([agentNodeId, executionStatus]) @@index([addedTime, queuedTime]) } @@ -415,12 +417,13 @@ model AgentNodeExecutionInputOutput { } model AgentNodeExecutionKeyValueData { - userId String - key String - agentNodeExecutionId String - data Json? - createdAt DateTime @default(now()) - updatedAt DateTime? @updatedAt + userId String + key String + agentNodeExecutionId String + data Json? + createdAt DateTime @default(now()) + updatedAt DateTime? @updatedAt + @@id([userId, key]) } @@ -445,8 +448,6 @@ model IntegrationWebhook { AgentNodes AgentNode[] AgentPresets AgentPreset[] - - @@index([userId]) } model AnalyticsDetails { @@ -470,8 +471,7 @@ model AnalyticsDetails { // Indexable field for any count based analytical measures like page order clicking, tutorial step completion, etc. dataIndex String? - @@index([userId, type], name: "analyticsDetails") - @@index([type]) + @@index([userId, type]) } //////////////////////////////////////////////////////////// @@ -495,8 +495,6 @@ model AnalyticsMetrics { // Link to User model userId String User User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@index([userId]) } //////////////////////////////////////////////////////////// @@ -582,7 +580,6 @@ model Profile { LibraryAgents LibraryAgent[] - @@index([username]) @@index([userId]) } @@ -600,21 +597,13 @@ view Creator { agent_runs Int is_featured Boolean - // Note: Prisma doesn't support indexes on views, but the following indexes exist in the database: - // - // Optimized indexes (partial indexes to reduce size and improve performance): - // - idx_profile_user on Profile(userId) - // - idx_store_listing_approved on StoreListing(owningUserId) WHERE isDeleted = false AND hasApprovedVersion = true - // - idx_store_listing_version_status on StoreListingVersion(storeListingId) WHERE submissionStatus = 'APPROVED' - // - idx_slv_categories_gin - GIN index on StoreListingVersion(categories) WHERE submissionStatus = 'APPROVED' - // - idx_slv_agent on StoreListingVersion(agentGraphId, agentGraphVersion) WHERE submissionStatus = 'APPROVED' - // - idx_store_listing_review_version on StoreListingReview(storeListingVersionId) - // - idx_store_listing_version_approved_listing on StoreListingVersion(storeListingId, version) WHERE submissionStatus = 'APPROVED' - // - idx_agent_graph_execution_agent on AgentGraphExecution(agentGraphId) - // // Materialized views used (refreshed every 15 minutes via pg_cron): // - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId + // * idx_mv_agent_run_counts (UNIQUE on agentGraphId) - Primary lookup // - mv_review_stats - Pre-aggregated review statistics (count, avg rating) by storeListingId + // * idx_mv_review_stats (UNIQUE on storeListingId) - Primary lookup + // * idx_mv_review_stats_rating (avg_rating DESC) - Sort by rating performance + // * idx_mv_review_stats_count (review_count DESC) - Sort by review count performance // // Query strategy: Uses CTEs to efficiently aggregate creator statistics leveraging materialized views } @@ -639,28 +628,13 @@ view StoreAgent { rating Float versions String[] - // Note: Prisma doesn't support indexes on views, but the following indexes exist in the database: - // - // Optimized indexes (partial indexes to reduce size and improve performance): - // - idx_store_listing_approved on StoreListing(owningUserId) WHERE isDeleted = false AND hasApprovedVersion = true - // - idx_store_listing_version_status on StoreListingVersion(storeListingId) WHERE submissionStatus = 'APPROVED' - // - idx_slv_categories_gin - GIN index on StoreListingVersion(categories) WHERE submissionStatus = 'APPROVED' for array searches - // - idx_slv_agent on StoreListingVersion(agentGraphId, agentGraphVersion) WHERE submissionStatus = 'APPROVED' - // - idx_store_listing_review_version on StoreListingReview(storeListingVersionId) - // - idx_store_listing_version_approved_listing on StoreListingVersion(storeListingId, version) WHERE submissionStatus = 'APPROVED' - // - idx_agent_graph_execution_agent on AgentGraphExecution(agentGraphId) - // - idx_profile_user on Profile(userId) - // - // Additional indexes from earlier migrations: - // - StoreListing_agentId_owningUserId_idx - // - StoreListing_isDeleted_isApproved_idx (replaced by idx_store_listing_approved) - // - StoreListing_isDeleted_idx - // - StoreListing_agentId_key (unique on agentGraphId) - // - StoreListingVersion_agentId_agentVersion_isDeleted_idx - // // Materialized views used (refreshed every 15 minutes via pg_cron): // - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId + // * idx_mv_agent_run_counts (UNIQUE on agentGraphId) - Primary lookup // - mv_review_stats - Pre-aggregated review statistics (count, avg rating) by storeListingId + // * idx_mv_review_stats (UNIQUE on storeListingId) - Primary lookup + // * idx_mv_review_stats_rating (avg_rating DESC) - Sort by rating performance + // * idx_mv_review_stats_count (review_count DESC) - Sort by review count performance // // Query strategy: Uses CTE for version aggregation and joins with materialized views for performance } @@ -748,6 +722,7 @@ model StoreListing { @@unique([owningUserId, slug]) // Used in the view query @@index([isDeleted, hasApprovedVersion]) + @@index([agentGraphId, agentGraphVersion]) } model StoreListingVersion { @@ -821,6 +796,7 @@ model StoreListingReview { comments String? @@unique([storeListingVersionId, reviewByUserId]) + @@index([reviewByUserId]) } enum SubmissionStatus { @@ -856,9 +832,7 @@ model APIKey { userId String User User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@index([key]) - @@index([prefix]) - @@index([status]) + @@index([prefix, name]) @@index([userId, status]) } From bf73b42890096da411ebf824f004577e35c3a4bd Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Fri, 18 Jul 2025 21:43:59 +0800 Subject: [PATCH 004/200] fix(platform): Fix service health check mechanism on app service (#10401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Introduced correct health check API for AppService - Fixed service health check mechanism to properly handle health status monitoring ## Changes Made - Updated health check implementation in AppService. - Make rest service health check checks the health of DatabaseManager too. ## Test plan - [x] Verify health check endpoint responds correctly - [x] Test health check mechanism under various service states - [x] Validate monitoring and alerting integration 🤖 Generated with [Claude Code](https://claude.ai/code) --- .../backend/monitoring/notification_monitor.py | 10 ++++++++-- autogpt_platform/backend/backend/server/rest_api.py | 12 ++++++++++++ autogpt_platform/backend/backend/util/service.py | 8 +++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/autogpt_platform/backend/backend/monitoring/notification_monitor.py b/autogpt_platform/backend/backend/monitoring/notification_monitor.py index 418ef23e0ef7..3326dd91a200 100644 --- a/autogpt_platform/backend/backend/monitoring/notification_monitor.py +++ b/autogpt_platform/backend/backend/monitoring/notification_monitor.py @@ -2,6 +2,7 @@ import logging +from autogpt_libs.utils.cache import thread_cached from prisma.enums import NotificationType from pydantic import BaseModel @@ -16,6 +17,11 @@ class NotificationJobArgs(BaseModel): cron: str +@thread_cached +def get_notification_manager_client(): + return get_service_client(NotificationManagerClient) + + def process_existing_batches(**kwargs): """Process existing notification batches.""" args = NotificationJobArgs(**kwargs) @@ -23,7 +29,7 @@ def process_existing_batches(**kwargs): logging.info( f"Processing existing batches for notification type {args.notification_types}" ) - get_service_client(NotificationManagerClient).process_existing_batches( + get_notification_manager_client().process_existing_batches( args.notification_types ) except Exception as e: @@ -34,6 +40,6 @@ def process_weekly_summary(**kwargs): """Process weekly summary notifications.""" try: logging.info("Processing weekly summary") - get_service_client(NotificationManagerClient).queue_weekly_summary() + get_notification_manager_client().queue_weekly_summary() except Exception as e: logger.exception(f"Error processing weekly summary: {e}") diff --git a/autogpt_platform/backend/backend/server/rest_api.py b/autogpt_platform/backend/backend/server/rest_api.py index b81e53d7b855..abdb7448c50c 100644 --- a/autogpt_platform/backend/backend/server/rest_api.py +++ b/autogpt_platform/backend/backend/server/rest_api.py @@ -14,6 +14,7 @@ shutdown_launchdarkly, ) from autogpt_libs.logging.utils import generate_uvicorn_config +from autogpt_libs.utils.cache import thread_cached from fastapi.exceptions import RequestValidationError from fastapi.routing import APIRoute @@ -212,8 +213,19 @@ async def validation_error_handler( app.mount("/external-api", external_app) +@thread_cached +def get_db_async_client(): + from backend.executor import DatabaseManagerAsyncClient + + return backend.util.service.get_service_client( + DatabaseManagerAsyncClient, + health_check=False, + ) + + @app.get(path="/health", tags=["health"], dependencies=[]) async def health(): + await get_db_async_client().health_check_async() return {"status": "healthy"} diff --git a/autogpt_platform/backend/backend/util/service.py b/autogpt_platform/backend/backend/util/service.py index d9e3e55eb8a6..1096101107ac 100644 --- a/autogpt_platform/backend/backend/util/service.py +++ b/autogpt_platform/backend/backend/util/service.py @@ -216,7 +216,10 @@ def run(self): methods=["POST"], ) self.fastapi_app.add_api_route( - "/health_check", self.health_check, methods=["POST"] + "/health_check", self.health_check, methods=["POST", "GET"] + ) + self.fastapi_app.add_api_route( + "/health_check_async", self.health_check, methods=["POST", "GET"] ) self.fastapi_app.add_exception_handler( ValueError, self._handle_internal_http_error(400) @@ -248,6 +251,9 @@ def get_service_type(cls) -> Type[AppService]: def health_check(self): pass + async def health_check_async(self): + pass + def close(self): pass From e28eec6ff9bb2fce05dc7a3123453ab72081ffd3 Mon Sep 17 00:00:00 2001 From: Swifty Date: Mon, 21 Jul 2025 11:48:30 +0200 Subject: [PATCH 005/200] feat(backend): Add ReverseListOrderBlock for reversing list element order (#10352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Changes 🏗️ This PR adds a new utility block to the basic blocks collection. This block provides a simple way to reverse the order of elements in any list. **New Features:** - Added class in - Block accepts any list as input and returns the same list with elements in reversed order - Preserves the original list (creates a copy before reversing) - Works with lists containing any type of elements **Technical Details:** - Block ID: - Category: - Input: - The list to reverse (accepts ) - Output: - The list with elements in reversed order - Includes test input/output for validation ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Created an agent with the ReverseListOrderBlock - [x] Tested with various list types (numbers, strings, mixed types) - [x] Verified the block preserves the original list - [x] Confirmed the block correctly reverses the order of elements - [x] Tested with empty lists and single-element lists - [x] Verified the block integrates properly with other blocks in a workflow #### For configuration changes: - [x] is updated or already compatible with my changes - [x] is updated or already compatible with my changes - [x] I have included a list of my configuration changes in the PR description (under **Changes**) **Note:** No configuration changes required - this is a pure code addition that uses the existing block framework. Co-authored-by: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com> --- .../backend/backend/blocks/basic.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/autogpt_platform/backend/backend/blocks/basic.py b/autogpt_platform/backend/backend/blocks/basic.py index 1ac6abfc8caa..ef251489c742 100644 --- a/autogpt_platform/backend/backend/blocks/basic.py +++ b/autogpt_platform/backend/backend/blocks/basic.py @@ -188,3 +188,31 @@ async def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "value", converted_value except Exception as e: yield "error", f"Failed to convert value: {str(e)}" + + +class ReverseListOrderBlock(Block): + """ + A block which takes in a list and returns it in the opposite order. + """ + + class Input(BlockSchema): + input_list: list[Any] = SchemaField(description="The list to reverse") + + class Output(BlockSchema): + reversed_list: list[Any] = SchemaField(description="The list in reversed order") + + def __init__(self): + super().__init__( + id="422cb708-3109-4277-bfe3-bc2ae5812777", + description="Reverses the order of elements in a list", + categories={BlockCategory.BASIC}, + input_schema=ReverseListOrderBlock.Input, + output_schema=ReverseListOrderBlock.Output, + test_input={"input_list": [1, 2, 3, 4, 5]}, + test_output=[("reversed_list", [5, 4, 3, 2, 1])], + ) + + async def run(self, input_data: Input, **kwargs) -> BlockOutput: + reversed_list = list(input_data.input_list) + reversed_list.reverse() + yield "reversed_list", reversed_list From 0c9b7334c143cc0ae9c6e2f235b578499a739f4e Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Mon, 21 Jul 2025 19:54:42 +0800 Subject: [PATCH 006/200] feat(backend): Register agent subgraphs as library entries during agent import (#10409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, we only create a library entry of the top-most graph when importing the graph from an exported file. This can cause some complications, as there is no way to remove the library entry of it. ### Changes 🏗️ Create the library entry for all the subgraphs during the import process. ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Export an agent with subgraphs and import it back. --- .../webhooks/graph_lifecycle_hooks.py | 32 +++++++-- .../backend/backend/server/routers/v1.py | 15 ++-- .../backend/backend/server/v2/library/db.py | 69 ++++++++++++------- .../backend/server/v2/store/image_gen.py | 8 +-- .../backend/test/e2e_test_data.py | 6 +- 5 files changed, 82 insertions(+), 48 deletions(-) diff --git a/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py b/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py index b4e87d5080f6..bc363edd08e8 100644 --- a/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py +++ b/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py @@ -1,5 +1,6 @@ +import asyncio import logging -from typing import TYPE_CHECKING, Optional, cast +from typing import TYPE_CHECKING, Optional, cast, overload from backend.data.block import BlockSchema from backend.data.graph import set_node_webhook @@ -9,7 +10,7 @@ from .utils import setup_webhook_for_block if TYPE_CHECKING: - from backend.data.graph import GraphModel, NodeModel + from backend.data.graph import BaseGraph, GraphModel, Node, NodeModel from backend.data.model import Credentials from ._base import BaseWebhooksManager @@ -18,13 +19,29 @@ credentials_manager = IntegrationCredentialsManager() -async def on_graph_activate(graph: "GraphModel", user_id: str): +async def on_graph_activate(graph: "GraphModel", user_id: str) -> "GraphModel": """ Hook to be called when a graph is activated/created. ⚠️ Assuming node entities are not re-used between graph versions, ⚠️ this hook calls `on_node_activate` on all nodes in this graph. """ + graph = await _on_graph_activate(graph, user_id) + graph.sub_graphs = await asyncio.gather( + *(_on_graph_activate(sub_graph, user_id) for sub_graph in graph.sub_graphs) + ) + return graph + + +@overload +async def _on_graph_activate(graph: "GraphModel", user_id: str) -> "GraphModel": ... + + +@overload +async def _on_graph_activate(graph: "BaseGraph", user_id: str) -> "BaseGraph": ... + + +async def _on_graph_activate(graph: "BaseGraph | GraphModel", user_id: str): get_credentials = credentials_manager.cached_getter(user_id) updated_nodes = [] for new_node in graph.nodes: @@ -47,7 +64,7 @@ async def on_graph_activate(graph: "GraphModel", user_id: str): ) updated_node = await on_node_activate( - graph.user_id, new_node, credentials=node_credentials + user_id, graph.id, new_node, credentials=node_credentials ) updated_nodes.append(updated_node) @@ -94,10 +111,11 @@ async def on_graph_deactivate(graph: "GraphModel", user_id: str): async def on_node_activate( user_id: str, - node: "NodeModel", + graph_id: str, + node: "Node", *, credentials: Optional["Credentials"] = None, -) -> "NodeModel": +) -> "Node": """Hook to be called when the node is activated/created""" if node.block.webhook_config: @@ -105,7 +123,7 @@ async def on_node_activate( user_id=user_id, trigger_block=node.block, trigger_config=node.input_default, - for_graph_id=node.graph_id, + for_graph_id=graph_id, ) if new_webhook: node = await set_node_webhook(node.id, new_webhook.id) diff --git a/autogpt_platform/backend/backend/server/routers/v1.py b/autogpt_platform/backend/backend/server/routers/v1.py index b4d8670b824d..f874f5d0c743 100644 --- a/autogpt_platform/backend/backend/server/routers/v1.py +++ b/autogpt_platform/backend/backend/server/routers/v1.py @@ -621,16 +621,11 @@ async def create_new_graph( graph.reassign_ids(user_id=user_id, reassign_graph_id=True) graph.validate_graph(for_run=False) - graph = await graph_db.create_graph(graph, user_id=user_id) - - # Create a library agent for the new graph - library_agent = await library_db.create_library_agent(graph, user_id) - _ = asyncio.create_task( - library_db.add_generated_agent_image(graph, library_agent.id) - ) - - graph = await on_graph_activate(graph, user_id=user_id) - return graph + # The return value of the create graph & library function is intentionally not used here, + # as the graph already valid and no sub-graphs are returned back. + await graph_db.create_graph(graph, user_id=user_id) + await library_db.create_library_agent(graph, user_id=user_id) + return await on_graph_activate(graph, user_id=user_id) @v1_router.delete( diff --git a/autogpt_platform/backend/backend/server/v2/library/db.py b/autogpt_platform/backend/backend/server/v2/library/db.py index 915bd42e778e..b4368b4296b4 100644 --- a/autogpt_platform/backend/backend/server/v2/library/db.py +++ b/autogpt_platform/backend/backend/server/v2/library/db.py @@ -1,3 +1,4 @@ +import asyncio import logging from typing import Literal, Optional @@ -246,13 +247,13 @@ async def get_library_agent_by_graph_id( async def add_generated_agent_image( - graph: graph_db.GraphModel, + graph: graph_db.BaseGraph, + user_id: str, library_agent_id: str, ) -> Optional[prisma.models.LibraryAgent]: """ Generates an image for the specified LibraryAgent and updates its record. """ - user_id = graph.user_id graph_id = graph.id # Use .jpeg here since we are generating JPEG images @@ -281,16 +282,19 @@ async def add_generated_agent_image( async def create_library_agent( graph: graph_db.GraphModel, user_id: str, -) -> library_model.LibraryAgent: + create_library_agents_for_sub_graphs: bool = True, +) -> list[library_model.LibraryAgent]: """ Adds an agent to the user's library (LibraryAgent table). Args: agent: The agent/Graph to add to the library. user_id: The user to whom the agent will be added. + create_library_agents_for_sub_graphs: If True, creates LibraryAgent records for sub-graphs as well. Returns: - The newly created LibraryAgent record. + The newly created LibraryAgent records. + If the graph has sub-graphs, the parent graph will always be the first entry in the list. Raises: AgentNotFoundError: If the specified agent does not exist. @@ -300,26 +304,39 @@ async def create_library_agent( f"Creating library agent for graph #{graph.id} v{graph.version}; " f"user #{user_id}" ) + graph_entries = ( + [graph, *graph.sub_graphs] if create_library_agents_for_sub_graphs else [graph] + ) - try: - agent = await prisma.models.LibraryAgent.prisma().create( - data=prisma.types.LibraryAgentCreateInput( - isCreatedByUser=(user_id == graph.user_id), - useGraphIsActiveVersion=True, - User={"connect": {"id": user_id}}, - # Creator={"connect": {"id": graph.user_id}}, - AgentGraph={ - "connect": { - "graphVersionId": {"id": graph.id, "version": graph.version} - } - }, - ), - include={"AgentGraph": True}, + async with transaction() as tx: + library_agents = await asyncio.gather( + *( + prisma.models.LibraryAgent.prisma(tx).create( + data=prisma.types.LibraryAgentCreateInput( + isCreatedByUser=(user_id == user_id), + useGraphIsActiveVersion=True, + User={"connect": {"id": user_id}}, + # Creator={"connect": {"id": user_id}}, + AgentGraph={ + "connect": { + "graphVersionId": { + "id": graph_entry.id, + "version": graph_entry.version, + } + } + }, + ), + include=library_agent_include(user_id), + ) + for graph_entry in graph_entries + ) ) - return library_model.LibraryAgent.from_db(agent) - except prisma.errors.PrismaError as e: - logger.error(f"Database error creating agent in library: {e}") - raise store_exceptions.DatabaseError("Failed to create agent in library") from e + + # Generate images for the main graph and sub-graphs + for agent, graph in zip(library_agents, graph_entries): + asyncio.create_task(add_generated_agent_image(graph, user_id, agent.id)) + + return [library_model.LibraryAgent.from_db(agent) for agent in library_agents] async def update_agent_version_in_library( @@ -872,7 +889,9 @@ async def delete_preset(user_id: str, preset_id: str) -> None: raise store_exceptions.DatabaseError("Failed to delete preset") from e -async def fork_library_agent(library_agent_id: str, user_id: str): +async def fork_library_agent( + library_agent_id: str, user_id: str +) -> library_model.LibraryAgent: """ Clones a library agent and its underyling graph and nodes (with new ids) for the given user. @@ -881,7 +900,7 @@ async def fork_library_agent(library_agent_id: str, user_id: str): user_id: The ID of the user who owns the library agent. Returns: - The forked LibraryAgent. + The forked parent (if it has sub-graphs) LibraryAgent. Raises: DatabaseError: If there's an error during the forking process. @@ -907,7 +926,7 @@ async def fork_library_agent(library_agent_id: str, user_id: str): new_graph = await on_graph_activate(new_graph, user_id=user_id) # Create a library agent for the new graph - return await create_library_agent(new_graph, user_id) + return (await create_library_agent(new_graph, user_id))[0] except prisma.errors.PrismaError as e: logger.error(f"Database error cloning library agent: {e}") raise store_exceptions.DatabaseError("Failed to fork library agent") from e diff --git a/autogpt_platform/backend/backend/server/v2/store/image_gen.py b/autogpt_platform/backend/backend/server/v2/store/image_gen.py index b75536d3cd33..4d2bcb7b565c 100644 --- a/autogpt_platform/backend/backend/server/v2/store/image_gen.py +++ b/autogpt_platform/backend/backend/server/v2/store/image_gen.py @@ -16,7 +16,7 @@ StyleType, UpscaleOption, ) -from backend.data.graph import Graph +from backend.data.graph import BaseGraph from backend.data.model import CredentialsMetaInput, ProviderName from backend.integrations.credentials_store import ideogram_credentials from backend.util.request import Requests @@ -34,14 +34,14 @@ class ImageStyle(str, Enum): DIGITAL_ART = "digital art" -async def generate_agent_image(agent: Graph | AgentGraph) -> io.BytesIO: +async def generate_agent_image(agent: BaseGraph | AgentGraph) -> io.BytesIO: if settings.config.use_agent_image_generation_v2: return await generate_agent_image_v2(graph=agent) else: return await generate_agent_image_v1(agent=agent) -async def generate_agent_image_v2(graph: Graph | AgentGraph) -> io.BytesIO: +async def generate_agent_image_v2(graph: BaseGraph | AgentGraph) -> io.BytesIO: """ Generate an image for an agent using Ideogram model. Returns: @@ -99,7 +99,7 @@ async def generate_agent_image_v2(graph: Graph | AgentGraph) -> io.BytesIO: return io.BytesIO(response.content) -async def generate_agent_image_v1(agent: Graph | AgentGraph) -> io.BytesIO: +async def generate_agent_image_v1(agent: BaseGraph | AgentGraph) -> io.BytesIO: """ Generate an image for an agent using Flux model via Replicate API. diff --git a/autogpt_platform/backend/test/e2e_test_data.py b/autogpt_platform/backend/test/e2e_test_data.py index dbb5c10919a8..b282f02083bf 100644 --- a/autogpt_platform/backend/test/e2e_test_data.py +++ b/autogpt_platform/backend/test/e2e_test_data.py @@ -386,8 +386,10 @@ async def create_test_library_agents(self) -> List[Dict[str, Any]]: ) if graph: # Use the API function to create library agent - library_agent = await create_library_agent(graph, user["id"]) - library_agents.append(library_agent.model_dump()) + library_agents.extend( + v.model_dump() + for v in await create_library_agent(graph, user["id"]) + ) except Exception as e: print(f"Error creating library agent: {e}") continue From 61d08926862c33bd141dd08414ef42b378ea0cb7 Mon Sep 17 00:00:00 2001 From: Ubbe Date: Mon, 21 Jul 2025 17:35:52 +0400 Subject: [PATCH 007/200] fix(frontend): init LD on the client (#10414) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes 🏗️ Launch Darkly is not being initialised in production, despite on paper all env variables being well set 🧐 I tried doing production builds locally, and I noticed this provider needs to be initialised on the client because Launch Darkly flags are designed to work on the client side, where they can access user context and browser environment. ## Checklist 📋 ### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Did production build locally and run it - [x] See LD initialised on the console after the `use client` directive was added ### For configuration changes: None --- .../layout/Navbar/components/AccountMenu/AccountMenu.tsx | 5 ++++- .../src/services/feature-flags/feature-flag-provider.tsx | 2 ++ autogpt_platform/frontend/src/tests/signin.spec.ts | 9 ++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/autogpt_platform/frontend/src/components/layout/Navbar/components/AccountMenu/AccountMenu.tsx b/autogpt_platform/frontend/src/components/layout/Navbar/components/AccountMenu/AccountMenu.tsx index 18312607e59f..794c0aab4220 100644 --- a/autogpt_platform/frontend/src/components/layout/Navbar/components/AccountMenu/AccountMenu.tsx +++ b/autogpt_platform/frontend/src/components/layout/Navbar/components/AccountMenu/AccountMenu.tsx @@ -63,7 +63,10 @@ export function AccountMenu({
{userName}
-
+
{userEmail}
diff --git a/autogpt_platform/frontend/src/services/feature-flags/feature-flag-provider.tsx b/autogpt_platform/frontend/src/services/feature-flags/feature-flag-provider.tsx index 10677f9b6c1d..d47026b50d62 100644 --- a/autogpt_platform/frontend/src/services/feature-flags/feature-flag-provider.tsx +++ b/autogpt_platform/frontend/src/services/feature-flags/feature-flag-provider.tsx @@ -1,3 +1,5 @@ +"use client"; + import { LDProvider } from "launchdarkly-react-client-sdk"; import { ReactNode } from "react"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; diff --git a/autogpt_platform/frontend/src/tests/signin.spec.ts b/autogpt_platform/frontend/src/tests/signin.spec.ts index 4a102ec68727..21993d5224ef 100644 --- a/autogpt_platform/frontend/src/tests/signin.spec.ts +++ b/autogpt_platform/frontend/src/tests/signin.spec.ts @@ -31,7 +31,7 @@ test("check the navigation when logged out", async ({ page }) => { test("user can login successfully", async ({ page }) => { const testUser = await getTestUser(); const loginPage = new LoginPage(page); - const { getId, getButton, getText, getRole } = getSelectors(page); + const { getId, getButton, getRole } = getSelectors(page); await loginPage.login(testUser.email, testUser.password); await hasUrl(page, "/marketplace"); @@ -44,8 +44,11 @@ test("user can login successfully", async ({ page }) => { const accountMenuPopover = getRole("dialog"); await isVisible(accountMenuPopover); - const username = testUser.email.split("@")[0]; - await isVisible(getText(username)); + const accountMenuUserEmail = getId("account-menu-user-email"); + await isVisible(accountMenuUserEmail); + await test + .expect(accountMenuUserEmail) + .toHaveText(testUser.email.split("@")[0].toLowerCase()); const logoutBtn = getButton("Log out"); await isVisible(logoutBtn); From 3b963e59ccd083c041ca44f6656b42c8268097d7 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 22 Jul 2025 08:42:05 +0800 Subject: [PATCH 008/200] feat(platform): Move NotificationManager service from rest-api pod to scheduler pod (#10425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rest-Api service is a vital service where we should minimize the amount of external resources impacting it. And the NotificationManager service is running as a singleton in nature (similar to the scheduler service), so it makes sense to put it in a single pod. ### Changes 🏗️ Move the NotificationManager service from rest-api pod to the scheduler pod ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Manual test --- autogpt_platform/backend/backend/rest.py | 2 -- autogpt_platform/backend/backend/scheduler.py | 6 +++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/autogpt_platform/backend/backend/rest.py b/autogpt_platform/backend/backend/rest.py index 0fb1eed87533..4a45d407d189 100644 --- a/autogpt_platform/backend/backend/rest.py +++ b/autogpt_platform/backend/backend/rest.py @@ -1,6 +1,5 @@ from backend.app import run_processes from backend.executor import DatabaseManager -from backend.notifications.notifications import NotificationManager from backend.server.rest_api import AgentServer @@ -9,7 +8,6 @@ def main(): Run all the processes required for the AutoGPT-server REST API. """ run_processes( - NotificationManager(), DatabaseManager(), AgentServer(), ) diff --git a/autogpt_platform/backend/backend/scheduler.py b/autogpt_platform/backend/backend/scheduler.py index 22be4bf7fd87..b42e32c2d908 100644 --- a/autogpt_platform/backend/backend/scheduler.py +++ b/autogpt_platform/backend/backend/scheduler.py @@ -1,12 +1,16 @@ from backend.app import run_processes from backend.executor.scheduler import Scheduler +from backend.notifications.notifications import NotificationManager def main(): """ Run all the processes required for the AutoGPT-server Scheduling System. """ - run_processes(Scheduler()) + run_processes( + NotificationManager(), + Scheduler(), + ) if __name__ == "__main__": From 6d25e8f1950714626a6cc0da8462418cc567206e Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 22 Jul 2025 08:42:05 +0800 Subject: [PATCH 009/200] feat(platform): Move NotificationManager service from rest-api pod to scheduler pod (#10425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rest-Api service is a vital service where we should minimize the amount of external resources impacting it. And the NotificationManager service is running as a singleton in nature (similar to the scheduler service), so it makes sense to put it in a single pod. ### Changes 🏗️ Move the NotificationManager service from rest-api pod to the scheduler pod ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Manual test --- autogpt_platform/backend/backend/rest.py | 2 -- autogpt_platform/backend/backend/scheduler.py | 6 +++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/autogpt_platform/backend/backend/rest.py b/autogpt_platform/backend/backend/rest.py index 0fb1eed87533..4a45d407d189 100644 --- a/autogpt_platform/backend/backend/rest.py +++ b/autogpt_platform/backend/backend/rest.py @@ -1,6 +1,5 @@ from backend.app import run_processes from backend.executor import DatabaseManager -from backend.notifications.notifications import NotificationManager from backend.server.rest_api import AgentServer @@ -9,7 +8,6 @@ def main(): Run all the processes required for the AutoGPT-server REST API. """ run_processes( - NotificationManager(), DatabaseManager(), AgentServer(), ) diff --git a/autogpt_platform/backend/backend/scheduler.py b/autogpt_platform/backend/backend/scheduler.py index 22be4bf7fd87..b42e32c2d908 100644 --- a/autogpt_platform/backend/backend/scheduler.py +++ b/autogpt_platform/backend/backend/scheduler.py @@ -1,12 +1,16 @@ from backend.app import run_processes from backend.executor.scheduler import Scheduler +from backend.notifications.notifications import NotificationManager def main(): """ Run all the processes required for the AutoGPT-server Scheduling System. """ - run_processes(Scheduler()) + run_processes( + NotificationManager(), + Scheduler(), + ) if __name__ == "__main__": From f4a179e5d6950dc417d59756d8f49cd6f464b593 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 22 Jul 2025 09:11:46 +0800 Subject: [PATCH 010/200] feat(backend): Add thread safety to NodeExecutionProgress output handling (#10415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add thread safety to NodeExecutionProgress class to prevent race conditions between graph executor and node executor threads - Fixes potential data corruption and lost outputs during concurrent access to shared output lists - Uses single global lock per node for minimal performance impact - Instead of blocking the node evaluation before adding another node evaluation, we move on to the next node, in case another node completes it. ## Changes - Added `threading.Lock` to NodeExecutionProgress class - Protected `add_output()` calls from node executor thread with lock - Protected `pop_output()` calls from graph executor thread with lock - Protected `_pop_done_task()` output checks with lock ## Problem Solved The `NodeExecutionProgress.output` dictionary was being accessed concurrently: - `add_output()` called from node executor thread (asyncio thread) - `pop_output()` called from graph executor thread (main thread) - Python lists are not thread-safe for concurrent append/pop operations - This could cause data corruption, index errors, and lost outputs ## Test Plan - [x] Existing executor tests pass - [x] No performance regression (operations are microsecond-level) - [x] Thread safety verified through code analysis ## Technical Details - Single `threading.Lock()` per NodeExecutionProgress instance (~64 bytes) - Lock acquisition time (~100-200ns) is minimal compared to list operations - Maintains order guarantees for same node_execution_id processing - No GIL contention issues as operations are very brief 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude --- .../backend/backend/executor/manager.py | 8 +++++--- .../backend/backend/executor/utils.py | 15 ++++++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/autogpt_platform/backend/backend/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py index 709b37560480..df246abb05a3 100644 --- a/autogpt_platform/backend/backend/executor/manager.py +++ b/autogpt_platform/backend/backend/executor/manager.py @@ -783,11 +783,13 @@ def on_done_task(node_exec_id: str, result: object): # node evaluation future ----------------- if inflight_eval := running_node_evaluation.get(node_id): + if not inflight_eval.done(): + continue try: - inflight_eval.result() + inflight_eval.result(timeout=0) running_node_evaluation.pop(node_id) - except TimeoutError: - continue + except Exception as e: + log_metadata.error(f"Node eval #{node_id} failed: {e}") # node execution future --------------------------- if inflight_exec.is_done(): diff --git a/autogpt_platform/backend/backend/executor/utils.py b/autogpt_platform/backend/backend/executor/utils.py index e27c43d63f2d..8568c4139441 100644 --- a/autogpt_platform/backend/backend/executor/utils.py +++ b/autogpt_platform/backend/backend/executor/utils.py @@ -1,5 +1,6 @@ import asyncio import logging +import threading import time from collections import defaultdict from concurrent.futures import Future @@ -885,12 +886,14 @@ def __init__( self.output: dict[str, list[ExecutionOutputEntry]] = defaultdict(list) self.tasks: dict[str, Future] = {} self.on_done_task = on_done_task + self._lock = threading.Lock() def add_task(self, node_exec_id: str, task: Future): self.tasks[node_exec_id] = task def add_output(self, output: ExecutionOutputEntry): - self.output[output.node_exec_id].append(output) + with self._lock: + self.output[output.node_exec_id].append(output) def pop_output(self) -> ExecutionOutputEntry | None: exec_id = self._next_exec() @@ -900,8 +903,9 @@ def pop_output(self) -> ExecutionOutputEntry | None: if self._pop_done_task(exec_id): return self.pop_output() - if next_output := self.output[exec_id]: - return next_output.pop(0) + with self._lock: + if next_output := self.output[exec_id]: + return next_output.pop(0) return None @@ -966,8 +970,9 @@ def _pop_done_task(self, exec_id: str) -> bool: if not task.done(): return False - if self.output[exec_id]: - return False + with self._lock: + if self.output[exec_id]: + return False if task := self.tasks.pop(exec_id): try: From ae6ef8c0c2ddbd6af243c7fcb447752105f5b24f Mon Sep 17 00:00:00 2001 From: Nicholas Tindle Date: Tue, 22 Jul 2025 06:47:04 -0500 Subject: [PATCH 011/200] refactor(frontend): improve LaunchDarkly provider initialization (#10422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Changes ️ The previous implementation of the `LaunchDarklyProvider` had a race condition where it would only initialize after the user's authentication state was fully resolved. This caused two primary issues: 1. A delay in evaluating any feature flags, leading to a "flash of un-styled/un-flagged content" until the user session was loaded. 2. An unreliable transition from an un-flagged state to a flagged state, which could cause UI flicker or incorrect flag evaluations upon login. This pull request refactors the provider to follow a more robust, industry-standard pattern. It now initializes immediately with an `anonymous` context, ensuring flags are available from the very start of the application lifecycle. When the user logs in and their session becomes available, the provider seamlessly transitions to an authenticated context, guaranteeing that the correct flags are evaluated consistently. ### Checklist #### For code changes: - I have clearly listed my changes in the PR description - I have made a test plan - I have tested my changes according to the test plan: **Test Plan:** - [x] **Anonymous User:** Load the application in an incognito window without logging in. Verify that feature flags are evaluated correctly for an anonymous user. Check the browser console for the `[LaunchDarklyProvider] Using anonymous context` message. - [x] **Login Flow:** While on the site, log in. Verify that the UI updates with the correct feature flags for the authenticated user. Check the console for the `[LaunchDarklyProvider] Using authenticated context` message and confirm the LaunchDarkly client re-initializes. - [x] **Authenticated User (Page Refresh):** As a logged-in user, refresh the page. Verify that the application loads directly with the authenticated user's flags, leveraging the cached session and bootstrapped flags from `localStorage`. - [x] **Logout Flow:** While logged in, log out. Verify that the UI reverts to the anonymous user's state and flags. The provider `key` should change back to "anonymous", triggering another re-mount.
Summary of Code Changes - Refactored `LaunchDarklyProvider` to handle user authentication state changes gracefully. - The provider now initializes immediately with an `anonymous` user context while the Supabase user session is loading. - Once the user is authenticated, the provider's context is updated to reflect the logged-in user's details. - Added a `key` prop to the `` component, using the user's ID (or "anonymous"). This forces React to re-mount the provider when the user's identity changes, ensuring a clean re-initialization of the LaunchDarkly SDK. - Enabled `localStorage` bootstrapping (`options={{ bootstrap: "localStorage" }}`) to cache flags and improve performance on subsequent page loads. - Added `console.debug` statements for improved observability into the provider's state (anonymous vs. authenticated).
#### For configuration changes: - `.env.example` is updated or already compatible with my changes - `docker-compose.yml` is updated or already compatible with my changes - I have included a list of my configuration changes in the PR description (under **Changes**)
Configuration Changes - No configuration changes were made. This PR relies on existing environment variables (`NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID` and `NEXT_PUBLIC_LAUNCHDARKLY_ENABLED`).
--------- Co-authored-by: Lluis Agusti --- .../feature-flags/feature-flag-provider.tsx | 63 +++++++++++++------ 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/autogpt_platform/frontend/src/services/feature-flags/feature-flag-provider.tsx b/autogpt_platform/frontend/src/services/feature-flags/feature-flag-provider.tsx index d47026b50d62..ad12b0e131fc 100644 --- a/autogpt_platform/frontend/src/services/feature-flags/feature-flag-provider.tsx +++ b/autogpt_platform/frontend/src/services/feature-flags/feature-flag-provider.tsx @@ -1,7 +1,8 @@ "use client"; import { LDProvider } from "launchdarkly-react-client-sdk"; -import { ReactNode } from "react"; +import type { ReactNode } from "react"; +import { useMemo } from "react"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { BehaveAs, getBehaveAs } from "@/lib/utils"; @@ -9,33 +10,59 @@ const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID; const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true"; export function LaunchDarklyProvider({ children }: { children: ReactNode }) { - const { user } = useSupabase(); + const { user, isUserLoading } = useSupabase(); const isCloud = getBehaveAs() === BehaveAs.CLOUD; - const enabled = isCloud && envEnabled && clientId && user; + const isLaunchDarklyConfigured = isCloud && envEnabled && clientId; - if (!enabled) return <>{children}; - - const userContext = user - ? { - kind: "user", - key: user.id, - email: user.email, - anonymous: false, - custom: { - role: user.role, - }, - } - : { - kind: "user", + const context = useMemo(() => { + if (isUserLoading || !user) { + console.debug("[LaunchDarklyProvider] Using anonymous context", { + isUserLoading, + hasUser: !!user, + }); + return { + kind: "user" as const, key: "anonymous", anonymous: true, }; + } + + console.debug("[LaunchDarklyProvider] Using authenticated context", { + userId: user.id, + email: user.email, + role: user.role, + }); + return { + kind: "user" as const, + key: user.id, + ...(user.email && { email: user.email }), + anonymous: false, + custom: { + ...(user.role && { role: user.role }), + }, + }; + }, [user, isUserLoading]); + + if (!isLaunchDarklyConfigured) { + console.debug( + "[LaunchDarklyProvider] Not configured for this environment", + { + isCloud, + envEnabled, + hasClientId: !!clientId, + }, + ); + return <>{children}; + } return ( {children} From f8b9e80829cefdcb8033cd31bb77a07307d9e2b1 Mon Sep 17 00:00:00 2001 From: Nicholas Tindle Date: Tue, 22 Jul 2025 10:41:20 -0500 Subject: [PATCH 012/200] feat(backend): enable the google blocks + fix .env (#10430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We setup launchdarkly so now we can dynamically control which blocks are available on the UI ### Changes 🏗️ enables the google blocks + fixes the LD .env.example for the local env ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Deploy to test environment and verify the blocks show correctly vs are hidden when toggling Launch darlky rules --- .../backend/backend/blocks/google/calendar.py | 12 +++--------- .../backend/backend/blocks/google/sheets.py | 7 ++----- autogpt_platform/frontend/.env.example | 2 +- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/autogpt_platform/backend/backend/blocks/google/calendar.py b/autogpt_platform/backend/backend/blocks/google/calendar.py index 27cc9e595815..c75e0ec9eb68 100644 --- a/autogpt_platform/backend/backend/blocks/google/calendar.py +++ b/autogpt_platform/backend/backend/blocks/google/calendar.py @@ -10,7 +10,7 @@ from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.settings import AppEnvironment, Settings +from backend.util.settings import Settings from ._auth import ( GOOGLE_OAUTH_IS_CONFIGURED, @@ -88,8 +88,6 @@ class Output(BlockSchema): ) def __init__(self): - settings = Settings() - # Create realistic test data for events test_now = datetime.now(tz=timezone.utc) test_tomorrow = test_now + timedelta(days=1) @@ -116,8 +114,7 @@ def __init__(self): categories={BlockCategory.PRODUCTIVITY, BlockCategory.DATA}, input_schema=GoogleCalendarReadEventsBlock.Input, output_schema=GoogleCalendarReadEventsBlock.Output, - disabled=not GOOGLE_OAUTH_IS_CONFIGURED - or settings.config.app_env == AppEnvironment.PRODUCTION, + disabled=not GOOGLE_OAUTH_IS_CONFIGURED, test_input={ "credentials": TEST_CREDENTIALS_INPUT, "calendar_id": "primary", @@ -442,16 +439,13 @@ class Output(BlockSchema): error: str = SchemaField(description="Error message if event creation failed") def __init__(self): - settings = Settings() - super().__init__( id="ed2ec950-fbff-4204-94c0-023fb1d625e0", description="This block creates a new event in Google Calendar with customizable parameters.", categories={BlockCategory.PRODUCTIVITY}, input_schema=GoogleCalendarCreateEventBlock.Input, output_schema=GoogleCalendarCreateEventBlock.Output, - disabled=not GOOGLE_OAUTH_IS_CONFIGURED - or settings.config.app_env == AppEnvironment.PRODUCTION, + disabled=not GOOGLE_OAUTH_IS_CONFIGURED, test_input={ "credentials": TEST_CREDENTIALS_INPUT, "event_title": "Team Meeting", diff --git a/autogpt_platform/backend/backend/blocks/google/sheets.py b/autogpt_platform/backend/backend/blocks/google/sheets.py index 6119fd15279a..6e63958c828c 100644 --- a/autogpt_platform/backend/backend/blocks/google/sheets.py +++ b/autogpt_platform/backend/backend/blocks/google/sheets.py @@ -7,7 +7,7 @@ from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.settings import AppEnvironment, Settings +from backend.util.settings import Settings from ._auth import ( GOOGLE_OAUTH_IS_CONFIGURED, @@ -19,10 +19,7 @@ ) settings = Settings() -GOOGLE_SHEETS_DISABLED = ( - not GOOGLE_OAUTH_IS_CONFIGURED - or settings.config.app_env == AppEnvironment.PRODUCTION -) +GOOGLE_SHEETS_DISABLED = not GOOGLE_OAUTH_IS_CONFIGURED def parse_a1_notation(a1: str) -> tuple[str | None, str]: diff --git a/autogpt_platform/frontend/.env.example b/autogpt_platform/frontend/.env.example index 5e1edb8a86a8..a23ac835518a 100644 --- a/autogpt_platform/frontend/.env.example +++ b/autogpt_platform/frontend/.env.example @@ -5,7 +5,7 @@ NEXT_PUBLIC_AGPT_SERVER_URL=http://localhost:8006/api NEXT_PUBLIC_AGPT_WS_SERVER_URL=ws://localhost:8001/ws NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8015/api/v1/market NEXT_PUBLIC_LAUNCHDARKLY_ENABLED=false -NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=687910b9638a3d099c11ab7f # Local environment on Launch darkly +NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=687ab1372f497809b131e06e # Local environment on Launch darkly NEXT_PUBLIC_APP_ENV=local NEXT_PUBLIC_AGPT_SERVER_BASE_URL=http://localhost:8006 From a58613a84c0bee8de2992264de4f088be685ce52 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Wed, 23 Jul 2025 00:21:08 +0800 Subject: [PATCH 013/200] fix(backend): Fix Agent Input with empty string default value not being rendered (#10431) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some AgentInput can store empty string as the default value, this will cause error on non string input. Error: ``` 2025-07-22 23:14:10,424 WARNING Invalid : {'name': 'Enriched info (including email), will double the search cost', 'value': '', 'advanced': False, 'placeholder_values': []}, 1 validation error for Input value Input should be a valid boolean, unable to interpret input [type=bool_parsing, input_value='', input_type=str] For further information visit https://errors.pydantic.dev/2.11/v/bool_parsing 2025-07-22 23:14:10,424 WARNING Invalid : {'name': 'Expected New Leads Count', 'value': '', 'advanced': False, 'placeholder_values': []}, 1 validation error for Input value Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='', input_type=str] For further information visit https://errors.pydantic.dev/2.11/v/int_parsing ``` ### Changes 🏗️ Ignore invalid field when constructing agent input schema. ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Use AgentNumberInput and AgenToggleInput with empty string value. --- autogpt_platform/backend/backend/data/graph.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index f2ec27bd60ac..c301e3a5c18d 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -212,9 +212,9 @@ def _generate_schema( schema_fields: list[AgentInputBlock.Input | AgentOutputBlock.Input] = [] for type_class, input_default in props: try: - schema_fields.append(type_class(**input_default)) + schema_fields.append(type_class.model_construct(**input_default)) except Exception as e: - logger.warning(f"Invalid {type_class}: {input_default}, {e}") + logger.error(f"Invalid {type_class}: {input_default}, {e}") return { "type": "object", From 41363b1cbe39ca58e700bc61745b114b2aeed5bd Mon Sep 17 00:00:00 2001 From: Ubbe Date: Tue, 22 Jul 2025 21:29:09 +0400 Subject: [PATCH 014/200] feat(frontend): agent activity dropdown (#10416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes 🏗️ https://github.com/user-attachments/assets/42e1c896-5f3b-447c-aee9-4f5963c217d9 There is now a 🔔 icon on the Navigation bar that shows previous agent runs and displays real-time agent running status. If you run an agent, the bell will show on a badge how many agents are running. If you hover over it, a hint appears. If you click on it, it opens a dropdown and displays the executions with their status ( _which should match what we have in library functionality, not design-wise_ ). I leveraged the existing APIs for this purpose. Most of the run logic is [encapsulated on this hook](https://github.com/Significant-Gravitas/AutoGPT/compare/dev...feat/agent-notifications?expand=1#diff-a9e7f2904d6283b094aca19b64c7168e8c66be1d5e0bb454be8978cb98526617) and is also an independent `` component. Clicking on an agent run opens that run in the library page. This new functionality is covered by E2E tests 💆🏽 ✔️ ## Checklist 📋 ### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] The navigation bar layout looks good when logged out - [x] The navigation bar layout looks good when logged in - [x] Open an agent in the library and click `Run` - [x] See the real-time activity of the agent running on the navigation bar bell icon ### For configuration changes: _No configuration changes needed._ --- .github/workflows/platform-frontend-ci.yml | 39 ++- autogpt_platform/frontend/.env.example | 4 + autogpt_platform/frontend/README.md | 41 +++ autogpt_platform/frontend/package.json | 1 + .../frontend/playwright.config.ts | 9 +- autogpt_platform/frontend/pnpm-lock.yaml | 33 ++ .../(platform)/library/agents/[id]/page.tsx | 11 +- .../frontend/src/app/providers.tsx | 40 ++- .../src/components/PrimaryActionButton.tsx | 3 +- .../agents/agent-run-details-view.tsx | 1 + .../agents/agent-runs-selector-list.tsx | 26 +- .../src/components/layout/Navbar/Navbar.tsx | 2 +- .../AgentActivityDropdown.tsx | 70 ++++ .../components/ActivityDropdown.tsx | 92 +++++ .../components/ActivityItem.tsx | 113 ++++++ .../AgentActivityDropdown/helpers.tsx | 324 ++++++++++++++++++ .../useAgentActivityDropdown.ts | 164 +++++++++ .../{NavbarMainPage.tsx => NavbarView.tsx} | 10 +- .../src/components/layout/Navbar/useNavbar.ts | 28 ++ .../frontend/src/components/tutorial.ts | 18 +- .../frontend/src/components/ui/popover.tsx | 8 +- .../frontend/src/lib/supabase/helpers.ts | 9 - .../feature-flags/feature-flag-provider.tsx | 17 - .../services/feature-flags/use-get-flag.ts | 14 +- .../frontend/src/tests/agent-activity.spec.ts | 105 ++++++ .../frontend/src/tests/pages/build.page.ts | 24 +- .../frontend/src/tests/pages/library.page.ts | 6 +- 27 files changed, 1112 insertions(+), 100 deletions(-) create mode 100644 autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/AgentActivityDropdown.tsx create mode 100644 autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown.tsx create mode 100644 autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityItem.tsx create mode 100644 autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/helpers.tsx create mode 100644 autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/useAgentActivityDropdown.ts rename autogpt_platform/frontend/src/components/layout/Navbar/components/{NavbarMainPage.tsx => NavbarView.tsx} (90%) create mode 100644 autogpt_platform/frontend/src/components/layout/Navbar/useNavbar.ts create mode 100644 autogpt_platform/frontend/src/tests/agent-activity.spec.ts diff --git a/.github/workflows/platform-frontend-ci.yml b/.github/workflows/platform-frontend-ci.yml index 749e5232c0c9..ed57d5d1fe3f 100644 --- a/.github/workflows/platform-frontend-ci.yml +++ b/.github/workflows/platform-frontend-ci.yml @@ -151,7 +151,7 @@ jobs: exitOnceUploaded: true test: - runs-on: ubuntu-latest + runs-on: big-boi needs: setup strategy: fail-fast: false @@ -170,12 +170,6 @@ jobs: - name: Enable corepack run: corepack enable - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@main - with: - large-packages: false # slow - docker-images: false # limited benefit - - name: Copy default supabase .env run: | cp ../.env.example ../.env @@ -184,9 +178,24 @@ jobs: run: | cp ../backend/.env.example ../backend/.env + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-frontend-test-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-frontend-test- + - name: Run docker compose run: | docker compose -f ../docker-compose.yml up -d + env: + DOCKER_BUILDKIT: 1 + BUILDX_CACHE_FROM: type=local,src=/tmp/.buildx-cache + BUILDX_CACHE_TO: type=local,dest=/tmp/.buildx-cache-new,mode=max - name: Restore dependencies cache uses: actions/cache@v4 @@ -205,6 +214,8 @@ jobs: - name: Build frontend run: pnpm build --turbo # uses Turbopack, much faster and safe enough for a test pipeline + env: + NEXT_PUBLIC_PW_TEST: true - name: Install Browser 'chromium' run: pnpm playwright install --with-deps chromium @@ -212,13 +223,13 @@ jobs: - name: Run Playwright tests run: pnpm test:no-build + - name: Upload Playwright artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report + - name: Print Final Docker Compose logs if: always() run: docker compose -f ../docker-compose.yml logs - - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report-${{ matrix.browser }} - path: playwright-report/ - retention-days: 30 diff --git a/autogpt_platform/frontend/.env.example b/autogpt_platform/frontend/.env.example index a23ac835518a..6532e200b3fd 100644 --- a/autogpt_platform/frontend/.env.example +++ b/autogpt_platform/frontend/.env.example @@ -38,3 +38,7 @@ NEXT_PUBLIC_TURNSTILE=disabled # Devtools NEXT_PUBLIC_REACT_QUERY_DEVTOOL=true + +# In case you are running Playwright locally +# NEXT_PUBLIC_PW_TEST=true + diff --git a/autogpt_platform/frontend/README.md b/autogpt_platform/frontend/README.md index 94c65a6e3b21..c0b14448a58c 100644 --- a/autogpt_platform/frontend/README.md +++ b/autogpt_platform/frontend/README.md @@ -207,6 +207,47 @@ The Orval configuration is located in `autogpt_platform/frontend/orval.config.ts For more details, see the [Orval documentation](https://orval.dev/) or check the configuration file. +## 🚩 Feature Flags + +This project uses [LaunchDarkly](https://launchdarkly.com/) for feature flags, allowing us to control feature rollouts and A/B testing. + +### Using Feature Flags + +#### Check if a feature is enabled + +```typescript +import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; + +function MyComponent() { + const isAgentActivityEnabled = useGetFlag(Flag.AGENT_ACTIVITY); + + if (!isAgentActivityEnabled) { + return null; // Hide feature + } + + return
Feature is enabled!
; +} +``` + +#### Protect entire components + +```typescript +import { withFeatureFlag } from "@/services/feature-flags/with-feature-flag"; + +const MyFeaturePage = withFeatureFlag(MyPageComponent, "my-feature-flag"); +``` + +### Testing with Feature Flags + +For local development or running Playwright tests locally, use mocked feature flags by setting `NEXT_PUBLIC_PW_TEST=true` in your `.env` file. This bypasses LaunchDarkly and uses the mock values defined in the code. + +### Adding New Flags + +1. Add the flag to the `Flag` enum in `use-get-flag.ts` +2. Add the flag type to `FlagValues` type +3. Add mock value to `mockFlags` for testing +4. Configure the flag in LaunchDarkly dashboard + ## 🚚 Deploy TODO diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index 5b34a7bb4f87..d9e89aebd8a8 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -75,6 +75,7 @@ "moment": "2.30.1", "next": "15.3.5", "next-themes": "0.4.6", + "nuqs": "2.4.3", "party-js": "2.2.0", "react": "18.3.1", "react-day-picker": "9.8.0", diff --git a/autogpt_platform/frontend/playwright.config.ts b/autogpt_platform/frontend/playwright.config.ts index 58bed015a571..910b2fefb291 100644 --- a/autogpt_platform/frontend/playwright.config.ts +++ b/autogpt_platform/frontend/playwright.config.ts @@ -24,23 +24,26 @@ export default defineConfig({ /* use more workers on CI. */ workers: process.env.CI ? 4 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [["html"], ["line"]], + reporter: [["list"], ["html", { open: "never" }]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: "http://localhost:3000/", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "on-first-retry", screenshot: "only-on-failure", bypassCSP: true, + + /* Helps debugging failures */ + trace: "retain-on-failure", + video: "retain-on-failure", }, /* Maximum time one test can run for */ timeout: 25000, /* Configure web server to start automatically */ webServer: { - command: "NEXT_PUBLIC_PW_TEST=true pnpm start", + command: "pnpm start", url: "http://localhost:3000", reuseExistingServer: !process.env.CI, }, diff --git a/autogpt_platform/frontend/pnpm-lock.yaml b/autogpt_platform/frontend/pnpm-lock.yaml index 0dd33827099a..a48cf3cbb3a4 100644 --- a/autogpt_platform/frontend/pnpm-lock.yaml +++ b/autogpt_platform/frontend/pnpm-lock.yaml @@ -155,6 +155,9 @@ importers: next-themes: specifier: 0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nuqs: + specifier: 2.4.3 + version: 2.4.3(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) party-js: specifier: 2.2.0 version: 2.2.0 @@ -5333,6 +5336,9 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} @@ -5463,6 +5469,24 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nuqs@2.4.3: + resolution: {integrity: sha512-BgtlYpvRwLYiJuWzxt34q2bXu/AIS66sLU1QePIMr2LWkb+XH0vKXdbLSgn9t6p7QKzwI7f38rX3Wl9llTXQ8Q==} + peerDependencies: + '@remix-run/react': '>=2' + next: '>=14.2.0' + react: '>=18.2.0 || ^19.0.0-0' + react-router: ^6 || ^7 + react-router-dom: ^6 || ^7 + peerDependenciesMeta: + '@remix-run/react': + optional: true + next: + optional: true + react-router: + optional: true + react-router-dom: + optional: true + oas-kit-common@1.0.8: resolution: {integrity: sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==} @@ -13010,6 +13034,8 @@ snapshots: minipass@7.1.2: {} + mitt@3.0.1: {} + module-details-from-path@1.0.4: {} moment@2.30.1: {} @@ -13172,6 +13198,13 @@ snapshots: dependencies: boolbase: 1.0.0 + nuqs@2.4.3(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + dependencies: + mitt: 3.0.1 + react: 18.3.1 + optionalDependencies: + next: 15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + oas-kit-common@1.0.8: dependencies: fast-safe-stringify: 2.1.1 diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx index a4d686edc631..710ef8dc229f 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx @@ -1,5 +1,6 @@ "use client"; import { useParams, useRouter } from "next/navigation"; +import { useQueryState } from "nuqs"; import React, { useCallback, useEffect, @@ -45,6 +46,7 @@ import { useToast } from "@/components/molecules/Toast/use-toast"; export default function AgentRunsPage(): React.ReactElement { const { id: agentID }: { id: LibraryAgentID } = useParams(); + const [executionId, setExecutionId] = useQueryState("executionId"); const { toast } = useToast(); const router = useRouter(); const api = useBackendAPI(); @@ -202,6 +204,13 @@ export default function AgentRunsPage(): React.ReactElement { selectPreset, ]); + useEffect(() => { + if (executionId) { + selectRun(executionId as GraphExecutionID); + setExecutionId(null); + } + }, [executionId, selectRun, setExecutionId]); + // Initial load useEffect(() => { refreshPageData(); @@ -468,7 +477,7 @@ export default function AgentRunsPage(): React.ReactElement { } return ( -
+
{/* Sidebar w/ list of runs */} {/* TODO: render this below header in sm and md layouts */} - - - - - - {children} - - - - - + + + + + + + {children} + + + + + + ); } diff --git a/autogpt_platform/frontend/src/components/PrimaryActionButton.tsx b/autogpt_platform/frontend/src/components/PrimaryActionButton.tsx index e5a07d1d5a99..1d2134321b72 100644 --- a/autogpt_platform/frontend/src/components/PrimaryActionButton.tsx +++ b/autogpt_platform/frontend/src/components/PrimaryActionButton.tsx @@ -59,7 +59,8 @@ const PrimaryActionBar: React.FC = ({ onClick={onClickRunAgent} disabled={!onClickRunAgent} title="Run the agent" - data-id="primary-action-run-agent" + aria-label="Run the agent" + data-testid="primary-action-run-agent" > Run diff --git a/autogpt_platform/frontend/src/components/agents/agent-run-details-view.tsx b/autogpt_platform/frontend/src/components/agents/agent-run-details-view.tsx index e97dfd3d4ba0..4588f69a2ba6 100644 --- a/autogpt_platform/frontend/src/components/agents/agent-run-details-view.tsx +++ b/autogpt_platform/frontend/src/components/agents/agent-run-details-view.tsx @@ -180,6 +180,7 @@ export default function AgentRunDetailsView({ ), callback: runAgain, + dataTestId: "run-again-button", }, ] : []), diff --git a/autogpt_platform/frontend/src/components/agents/agent-runs-selector-list.tsx b/autogpt_platform/frontend/src/components/agents/agent-runs-selector-list.tsx index 7d6dc483828f..5718baf894de 100644 --- a/autogpt_platform/frontend/src/components/agents/agent-runs-selector-list.tsx +++ b/autogpt_platform/frontend/src/components/agents/agent-runs-selector-list.tsx @@ -1,8 +1,7 @@ "use client"; -import React, { useEffect, useState } from "react"; import { Plus } from "lucide-react"; +import React, { useEffect, useState } from "react"; -import { cn } from "@/lib/utils"; import { GraphExecutionID, GraphExecutionMeta, @@ -12,14 +11,15 @@ import { Schedule, ScheduleID, } from "@/lib/autogpt-server-api"; +import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; -import { Button } from "@/components/agptui/Button"; -import { Badge } from "@/components/ui/badge"; import { agentRunStatusMap } from "@/components/agents/agent-run-status-chip"; import AgentRunSummaryCard from "@/components/agents/agent-run-summary-card"; +import { Button } from "../atoms/Button/Button"; interface AgentRunsSelectorListProps { agent: LibraryAgent; @@ -72,17 +72,11 @@ export default function AgentRunsSelectorList({