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 b2750e6578..7c00290821 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 @@ -166,24 +166,32 @@ class RunBlockTool(BaseTool): ) # Get block schemas for details/validation - input_schema: dict[str, Any] = {} - output_schema: dict[str, Any] = {} try: - input_schema = block.input_schema.jsonschema() + input_schema: dict[str, Any] = block.input_schema.jsonschema() except Exception as e: - logger.debug( + logger.warning( "Failed to generate input schema for block %s: %s", block_id, e, ) + return ErrorResponse( + message=f"Block '{block.name}' has an invalid input schema", + error=str(e), + session_id=session_id, + ) try: - output_schema = block.output_schema.jsonschema() + output_schema: dict[str, Any] = block.output_schema.jsonschema() except Exception as e: - logger.debug( + logger.warning( "Failed to generate output schema for block %s: %s", block_id, e, ) + return ErrorResponse( + message=f"Block '{block.name}' has an invalid output schema", + error=str(e), + session_id=session_id, + ) if missing_credentials: # Return setup requirements response with missing credentials @@ -217,17 +225,17 @@ class RunBlockTool(BaseTool): graph_version=None, ) - # Check if this is a first attempt (no input data provided for a block that has inputs) + # Check if this is a first attempt (required inputs missing) # Return block details so user can see what inputs are needed - input_properties = input_schema.get("properties", {}) credentials_fields = set(block.input_schema.get_credentials_fields().keys()) - non_credential_properties = { - k: v for k, v in input_properties.items() if k not in credentials_fields - } + required_keys = set(input_schema.get("required", [])) + required_non_credential_keys = required_keys - credentials_fields provided_input_keys = set(input_data.keys()) - credentials_fields - # If block has non-credential inputs but none were provided, show details first - if non_credential_properties and not provided_input_keys: + # Show details when there are required non-credential inputs and none are provided + if required_non_credential_keys and not ( + required_non_credential_keys & provided_input_keys + ): # Get credentials info for the response credentials_meta = [] for field_name, cred_meta in matched_credentials.items(): diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/test_run_block_details.py b/autogpt_platform/backend/backend/api/features/chat/tools/test_run_block_details.py index a90630eedd..1aa827ae4e 100644 --- a/autogpt_platform/backend/backend/api/features/chat/tools/test_run_block_details.py +++ b/autogpt_platform/backend/backend/api/features/chat/tools/test_run_block_details.py @@ -7,6 +7,7 @@ import pytest from backend.api.features.chat.tools.models import BlockDetailsResponse from backend.api.features.chat.tools.run_block import RunBlockTool from backend.data.block import BlockType +from backend.data.model import CredentialsMetaInput from ._test_data import make_session @@ -65,7 +66,7 @@ async def test_run_block_returns_details_when_no_input_provided(): # Mock credentials check to return no missing credentials with patch.object( RunBlockTool, - "_check_block_credentials", + "_resolve_block_credentials", new_callable=AsyncMock, return_value=({}, []), # (matched_credentials, missing_credentials) ): @@ -123,9 +124,19 @@ async def test_run_block_returns_details_when_only_credentials_provided(): ): with patch.object( RunBlockTool, - "_check_block_credentials", + "_resolve_block_credentials", new_callable=AsyncMock, - return_value=({"credentials": MagicMock()}, []), + return_value=( + { + "credentials": CredentialsMetaInput( + id="cred-id", + provider="test_provider", + type="api_key", + title="Test Credential", + ) + }, + [], + ), ): tool = RunBlockTool() response = await tool._execute( diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/helpers.tsx index 659c676245..b2431f40db 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/helpers.tsx @@ -10,6 +10,22 @@ import { import type { ToolUIPart } from "ai"; import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader"; +/** Block details returned on first run_block attempt (before input_data provided). */ +export interface BlockDetailsResponse { + type: typeof ResponseType.block_details; + message: string; + session_id?: string | null; + block: { + id: string; + name: string; + description: string; + inputs: Record; + outputs: Record; + credentials: unknown[]; + }; + user_authenticated: boolean; +} + export interface RunBlockInput { block_id?: string; block_name?: string; @@ -18,11 +34,13 @@ export interface RunBlockInput { export type RunBlockToolOutput = | SetupRequirementsResponse + | BlockDetailsResponse | BlockOutputResponse | ErrorResponse; const RUN_BLOCK_OUTPUT_TYPES = new Set([ ResponseType.setup_requirements, + ResponseType.block_details, ResponseType.block_output, ResponseType.error, ]); @@ -36,6 +54,15 @@ export function isRunBlockSetupRequirementsOutput( ); } +export function isRunBlockDetailsOutput( + output: RunBlockToolOutput, +): output is BlockDetailsResponse { + return ( + output.type === ResponseType.block_details || + ("block" in output && typeof output.block === "object") + ); +} + export function isRunBlockBlockOutput( output: RunBlockToolOutput, ): output is BlockOutputResponse { @@ -65,6 +92,7 @@ function parseOutput(output: unknown): RunBlockToolOutput | null { return output as RunBlockToolOutput; } if ("block_id" in output) return output as BlockOutputResponse; + if ("block" in output) return output as BlockDetailsResponse; if ("setup_info" in output) return output as SetupRequirementsResponse; if ("error" in output || "details" in output) return output as ErrorResponse; @@ -102,6 +130,8 @@ export function getAnimationText(part: { const output = parseOutput(part.output); if (!output) return `Running${blockText}`; if (isRunBlockBlockOutput(output)) return `Ran "${output.block_name}"`; + if (isRunBlockDetailsOutput(output)) + return `Details for "${output.block.name}"`; if (isRunBlockSetupRequirementsOutput(output)) { return `Setup needed for "${output.setup_info.agent_name}"`; } @@ -165,6 +195,21 @@ export function getAccordionMeta(output: RunBlockToolOutput): { }; } + if (isRunBlockDetailsOutput(output)) { + const inputKeys = Object.keys( + (output.block.inputs as { properties?: Record }) + ?.properties ?? {}, + ); + return { + icon, + title: output.block.name, + description: + inputKeys.length > 0 + ? `${inputKeys.length} input field${inputKeys.length === 1 ? "" : "s"} available` + : output.message, + }; + } + if (isRunBlockSetupRequirementsOutput(output)) { const missingCredsCount = Object.keys( (output.setup_info.user_readiness?.missing_credentials ?? {}) as Record<