+
+
- {isAgentFavoritingEnabled && (
-
- )}
+
+
void;
+ onClick: (e: MouseEvent) => void;
+ className?: string;
}
-export function FavoriteButton({ isFavorite, onClick }: FavoriteButtonProps) {
+export function FavoriteButton({
+ isFavorite,
+ onClick,
+ className,
+}: FavoriteButtonProps) {
return (
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts
index 4f86b89278..4232847226 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts
@@ -8,7 +8,6 @@ import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { okData } from "@/app/api/helpers";
import { useToast } from "@/components/molecules/Toast/use-toast";
-import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { updateFavoriteInQueries } from "./helpers";
interface Props {
@@ -20,7 +19,6 @@ export function useLibraryAgentCard({ agent }: Props) {
agent;
const isFromMarketplace = Boolean(marketplace_listing);
- const isAgentFavoritingEnabled = useGetFlag(Flag.AGENT_FAVORITING);
const [isFavorite, setIsFavorite] = useState(is_favorite);
const { toast } = useToast();
const queryClient = getQueryClient();
@@ -49,8 +47,6 @@ export function useLibraryAgentCard({ agent }: Props) {
e.preventDefault();
e.stopPropagation();
- if (!isAgentFavoritingEnabled) return;
-
const newIsFavorite = !isFavorite;
setIsFavorite(newIsFavorite);
@@ -80,7 +76,6 @@ export function useLibraryAgentCard({ agent }: Props) {
return {
isFromMarketplace,
- isAgentFavoritingEnabled,
isFavorite,
profile,
creator_image_url,
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/hooks/useFavoriteAgents.ts b/autogpt_platform/frontend/src/app/(platform)/library/hooks/useFavoriteAgents.ts
index 933670ca80..d5f8cce057 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/hooks/useFavoriteAgents.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/library/hooks/useFavoriteAgents.ts
@@ -1,13 +1,15 @@
"use client";
-import {
- getPaginatedTotalCount,
- getPaginationNextPageNumber,
- unpaginate,
-} from "@/app/api/helpers";
import { useGetV2ListFavoriteLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
+import { getPaginationNextPageNumber, unpaginate } from "@/app/api/helpers";
+import { useMemo } from "react";
+import { filterAgents } from "../components/LibraryAgentList/helpers";
-export function useFavoriteAgents() {
+interface Props {
+ searchTerm: string;
+}
+
+export function useFavoriteAgents({ searchTerm }: Props) {
const {
data: agentsQueryData,
fetchNextPage,
@@ -27,10 +29,16 @@ export function useFavoriteAgents() {
const allAgents = agentsQueryData
? unpaginate(agentsQueryData, "agents")
: [];
- const agentCount = getPaginatedTotalCount(agentsQueryData);
+
+ const filteredAgents = useMemo(
+ () => filterAgents(allAgents, searchTerm),
+ [allAgents, searchTerm],
+ );
+
+ const agentCount = filteredAgents.length;
return {
- allAgents,
+ allAgents: filteredAgents,
agentLoading,
hasNextPage,
agentCount,
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/page.tsx b/autogpt_platform/frontend/src/app/(platform)/library/page.tsx
index ef5c64183c..c619d480e0 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/page.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/page.tsx
@@ -17,7 +17,7 @@ export default function LibraryPage() {
return (
-
+
,
- error: ,
- warning: ,
- info: ,
+ success: null,
+ error: null,
+ warning: null,
+ info: null,
}}
/>
);
From 47a3a5ef41049e90eeaa82651f0814680ff80a46 Mon Sep 17 00:00:00 2001
From: Nicholas Tindle
Date: Fri, 9 Jan 2026 07:11:35 -0700
Subject: [PATCH 09/23] feat(backend,frontend): optional credentials flag for
blocks at agent level (#11716)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This feature allows agent makers to mark credential fields as optional.
When credentials are not configured for an optional block, the block
will be skipped during execution rather than causing a validation error.
**Use case:** An agent with multiple notification channels (Discord,
Twilio, Slack) where the user only needs to configure one - unconfigured
channels are simply skipped.
### Changes 🏗️
#### Backend
**Data Model Changes:**
- `backend/data/graph.py`: Added `credentials_optional` property to
`Node` model that reads from node metadata
- `backend/data/execution.py`: Added `nodes_to_skip` field to
`GraphExecutionEntry` model to track nodes that should be skipped
**Validation Changes:**
- `backend/executor/utils.py`:
- Updated `_validate_node_input_credentials()` to return a tuple of
`(credential_errors, nodes_to_skip)`
- Nodes with `credentials_optional=True` and missing credentials are
added to `nodes_to_skip` instead of raising validation errors
- Updated `validate_graph_with_credentials()` to propagate
`nodes_to_skip` set
- Updated `validate_and_construct_node_execution_input()` to return
`nodes_to_skip`
- Updated `add_graph_execution()` to pass `nodes_to_skip` to execution
entry
**Execution Changes:**
- `backend/executor/manager.py`:
- Added skip logic in `_on_graph_execution()` dispatch loop
- When a node is in `nodes_to_skip`, it is marked as `COMPLETED` without
execution
- No outputs are produced, so downstream nodes won't trigger
#### Frontend
**Node Store:**
- `frontend/src/app/(platform)/build/stores/nodeStore.ts`:
- Added `credentials_optional` to node metadata serialization in
`convertCustomNodeToBackendNode()`
- Added `getCredentialsOptional()` and `setCredentialsOptional()` helper
methods
**Credential Field Component:**
-
`frontend/src/components/renderers/input-renderer/fields/CredentialField/CredentialField.tsx`:
- Added "Optional - skip block if not configured" switch toggle
- Switch controls the `credentials_optional` metadata flag
- Placeholder text updates based on optional state
**Credential Field Hook:**
-
`frontend/src/components/renderers/input-renderer/fields/CredentialField/useCredentialField.ts`:
- Added `disableAutoSelect` parameter
- When credentials are optional, auto-selection of credentials is
disabled
**Feature Flags:**
- `frontend/src/services/feature-flags/use-get-flag.ts`: Minor refactor
(condition ordering)
### 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] Build an agent using smart decision maker and down stream blocks
to test this
---
> [!NOTE]
> Introduces optional credentials across graph execution and UI,
allowing nodes to be skipped (no outputs, no downstream triggers) when
their credentials are not configured.
>
> - Backend
> - Adds `Node.credentials_optional` (from node `metadata`) and computes
required credential fields in `Graph.credentials_input_schema` based on
usage.
> - Validates credentials with `_validate_node_input_credentials` →
returns `(errors, nodes_to_skip)`; plumbs `nodes_to_skip` through
`validate_graph_with_credentials`,
`_construct_starting_node_execution_input`,
`validate_and_construct_node_execution_input`, and `add_graph_execution`
into `GraphExecutionEntry`.
> - Executor: dispatch loop skips nodes in `nodes_to_skip` (marks
`COMPLETED`); `execute_node`/`on_node_execution` accept `nodes_to_skip`;
`SmartDecisionMakerBlock.run` filters tool functions whose
`_sink_node_id` is in `nodes_to_skip` and errors only if all tools are
filtered.
> - Models: `GraphExecutionEntry` gains `nodes_to_skip` field. Tests and
snapshots updated accordingly.
>
> - Frontend
> - Builder: credential field uses `custom/credential_field` with an
"Optional – skip block if not configured" toggle; `nodeStore` persists
`credentials_optional` and history; UI hides optional toggle in run
dialogs.
> - Run dialogs: compute required credentials from
`credentials_input_schema.required`; allow selecting "None"; avoid
auto-select for optional; filter out incomplete creds before execute.
> - Minor schema/UI wiring updates (`uiSchema`, form context flags).
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5e01fd6a3e22bb4360138872c7e150eba11a2ec9. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---------
Co-authored-by: Claude
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Nicholas Tindle
---
.../backend/blocks/smart_decision_maker.py | 18 ++
.../backend/backend/data/execution.py | 4 +
.../backend/backend/data/graph.py | 39 +++-
.../backend/backend/data/graph_test.py | 55 +++++
.../backend/backend/executor/manager.py | 22 ++
.../backend/backend/executor/utils.py | 123 +++++++---
.../backend/backend/executor/utils_test.py | 212 ++++++++++++++++++
.../backend/snapshots/grph_single | 1 +
autogpt_platform/backend/snapshots/grphs_all | 1 +
.../RunInputDialog/RunInputDialog.tsx | 1 +
.../RunInputDialog/useRunInputDialog.ts | 10 +-
.../components/FlowEditor/nodes/uiSchema.ts | 2 +-
.../app/(platform)/build/stores/nodeStore.ts | 32 +++
.../CredentialsInputs/CredentialsInputs.tsx | 16 +-
.../CredentialsSelect/CredentialsSelect.tsx | 31 ++-
.../CredentialsInputs/useCredentialsInput.ts | 13 +-
.../ModalRunSection/ModalRunSection.tsx | 12 +-
.../modals/RunAgentModal/useAgentRunModal.tsx | 28 ++-
.../CredentialField/CredentialField.tsx | 50 ++++-
.../components/CredentialFieldTitle.tsx | 5 +-
.../renderers/InputRenderer/types.ts | 1 +
21 files changed, 609 insertions(+), 67 deletions(-)
diff --git a/autogpt_platform/backend/backend/blocks/smart_decision_maker.py b/autogpt_platform/backend/backend/blocks/smart_decision_maker.py
index 751f6af37f..d96fa13efd 100644
--- a/autogpt_platform/backend/backend/blocks/smart_decision_maker.py
+++ b/autogpt_platform/backend/backend/blocks/smart_decision_maker.py
@@ -975,10 +975,28 @@ class SmartDecisionMakerBlock(Block):
graph_version: int,
execution_context: ExecutionContext,
execution_processor: "ExecutionProcessor",
+ nodes_to_skip: set[str] | None = None,
**kwargs,
) -> BlockOutput:
tool_functions = await self._create_tool_node_signatures(node_id)
+ original_tool_count = len(tool_functions)
+
+ # Filter out tools for nodes that should be skipped (e.g., missing optional credentials)
+ if nodes_to_skip:
+ tool_functions = [
+ tf
+ for tf in tool_functions
+ if tf.get("function", {}).get("_sink_node_id") not in nodes_to_skip
+ ]
+
+ # Only raise error if we had tools but they were all filtered out
+ if original_tool_count > 0 and not tool_functions:
+ raise ValueError(
+ "No available tools to execute - all downstream nodes are unavailable "
+ "(possibly due to missing optional credentials)"
+ )
+
yield "tool_functions", json.dumps(tool_functions)
conversation_history = input_data.conversation_history or []
diff --git a/autogpt_platform/backend/backend/data/execution.py b/autogpt_platform/backend/backend/data/execution.py
index 020a5a1906..2759dfe179 100644
--- a/autogpt_platform/backend/backend/data/execution.py
+++ b/autogpt_platform/backend/backend/data/execution.py
@@ -383,6 +383,7 @@ class GraphExecutionWithNodes(GraphExecution):
self,
execution_context: ExecutionContext,
compiled_nodes_input_masks: Optional[NodesInputMasks] = None,
+ nodes_to_skip: Optional[set[str]] = None,
):
return GraphExecutionEntry(
user_id=self.user_id,
@@ -390,6 +391,7 @@ class GraphExecutionWithNodes(GraphExecution):
graph_version=self.graph_version or 0,
graph_exec_id=self.id,
nodes_input_masks=compiled_nodes_input_masks,
+ nodes_to_skip=nodes_to_skip or set(),
execution_context=execution_context,
)
@@ -1145,6 +1147,8 @@ class GraphExecutionEntry(BaseModel):
graph_id: str
graph_version: int
nodes_input_masks: Optional[NodesInputMasks] = None
+ nodes_to_skip: set[str] = Field(default_factory=set)
+ """Node IDs that should be skipped due to optional credentials not being configured."""
execution_context: ExecutionContext = Field(default_factory=ExecutionContext)
diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py
index 0757a86f4a..9a578c2e62 100644
--- a/autogpt_platform/backend/backend/data/graph.py
+++ b/autogpt_platform/backend/backend/data/graph.py
@@ -94,6 +94,15 @@ class Node(BaseDbModel):
input_links: list[Link] = []
output_links: list[Link] = []
+ @property
+ def credentials_optional(self) -> bool:
+ """
+ Whether credentials are optional for this node.
+ When True and credentials are not configured, the node will be skipped
+ during execution rather than causing a validation error.
+ """
+ return self.metadata.get("credentials_optional", False)
+
@property
def block(self) -> AnyBlockSchema | "_UnknownBlockBase":
"""Get the block for this node. Returns UnknownBlock if block is deleted/missing."""
@@ -326,7 +335,35 @@ class Graph(BaseGraph):
@computed_field
@property
def credentials_input_schema(self) -> dict[str, Any]:
- return self._credentials_input_schema.jsonschema()
+ schema = self._credentials_input_schema.jsonschema()
+
+ # Determine which credential fields are required based on credentials_optional metadata
+ graph_credentials_inputs = self.aggregate_credentials_inputs()
+ required_fields = []
+
+ # Build a map of node_id -> node for quick lookup
+ all_nodes = {node.id: node for node in self.nodes}
+ for sub_graph in self.sub_graphs:
+ for node in sub_graph.nodes:
+ all_nodes[node.id] = node
+
+ for field_key, (
+ _field_info,
+ node_field_pairs,
+ ) in graph_credentials_inputs.items():
+ # A field is required if ANY node using it has credentials_optional=False
+ is_required = False
+ for node_id, _field_name in node_field_pairs:
+ node = all_nodes.get(node_id)
+ if node and not node.credentials_optional:
+ is_required = True
+ break
+
+ if is_required:
+ required_fields.append(field_key)
+
+ schema["required"] = required_fields
+ return schema
@property
def _credentials_input_schema(self) -> type[BlockSchema]:
diff --git a/autogpt_platform/backend/backend/data/graph_test.py b/autogpt_platform/backend/backend/data/graph_test.py
index 044d75e0ca..eea7277eb9 100644
--- a/autogpt_platform/backend/backend/data/graph_test.py
+++ b/autogpt_platform/backend/backend/data/graph_test.py
@@ -396,3 +396,58 @@ async def test_access_store_listing_graph(server: SpinTestServer):
created_graph.id, created_graph.version, "3e53486c-cf57-477e-ba2a-cb02dc828e1b"
)
assert got_graph is not None
+
+
+# ============================================================================
+# Tests for Optional Credentials Feature
+# ============================================================================
+
+
+def test_node_credentials_optional_default():
+ """Test that credentials_optional defaults to False when not set in metadata."""
+ node = Node(
+ id="test_node",
+ block_id=StoreValueBlock().id,
+ input_default={},
+ metadata={},
+ )
+ assert node.credentials_optional is False
+
+
+def test_node_credentials_optional_true():
+ """Test that credentials_optional returns True when explicitly set."""
+ node = Node(
+ id="test_node",
+ block_id=StoreValueBlock().id,
+ input_default={},
+ metadata={"credentials_optional": True},
+ )
+ assert node.credentials_optional is True
+
+
+def test_node_credentials_optional_false():
+ """Test that credentials_optional returns False when explicitly set to False."""
+ node = Node(
+ id="test_node",
+ block_id=StoreValueBlock().id,
+ input_default={},
+ metadata={"credentials_optional": False},
+ )
+ assert node.credentials_optional is False
+
+
+def test_node_credentials_optional_with_other_metadata():
+ """Test that credentials_optional works correctly with other metadata present."""
+ node = Node(
+ id="test_node",
+ block_id=StoreValueBlock().id,
+ input_default={},
+ metadata={
+ "position": {"x": 100, "y": 200},
+ "customized_name": "My Custom Node",
+ "credentials_optional": True,
+ },
+ )
+ assert node.credentials_optional is True
+ assert node.metadata["position"] == {"x": 100, "y": 200}
+ assert node.metadata["customized_name"] == "My Custom Node"
diff --git a/autogpt_platform/backend/backend/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py
index 75459c5a2a..39d4f984eb 100644
--- a/autogpt_platform/backend/backend/executor/manager.py
+++ b/autogpt_platform/backend/backend/executor/manager.py
@@ -178,6 +178,7 @@ async def execute_node(
execution_processor: "ExecutionProcessor",
execution_stats: NodeExecutionStats | None = None,
nodes_input_masks: Optional[NodesInputMasks] = None,
+ nodes_to_skip: Optional[set[str]] = None,
) -> BlockOutput:
"""
Execute a node in the graph. This will trigger a block execution on a node,
@@ -245,6 +246,7 @@ async def execute_node(
"user_id": user_id,
"execution_context": execution_context,
"execution_processor": execution_processor,
+ "nodes_to_skip": nodes_to_skip or set(),
}
# Last-minute fetch credentials + acquire a system-wide read-write lock to prevent
@@ -542,6 +544,7 @@ class ExecutionProcessor:
node_exec_progress: NodeExecutionProgress,
nodes_input_masks: Optional[NodesInputMasks],
graph_stats_pair: tuple[GraphExecutionStats, threading.Lock],
+ nodes_to_skip: Optional[set[str]] = None,
) -> NodeExecutionStats:
log_metadata = LogMetadata(
logger=_logger,
@@ -564,6 +567,7 @@ class ExecutionProcessor:
db_client=db_client,
log_metadata=log_metadata,
nodes_input_masks=nodes_input_masks,
+ nodes_to_skip=nodes_to_skip,
)
if isinstance(status, BaseException):
raise status
@@ -609,6 +613,7 @@ class ExecutionProcessor:
db_client: "DatabaseManagerAsyncClient",
log_metadata: LogMetadata,
nodes_input_masks: Optional[NodesInputMasks] = None,
+ nodes_to_skip: Optional[set[str]] = None,
) -> ExecutionStatus:
status = ExecutionStatus.RUNNING
@@ -645,6 +650,7 @@ class ExecutionProcessor:
execution_processor=self,
execution_stats=stats,
nodes_input_masks=nodes_input_masks,
+ nodes_to_skip=nodes_to_skip,
):
await persist_output(output_name, output_data)
@@ -956,6 +962,21 @@ class ExecutionProcessor:
queued_node_exec = execution_queue.get()
+ # Check if this node should be skipped due to optional credentials
+ if queued_node_exec.node_id in graph_exec.nodes_to_skip:
+ log_metadata.info(
+ f"Skipping node execution {queued_node_exec.node_exec_id} "
+ f"for node {queued_node_exec.node_id} - optional credentials not configured"
+ )
+ # Mark the node as completed without executing
+ # No outputs will be produced, so downstream nodes won't trigger
+ update_node_execution_status(
+ db_client=db_client,
+ exec_id=queued_node_exec.node_exec_id,
+ status=ExecutionStatus.COMPLETED,
+ )
+ continue
+
log_metadata.debug(
f"Dispatching node execution {queued_node_exec.node_exec_id} "
f"for node {queued_node_exec.node_id}",
@@ -1016,6 +1037,7 @@ class ExecutionProcessor:
execution_stats,
execution_stats_lock,
),
+ nodes_to_skip=graph_exec.nodes_to_skip,
),
self.node_execution_loop,
)
diff --git a/autogpt_platform/backend/backend/executor/utils.py b/autogpt_platform/backend/backend/executor/utils.py
index bcd3dcf3b6..1fb2b9404f 100644
--- a/autogpt_platform/backend/backend/executor/utils.py
+++ b/autogpt_platform/backend/backend/executor/utils.py
@@ -239,14 +239,19 @@ async def _validate_node_input_credentials(
graph: GraphModel,
user_id: str,
nodes_input_masks: Optional[NodesInputMasks] = None,
-) -> dict[str, dict[str, str]]:
+) -> tuple[dict[str, dict[str, str]], set[str]]:
"""
- Checks all credentials for all nodes of the graph and returns structured errors.
+ Checks all credentials for all nodes of the graph and returns structured errors
+ and a set of nodes that should be skipped due to optional missing credentials.
Returns:
- dict[node_id, dict[field_name, error_message]]: Credential validation errors per node
+ tuple[
+ dict[node_id, dict[field_name, error_message]]: Credential validation errors per node,
+ set[node_id]: Nodes that should be skipped (optional credentials not configured)
+ ]
"""
credential_errors: dict[str, dict[str, str]] = defaultdict(dict)
+ nodes_to_skip: set[str] = set()
for node in graph.nodes:
block = node.block
@@ -256,27 +261,46 @@ async def _validate_node_input_credentials(
if not credentials_fields:
continue
+ # Track if any credential field is missing for this node
+ has_missing_credentials = False
+
for field_name, credentials_meta_type in credentials_fields.items():
try:
+ # Check nodes_input_masks first, then input_default
+ field_value = None
if (
nodes_input_masks
and (node_input_mask := nodes_input_masks.get(node.id))
and field_name in node_input_mask
):
- credentials_meta = credentials_meta_type.model_validate(
- node_input_mask[field_name]
- )
+ field_value = node_input_mask[field_name]
elif field_name in node.input_default:
- credentials_meta = credentials_meta_type.model_validate(
- node.input_default[field_name]
- )
- else:
- # Missing credentials
- credential_errors[node.id][
- field_name
- ] = "These credentials are required"
- continue
+ # For optional credentials, don't use input_default - treat as missing
+ # This prevents stale credential IDs from failing validation
+ if node.credentials_optional:
+ field_value = None
+ else:
+ field_value = node.input_default[field_name]
+
+ # Check if credentials are missing (None, empty, or not present)
+ if field_value is None or (
+ isinstance(field_value, dict) and not field_value.get("id")
+ ):
+ has_missing_credentials = True
+ # If node has credentials_optional flag, mark for skipping instead of error
+ if node.credentials_optional:
+ continue # Don't add error, will be marked for skip after loop
+ else:
+ credential_errors[node.id][
+ field_name
+ ] = "These credentials are required"
+ continue
+
+ credentials_meta = credentials_meta_type.model_validate(field_value)
+
except ValidationError as e:
+ # Validation error means credentials were provided but invalid
+ # This should always be an error, even if optional
credential_errors[node.id][field_name] = f"Invalid credentials: {e}"
continue
@@ -287,6 +311,7 @@ async def _validate_node_input_credentials(
)
except Exception as e:
# Handle any errors fetching credentials
+ # If credentials were explicitly configured but unavailable, it's an error
credential_errors[node.id][
field_name
] = f"Credentials not available: {e}"
@@ -313,7 +338,19 @@ async def _validate_node_input_credentials(
] = "Invalid credentials: type/provider mismatch"
continue
- return credential_errors
+ # If node has optional credentials and any are missing, mark for skipping
+ # But only if there are no other errors for this node
+ if (
+ has_missing_credentials
+ and node.credentials_optional
+ and node.id not in credential_errors
+ ):
+ nodes_to_skip.add(node.id)
+ logger.info(
+ f"Node #{node.id} will be skipped: optional credentials not configured"
+ )
+
+ return credential_errors, nodes_to_skip
def make_node_credentials_input_map(
@@ -355,21 +392,25 @@ async def validate_graph_with_credentials(
graph: GraphModel,
user_id: str,
nodes_input_masks: Optional[NodesInputMasks] = None,
-) -> Mapping[str, Mapping[str, str]]:
+) -> tuple[Mapping[str, Mapping[str, str]], set[str]]:
"""
- Validate graph including credentials and return structured errors per node.
+ Validate graph including credentials and return structured errors per node,
+ along with a set of nodes that should be skipped due to optional missing credentials.
Returns:
- dict[node_id, dict[field_name, error_message]]: Validation errors per node
+ tuple[
+ dict[node_id, dict[field_name, error_message]]: Validation errors per node,
+ set[node_id]: Nodes that should be skipped (optional credentials not configured)
+ ]
"""
# Get input validation errors
node_input_errors = GraphModel.validate_graph_get_errors(
graph, for_run=True, nodes_input_masks=nodes_input_masks
)
- # Get credential input/availability/validation errors
- node_credential_input_errors = await _validate_node_input_credentials(
- graph, user_id, nodes_input_masks
+ # Get credential input/availability/validation errors and nodes to skip
+ node_credential_input_errors, nodes_to_skip = (
+ await _validate_node_input_credentials(graph, user_id, nodes_input_masks)
)
# Merge credential errors with structural errors
@@ -378,7 +419,7 @@ async def validate_graph_with_credentials(
node_input_errors[node_id] = {}
node_input_errors[node_id].update(field_errors)
- return node_input_errors
+ return node_input_errors, nodes_to_skip
async def _construct_starting_node_execution_input(
@@ -386,7 +427,7 @@ async def _construct_starting_node_execution_input(
user_id: str,
graph_inputs: BlockInput,
nodes_input_masks: Optional[NodesInputMasks] = None,
-) -> list[tuple[str, BlockInput]]:
+) -> tuple[list[tuple[str, BlockInput]], set[str]]:
"""
Validates and prepares the input data for executing a graph.
This function checks the graph for starting nodes, validates the input data
@@ -400,11 +441,14 @@ async def _construct_starting_node_execution_input(
node_credentials_map: `dict[node_id, dict[input_name, CredentialsMetaInput]]`
Returns:
- list[tuple[str, BlockInput]]: A list of tuples, each containing the node ID and
- the corresponding input data for that node.
+ tuple[
+ list[tuple[str, BlockInput]]: A list of tuples, each containing the node ID
+ and the corresponding input data for that node.
+ set[str]: Node IDs that should be skipped (optional credentials not configured)
+ ]
"""
# Use new validation function that includes credentials
- validation_errors = await validate_graph_with_credentials(
+ validation_errors, nodes_to_skip = await validate_graph_with_credentials(
graph, user_id, nodes_input_masks
)
n_error_nodes = len(validation_errors)
@@ -445,7 +489,7 @@ async def _construct_starting_node_execution_input(
"No starting nodes found for the graph, make sure an AgentInput or blocks with no inbound links are present as starting nodes."
)
- return nodes_input
+ return nodes_input, nodes_to_skip
async def validate_and_construct_node_execution_input(
@@ -456,7 +500,7 @@ async def validate_and_construct_node_execution_input(
graph_credentials_inputs: Optional[Mapping[str, CredentialsMetaInput]] = None,
nodes_input_masks: Optional[NodesInputMasks] = None,
is_sub_graph: bool = False,
-) -> tuple[GraphModel, list[tuple[str, BlockInput]], NodesInputMasks]:
+) -> tuple[GraphModel, list[tuple[str, BlockInput]], NodesInputMasks, set[str]]:
"""
Public wrapper that handles graph fetching, credential mapping, and validation+construction.
This centralizes the logic used by both scheduler validation and actual execution.
@@ -473,6 +517,7 @@ async def validate_and_construct_node_execution_input(
GraphModel: Full graph object for the given `graph_id`.
list[tuple[node_id, BlockInput]]: Starting node IDs with corresponding inputs.
dict[str, BlockInput]: Node input masks including all passed-in credentials.
+ set[str]: Node IDs that should be skipped (optional credentials not configured).
Raises:
NotFoundError: If the graph is not found.
@@ -514,14 +559,16 @@ async def validate_and_construct_node_execution_input(
nodes_input_masks or {},
)
- starting_nodes_input = await _construct_starting_node_execution_input(
- graph=graph,
- user_id=user_id,
- graph_inputs=graph_inputs,
- nodes_input_masks=nodes_input_masks,
+ starting_nodes_input, nodes_to_skip = (
+ await _construct_starting_node_execution_input(
+ graph=graph,
+ user_id=user_id,
+ graph_inputs=graph_inputs,
+ nodes_input_masks=nodes_input_masks,
+ )
)
- return graph, starting_nodes_input, nodes_input_masks
+ return graph, starting_nodes_input, nodes_input_masks, nodes_to_skip
def _merge_nodes_input_masks(
@@ -779,6 +826,9 @@ async def add_graph_execution(
# Use existing execution's compiled input masks
compiled_nodes_input_masks = graph_exec.nodes_input_masks or {}
+ # For resumed executions, nodes_to_skip was already determined at creation time
+ # TODO: Consider storing nodes_to_skip in DB if we need to preserve it across resumes
+ nodes_to_skip: set[str] = set()
logger.info(f"Resuming graph execution #{graph_exec.id} for graph #{graph_id}")
else:
@@ -787,7 +837,7 @@ async def add_graph_execution(
)
# Create new execution
- graph, starting_nodes_input, compiled_nodes_input_masks = (
+ graph, starting_nodes_input, compiled_nodes_input_masks, nodes_to_skip = (
await validate_and_construct_node_execution_input(
graph_id=graph_id,
user_id=user_id,
@@ -836,6 +886,7 @@ async def add_graph_execution(
try:
graph_exec_entry = graph_exec.to_graph_execution_entry(
compiled_nodes_input_masks=compiled_nodes_input_masks,
+ nodes_to_skip=nodes_to_skip,
execution_context=execution_context,
)
logger.info(f"Publishing execution {graph_exec.id} to execution queue")
diff --git a/autogpt_platform/backend/backend/executor/utils_test.py b/autogpt_platform/backend/backend/executor/utils_test.py
index 8854214e14..0e652f9627 100644
--- a/autogpt_platform/backend/backend/executor/utils_test.py
+++ b/autogpt_platform/backend/backend/executor/utils_test.py
@@ -367,10 +367,13 @@ async def test_add_graph_execution_is_repeatable(mocker: MockerFixture):
)
# Setup mock returns
+ # The function returns (graph, starting_nodes_input, compiled_nodes_input_masks, nodes_to_skip)
+ nodes_to_skip: set[str] = set()
mock_validate.return_value = (
mock_graph,
starting_nodes_input,
compiled_nodes_input_masks,
+ nodes_to_skip,
)
mock_prisma.is_connected.return_value = True
mock_edb.create_graph_execution = mocker.AsyncMock(return_value=mock_graph_exec)
@@ -456,3 +459,212 @@ async def test_add_graph_execution_is_repeatable(mocker: MockerFixture):
# Both executions should succeed (though they create different objects)
assert result1 == mock_graph_exec
assert result2 == mock_graph_exec_2
+
+
+# ============================================================================
+# Tests for Optional Credentials Feature
+# ============================================================================
+
+
+@pytest.mark.asyncio
+async def test_validate_node_input_credentials_returns_nodes_to_skip(
+ mocker: MockerFixture,
+):
+ """
+ Test that _validate_node_input_credentials returns nodes_to_skip set
+ for nodes with credentials_optional=True and missing credentials.
+ """
+ from backend.executor.utils import _validate_node_input_credentials
+
+ # Create a mock node with credentials_optional=True
+ mock_node = mocker.MagicMock()
+ mock_node.id = "node-with-optional-creds"
+ mock_node.credentials_optional = True
+ mock_node.input_default = {} # No credentials configured
+
+ # Create a mock block with credentials field
+ mock_block = mocker.MagicMock()
+ mock_credentials_field_type = mocker.MagicMock()
+ mock_block.input_schema.get_credentials_fields.return_value = {
+ "credentials": mock_credentials_field_type
+ }
+ mock_node.block = mock_block
+
+ # Create mock graph
+ mock_graph = mocker.MagicMock()
+ mock_graph.nodes = [mock_node]
+
+ # Call the function
+ errors, nodes_to_skip = await _validate_node_input_credentials(
+ graph=mock_graph,
+ user_id="test-user-id",
+ nodes_input_masks=None,
+ )
+
+ # Node should be in nodes_to_skip, not in errors
+ assert mock_node.id in nodes_to_skip
+ assert mock_node.id not in errors
+
+
+@pytest.mark.asyncio
+async def test_validate_node_input_credentials_required_missing_creds_error(
+ mocker: MockerFixture,
+):
+ """
+ Test that _validate_node_input_credentials returns errors
+ for nodes with credentials_optional=False and missing credentials.
+ """
+ from backend.executor.utils import _validate_node_input_credentials
+
+ # Create a mock node with credentials_optional=False (required)
+ mock_node = mocker.MagicMock()
+ mock_node.id = "node-with-required-creds"
+ mock_node.credentials_optional = False
+ mock_node.input_default = {} # No credentials configured
+
+ # Create a mock block with credentials field
+ mock_block = mocker.MagicMock()
+ mock_credentials_field_type = mocker.MagicMock()
+ mock_block.input_schema.get_credentials_fields.return_value = {
+ "credentials": mock_credentials_field_type
+ }
+ mock_node.block = mock_block
+
+ # Create mock graph
+ mock_graph = mocker.MagicMock()
+ mock_graph.nodes = [mock_node]
+
+ # Call the function
+ errors, nodes_to_skip = await _validate_node_input_credentials(
+ graph=mock_graph,
+ user_id="test-user-id",
+ nodes_input_masks=None,
+ )
+
+ # Node should be in errors, not in nodes_to_skip
+ assert mock_node.id in errors
+ assert "credentials" in errors[mock_node.id]
+ assert "required" in errors[mock_node.id]["credentials"].lower()
+ assert mock_node.id not in nodes_to_skip
+
+
+@pytest.mark.asyncio
+async def test_validate_graph_with_credentials_returns_nodes_to_skip(
+ mocker: MockerFixture,
+):
+ """
+ Test that validate_graph_with_credentials returns nodes_to_skip set
+ from _validate_node_input_credentials.
+ """
+ from backend.executor.utils import validate_graph_with_credentials
+
+ # Mock _validate_node_input_credentials to return specific values
+ mock_validate = mocker.patch(
+ "backend.executor.utils._validate_node_input_credentials"
+ )
+ expected_errors = {"node1": {"field": "error"}}
+ expected_nodes_to_skip = {"node2", "node3"}
+ mock_validate.return_value = (expected_errors, expected_nodes_to_skip)
+
+ # Mock GraphModel with validate_graph_get_errors method
+ mock_graph = mocker.MagicMock()
+ mock_graph.validate_graph_get_errors.return_value = {}
+
+ # Call the function
+ errors, nodes_to_skip = await validate_graph_with_credentials(
+ graph=mock_graph,
+ user_id="test-user-id",
+ nodes_input_masks=None,
+ )
+
+ # Verify nodes_to_skip is passed through
+ assert nodes_to_skip == expected_nodes_to_skip
+ assert "node1" in errors
+
+
+@pytest.mark.asyncio
+async def test_add_graph_execution_with_nodes_to_skip(mocker: MockerFixture):
+ """
+ Test that add_graph_execution properly passes nodes_to_skip
+ to the graph execution entry.
+ """
+ from backend.data.execution import GraphExecutionWithNodes
+ from backend.executor.utils import add_graph_execution
+
+ # Mock data
+ graph_id = "test-graph-id"
+ user_id = "test-user-id"
+ inputs = {"test_input": "test_value"}
+ graph_version = 1
+
+ # Mock the graph object
+ mock_graph = mocker.MagicMock()
+ mock_graph.version = graph_version
+
+ # Starting nodes and masks
+ starting_nodes_input = [("node1", {"input1": "value1"})]
+ compiled_nodes_input_masks = {}
+ nodes_to_skip = {"skipped-node-1", "skipped-node-2"}
+
+ # Mock the graph execution object
+ mock_graph_exec = mocker.MagicMock(spec=GraphExecutionWithNodes)
+ mock_graph_exec.id = "execution-id-123"
+ mock_graph_exec.node_executions = []
+
+ # Track what's passed to to_graph_execution_entry
+ captured_kwargs = {}
+
+ def capture_to_entry(**kwargs):
+ captured_kwargs.update(kwargs)
+ return mocker.MagicMock()
+
+ mock_graph_exec.to_graph_execution_entry.side_effect = capture_to_entry
+
+ # Setup mocks
+ mock_validate = mocker.patch(
+ "backend.executor.utils.validate_and_construct_node_execution_input"
+ )
+ mock_edb = mocker.patch("backend.executor.utils.execution_db")
+ mock_prisma = mocker.patch("backend.executor.utils.prisma")
+ mock_udb = mocker.patch("backend.executor.utils.user_db")
+ mock_gdb = mocker.patch("backend.executor.utils.graph_db")
+ mock_get_queue = mocker.patch("backend.executor.utils.get_async_execution_queue")
+ mock_get_event_bus = mocker.patch(
+ "backend.executor.utils.get_async_execution_event_bus"
+ )
+
+ # Setup returns - include nodes_to_skip in the tuple
+ mock_validate.return_value = (
+ mock_graph,
+ starting_nodes_input,
+ compiled_nodes_input_masks,
+ nodes_to_skip, # This should be passed through
+ )
+ mock_prisma.is_connected.return_value = True
+ mock_edb.create_graph_execution = mocker.AsyncMock(return_value=mock_graph_exec)
+ mock_edb.update_graph_execution_stats = mocker.AsyncMock(
+ return_value=mock_graph_exec
+ )
+ mock_edb.update_node_execution_status_batch = mocker.AsyncMock()
+
+ mock_user = mocker.MagicMock()
+ mock_user.timezone = "UTC"
+ mock_settings = mocker.MagicMock()
+ mock_settings.human_in_the_loop_safe_mode = True
+
+ mock_udb.get_user_by_id = mocker.AsyncMock(return_value=mock_user)
+ mock_gdb.get_graph_settings = mocker.AsyncMock(return_value=mock_settings)
+ mock_get_queue.return_value = mocker.AsyncMock()
+ mock_get_event_bus.return_value = mocker.MagicMock(publish=mocker.AsyncMock())
+
+ # Call the function
+ await add_graph_execution(
+ graph_id=graph_id,
+ user_id=user_id,
+ inputs=inputs,
+ graph_version=graph_version,
+ )
+
+ # Verify nodes_to_skip was passed to to_graph_execution_entry
+ assert "nodes_to_skip" in captured_kwargs
+ assert captured_kwargs["nodes_to_skip"] == nodes_to_skip
diff --git a/autogpt_platform/backend/snapshots/grph_single b/autogpt_platform/backend/snapshots/grph_single
index 7ba26f6171..7ce8695e6b 100644
--- a/autogpt_platform/backend/snapshots/grph_single
+++ b/autogpt_platform/backend/snapshots/grph_single
@@ -2,6 +2,7 @@
"created_at": "2025-09-04T13:37:00",
"credentials_input_schema": {
"properties": {},
+ "required": [],
"title": "TestGraphCredentialsInputSchema",
"type": "object"
},
diff --git a/autogpt_platform/backend/snapshots/grphs_all b/autogpt_platform/backend/snapshots/grphs_all
index d54df2bc18..f69b45a6de 100644
--- a/autogpt_platform/backend/snapshots/grphs_all
+++ b/autogpt_platform/backend/snapshots/grphs_all
@@ -2,6 +2,7 @@
{
"credentials_input_schema": {
"properties": {},
+ "required": [],
"title": "TestGraphCredentialsInputSchema",
"type": "object"
},
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunInputDialog/RunInputDialog.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunInputDialog/RunInputDialog.tsx
index 431feeaade..bd08aa8ee0 100644
--- a/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunInputDialog/RunInputDialog.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunInputDialog/RunInputDialog.tsx
@@ -66,6 +66,7 @@ export const RunInputDialog = ({
formContext={{
showHandles: false,
size: "large",
+ showOptionalToggle: false,
}}
/>
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunInputDialog/useRunInputDialog.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunInputDialog/useRunInputDialog.ts
index a71ad0bd07..ddd77bae48 100644
--- a/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunInputDialog/useRunInputDialog.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunInputDialog/useRunInputDialog.ts
@@ -66,7 +66,7 @@ export const useRunInputDialog = ({
if (isCredentialFieldSchema(fieldSchema)) {
dynamicUiSchema[fieldName] = {
...dynamicUiSchema[fieldName],
- "ui:field": "credentials",
+ "ui:field": "custom/credential_field",
};
}
});
@@ -76,12 +76,18 @@ export const useRunInputDialog = ({
}, [credentialsSchema]);
const handleManualRun = async () => {
+ // Filter out incomplete credentials (those without a valid id)
+ // RJSF auto-populates const values (provider, type) but not id field
+ const validCredentials = Object.fromEntries(
+ Object.entries(credentialValues).filter(([_, cred]) => cred && cred.id),
+ );
+
await executeGraph({
graphId: flowID ?? "",
graphVersion: flowVersion || null,
data: {
inputs: inputValues,
- credentials_inputs: credentialValues,
+ credentials_inputs: validCredentials,
source: "builder",
},
});
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/uiSchema.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/uiSchema.ts
index ad1fab7c95..065e697828 100644
--- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/uiSchema.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/uiSchema.ts
@@ -1,6 +1,6 @@
export const uiSchema = {
credentials: {
- "ui:field": "credentials",
+ "ui:field": "custom/credential_field",
provider: { "ui:widget": "hidden" },
type: { "ui:widget": "hidden" },
id: { "ui:autofocus": true },
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/stores/nodeStore.ts b/autogpt_platform/frontend/src/app/(platform)/build/stores/nodeStore.ts
index 96478c5b6f..c151f90faa 100644
--- a/autogpt_platform/frontend/src/app/(platform)/build/stores/nodeStore.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/build/stores/nodeStore.ts
@@ -68,6 +68,9 @@ type NodeStore = {
clearAllNodeErrors: () => void; // Add this
syncHardcodedValuesWithHandleIds: (nodeId: string) => void;
+
+ // Credentials optional helpers
+ setCredentialsOptional: (nodeId: string, optional: boolean) => void;
};
export const useNodeStore = create
((set, get) => ({
@@ -226,6 +229,9 @@ export const useNodeStore = create((set, get) => ({
...(node.data.metadata?.customized_name !== undefined && {
customized_name: node.data.metadata.customized_name,
}),
+ ...(node.data.metadata?.credentials_optional !== undefined && {
+ credentials_optional: node.data.metadata.credentials_optional,
+ }),
},
};
},
@@ -342,4 +348,30 @@ export const useNodeStore = create((set, get) => ({
}));
}
},
+
+ setCredentialsOptional: (nodeId: string, optional: boolean) => {
+ set((state) => ({
+ nodes: state.nodes.map((n) =>
+ n.id === nodeId
+ ? {
+ ...n,
+ data: {
+ ...n.data,
+ metadata: {
+ ...n.data.metadata,
+ credentials_optional: optional,
+ },
+ },
+ }
+ : n,
+ ),
+ }));
+
+ const newState = {
+ nodes: get().nodes,
+ edges: useEdgeStore.getState().edges,
+ };
+
+ useHistoryStore.getState().pushState(newState);
+ },
}));
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs.tsx
index 60d61fab57..79767c0c81 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs.tsx
@@ -34,6 +34,7 @@ type Props = {
onSelectCredentials: (newValue?: CredentialsMetaInput) => void;
onLoaded?: (loaded: boolean) => void;
readOnly?: boolean;
+ isOptional?: boolean;
showTitle?: boolean;
};
@@ -45,6 +46,7 @@ export function CredentialsInput({
siblingInputs,
onLoaded,
readOnly = false,
+ isOptional = false,
showTitle = true,
}: Props) {
const hookData = useCredentialsInput({
@@ -54,6 +56,7 @@ export function CredentialsInput({
siblingInputs,
onLoaded,
readOnly,
+ isOptional,
});
if (!isLoaded(hookData)) {
@@ -94,7 +97,14 @@ export function CredentialsInput({
{showTitle && (
-
{displayName} credentials
+
+ {displayName} credentials
+ {isOptional && (
+
+ (optional)
+
+ )}
+
{schema.description && (
)}
@@ -103,14 +113,16 @@ export function CredentialsInput({
{hasCredentialsToShow ? (
<>
- {credentialsToShow.length > 1 && !readOnly ? (
+ {(credentialsToShow.length > 1 || isOptional) && !readOnly ? (
onSelectCredential(undefined)}
readOnly={readOnly}
+ allowNone={isOptional}
/>
) : (
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialsSelect/CredentialsSelect.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialsSelect/CredentialsSelect.tsx
index 7adfa5772b..1ada56eb30 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialsSelect/CredentialsSelect.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialsSelect/CredentialsSelect.tsx
@@ -23,7 +23,9 @@ interface Props {
displayName: string;
selectedCredentials?: CredentialsMetaInput;
onSelectCredential: (credentialId: string) => void;
+ onClearCredential?: () => void;
readOnly?: boolean;
+ allowNone?: boolean;
}
export function CredentialsSelect({
@@ -32,20 +34,30 @@ export function CredentialsSelect({
displayName,
selectedCredentials,
onSelectCredential,
+ onClearCredential,
readOnly = false,
+ allowNone = true,
}: Props) {
- // Auto-select first credential if none is selected
+ // Auto-select first credential if none is selected (only if allowNone is false)
useEffect(() => {
- if (!selectedCredentials && credentials.length > 0) {
+ if (!allowNone && !selectedCredentials && credentials.length > 0) {
onSelectCredential(credentials[0].id);
}
- }, [selectedCredentials, credentials, onSelectCredential]);
+ }, [allowNone, selectedCredentials, credentials, onSelectCredential]);
+
+ const handleValueChange = (value: string) => {
+ if (value === "__none__") {
+ onClearCredential?.();
+ } else {
+ onSelectCredential(value);
+ }
+ };
return (
onSelectCredential(value)}
+ value={selectedCredentials?.id || (allowNone ? "__none__" : "")}
+ onValueChange={handleValueChange}
>
{selectedCredentials ? (
@@ -70,6 +82,15 @@ export function CredentialsSelect({
)}
+ {allowNone && (
+
+
+
+ None (skip this credential)
+
+
+
+ )}
{credentials.map((credential) => (
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/useCredentialsInput.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/useCredentialsInput.ts
index 6f5ca48126..c780ffeffc 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/useCredentialsInput.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/useCredentialsInput.ts
@@ -22,6 +22,7 @@ type Params = {
siblingInputs?: Record
;
onLoaded?: (loaded: boolean) => void;
readOnly?: boolean;
+ isOptional?: boolean;
};
export function useCredentialsInput({
@@ -31,6 +32,7 @@ export function useCredentialsInput({
siblingInputs,
onLoaded,
readOnly = false,
+ isOptional = false,
}: Params) {
const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] =
useState(false);
@@ -99,13 +101,20 @@ export function useCredentialsInput({
: null;
}, [credentials]);
- // Auto-select the one available credential
+ // Auto-select the one available credential (only if not optional)
useEffect(() => {
if (readOnly) return;
+ if (isOptional) return; // Don't auto-select when credential is optional
if (singleCredential && !selectedCredential) {
onSelectCredential(singleCredential);
}
- }, [singleCredential, selectedCredential, onSelectCredential, readOnly]);
+ }, [
+ singleCredential,
+ selectedCredential,
+ onSelectCredential,
+ readOnly,
+ isOptional,
+ ]);
if (
!credentials ||
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/ModalRunSection/ModalRunSection.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/ModalRunSection/ModalRunSection.tsx
index f3b02bfbc9..aba4caee7a 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/ModalRunSection/ModalRunSection.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/ModalRunSection/ModalRunSection.tsx
@@ -8,6 +8,7 @@ import { WebhookTriggerBanner } from "../WebhookTriggerBanner/WebhookTriggerBann
export function ModalRunSection() {
const {
+ agent,
defaultRunType,
presetName,
setPresetName,
@@ -24,6 +25,11 @@ export function ModalRunSection() {
const inputFields = Object.entries(agentInputFields || {});
const credentialFields = Object.entries(agentCredentialsInputFields || {});
+ // Get the list of required credentials from the schema
+ const requiredCredentials = new Set(
+ (agent.credentials_input_schema?.required as string[]) || [],
+ );
+
return (
{defaultRunType === "automatic-trigger" ||
@@ -99,14 +105,12 @@ export function ModalRunSection() {
schema={
{ ...inputSubSchema, discriminator: undefined } as any
}
- selectedCredentials={
- (inputCredentials && inputCredentials[key]) ??
- inputSubSchema.default
- }
+ selectedCredentials={inputCredentials?.[key]}
onSelectCredentials={(value) =>
setInputCredentialsValue(key, value)
}
siblingInputs={inputValues}
+ isOptional={!requiredCredentials.has(key)}
/>
),
)}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/useAgentRunModal.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/useAgentRunModal.tsx
index b997a33fcf..eb32083004 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/useAgentRunModal.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/useAgentRunModal.tsx
@@ -163,15 +163,21 @@ export function useAgentRunModal(
}, [agentInputSchema.required, inputValues]);
const [allCredentialsAreSet, missingCredentials] = useMemo(() => {
- const availableCredentials = new Set(Object.keys(inputCredentials));
- const allCredentials = new Set(
- Object.keys(agentCredentialsInputFields || {}) ?? [],
- );
- const missing = [...allCredentials].filter(
- (key) => !availableCredentials.has(key),
+ // Only check required credentials from schema, not all properties
+ // Credentials marked as optional in node metadata won't be in the required array
+ const requiredCredentials = new Set(
+ (agent.credentials_input_schema?.required as string[]) || [],
);
+
+ // Check if required credentials have valid id (not just key existence)
+ // A credential is valid only if it has an id field set
+ const missing = [...requiredCredentials].filter((key) => {
+ const cred = inputCredentials[key];
+ return !cred || !cred.id;
+ });
+
return [missing.length === 0, missing];
- }, [agentCredentialsInputFields, inputCredentials]);
+ }, [agent.credentials_input_schema, inputCredentials]);
const credentialsRequired = useMemo(
() => Object.keys(agentCredentialsInputFields || {}).length > 0,
@@ -239,12 +245,18 @@ export function useAgentRunModal(
});
} else {
// Manual execution
+ // Filter out incomplete credentials (optional ones not selected)
+ // Only send credentials that have a valid id field
+ const validCredentials = Object.fromEntries(
+ Object.entries(inputCredentials).filter(([_, cred]) => cred && cred.id),
+ );
+
executeGraphMutation.mutate({
graphId: agent.graph_id,
graphVersion: agent.graph_version,
data: {
inputs: inputValues,
- credentials_inputs: inputCredentials,
+ credentials_inputs: validCredentials,
source: "library",
},
});
diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/CredentialField/CredentialField.tsx b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/CredentialField/CredentialField.tsx
index f814fba93f..189b73e34b 100644
--- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/CredentialField/CredentialField.tsx
+++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/CredentialField/CredentialField.tsx
@@ -8,19 +8,34 @@ import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/component
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
import { useShallow } from "zustand/react/shallow";
import { CredentialFieldTitle } from "./components/CredentialFieldTitle";
+import { Switch } from "@/components/atoms/Switch/Switch";
export const CredentialsField = (props: FieldProps) => {
- const { formData, onChange, schema, registry, fieldPathId } = props;
+ const { formData, onChange, schema, registry, fieldPathId, required } = props;
const formContext = registry.formContext;
const uiOptions = getUiOptions(props.uiSchema);
const nodeId = formContext?.nodeId;
- // Get sibling inputs (hardcoded values) from the node store
- const hardcodedValues = useNodeStore(
- useShallow((state) => (nodeId ? state.getHardCodedValues(nodeId) : {})),
+ // Get sibling inputs (hardcoded values) and credentials optional state from the node store
+ // Note: We select the node data directly instead of using getter functions to avoid
+ // creating new object references that would cause infinite re-render loops with useShallow
+ const { node, setCredentialsOptional } = useNodeStore(
+ useShallow((state) => ({
+ node: nodeId ? state.nodes.find((n) => n.id === nodeId) : undefined,
+ setCredentialsOptional: state.setCredentialsOptional,
+ })),
);
+ const hardcodedValues = useMemo(
+ () => node?.data?.hardcodedValues || {},
+ [node?.data?.hardcodedValues],
+ );
+ const credentialsOptional = useMemo(() => {
+ const value = node?.data?.metadata?.credentials_optional;
+ return typeof value === "boolean" ? value : false;
+ }, [node?.data?.metadata?.credentials_optional]);
+
const handleChange = (newValue: any) => {
onChange(newValue, fieldPathId?.path);
};
@@ -52,6 +67,10 @@ export const CredentialsField = (props: FieldProps) => {
[formData?.id, formData?.provider, formData?.title, formData?.type],
);
+ // In builder canvas (nodeId exists): show star based on credentialsOptional toggle
+ // In run dialogs (no nodeId): show star based on schema's required array
+ const isRequired = nodeId ? !credentialsOptional : required;
+
return (
{
registry={registry}
uiOptions={uiOptions}
schema={schema}
+ required={isRequired}
/>
{
siblingInputs={hardcodedValues}
showTitle={false}
readOnly={formContext?.readOnly}
+ isOptional={!isRequired}
/>
+
+ {/* Optional credentials toggle - only show in builder canvas, not run dialogs */}
+ {nodeId &&
+ !formContext?.readOnly &&
+ formContext?.showOptionalToggle !== false && (
+
+
+ setCredentialsOptional(nodeId, checked)
+ }
+ />
+
+ Optional - skip block if not configured
+
+
+ )}
);
};
diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/CredentialField/components/CredentialFieldTitle.tsx b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/CredentialField/components/CredentialFieldTitle.tsx
index ca14c8a4ce..347f4e089a 100644
--- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/CredentialField/components/CredentialFieldTitle.tsx
+++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/CredentialField/components/CredentialFieldTitle.tsx
@@ -18,8 +18,9 @@ export const CredentialFieldTitle = (props: {
uiOptions: UiSchema;
schema: RJSFSchema;
fieldPathId: FieldPathId;
+ required?: boolean;
}) => {
- const { registry, uiOptions, schema, fieldPathId } = props;
+ const { registry, uiOptions, schema, fieldPathId, required = false } = props;
const { nodeId } = registry.formContext;
const TitleFieldTemplate = getTemplate(
@@ -50,7 +51,7 @@ export const CredentialFieldTitle = (props: {
Date: Fri, 9 Jan 2026 16:31:10 +0100
Subject: [PATCH 10/23] feat(backend): add prisma types stub generator for
pyright compatibility (#11736)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Prisma's generated `types.py` file is 57,000+ lines with complex
recursive TypedDict definitions that exhaust Pyright's type inference
budget. This causes random type errors and makes the type checker
unreliable.
### Changes 🏗️
- Add `gen_prisma_types_stub.py` script that generates a lightweight
`.pyi` stub file
- The stub preserves safe types (Literal, TypeVar) while collapsing
complex TypedDicts to `dict[str, Any]`
- Integrate stub generation into all workflows that run `prisma
generate`:
- `platform-backend-ci.yml`
- `claude.yml`
- `claude-dependabot.yml`
- `copilot-setup-steps.yml`
- `docker-compose.platform.yml`
- `Dockerfile`
- `Makefile` (migrate & reset-db targets)
- `linter.py` (lint & format commands)
- Add `gen-prisma-stub` poetry script entry
- Fix two pre-existing type errors that were previously masked:
- `store/db.py`: Replace private type
`_StoreListingVersion_version_OrderByInput` with dict literal
- `airtable/_webhook.py`: Add cast for `Serializable` type
### 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] Run `poetry run format` - passes with 0 errors (down from 57+)
- [x] Run `poetry run lint` - passes with 0 errors
- [x] Run `poetry run gen-prisma-stub` - generates stub successfully
- [x] Verify stub file is created at correct location with proper
content
#### For configuration changes:
- [x] `.env.default` 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**)
## Summary by CodeRabbit
* **Chores**
* Added a lightweight Prisma type-stub generator and integrated it into
build, lint, CI/CD, and container workflows.
* Build, migration, formatting, and lint steps now generate these stubs
to improve type-checking performance and reduce overhead during builds
and deployments.
* Exposed a project command to run stub generation manually.
✏️ Tip: You can customize this high-level summary in your review
settings.
---
.dockerignore | 1 +
.github/workflows/claude-dependabot.yml | 2 +-
.github/workflows/claude.yml | 2 +-
.github/workflows/copilot-setup-steps.yml | 12 +-
.github/workflows/platform-backend-ci.yml | 2 +-
autogpt_platform/Makefile | 2 +
autogpt_platform/backend/Dockerfile | 3 +-
.../backend/backend/api/features/store/db.py | 4 +-
.../backend/blocks/airtable/_webhook.py | 7 +-
.../backend/gen_prisma_types_stub.py | 227 ++++++++++++++++++
autogpt_platform/backend/linter.py | 5 +
autogpt_platform/backend/pyproject.toml | 1 +
autogpt_platform/docker-compose.platform.yml | 2 +-
13 files changed, 260 insertions(+), 10 deletions(-)
create mode 100644 autogpt_platform/backend/gen_prisma_types_stub.py
diff --git a/.dockerignore b/.dockerignore
index 94bf1742f1..c9524ce700 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -16,6 +16,7 @@
!autogpt_platform/backend/poetry.lock
!autogpt_platform/backend/README.md
!autogpt_platform/backend/.env
+!autogpt_platform/backend/gen_prisma_types_stub.py
# Platform - Market
!autogpt_platform/market/market/
diff --git a/.github/workflows/claude-dependabot.yml b/.github/workflows/claude-dependabot.yml
index 20b6f1d28e..1fd0da3d8e 100644
--- a/.github/workflows/claude-dependabot.yml
+++ b/.github/workflows/claude-dependabot.yml
@@ -74,7 +74,7 @@ jobs:
- name: Generate Prisma Client
working-directory: autogpt_platform/backend
- run: poetry run prisma generate
+ run: poetry run prisma generate && poetry run gen-prisma-stub
# Frontend Node.js/pnpm setup (mirrors platform-frontend-ci.yml)
- name: Set up Node.js
diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
index 3f5e8c22ec..71c6ef49c2 100644
--- a/.github/workflows/claude.yml
+++ b/.github/workflows/claude.yml
@@ -90,7 +90,7 @@ jobs:
- name: Generate Prisma Client
working-directory: autogpt_platform/backend
- run: poetry run prisma generate
+ run: poetry run prisma generate && poetry run gen-prisma-stub
# Frontend Node.js/pnpm setup (mirrors platform-frontend-ci.yml)
- name: Set up Node.js
diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml
index 13ef01cc44..aac8befee0 100644
--- a/.github/workflows/copilot-setup-steps.yml
+++ b/.github/workflows/copilot-setup-steps.yml
@@ -72,7 +72,7 @@ jobs:
- name: Generate Prisma Client
working-directory: autogpt_platform/backend
- run: poetry run prisma generate
+ run: poetry run prisma generate && poetry run gen-prisma-stub
# Frontend Node.js/pnpm setup (mirrors platform-frontend-ci.yml)
- name: Set up Node.js
@@ -108,6 +108,16 @@ jobs:
# run: pnpm playwright install --with-deps chromium
# Docker setup for development environment
+ - name: Free up disk space
+ run: |
+ # Remove large unused tools to free disk space for Docker builds
+ sudo rm -rf /usr/share/dotnet
+ sudo rm -rf /usr/local/lib/android
+ sudo rm -rf /opt/ghc
+ sudo rm -rf /opt/hostedtoolcache/CodeQL
+ sudo docker system prune -af
+ df -h
+
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
diff --git a/.github/workflows/platform-backend-ci.yml b/.github/workflows/platform-backend-ci.yml
index f962382fa5..da5ab83c1c 100644
--- a/.github/workflows/platform-backend-ci.yml
+++ b/.github/workflows/platform-backend-ci.yml
@@ -134,7 +134,7 @@ jobs:
run: poetry install
- name: Generate Prisma Client
- run: poetry run prisma generate
+ run: poetry run prisma generate && poetry run gen-prisma-stub
- id: supabase
name: Start Supabase
diff --git a/autogpt_platform/Makefile b/autogpt_platform/Makefile
index d99fee49d7..2ff454e392 100644
--- a/autogpt_platform/Makefile
+++ b/autogpt_platform/Makefile
@@ -12,6 +12,7 @@ reset-db:
rm -rf db/docker/volumes/db/data
cd backend && poetry run prisma migrate deploy
cd backend && poetry run prisma generate
+ cd backend && poetry run gen-prisma-stub
# View logs for core services
logs-core:
@@ -33,6 +34,7 @@ init-env:
migrate:
cd backend && poetry run prisma migrate deploy
cd backend && poetry run prisma generate
+ cd backend && poetry run gen-prisma-stub
run-backend:
cd backend && poetry run app
diff --git a/autogpt_platform/backend/Dockerfile b/autogpt_platform/backend/Dockerfile
index 7f51bad3a1..b3389d1787 100644
--- a/autogpt_platform/backend/Dockerfile
+++ b/autogpt_platform/backend/Dockerfile
@@ -48,7 +48,8 @@ RUN poetry install --no-ansi --no-root
# Generate Prisma client
COPY autogpt_platform/backend/schema.prisma ./
COPY autogpt_platform/backend/backend/data/partial_types.py ./backend/data/partial_types.py
-RUN poetry run prisma generate
+COPY autogpt_platform/backend/gen_prisma_types_stub.py ./
+RUN poetry run prisma generate && poetry run gen-prisma-stub
FROM debian:13-slim AS server_dependencies
diff --git a/autogpt_platform/backend/backend/api/features/store/db.py b/autogpt_platform/backend/backend/api/features/store/db.py
index 18db6f43ce..8e4310ee02 100644
--- a/autogpt_platform/backend/backend/api/features/store/db.py
+++ b/autogpt_platform/backend/backend/api/features/store/db.py
@@ -1851,9 +1851,7 @@ async def get_admin_listings_with_versions(
where = prisma.types.StoreListingWhereInput(**where_dict)
include = prisma.types.StoreListingInclude(
Versions=prisma.types.FindManyStoreListingVersionArgsFromStoreListing(
- order_by=prisma.types._StoreListingVersion_version_OrderByInput(
- version="desc"
- )
+ order_by={"version": "desc"}
),
OwningUser=True,
)
diff --git a/autogpt_platform/backend/backend/blocks/airtable/_webhook.py b/autogpt_platform/backend/backend/blocks/airtable/_webhook.py
index 58e6f95d0c..452630953e 100644
--- a/autogpt_platform/backend/backend/blocks/airtable/_webhook.py
+++ b/autogpt_platform/backend/backend/blocks/airtable/_webhook.py
@@ -6,6 +6,9 @@ import hashlib
import hmac
import logging
from enum import Enum
+from typing import cast
+
+from prisma.types import Serializable
from backend.sdk import (
BaseWebhooksManager,
@@ -84,7 +87,9 @@ class AirtableWebhookManager(BaseWebhooksManager):
# update webhook config
await update_webhook(
webhook.id,
- config={"base_id": base_id, "cursor": response.cursor},
+ config=cast(
+ dict[str, Serializable], {"base_id": base_id, "cursor": response.cursor}
+ ),
)
event_type = "notification"
diff --git a/autogpt_platform/backend/gen_prisma_types_stub.py b/autogpt_platform/backend/gen_prisma_types_stub.py
new file mode 100644
index 0000000000..3f6073b2ff
--- /dev/null
+++ b/autogpt_platform/backend/gen_prisma_types_stub.py
@@ -0,0 +1,227 @@
+#!/usr/bin/env python3
+"""
+Generate a lightweight stub for prisma/types.py that collapses all exported
+symbols to Any. This prevents Pyright from spending time/budget on Prisma's
+query DSL types while keeping runtime behavior unchanged.
+
+Usage:
+ poetry run gen-prisma-stub
+
+This script automatically finds the prisma package location and generates
+the types.pyi stub file in the same directory as types.py.
+"""
+
+from __future__ import annotations
+
+import ast
+import importlib.util
+import sys
+from pathlib import Path
+from typing import Iterable, Set
+
+
+def _iter_assigned_names(target: ast.expr) -> Iterable[str]:
+ """Extract names from assignment targets (handles tuple unpacking)."""
+ if isinstance(target, ast.Name):
+ yield target.id
+ elif isinstance(target, (ast.Tuple, ast.List)):
+ for elt in target.elts:
+ yield from _iter_assigned_names(elt)
+
+
+def _is_private(name: str) -> bool:
+ """Check if a name is private (starts with _ but not __)."""
+ return name.startswith("_") and not name.startswith("__")
+
+
+def _is_safe_type_alias(node: ast.Assign) -> bool:
+ """Check if an assignment is a safe type alias that shouldn't be stubbed.
+
+ Safe types are:
+ - Literal types (don't cause type budget issues)
+ - Simple type references (SortMode, SortOrder, etc.)
+ - TypeVar definitions
+ """
+ if not node.value:
+ return False
+
+ # Check if it's a Subscript (like Literal[...], Union[...], TypeVar[...])
+ if isinstance(node.value, ast.Subscript):
+ # Get the base type name
+ if isinstance(node.value.value, ast.Name):
+ base_name = node.value.value.id
+ # Literal types are safe
+ if base_name == "Literal":
+ return True
+ # TypeVar is safe
+ if base_name == "TypeVar":
+ return True
+ elif isinstance(node.value.value, ast.Attribute):
+ # Handle typing_extensions.Literal etc.
+ if node.value.value.attr == "Literal":
+ return True
+
+ # Check if it's a simple Name reference (like SortMode = _types.SortMode)
+ if isinstance(node.value, ast.Attribute):
+ return True
+
+ # Check if it's a Call (like TypeVar(...))
+ if isinstance(node.value, ast.Call):
+ if isinstance(node.value.func, ast.Name):
+ if node.value.func.id == "TypeVar":
+ return True
+
+ return False
+
+
+def collect_top_level_symbols(
+ tree: ast.Module, source_lines: list[str]
+) -> tuple[Set[str], Set[str], list[str], Set[str]]:
+ """Collect all top-level symbols from an AST module.
+
+ Returns:
+ Tuple of (class_names, function_names, safe_variable_sources, unsafe_variable_names)
+ safe_variable_sources contains the actual source code lines for safe variables
+ """
+ classes: Set[str] = set()
+ functions: Set[str] = set()
+ safe_variable_sources: list[str] = []
+ unsafe_variables: Set[str] = set()
+
+ for node in tree.body:
+ if isinstance(node, ast.ClassDef):
+ if not _is_private(node.name):
+ classes.add(node.name)
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
+ if not _is_private(node.name):
+ functions.add(node.name)
+ elif isinstance(node, ast.Assign):
+ is_safe = _is_safe_type_alias(node)
+ names = []
+ for t in node.targets:
+ for n in _iter_assigned_names(t):
+ if not _is_private(n):
+ names.append(n)
+ if names:
+ if is_safe:
+ # Extract the source code for this assignment
+ start_line = node.lineno - 1 # 0-indexed
+ end_line = node.end_lineno if node.end_lineno else node.lineno
+ source = "\n".join(source_lines[start_line:end_line])
+ safe_variable_sources.append(source)
+ else:
+ unsafe_variables.update(names)
+ elif isinstance(node, ast.AnnAssign) and node.target:
+ # Annotated assignments are always stubbed
+ for n in _iter_assigned_names(node.target):
+ if not _is_private(n):
+ unsafe_variables.add(n)
+
+ return classes, functions, safe_variable_sources, unsafe_variables
+
+
+def find_prisma_types_path() -> Path:
+ """Find the prisma types.py file in the installed package."""
+ spec = importlib.util.find_spec("prisma")
+ if spec is None or spec.origin is None:
+ raise RuntimeError("Could not find prisma package. Is it installed?")
+
+ prisma_dir = Path(spec.origin).parent
+ types_path = prisma_dir / "types.py"
+
+ if not types_path.exists():
+ raise RuntimeError(f"prisma/types.py not found at {types_path}")
+
+ return types_path
+
+
+def generate_stub(src_path: Path, stub_path: Path) -> int:
+ """Generate the .pyi stub file from the source types.py."""
+ code = src_path.read_text(encoding="utf-8", errors="ignore")
+ source_lines = code.splitlines()
+ tree = ast.parse(code, filename=str(src_path))
+ classes, functions, safe_variable_sources, unsafe_variables = (
+ collect_top_level_symbols(tree, source_lines)
+ )
+
+ header = """\
+# -*- coding: utf-8 -*-
+# Auto-generated stub file - DO NOT EDIT
+# Generated by gen_prisma_types_stub.py
+#
+# This stub intentionally collapses complex Prisma query DSL types to Any.
+# Prisma's generated types can explode Pyright's type inference budgets
+# on large schemas. We collapse them to Any so the rest of the codebase
+# can remain strongly typed while keeping runtime behavior unchanged.
+#
+# Safe types (Literal, TypeVar, simple references) are preserved from the
+# original types.py to maintain proper type checking where possible.
+
+from __future__ import annotations
+from typing import Any
+from typing_extensions import Literal
+
+# Re-export commonly used typing constructs that may be imported from this module
+from typing import TYPE_CHECKING, TypeVar, Generic, Union, Optional, List, Dict
+
+# Base type alias for stubbed Prisma types - allows any dict structure
+_PrismaDict = dict[str, Any]
+
+"""
+
+ lines = [header]
+
+ # Include safe variable definitions (Literal types, TypeVars, etc.)
+ lines.append("# Safe type definitions preserved from original types.py")
+ for source in safe_variable_sources:
+ lines.append(source)
+ lines.append("")
+
+ # Stub all classes and unsafe variables uniformly as dict[str, Any] aliases
+ # This allows:
+ # 1. Use in type annotations: x: SomeType
+ # 2. Constructor calls: SomeType(...)
+ # 3. Dict literal assignments: x: SomeType = {...}
+ lines.append(
+ "# Stubbed types (collapsed to dict[str, Any] to prevent type budget exhaustion)"
+ )
+ all_stubbed = sorted(classes | unsafe_variables)
+ for name in all_stubbed:
+ lines.append(f"{name} = _PrismaDict")
+
+ lines.append("")
+
+ # Stub functions
+ for name in sorted(functions):
+ lines.append(f"def {name}(*args: Any, **kwargs: Any) -> Any: ...")
+
+ lines.append("")
+
+ stub_path.write_text("\n".join(lines), encoding="utf-8")
+ return (
+ len(classes)
+ + len(functions)
+ + len(safe_variable_sources)
+ + len(unsafe_variables)
+ )
+
+
+def main() -> None:
+ """Main entry point."""
+ try:
+ types_path = find_prisma_types_path()
+ stub_path = types_path.with_suffix(".pyi")
+
+ print(f"Found prisma types.py at: {types_path}")
+ print(f"Generating stub at: {stub_path}")
+
+ num_symbols = generate_stub(types_path, stub_path)
+ print(f"Generated {stub_path.name} with {num_symbols} Any-typed symbols")
+
+ except Exception as e:
+ print(f"Error: {e}", file=sys.stderr)
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/autogpt_platform/backend/linter.py b/autogpt_platform/backend/linter.py
index a86e6761f7..599aae4580 100644
--- a/autogpt_platform/backend/linter.py
+++ b/autogpt_platform/backend/linter.py
@@ -25,6 +25,9 @@ def run(*command: str) -> None:
def lint():
+ # Generate Prisma types stub before running pyright to prevent type budget exhaustion
+ run("gen-prisma-stub")
+
lint_step_args: list[list[str]] = [
["ruff", "check", *TARGET_DIRS, "--exit-zero"],
["ruff", "format", "--diff", "--check", LIBS_DIR],
@@ -49,4 +52,6 @@ def format():
run("ruff", "format", LIBS_DIR)
run("isort", "--profile", "black", BACKEND_DIR)
run("black", BACKEND_DIR)
+ # Generate Prisma types stub before running pyright to prevent type budget exhaustion
+ run("gen-prisma-stub")
run("pyright", *TARGET_DIRS)
diff --git a/autogpt_platform/backend/pyproject.toml b/autogpt_platform/backend/pyproject.toml
index e8b8fd0ba5..88aff1d0b0 100644
--- a/autogpt_platform/backend/pyproject.toml
+++ b/autogpt_platform/backend/pyproject.toml
@@ -117,6 +117,7 @@ lint = "linter:lint"
test = "run_tests:test"
load-store-agents = "test.load_store_agents:run"
export-api-schema = "backend.cli.generate_openapi_json:main"
+gen-prisma-stub = "gen_prisma_types_stub:main"
oauth-tool = "backend.cli.oauth_tool:cli"
[tool.isort]
diff --git a/autogpt_platform/docker-compose.platform.yml b/autogpt_platform/docker-compose.platform.yml
index b2df626029..de6ecfd612 100644
--- a/autogpt_platform/docker-compose.platform.yml
+++ b/autogpt_platform/docker-compose.platform.yml
@@ -37,7 +37,7 @@ services:
context: ../
dockerfile: autogpt_platform/backend/Dockerfile
target: migrate
- command: ["sh", "-c", "poetry run prisma generate && poetry run prisma migrate deploy"]
+ command: ["sh", "-c", "poetry run prisma generate && poetry run gen-prisma-stub && poetry run prisma migrate deploy"]
develop:
watch:
- path: ./
From a318832414b7015f4597d3ee4e394cd69f9ec7e9 Mon Sep 17 00:00:00 2001
From: Nicholas Tindle
Date: Fri, 9 Jan 2026 12:22:05 -0700
Subject: [PATCH 11/23] feat(docs): update dev from gitbook changes (#11740)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
gitbook branch has changes that need synced to dev
### Changes 🏗️
Pull changes from gitbook into dev
---
> [!NOTE]
> Migrates documentation to GitBook and removes the old MkDocs setup.
>
> - Removes MkDocs configuration and infra: `docs/mkdocs.yml`,
`docs/netlify.toml`, `docs/overrides/main.html`,
`docs/requirements.txt`, and JS assets (`_javascript/mathjax.js`,
`_javascript/tablesort.js`)
> - Updates `docs/content/contribute/index.md` to describe GitBook
workflow (gitbook branch, editing, previews, and `SUMMARY.md`)
> - Adds GitBook navigation file `docs/platform/SUMMARY.md` and a new
platform overview page `docs/platform/what-is-autogpt-platform.md`
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e7e118b5a82ef870d7b7b304dc0f30f58e2e7d6b. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
## Summary by CodeRabbit
* **Documentation**
* Updated contribution guide for new documentation platform and workflow
* Added new platform overview and navigation documentation
* **Chores**
* Removed MkDocs configuration and related dependencies
* Removed deprecated JavaScript integrations and deployment overrides
✏️ Tip: You can customize this high-level summary in your review
settings.
---------
Co-authored-by: Claude Opus 4.5
---
docs/_javascript/mathjax.js | 16 --
docs/_javascript/tablesort.js | 6 -
docs/content/contribute/index.md | 55 ++---
docs/mkdocs.yml | 194 ------------------
docs/netlify.toml | 5 -
docs/overrides/assets/favicon.png | Bin 47680 -> 0 bytes
docs/overrides/main.html | 61 ------
docs/platform/SUMMARY.md | 34 +++
docs/{content => }/platform/advanced_setup.md | 0
docs/{content => }/platform/agent-blocks.md | 0
docs/{content => }/platform/aimlapi.md | 0
.../{content => }/platform/block-sdk-guide.md | 0
.../platform/blocks/ai_condition.md | 0
.../blocks/ai_shortform_video_block.md | 0
docs/{content => }/platform/blocks/basic.md | 0
docs/{content => }/platform/blocks/blocks.md | 0
.../platform/blocks/branching.md | 0
docs/{content => }/platform/blocks/csv.md | 0
.../platform/blocks/decoder_block.md | 0
docs/{content => }/platform/blocks/discord.md | 0
.../platform/blocks/email_block.md | 0
.../platform/blocks/flux_kontext.md | 0
.../platform/blocks/github/issues.md | 0
.../platform/blocks/github/pull_requests.md | 0
.../platform/blocks/github/repo.md | 0
.../platform/blocks/google/gmail.md | 0
.../platform/blocks/google/sheet.md | 0
.../platform/blocks/google_maps.md | 0
docs/{content => }/platform/blocks/http.md | 0
.../{content => }/platform/blocks/ideogram.md | 0
.../platform/blocks/iteration.md | 0
docs/{content => }/platform/blocks/llm.md | 0
docs/{content => }/platform/blocks/maths.md | 0
docs/{content => }/platform/blocks/medium.md | 0
docs/{content => }/platform/blocks/reddit.md | 0
.../blocks/replicate_flux_advanced.md | 0
docs/{content => }/platform/blocks/rss.md | 0
.../{content => }/platform/blocks/sampling.md | 0
docs/{content => }/platform/blocks/search.md | 0
.../platform/blocks/talking_head.md | 0
docs/{content => }/platform/blocks/text.md | 0
.../platform/blocks/text_to_speech_block.md | 0
.../platform/blocks/time_blocks.md | 0
docs/{content => }/platform/blocks/todoist.md | 0
.../platform/blocks/twitter/twitter.md | 0
docs/{content => }/platform/blocks/youtube.md | 0
.../contributing/oauth-integration-flow.md | 0
.../platform/contributing/tests.md | 0
.../platform/create-basic-agent.md | 0
docs/{content => }/platform/d_id.md | 0
docs/{content => }/platform/delete-agent.md | 0
.../download-agent-from-marketplace-local.md | 0
docs/{content => }/platform/edit-agent.md | 0
.../{content => }/platform/getting-started.md | 0
docs/{content => }/platform/installer.md | 0
.../platform/integrating/api-guide.md | 0
.../platform/integrating/oauth-guide.md | 0
docs/{content => }/platform/new_blocks.md | 0
docs/{content => }/platform/ollama.md | 0
.../platform/submit-agent-to-marketplace.md | 0
docs/platform/what-is-autogpt-platform.md | 82 ++++++++
docs/requirements.txt | 8 -
62 files changed, 137 insertions(+), 324 deletions(-)
delete mode 100644 docs/_javascript/mathjax.js
delete mode 100644 docs/_javascript/tablesort.js
delete mode 100644 docs/mkdocs.yml
delete mode 100644 docs/netlify.toml
delete mode 100644 docs/overrides/assets/favicon.png
delete mode 100644 docs/overrides/main.html
create mode 100644 docs/platform/SUMMARY.md
rename docs/{content => }/platform/advanced_setup.md (100%)
rename docs/{content => }/platform/agent-blocks.md (100%)
rename docs/{content => }/platform/aimlapi.md (100%)
rename docs/{content => }/platform/block-sdk-guide.md (100%)
rename docs/{content => }/platform/blocks/ai_condition.md (100%)
rename docs/{content => }/platform/blocks/ai_shortform_video_block.md (100%)
rename docs/{content => }/platform/blocks/basic.md (100%)
rename docs/{content => }/platform/blocks/blocks.md (100%)
rename docs/{content => }/platform/blocks/branching.md (100%)
rename docs/{content => }/platform/blocks/csv.md (100%)
rename docs/{content => }/platform/blocks/decoder_block.md (100%)
rename docs/{content => }/platform/blocks/discord.md (100%)
rename docs/{content => }/platform/blocks/email_block.md (100%)
rename docs/{content => }/platform/blocks/flux_kontext.md (100%)
rename docs/{content => }/platform/blocks/github/issues.md (100%)
rename docs/{content => }/platform/blocks/github/pull_requests.md (100%)
rename docs/{content => }/platform/blocks/github/repo.md (100%)
rename docs/{content => }/platform/blocks/google/gmail.md (100%)
rename docs/{content => }/platform/blocks/google/sheet.md (100%)
rename docs/{content => }/platform/blocks/google_maps.md (100%)
rename docs/{content => }/platform/blocks/http.md (100%)
rename docs/{content => }/platform/blocks/ideogram.md (100%)
rename docs/{content => }/platform/blocks/iteration.md (100%)
rename docs/{content => }/platform/blocks/llm.md (100%)
rename docs/{content => }/platform/blocks/maths.md (100%)
rename docs/{content => }/platform/blocks/medium.md (100%)
rename docs/{content => }/platform/blocks/reddit.md (100%)
rename docs/{content => }/platform/blocks/replicate_flux_advanced.md (100%)
rename docs/{content => }/platform/blocks/rss.md (100%)
rename docs/{content => }/platform/blocks/sampling.md (100%)
rename docs/{content => }/platform/blocks/search.md (100%)
rename docs/{content => }/platform/blocks/talking_head.md (100%)
rename docs/{content => }/platform/blocks/text.md (100%)
rename docs/{content => }/platform/blocks/text_to_speech_block.md (100%)
rename docs/{content => }/platform/blocks/time_blocks.md (100%)
rename docs/{content => }/platform/blocks/todoist.md (100%)
rename docs/{content => }/platform/blocks/twitter/twitter.md (100%)
rename docs/{content => }/platform/blocks/youtube.md (100%)
rename docs/{content => }/platform/contributing/oauth-integration-flow.md (100%)
rename docs/{content => }/platform/contributing/tests.md (100%)
rename docs/{content => }/platform/create-basic-agent.md (100%)
rename docs/{content => }/platform/d_id.md (100%)
rename docs/{content => }/platform/delete-agent.md (100%)
rename docs/{content => }/platform/download-agent-from-marketplace-local.md (100%)
rename docs/{content => }/platform/edit-agent.md (100%)
rename docs/{content => }/platform/getting-started.md (100%)
rename docs/{content => }/platform/installer.md (100%)
rename docs/{content => }/platform/integrating/api-guide.md (100%)
rename docs/{content => }/platform/integrating/oauth-guide.md (100%)
rename docs/{content => }/platform/new_blocks.md (100%)
rename docs/{content => }/platform/ollama.md (100%)
rename docs/{content => }/platform/submit-agent-to-marketplace.md (100%)
create mode 100644 docs/platform/what-is-autogpt-platform.md
delete mode 100644 docs/requirements.txt
diff --git a/docs/_javascript/mathjax.js b/docs/_javascript/mathjax.js
deleted file mode 100644
index a80ddbff75..0000000000
--- a/docs/_javascript/mathjax.js
+++ /dev/null
@@ -1,16 +0,0 @@
-window.MathJax = {
- tex: {
- inlineMath: [["\\(", "\\)"]],
- displayMath: [["\\[", "\\]"]],
- processEscapes: true,
- processEnvironments: true
- },
- options: {
- ignoreHtmlClass: ".*|",
- processHtmlClass: "arithmatex"
- }
-};
-
-document$.subscribe(() => {
- MathJax.typesetPromise()
-})
\ No newline at end of file
diff --git a/docs/_javascript/tablesort.js b/docs/_javascript/tablesort.js
deleted file mode 100644
index ee04e90082..0000000000
--- a/docs/_javascript/tablesort.js
+++ /dev/null
@@ -1,6 +0,0 @@
-document$.subscribe(function () {
- var tables = document.querySelectorAll("article table:not([class])")
- tables.forEach(function (table) {
- new Tablesort(table)
- })
-})
\ No newline at end of file
diff --git a/docs/content/contribute/index.md b/docs/content/contribute/index.md
index 4cce72aacf..3b458a04df 100644
--- a/docs/content/contribute/index.md
+++ b/docs/content/contribute/index.md
@@ -1,48 +1,35 @@
# Contributing to the Docs
-We welcome contributions to our documentation! If you would like to contribute, please follow the steps below.
+We welcome contributions to our documentation! Our docs are hosted on GitBook and synced with GitHub.
-## Setting up the Docs
+## How It Works
-1. Clone the repository:
+- Documentation lives in the `docs/` directory on the `gitbook` branch
+- GitBook automatically syncs changes from GitHub
+- You can edit docs directly on GitHub or locally
+
+## Editing Docs Locally
+
+1. Clone the repository and switch to the gitbook branch:
```shell
- git clone github.com/Significant-Gravitas/AutoGPT.git
+ git clone https://github.com/Significant-Gravitas/AutoGPT.git
+ cd AutoGPT
+ git checkout gitbook
```
-1. Install the dependencies:
+2. Make your changes to markdown files in `docs/`
- ```shell
- python -m pip install -r docs/requirements.txt
- ```
+3. Preview changes:
+ - Push to a branch and create a PR - GitBook will generate a preview
+ - Or use any markdown preview tool locally
- or
+## Adding a New Page
- ```shell
- python3 -m pip install -r docs/requirements.txt
- ```
-
-1. Start iterating using mkdocs' live server:
-
- ```shell
- mkdocs serve
- ```
-
-1. Open your browser and navigate to `http://127.0.0.1:8000`.
-
-1. The server will automatically reload the docs when you save your changes.
-
-## Adding a new page
-
-1. Create a new markdown file in the `docs/content` directory.
-1. Add the new page to the `nav` section in the `mkdocs.yml` file.
-1. Add the content to the new markdown file.
-1. Run `mkdocs serve` to see your changes.
-
-## Checking links
-
-To check for broken links in the documentation, run `mkdocs build` and look for warnings in the console output.
+1. Create a new markdown file in the appropriate `docs/` subdirectory
+2. Add the new page to the relevant `SUMMARY.md` file to include it in the navigation
+3. Submit a pull request to the `gitbook` branch
## Submitting a Pull Request
-When you're ready to submit your changes, please create a pull request. We will review your changes and merge them if they are appropriate.
+When you're ready to submit your changes, create a pull request targeting the `gitbook` branch. We will review your changes and merge them if appropriate.
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
deleted file mode 100644
index 876467633e..0000000000
--- a/docs/mkdocs.yml
+++ /dev/null
@@ -1,194 +0,0 @@
-site_name: AutoGPT Documentation
-site_url: https://docs.agpt.co/
-repo_url: https://github.com/Significant-Gravitas/AutoGPT
-repo_name: AutoGPT
-edit_uri: edit/master/docs/content
-docs_dir: content
-nav:
- - Home: index.md
-
- - The AutoGPT Platform 🆕:
- - Getting Started:
- - Setup AutoGPT (Local-Host): platform/getting-started.md
- - Edit an Agent: platform/edit-agent.md
- - Delete an Agent: platform/delete-agent.md
- - Download & Import and Agent: platform/download-agent-from-marketplace-local.md
- - Create a Basic Agent: platform/create-basic-agent.md
- - Submit an Agent to the Marketplace: platform/submit-agent-to-marketplace.md
- - Advanced Setup: platform/advanced_setup.md
- - Agent Blocks: platform/agent-blocks.md
- - Build your own Blocks: platform/new_blocks.md
- - Block SDK Guide: platform/block-sdk-guide.md
- - Using Ollama: platform/ollama.md
- - Using AI/ML API: platform/aimlapi.md
- - Using D-ID: platform/d_id.md
- - Blocks: platform/blocks/blocks.md
- - API:
- - Introduction: platform/integrating/api-guide.md
- - OAuth & SSO: platform/integrating/oauth-guide.md
- - Contributing:
- - Tests: platform/contributing/tests.md
- - OAuth Flows: platform/contributing/oauth-integration-flow.md
- - AutoGPT Classic:
- - Introduction: classic/index.md
- - Setup:
- - Setting up AutoGPT: classic/setup/index.md
- - Set up with Docker: classic/setup/docker.md
- - For Developers: classic/setup/for-developers.md
- - Configuration:
- - Options: classic/configuration/options.md
- - Search: classic/configuration/search.md
- - Voice: classic/configuration/voice.md
- - Usage: classic/usage.md
- - Help us improve AutoGPT:
- - Share your debug logs with us: classic/share-your-logs.md
- - Contribution guide: contributing.md
- - Running tests: classic/testing.md
- - Code of Conduct: code-of-conduct.md
- - Benchmark:
- - Readme: https://github.com/Significant-Gravitas/AutoGPT/blob/master/classic/benchmark/README.md
- - Forge:
- - Introduction: forge/get-started.md
- - Components:
- - Introduction: forge/components/introduction.md
- - Agents: forge/components/agents.md
- - Components: forge/components/components.md
- - Protocols: forge/components/protocols.md
- - Commands: forge/components/commands.md
- - Built in Components: forge/components/built-in-components.md
- - Creating Components: forge/components/creating-components.md
- - Frontend:
- - Readme: https://github.com/Significant-Gravitas/AutoGPT/blob/master/classic/frontend/README.md
-
- - Contribute:
- - Introduction: contribute/index.md
- - Testing: ../../autogpt_platform/backend/TESTING.md
-
- # - Challenges:
- # - Introduction: challenges/introduction.md
- # - List of Challenges:
- # - Memory:
- # - Introduction: challenges/memory/introduction.md
- # - Memory Challenge A: challenges/memory/challenge_a.md
- # - Memory Challenge B: challenges/memory/challenge_b.md
- # - Memory Challenge C: challenges/memory/challenge_c.md
- # - Memory Challenge D: challenges/memory/challenge_d.md
- # - Information retrieval:
- # - Introduction: challenges/information_retrieval/introduction.md
- # - Information Retrieval Challenge A: challenges/information_retrieval/challenge_a.md
- # - Information Retrieval Challenge B: challenges/information_retrieval/challenge_b.md
- # - Submit a Challenge: challenges/submit.md
- # - Beat a Challenge: challenges/beat.md
-
- - License: https://github.com/Significant-Gravitas/AutoGPT/blob/master/LICENSE
-
-theme:
- name: material
- custom_dir: overrides
- language: en
- icon:
- repo: fontawesome/brands/github
- logo: material/book-open-variant
- edit: material/pencil
- view: material/eye
- favicon: assets/favicon.png
- features:
- - navigation.sections
- - navigation.footer
- - navigation.top
- - navigation.tracking
- - navigation.tabs
- # - navigation.path
- - toc.follow
- - toc.integrate
- - content.action.edit
- - content.action.view
- - content.code.copy
- - content.code.annotate
- - content.tabs.link
- palette:
- # Palette toggle for light mode
- - media: "(prefers-color-scheme: light)"
- scheme: default
- toggle:
- icon: material/weather-night
- name: Switch to dark mode
-
- # Palette toggle for dark mode
- - media: "(prefers-color-scheme: dark)"
- scheme: slate
- toggle:
- icon: material/weather-sunny
- name: Switch to light mode
-
-markdown_extensions:
- # Python Markdown
- - abbr
- - admonition
- - attr_list
- - def_list
- - footnotes
- - md_in_html
- - toc:
- permalink: true
- - tables
-
- # Python Markdown Extensions
- - pymdownx.arithmatex:
- generic: true
- - pymdownx.betterem:
- smart_enable: all
- - pymdownx.critic
- - pymdownx.caret
- - pymdownx.details
- - pymdownx.emoji:
- emoji_index: !!python/name:material.extensions.emoji.twemoji
- emoji_generator: !!python/name:material.extensions.emoji.to_svg
- - pymdownx.highlight
- - pymdownx.inlinehilite
- - pymdownx.keys
- - pymdownx.mark
- - pymdownx.smartsymbols
- - pymdownx.snippets:
- base_path: ['.','../']
- check_paths: true
- dedent_subsections: true
- - pymdownx.superfences:
- custom_fences:
- - name: mermaid
- class: mermaid
- format: !!python/name:pymdownx.superfences.fence_code_format
- - pymdownx.tabbed:
- alternate_style: true
- - pymdownx.tasklist:
- custom_checkbox: true
- - pymdownx.tilde
-
-plugins:
- - table-reader
- - search
- - git-revision-date-localized:
- enable_creation_date: true
-
-extra:
- social:
- - icon: fontawesome/brands/github
- link: https://github.com/Significant-Gravitas/AutoGPT
- - icon: fontawesome/brands/x-twitter
- link: https://x.com/Auto_GPT
- - icon: fontawesome/brands/instagram
- link: https://www.instagram.com/autogpt/
- - icon: fontawesome/brands/discord
- link: https://discord.gg/autogpt
-
-extra:
- analytics:
- provider: google
- property: G-XKPNKB9XG6
-
-extra_javascript:
- - https://unpkg.com/tablesort@5.3.0/dist/tablesort.min.js
- - _javascript/tablesort.js
- - _javascript/mathjax.js
- - https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=es6
- - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
diff --git a/docs/netlify.toml b/docs/netlify.toml
deleted file mode 100644
index c07b733a57..0000000000
--- a/docs/netlify.toml
+++ /dev/null
@@ -1,5 +0,0 @@
-# Netlify config for AutoGPT docs
-
-[build]
- publish = "public/"
- command = "mkdocs build -d public"
diff --git a/docs/overrides/assets/favicon.png b/docs/overrides/assets/favicon.png
deleted file mode 100644
index 69bded6abf7e60cf2fc41fc9c8c33cfa2f1e918e..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 47680
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