diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/run_agent.py b/autogpt_platform/backend/backend/api/features/chat/tools/run_agent.py index 4d93a3af30..b212c11e8a 100644 --- a/autogpt_platform/backend/backend/api/features/chat/tools/run_agent.py +++ b/autogpt_platform/backend/backend/api/features/chat/tools/run_agent.py @@ -33,7 +33,7 @@ from .models import ( UserReadiness, ) from .utils import ( - check_user_has_required_credentials, + build_missing_credentials_from_graph, extract_credentials_from_schema, fetch_graph_from_store_slug, get_or_create_library_agent, @@ -237,15 +237,13 @@ class RunAgentTool(BaseTool): # Return credentials needed response with input data info # The UI handles credential setup automatically, so the message # focuses on asking about input data - credentials = extract_credentials_from_schema( - graph.credentials_input_schema + requirements_creds_dict = build_missing_credentials_from_graph( + graph, None ) - missing_creds_check = await check_user_has_required_credentials( - user_id, credentials + missing_credentials_dict = build_missing_credentials_from_graph( + graph, graph_credentials ) - missing_credentials_dict = { - c.id: c.model_dump() for c in missing_creds_check - } + requirements_creds_list = list(requirements_creds_dict.values()) return SetupRequirementsResponse( message=self._build_inputs_message(graph, MSG_WHAT_VALUES_TO_USE), @@ -259,7 +257,7 @@ class RunAgentTool(BaseTool): ready_to_run=False, ), requirements={ - "credentials": [c.model_dump() for c in credentials], + "credentials": requirements_creds_list, "inputs": self._get_inputs_list(graph.input_schema), "execution_modes": self._get_execution_modes(graph), }, diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/run_block.py b/autogpt_platform/backend/backend/api/features/chat/tools/run_block.py index 02f493df71..c29cc92556 100644 --- a/autogpt_platform/backend/backend/api/features/chat/tools/run_block.py +++ b/autogpt_platform/backend/backend/api/features/chat/tools/run_block.py @@ -22,6 +22,7 @@ from .models import ( ToolResponseBase, UserReadiness, ) +from .utils import build_missing_credentials_from_field_info logger = logging.getLogger(__name__) @@ -189,7 +190,11 @@ class RunBlockTool(BaseTool): if missing_credentials: # Return setup requirements response with missing credentials - missing_creds_dict = {c.id: c.model_dump() for c in missing_credentials} + credentials_fields_info = block.input_schema.get_credentials_fields_info() + missing_creds_dict = build_missing_credentials_from_field_info( + credentials_fields_info, set(matched_credentials.keys()) + ) + missing_creds_list = list(missing_creds_dict.values()) return SetupRequirementsResponse( message=( @@ -206,7 +211,7 @@ class RunBlockTool(BaseTool): ready_to_run=False, ), requirements={ - "credentials": [c.model_dump() for c in missing_credentials], + "credentials": missing_creds_list, "inputs": self._get_inputs_list(block), "execution_modes": ["immediate"], }, diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/utils.py b/autogpt_platform/backend/backend/api/features/chat/tools/utils.py index 19e092c312..a2ac91dc65 100644 --- a/autogpt_platform/backend/backend/api/features/chat/tools/utils.py +++ b/autogpt_platform/backend/backend/api/features/chat/tools/utils.py @@ -8,7 +8,7 @@ from backend.api.features.library import model as library_model from backend.api.features.store import db as store_db from backend.data import graph as graph_db from backend.data.graph import GraphModel -from backend.data.model import CredentialsMetaInput +from backend.data.model import CredentialsFieldInfo, CredentialsMetaInput from backend.integrations.creds_manager import IntegrationCredentialsManager from backend.util.exceptions import NotFoundError @@ -89,6 +89,59 @@ def extract_credentials_from_schema( return credentials +def _serialize_missing_credential( + field_key: str, field_info: CredentialsFieldInfo +) -> dict[str, Any]: + """ + Convert credential field info into a serializable dict that preserves all supported + credential types (e.g., api_key + oauth2) so the UI can offer multiple options. + """ + supported_types = sorted(field_info.supported_types) + provider = next(iter(field_info.provider), "unknown") + scopes = sorted(field_info.required_scopes or []) + + return { + "id": field_key, + "title": field_key.replace("_", " ").title(), + "provider": provider, + "provider_name": provider.replace("_", " ").title(), + "type": supported_types[0] if supported_types else "api_key", + "types": supported_types, + "scopes": scopes, + } + + +def build_missing_credentials_from_graph( + graph: GraphModel, matched_credentials: dict[str, CredentialsMetaInput] | None +) -> dict[str, Any]: + """ + Build a missing_credentials mapping from a graph's aggregated credentials inputs, + preserving all supported credential types for each field. + """ + matched_keys = set(matched_credentials.keys()) if matched_credentials else set() + aggregated_fields = graph.aggregate_credentials_inputs() + + return { + field_key: _serialize_missing_credential(field_key, field_info) + for field_key, (field_info, _node_fields) in aggregated_fields.items() + if field_key not in matched_keys + } + + +def build_missing_credentials_from_field_info( + credential_fields: dict[str, CredentialsFieldInfo], + matched_keys: set[str], +) -> dict[str, Any]: + """ + Build missing_credentials mapping from a simple credentials field info dictionary. + """ + return { + field_key: _serialize_missing_credential(field_key, field_info) + for field_key, field_info in credential_fields.items() + if field_key not in matched_keys + } + + def extract_credentials_as_dict( credentials_input_schema: dict[str, Any] | None, ) -> dict[str, CredentialsMetaInput]: diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunGraph/RunGraph.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunGraph/RunGraph.tsx index f381ccb93b..57890b1f17 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunGraph/RunGraph.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunGraph/RunGraph.tsx @@ -5,10 +5,11 @@ import { TooltipContent, TooltipTrigger, } from "@/components/atoms/Tooltip/BaseTooltip"; -import { PlayIcon, StopIcon } from "@phosphor-icons/react"; +import { CircleNotchIcon, PlayIcon, StopIcon } from "@phosphor-icons/react"; import { useShallow } from "zustand/react/shallow"; import { RunInputDialog } from "../RunInputDialog/RunInputDialog"; import { useRunGraph } from "./useRunGraph"; +import { cn } from "@/lib/utils"; export const RunGraph = ({ flowID }: { flowID: string | null }) => { const { @@ -24,6 +25,31 @@ export const RunGraph = ({ flowID }: { flowID: string | null }) => { useShallow((state) => state.isGraphRunning), ); + const isLoading = isExecutingGraph || isTerminatingGraph || isSaving; + + // Determine which icon to show with proper animation + const renderIcon = () => { + const iconClass = cn( + "size-4 transition-transform duration-200 ease-out", + !isLoading && "group-hover:scale-110", + ); + + if (isLoading) { + return ( + + ); + } + + if (isGraphRunning) { + return ; + } + + return ; + }; + return ( <> @@ -33,18 +59,18 @@ export const RunGraph = ({ flowID }: { flowID: string | null }) => { variant={isGraphRunning ? "destructive" : "primary"} data-id={isGraphRunning ? "stop-graph-button" : "run-graph-button"} onClick={isGraphRunning ? handleStopGraph : handleRunGraph} - disabled={!flowID || isExecutingGraph || isTerminatingGraph} - loading={isExecutingGraph || isTerminatingGraph || isSaving} + disabled={!flowID || isLoading} + className="group" > - {!isGraphRunning ? ( - - ) : ( - - )} + {renderIcon()} - {isGraphRunning ? "Stop agent" : "Run agent"} + {isLoading + ? "Processing..." + : isGraphRunning + ? "Stop agent" + : "Run agent"} -
- {/* Credentials Section */} - {hasCredentials() && credentialFields.length > 0 && ( -
-
- - Credentials - +
+
+ {/* Credentials Section */} + {hasCredentials() && credentialFields.length > 0 && ( +
+
+ + Credentials + +
+
+ +
-
- -
-
- )} + )} - {/* Inputs Section */} - {hasInputs() && ( -
-
- - Inputs - + {/* Inputs Section */} + {hasInputs() && ( +
+
+ + Inputs + +
+
+ handleInputChange(v.formData)} + uiSchema={uiSchema} + initialValues={{}} + formContext={{ + showHandles: false, + size: "large", + }} + /> +
-
- handleInputChange(v.formData)} - uiSchema={uiSchema} - initialValues={{}} - formContext={{ - showHandles: false, - size: "large", - }} - /> -
-
- )} + )} +
- {/* Action Button */}
{purpose === "run" && (