diff --git a/autogpt_platform/backend/backend/blocks/smart_decision_maker.py b/autogpt_platform/backend/backend/blocks/smart_decision_maker.py index 5f9d562e60..26ff3d61a1 100644 --- a/autogpt_platform/backend/backend/blocks/smart_decision_maker.py +++ b/autogpt_platform/backend/backend/blocks/smart_decision_maker.py @@ -603,9 +603,25 @@ class SmartDecisionMakerBlock(Block): graph_exec_id: str, node_exec_id: str, user_id: str, + nodes_to_skip: set[str] | None = None, **kwargs, ) -> BlockOutput: tool_functions = await self._create_tool_node_signatures(node_id) + + # 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 + ] + + if 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/graph.py b/autogpt_platform/backend/backend/data/graph.py index dbdca755e9..9a578c2e62 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -335,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/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py index c647f4de48..65f34f8064 100644 --- a/autogpt_platform/backend/backend/executor/manager.py +++ b/autogpt_platform/backend/backend/executor/manager.py @@ -147,6 +147,7 @@ async def execute_node( data: NodeExecutionEntry, 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, @@ -212,6 +213,7 @@ async def execute_node( "node_exec_id": node_exec_id, "user_id": user_id, "execution_context": execution_context, + "nodes_to_skip": nodes_to_skip or set(), } # Last-minute fetch credentials + acquire a system-wide read-write lock to prevent @@ -509,6 +511,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, @@ -531,6 +534,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 @@ -576,6 +580,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 @@ -612,6 +617,7 @@ class ExecutionProcessor: data=node_exec, execution_stats=stats, nodes_input_masks=nodes_input_masks, + nodes_to_skip=nodes_to_skip, ): await persist_output(output_name, output_data) @@ -993,6 +999,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 0663aee85e..1fb2b9404f 100644 --- a/autogpt_platform/backend/backend/executor/utils.py +++ b/autogpt_platform/backend/backend/executor/utils.py @@ -275,7 +275,12 @@ async def _validate_node_input_credentials( ): field_value = node_input_mask[field_name] elif field_name in node.input_default: - field_value = node.input_default[field_name] + # 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 ( @@ -554,11 +559,13 @@ async def validate_and_construct_node_execution_input( nodes_input_masks or {}, ) - 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, + 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, nodes_to_skip 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 2d9f51c8bf..da2fe5339a 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 f0bb3b1c98..d088f29521 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 @@ -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/stores/nodeStore.ts b/autogpt_platform/frontend/src/app/(platform)/build/stores/nodeStore.ts index 11ce78646e..7ed4d79008 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/stores/nodeStore.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/stores/nodeStore.ts @@ -313,9 +313,10 @@ export const useNodeStore = create((set, get) => ({ })); }, - getCredentialsOptional: (nodeId: string) => { + getCredentialsOptional: (nodeId: string): boolean => { const node = get().nodes.find((n) => n.id === nodeId); - return node?.data?.metadata?.credentials_optional ?? false; + const value = node?.data?.metadata?.credentials_optional; + return typeof value === "boolean" ? value : false; }, setCredentialsOptional: (nodeId: string, optional: boolean) => { 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 e63105c751..4cea513e80 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 @@ -33,6 +33,7 @@ type Props = { onSelectCredentials: (newValue?: CredentialsMetaInput) => void; onLoaded?: (loaded: boolean) => void; readOnly?: boolean; + isOptional?: boolean; }; export function CredentialsInput({ @@ -43,6 +44,7 @@ export function CredentialsInput({ siblingInputs, onLoaded, readOnly = false, + isOptional = false, }: Props) { const hookData = useCredentialsInputs({ schema, @@ -90,7 +92,14 @@ export function CredentialsInput({ return (
- {displayName} credentials + + {displayName} credentials + {isOptional && ( + + (optional) + + )} + {schema.description && ( )} 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 d8c4ecb730..4786e710dd 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" || @@ -106,14 +112,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/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index f8c5563476..8bc43272f8 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -2,6 +2,7 @@ "openapi": "3.1.0", "info": { "title": "AutoGPT Agent Server", + "summary": "AutoGPT Agent Server", "description": "This server is used to execute agents that are created by the AutoGPT system.", "version": "0.1" }, @@ -5477,6 +5478,7 @@ "APIKeyPermission": { "type": "string", "enum": [ + "IDENTITY", "EXECUTE_GRAPH", "READ_GRAPH", "EXECUTE_BLOCK", diff --git a/autogpt_platform/frontend/src/components/renderers/input-renderer/FormRenderer.tsx b/autogpt_platform/frontend/src/components/renderers/input-renderer/FormRenderer.tsx index 22c5496efb..accc475fed 100644 --- a/autogpt_platform/frontend/src/components/renderers/input-renderer/FormRenderer.tsx +++ b/autogpt_platform/frontend/src/components/renderers/input-renderer/FormRenderer.tsx @@ -13,6 +13,7 @@ type FormContextType = { uiType?: BlockUIType; showHandles?: boolean; size?: "small" | "medium" | "large"; + showOptionalToggle?: boolean; }; type FormRendererProps = { diff --git a/autogpt_platform/frontend/src/components/renderers/input-renderer/fields/CredentialField/CredentialField.tsx b/autogpt_platform/frontend/src/components/renderers/input-renderer/fields/CredentialField/CredentialField.tsx index 3c2e618ad6..64774798a1 100644 --- a/autogpt_platform/frontend/src/components/renderers/input-renderer/fields/CredentialField/CredentialField.tsx +++ b/autogpt_platform/frontend/src/components/renderers/input-renderer/fields/CredentialField/CredentialField.tsx @@ -22,6 +22,8 @@ export const CredentialsField = (props: FieldProps) => { } = props; const nodeId = formContext.nodeId; + // Only show the optional toggle when editing blocks in the builder canvas + const showOptionalToggle = formContext.showOptionalToggle !== false && nodeId; const { credentialsOptional, setCredentialsOptional } = useNodeStore( useShallow((state) => ({ @@ -72,7 +74,9 @@ export const CredentialsField = (props: FieldProps) => { disabled={false} label="Credential" placeholder={ - credentialsOptional ? "Select credential (optional)" : "Select credential" + credentialsOptional + ? "Select credential (optional)" + : "Select credential" } /> )} @@ -99,20 +103,24 @@ export const CredentialsField = (props: FieldProps) => { )}
- {/* Optional credentials toggle */} -
- setCredentialsOptional(nodeId, checked)} - /> - -
+ {/* Optional credentials toggle - only show in builder canvas, not run dialogs */} + {showOptionalToggle && ( +
+ + setCredentialsOptional(nodeId, checked) + } + /> + +
+ )}
); };