From 5bb919e7b5b609940ec73dbdfb57f2b917ce7cda Mon Sep 17 00:00:00 2001 From: anvyle Date: Wed, 8 Apr 2026 14:33:49 +0200 Subject: [PATCH] feat(copilot): add task decomposition for agent building Add a decompose_goal tool that breaks user goals into sub-instructions before building. Users see a plan checklist and can approve or modify before the agent is created, improving transparency and control. - Backend: DecomposeGoalTool, TaskDecompositionResponse model, system prompt update - Frontend: DecomposeGoal component with StepItem checklist, approve/modify buttons Co-Authored-By: Claude Opus 4.6 --- .../backend/backend/copilot/prompting.py | 11 ++ .../backend/backend/copilot/tools/__init__.py | 2 + .../backend/copilot/tools/decompose_goal.py | 130 +++++++++++++++ .../backend/backend/copilot/tools/models.py | 35 ++++ .../components/MessagePartRenderer.tsx | 3 + .../tools/DecomposeGoal/DecomposeGoal.tsx | 124 ++++++++++++++ .../DecomposeGoal/components/StepItem.tsx | 39 +++++ .../copilot/tools/DecomposeGoal/helpers.tsx | 152 ++++++++++++++++++ 8 files changed, 496 insertions(+) create mode 100644 autogpt_platform/backend/backend/copilot/tools/decompose_goal.py create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/components/StepItem.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/helpers.tsx diff --git a/autogpt_platform/backend/backend/copilot/prompting.py b/autogpt_platform/backend/backend/copilot/prompting.py index dd630a2e9b..3fdba6fc33 100644 --- a/autogpt_platform/backend/backend/copilot/prompting.py +++ b/autogpt_platform/backend/backend/copilot/prompting.py @@ -127,6 +127,17 @@ After building the file, reference it with `@@agptfile:` in other tools: non-overlapping scope to avoid redundant searches. +### Agent Building Workflow — ALWAYS follow this +When the user asks to create an agent, ALWAYS follow this workflow: +1. Analyze the goal and break it into logical sub-instructions. +2. Call `decompose_goal` with the steps (each step = one logical task like + "add input block", "add AI summarizer", "wire blocks together"). +3. Wait for user approval before proceeding. +4. After approval, call `create_agent` with the full agent JSON. + +For simple goals (1-2 blocks), keep the decomposition brief (2-3 steps). +For complex goals, decompose into 4-8 steps max. + ### Tool Discovery Priority When the user asks to interact with a service or API, follow this order: diff --git a/autogpt_platform/backend/backend/copilot/tools/__init__.py b/autogpt_platform/backend/backend/copilot/tools/__init__.py index 6d1a054c32..a15624776f 100644 --- a/autogpt_platform/backend/backend/copilot/tools/__init__.py +++ b/autogpt_platform/backend/backend/copilot/tools/__init__.py @@ -17,6 +17,7 @@ from .connect_integration import ConnectIntegrationTool from .continue_run_block import ContinueRunBlockTool from .create_agent import CreateAgentTool from .customize_agent import CustomizeAgentTool +from .decompose_goal import DecomposeGoalTool from .edit_agent import EditAgentTool from .feature_requests import CreateFeatureRequestTool, SearchFeatureRequestsTool from .find_agent import FindAgentTool @@ -59,6 +60,7 @@ TOOL_REGISTRY: dict[str, BaseTool] = { "ask_question": AskQuestionTool(), "create_agent": CreateAgentTool(), "customize_agent": CustomizeAgentTool(), + "decompose_goal": DecomposeGoalTool(), "edit_agent": EditAgentTool(), "find_agent": FindAgentTool(), "find_block": FindBlockTool(), diff --git a/autogpt_platform/backend/backend/copilot/tools/decompose_goal.py b/autogpt_platform/backend/backend/copilot/tools/decompose_goal.py new file mode 100644 index 0000000000..598b0c597b --- /dev/null +++ b/autogpt_platform/backend/backend/copilot/tools/decompose_goal.py @@ -0,0 +1,130 @@ +"""DecomposeGoalTool - Breaks agent-building goals into sub-instructions.""" + +import logging +from typing import Any + +from backend.copilot.model import ChatSession + +from .base import BaseTool +from .models import ( + DecompositionStepModel, + ErrorResponse, + TaskDecompositionResponse, + ToolResponseBase, +) + +logger = logging.getLogger(__name__) + +MAX_STEPS = 10 + + +class DecomposeGoalTool(BaseTool): + """Tool for decomposing an agent goal into sub-instructions.""" + + @property + def name(self) -> str: + return "decompose_goal" + + @property + def description(self) -> str: + return ( + "Break down an agent-building goal into logical sub-instructions. " + "Each step maps to one task (e.g. add a block, wire connections, " + "configure settings). ALWAYS call this before create_agent to show " + "the user your plan and get approval." + ) + + @property + def parameters(self) -> dict[str, Any]: + return { + "type": "object", + "properties": { + "goal": { + "type": "string", + "description": "The user's agent-building goal.", + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "Human-readable step description.", + }, + "action": { + "type": "string", + "description": ( + "Action type: 'add_block', 'connect_blocks', " + "'configure', 'add_input', 'add_output'." + ), + }, + "block_name": { + "type": "string", + "description": "Block name if adding a block.", + }, + }, + "required": ["description", "action"], + }, + "description": "List of sub-instructions for the plan.", + }, + "require_approval": { + "type": "boolean", + "description": "Whether to ask user for approval (default: true).", + "default": True, + }, + }, + "required": ["goal", "steps"], + } + + async def _execute( + self, + user_id: str | None, + session: ChatSession, + goal: str | None = None, + steps: list[dict[str, Any]] | None = None, + require_approval: bool = True, + **kwargs, + ) -> ToolResponseBase: + session_id = session.session_id if session else None + + if not goal: + return ErrorResponse( + message="Please provide a goal to decompose.", + error="missing_goal", + session_id=session_id, + ) + + if not steps: + return ErrorResponse( + message="Please provide at least one step in the plan.", + error="missing_steps", + session_id=session_id, + ) + + if len(steps) > MAX_STEPS: + return ErrorResponse( + message=f"Too many steps ({len(steps)}). Keep the plan to {MAX_STEPS} steps max.", + error="too_many_steps", + session_id=session_id, + ) + + decomposition_steps = [ + DecompositionStepModel( + step_id=f"step_{i + 1}", + description=step.get("description", ""), + action=step.get("action", "add_block"), + block_name=step.get("block_name"), + status="pending", + ) + for i, step in enumerate(steps) + ] + + return TaskDecompositionResponse( + message=f"Here's the plan to build your agent ({len(decomposition_steps)} steps):", + goal=goal, + steps=decomposition_steps, + step_count=len(decomposition_steps), + requires_approval=require_approval, + session_id=session_id, + ) diff --git a/autogpt_platform/backend/backend/copilot/tools/models.py b/autogpt_platform/backend/backend/copilot/tools/models.py index a0d1ad13ef..6c65764469 100644 --- a/autogpt_platform/backend/backend/copilot/tools/models.py +++ b/autogpt_platform/backend/backend/copilot/tools/models.py @@ -36,6 +36,9 @@ class ResponseType(str, Enum): AGENT_BUILDER_VALIDATION_RESULT = "agent_builder_validation_result" AGENT_BUILDER_FIX_RESULT = "agent_builder_fix_result" + # Task decomposition (goal → sub-instructions) + TASK_DECOMPOSITION = "task_decomposition" + # Block BLOCK_LIST = "block_list" BLOCK_DETAILS = "block_details" @@ -688,3 +691,35 @@ class AgentsMovedToFolderResponse(ToolResponseBase): agent_names: list[str] = [] folder_id: str | None = None count: int = 0 + + +# Task decomposition models + + +class DecompositionStepModel(BaseModel): + """A single step in a decomposed agent-building plan.""" + + step_id: str = Field(description="Unique step identifier, e.g. 'step_1'") + description: str = Field(description="Human-readable step description") + action: str = Field( + description="Action type: 'add_block', 'connect_blocks', 'configure', etc." + ) + block_name: str | None = Field( + default=None, description="Block being added, if applicable" + ) + status: str = Field( + default="pending", + description="Step status: pending, in_progress, completed, failed", + ) + + +class TaskDecompositionResponse(ToolResponseBase): + """Response for decompose_goal tool — shows the plan to the user.""" + + type: ResponseType = ResponseType.TASK_DECOMPOSITION + goal: str = Field(description="The original user goal") + steps: list[DecompositionStepModel] + step_count: int + requires_approval: bool = True + + diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx index 5d129a0a78..0668ea1e24 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx @@ -5,6 +5,7 @@ import { ToolUIPart, UIDataTypes, UIMessage, UITools } from "ai"; import { AskQuestionTool } from "../../../tools/AskQuestion/AskQuestion"; import { ConnectIntegrationTool } from "../../../tools/ConnectIntegrationTool/ConnectIntegrationTool"; import { CreateAgentTool } from "../../../tools/CreateAgent/CreateAgent"; +import { DecomposeGoalTool } from "../../../tools/DecomposeGoal/DecomposeGoal"; import { EditAgentTool } from "../../../tools/EditAgent/EditAgent"; import { CreateFeatureRequestTool, @@ -144,6 +145,8 @@ export function MessagePartRenderer({ case "tool-run_agent": case "tool-schedule_agent": return ; + case "tool-decompose_goal": + return ; case "tool-create_agent": return ; case "tool-edit_agent": diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx new file mode 100644 index 0000000000..18abeb9fba --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { Button } from "@/components/atoms/Button/Button"; +import { CheckIcon, PencilSimpleIcon } from "@phosphor-icons/react"; +import type { ToolUIPart } from "ai"; +import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions"; +import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; +import { + ContentGrid, + ContentHint, + ContentMessage, +} from "../../components/ToolAccordion/AccordionContent"; +import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion"; +import { ToolErrorCard } from "../../components/ToolErrorCard/ToolErrorCard"; +import { StepItem } from "./components/StepItem"; +import { + AccordionIcon, + getAnimationText, + getDecomposeGoalOutput, + isDecompositionOutput, + isErrorOutput, + ToolIcon, +} from "./helpers"; + +interface Props { + part: ToolUIPart; +} + +export function DecomposeGoalTool({ part }: Props) { + const text = getAnimationText(part); + const { onSend } = useCopilotChatActions(); + + const isStreaming = + part.state === "input-streaming" || part.state === "input-available"; + + const output = getDecomposeGoalOutput(part); + + const isError = + part.state === "output-error" || (!!output && isErrorOutput(output)); + + const isOperating = !output; + + function handleApprove() { + onSend("Approved. Please build the agent."); + } + + function handleModify() { + onSend("I'd like to modify the plan. Here are my changes: "); + } + + return ( +
+ {isOperating && ( +
+ + +
+ )} + + {isError && output && isErrorOutput(output) && ( + onSend("Please try decomposing the goal again."), + }, + ]} + /> + )} + + {output && isDecompositionOutput(output) && ( + } + title={`Build Plan — ${output.step_count} steps`} + description={output.goal} + defaultExpanded + > + + {output.message} + +
+ {output.steps.map((step, i) => ( + + ))} +
+ + {output.requires_approval && ( +
+ + +
+ )} + + + Review the plan above and approve to start building. + +
+
+ )} +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/components/StepItem.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/components/StepItem.tsx new file mode 100644 index 0000000000..ac1305e706 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/components/StepItem.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Text } from "@/components/atoms/Text/Text"; +import { CubeIcon } from "@phosphor-icons/react"; +import { StepStatusIcon } from "../helpers"; + +interface Props { + index: number; + description: string; + action: string; + blockName?: string | null; + status: string; +} + +export function StepItem({ index, description, blockName, status }: Props) { + return ( +
+
+ +
+
+ + {index + 1}. {description} + + {blockName && ( +
+ + + {blockName} + +
+ )} +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/helpers.tsx new file mode 100644 index 0000000000..ac1111d1e6 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/helpers.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { + CheckCircleIcon, + CircleDashedIcon, + ListChecksIcon, + SpinnerGapIcon, + WarningDiamondIcon, + XCircleIcon, +} from "@phosphor-icons/react"; +import type { ToolUIPart } from "ai"; +import { ScaleLoader } from "../../components/ScaleLoader/ScaleLoader"; + +interface DecompositionStep { + step_id: string; + description: string; + action: string; + block_name?: string | null; + status: string; +} + +export interface TaskDecompositionOutput { + type: string; + message: string; + goal: string; + steps: DecompositionStep[]; + step_count: number; + requires_approval: boolean; +} + +export interface DecomposeErrorOutput { + type: string; + error?: string; + message?: string; +} + +export type DecomposeGoalOutput = + | TaskDecompositionOutput + | DecomposeErrorOutput; + +function parseOutput(output: unknown): DecomposeGoalOutput | null { + if (!output) return null; + if (typeof output === "string") { + const trimmed = output.trim(); + if (!trimmed) return null; + try { + return parseOutput(JSON.parse(trimmed) as unknown); + } catch { + return null; + } + } + if (typeof output === "object") { + if ("steps" in output && "goal" in output) { + return output as TaskDecompositionOutput; + } + if ("error" in output) { + return output as DecomposeErrorOutput; + } + } + return null; +} + +export function getDecomposeGoalOutput( + part: unknown, +): DecomposeGoalOutput | null { + if (!part || typeof part !== "object") return null; + return parseOutput((part as { output?: unknown }).output); +} + +export function isDecompositionOutput( + output: DecomposeGoalOutput, +): output is TaskDecompositionOutput { + return "steps" in output && "goal" in output; +} + +export function isErrorOutput( + output: DecomposeGoalOutput, +): output is DecomposeErrorOutput { + return "error" in output; +} + +export function getAnimationText(part: { + state: ToolUIPart["state"]; + output?: unknown; +}): string { + switch (part.state) { + case "input-streaming": + case "input-available": + return "Analyzing your goal..."; + case "output-available": { + const output = parseOutput(part.output); + if (output && isDecompositionOutput(output)) + return `Plan ready (${output.step_count} steps)`; + return "Analyzing your goal..."; + } + case "output-error": + return "Error analyzing goal"; + default: + return "Analyzing your goal..."; + } +} + +export function ToolIcon({ + isStreaming, + isError, +}: { + isStreaming?: boolean; + isError?: boolean; +}) { + if (isError) { + return ( + + ); + } + if (isStreaming) { + return ; + } + return ( + + ); +} + +export function AccordionIcon() { + return ; +} + +export function StepStatusIcon({ status }: { status: string }) { + switch (status) { + case "completed": + return ( + + ); + case "in_progress": + return ( + + ); + case "failed": + return ; + default: + return ( + + ); + } +}