mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
feat(platform): complete optional credentials implementation
- Backend: Populate required array in credentials schema based on credentials_optional metadata - Backend: Skip input_default for optional credentials to prevent stale credential validation errors - Backend: Pass nodes_to_skip through execution chain to blocks - Backend: Filter Smart Decision Maker tools based on nodes_to_skip to prevent LLM from selecting unavailable tools - Frontend: Filter incomplete credentials before sending run requests - Frontend: Show (optional) indicator for non-required credentials - Frontend: Only show optional toggle in builder canvas, not run dialogs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 []
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -66,6 +66,7 @@ export const RunInputDialog = ({
|
||||
formContext={{
|
||||
showHandles: false,
|
||||
size: "large",
|
||||
showOptionalToggle: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -313,9 +313,10 @@ export const useNodeStore = create<NodeStore>((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) => {
|
||||
|
||||
@@ -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 (
|
||||
<div className={cn("mb-6", className)}>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Text variant="large-medium">{displayName} credentials</Text>
|
||||
<Text variant="large-medium">
|
||||
{displayName} credentials
|
||||
{isOptional && (
|
||||
<span className="ml-1 text-sm font-normal text-gray-500">
|
||||
(optional)
|
||||
</span>
|
||||
)}
|
||||
</Text>
|
||||
{schema.description && (
|
||||
<InformationTooltip description={schema.description} />
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col gap-4">
|
||||
{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)}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -13,6 +13,7 @@ type FormContextType = {
|
||||
uiType?: BlockUIType;
|
||||
showHandles?: boolean;
|
||||
size?: "small" | "medium" | "large";
|
||||
showOptionalToggle?: boolean;
|
||||
};
|
||||
|
||||
type FormRendererProps = {
|
||||
|
||||
@@ -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) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Optional credentials toggle */}
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Switch
|
||||
id={`credentials-optional-${nodeId}`}
|
||||
checked={credentialsOptional}
|
||||
onCheckedChange={(checked) => setCredentialsOptional(nodeId, checked)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`credentials-optional-${nodeId}`}
|
||||
className="text-xs text-gray-500 cursor-pointer"
|
||||
>
|
||||
Optional - skip block if not configured
|
||||
</label>
|
||||
</div>
|
||||
{/* Optional credentials toggle - only show in builder canvas, not run dialogs */}
|
||||
{showOptionalToggle && (
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<Switch
|
||||
id={`credentials-optional-${nodeId}`}
|
||||
checked={credentialsOptional}
|
||||
onCheckedChange={(checked) =>
|
||||
setCredentialsOptional(nodeId, checked)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`credentials-optional-${nodeId}`}
|
||||
className="cursor-pointer text-xs text-gray-500"
|
||||
>
|
||||
Optional - skip block if not configured
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user