mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -04:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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(),
|
||||
|
||||
130
autogpt_platform/backend/backend/copilot/tools/decompose_goal.py
Normal file
130
autogpt_platform/backend/backend/copilot/tools/decompose_goal.py
Normal file
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 <RunAgentTool key={key} part={part as ToolUIPart} />;
|
||||
case "tool-decompose_goal":
|
||||
return <DecomposeGoalTool key={key} part={part as ToolUIPart} />;
|
||||
case "tool-create_agent":
|
||||
return <CreateAgentTool key={key} part={part as ToolUIPart} />;
|
||||
case "tool-edit_agent":
|
||||
|
||||
@@ -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 (
|
||||
<div className="py-2">
|
||||
{isOperating && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<ToolIcon isStreaming={isStreaming} isError={isError} />
|
||||
<MorphingTextAnimation
|
||||
text={text}
|
||||
className={isError ? "text-red-500" : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && output && isErrorOutput(output) && (
|
||||
<ToolErrorCard
|
||||
message={output.message ?? ""}
|
||||
fallbackMessage="Failed to analyze the goal. Please try again."
|
||||
actions={[
|
||||
{
|
||||
label: "Try again",
|
||||
onClick: () => onSend("Please try decomposing the goal again."),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{output && isDecompositionOutput(output) && (
|
||||
<ToolAccordion
|
||||
icon={<AccordionIcon />}
|
||||
title={`Build Plan — ${output.step_count} steps`}
|
||||
description={output.goal}
|
||||
defaultExpanded
|
||||
>
|
||||
<ContentGrid>
|
||||
<ContentMessage>{output.message}</ContentMessage>
|
||||
|
||||
<div className="space-y-0.5 rounded-lg border border-slate-200 bg-white p-3">
|
||||
{output.steps.map((step, i) => (
|
||||
<StepItem
|
||||
key={step.step_id}
|
||||
index={i}
|
||||
description={step.description}
|
||||
action={step.action}
|
||||
blockName={step.block_name}
|
||||
status={step.status}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{output.requires_approval && (
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<Button variant="primary" onClick={handleApprove}>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<CheckIcon size={14} weight="bold" />
|
||||
Approve
|
||||
</span>
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={handleModify}>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<PencilSimpleIcon size={14} weight="bold" />
|
||||
Modify
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ContentHint>
|
||||
Review the plan above and approve to start building.
|
||||
</ContentHint>
|
||||
</ContentGrid>
|
||||
</ToolAccordion>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex items-start gap-3 py-1.5">
|
||||
<div className="mt-0.5 flex shrink-0 items-center">
|
||||
<StepStatusIcon status={status} />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<Text variant="body-medium" className="text-sm text-zinc-800">
|
||||
{index + 1}. {description}
|
||||
</Text>
|
||||
{blockName && (
|
||||
<div className="mt-0.5 flex items-center gap-1">
|
||||
<CubeIcon size={12} className="text-neutral-400" />
|
||||
<Text
|
||||
variant="small"
|
||||
className="font-mono text-xs text-neutral-500"
|
||||
>
|
||||
{blockName}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<WarningDiamondIcon size={14} weight="regular" className="text-red-500" />
|
||||
);
|
||||
}
|
||||
if (isStreaming) {
|
||||
return <ScaleLoader size={14} />;
|
||||
}
|
||||
return (
|
||||
<ListChecksIcon size={14} weight="regular" className="text-neutral-400" />
|
||||
);
|
||||
}
|
||||
|
||||
export function AccordionIcon() {
|
||||
return <ListChecksIcon size={32} weight="light" />;
|
||||
}
|
||||
|
||||
export function StepStatusIcon({ status }: { status: string }) {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return (
|
||||
<CheckCircleIcon size={18} weight="fill" className="text-emerald-500" />
|
||||
);
|
||||
case "in_progress":
|
||||
return (
|
||||
<SpinnerGapIcon
|
||||
size={18}
|
||||
weight="bold"
|
||||
className="animate-spin text-blue-500"
|
||||
/>
|
||||
);
|
||||
case "failed":
|
||||
return <XCircleIcon size={18} weight="fill" className="text-red-500" />;
|
||||
default:
|
||||
return (
|
||||
<CircleDashedIcon
|
||||
size={18}
|
||||
weight="regular"
|
||||
className="text-neutral-400"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user