mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(platform): add generic ask_question copilot tool
Add a generic `ask_question` tool that lets the copilot ask the user clarifying questions via a dedicated UI card instead of plain text (which gets collapsed into hidden reasoning). Reuses the existing `ClarificationNeededResponse` model and `ClarificationQuestionsCard` component. Backend: - New `AskQuestionTool` in `copilot/tools/ask_question.py` - Registered in `TOOL_REGISTRY` and `ToolName` permissions literal - Updated `agent_generation_guide.md` to reference `ask_question` Frontend: - Added `tool-ask_question` to `CUSTOM_TOOL_TYPES` (prevents collapse) - New `AskQuestion/` renderer reusing `ClarificationQuestionsCard` - Registered in `MessagePartRenderer` switch
This commit is contained in:
@@ -66,6 +66,7 @@ from pydantic import BaseModel, PrivateAttr
|
||||
ToolName = Literal[
|
||||
# Platform tools (must match keys in TOOL_REGISTRY)
|
||||
"add_understanding",
|
||||
"ask_question",
|
||||
"bash_exec",
|
||||
"browser_act",
|
||||
"browser_navigate",
|
||||
|
||||
@@ -10,7 +10,7 @@ Before starting the workflow below, check whether the user's goal is
|
||||
or trigger. If so:
|
||||
1. Call `find_block` with a query targeting the ambiguous dimension to
|
||||
discover what the platform actually supports.
|
||||
2. Ask the user **one concrete question** grounded in the discovered
|
||||
2. Call `ask_question` with a concrete question listing the discovered
|
||||
options (e.g. "The platform supports Gmail, Slack, and Google Docs —
|
||||
which should the agent use for delivery?").
|
||||
3. **Wait for the user's answer** before proceeding.
|
||||
|
||||
@@ -10,6 +10,7 @@ from backend.copilot.tracking import track_tool_called
|
||||
from .add_understanding import AddUnderstandingTool
|
||||
from .agent_browser import BrowserActTool, BrowserNavigateTool, BrowserScreenshotTool
|
||||
from .agent_output import AgentOutputTool
|
||||
from .ask_question import AskQuestionTool
|
||||
from .base import BaseTool
|
||||
from .bash_exec import BashExecTool
|
||||
from .connect_integration import ConnectIntegrationTool
|
||||
@@ -55,6 +56,7 @@ logger = logging.getLogger(__name__)
|
||||
# Single source of truth for all tools
|
||||
TOOL_REGISTRY: dict[str, BaseTool] = {
|
||||
"add_understanding": AddUnderstandingTool(),
|
||||
"ask_question": AskQuestionTool(),
|
||||
"create_agent": CreateAgentTool(),
|
||||
"customize_agent": CustomizeAgentTool(),
|
||||
"edit_agent": EditAgentTool(),
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""AskQuestionTool - Ask the user a clarifying question before proceeding."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from backend.copilot.model import ChatSession
|
||||
|
||||
from .base import BaseTool
|
||||
from .models import ClarificationNeededResponse, ClarifyingQuestion, ToolResponseBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AskQuestionTool(BaseTool):
|
||||
"""Ask the user a clarifying question and wait for their answer.
|
||||
|
||||
Use this tool when the user's request is ambiguous and you need more
|
||||
information before proceeding. Call find_block or other discovery tools
|
||||
first to ground your question in real platform options, then call this
|
||||
tool with a concrete question listing those options.
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "ask_question"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return (
|
||||
"Ask the user a clarifying question. Use when the request is "
|
||||
"ambiguous and you need to confirm intent, choose between options, "
|
||||
"or gather missing details before proceeding."
|
||||
)
|
||||
|
||||
@property
|
||||
def parameters(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"question": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"The concrete question to ask the user. Should list "
|
||||
"real options when applicable."
|
||||
),
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": (
|
||||
"Options for the user to choose from "
|
||||
"(e.g. ['Email', 'Slack', 'Google Docs'])."
|
||||
),
|
||||
},
|
||||
"keyword": {
|
||||
"type": "string",
|
||||
"description": "Short label identifying what the question is about.",
|
||||
},
|
||||
},
|
||||
"required": ["question"],
|
||||
}
|
||||
|
||||
@property
|
||||
def requires_auth(self) -> bool:
|
||||
return False
|
||||
|
||||
async def _execute(
|
||||
self,
|
||||
user_id: str | None,
|
||||
session: ChatSession,
|
||||
**kwargs: Any,
|
||||
) -> ToolResponseBase:
|
||||
del user_id # unused; required by BaseTool contract
|
||||
question: str = kwargs.get("question", "")
|
||||
options: list[str] = kwargs.get("options", [])
|
||||
keyword: str = kwargs.get("keyword", "")
|
||||
session_id = session.session_id if session else None
|
||||
|
||||
example = ", ".join(options) if options else None
|
||||
clarifying_question = ClarifyingQuestion(
|
||||
question=question,
|
||||
keyword=keyword,
|
||||
example=example,
|
||||
)
|
||||
return ClarificationNeededResponse(
|
||||
message=question,
|
||||
session_id=session_id,
|
||||
questions=[clarifying_question],
|
||||
)
|
||||
@@ -3,6 +3,7 @@ import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { ExclamationMarkIcon } from "@phosphor-icons/react";
|
||||
import { ToolUIPart, UIDataTypes, UIMessage, UITools } from "ai";
|
||||
import { useState } from "react";
|
||||
import { AskQuestionTool } from "../../../tools/AskQuestion/AskQuestion";
|
||||
import { ConnectIntegrationTool } from "../../../tools/ConnectIntegrationTool/ConnectIntegrationTool";
|
||||
import { CreateAgentTool } from "../../../tools/CreateAgent/CreateAgent";
|
||||
import { EditAgentTool } from "../../../tools/EditAgent/EditAgent";
|
||||
@@ -129,6 +130,8 @@ export function MessagePartRenderer({
|
||||
</MessageResponse>
|
||||
);
|
||||
}
|
||||
case "tool-ask_question":
|
||||
return <AskQuestionTool key={key} part={part as ToolUIPart} />;
|
||||
case "tool-find_block":
|
||||
return <FindBlocksTool key={key} part={part as ToolUIPart} />;
|
||||
case "tool-find_agent":
|
||||
|
||||
@@ -13,6 +13,7 @@ export type RenderSegment =
|
||||
| { kind: "collapsed-group"; parts: ToolUIPart[] };
|
||||
|
||||
const CUSTOM_TOOL_TYPES = new Set([
|
||||
"tool-ask_question",
|
||||
"tool-find_block",
|
||||
"tool-find_agent",
|
||||
"tool-find_library_agent",
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { ChatTeardropDotsIcon, WarningCircleIcon } from "@phosphor-icons/react";
|
||||
import type { ToolUIPart } from "ai";
|
||||
import { ClarificationQuestionsCard } from "../CreateAgent/components/ClarificationQuestionsCard";
|
||||
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import { normalizeClarifyingQuestions } from "../clarifying-questions";
|
||||
import {
|
||||
getAnimationText,
|
||||
getAskQuestionOutput,
|
||||
isClarificationOutput,
|
||||
isErrorOutput,
|
||||
} from "./helpers";
|
||||
|
||||
interface Props {
|
||||
part: ToolUIPart;
|
||||
}
|
||||
|
||||
export function AskQuestionTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
const { onSend } = useCopilotChatActions();
|
||||
|
||||
const isStreaming =
|
||||
part.state === "input-streaming" || part.state === "input-available";
|
||||
const isError = part.state === "output-error";
|
||||
|
||||
const output = getAskQuestionOutput(part);
|
||||
|
||||
function handleAnswers(answers: Record<string, string>) {
|
||||
if (!output || !isClarificationOutput(output)) return;
|
||||
const questions = output.questions ?? [];
|
||||
const message = questions
|
||||
.map((q) => {
|
||||
const answer = answers[q.keyword] || "";
|
||||
return `> ${q.question}\n\n${answer}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
onSend(`**Here are my answers:**\n\n${message}\n\nPlease proceed.`);
|
||||
}
|
||||
|
||||
if (output && isClarificationOutput(output)) {
|
||||
return (
|
||||
<ClarificationQuestionsCard
|
||||
questions={normalizeClarifyingQuestions(output.questions ?? [])}
|
||||
message={output.message}
|
||||
onSubmitAnswers={handleAnswers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-2 text-sm text-muted-foreground">
|
||||
{isError || (output && isErrorOutput(output)) ? (
|
||||
<WarningCircleIcon size={16} className="text-red-500" />
|
||||
) : isStreaming ? (
|
||||
<ChatTeardropDotsIcon size={16} className="animate-pulse" />
|
||||
) : (
|
||||
<ChatTeardropDotsIcon size={16} />
|
||||
)}
|
||||
<MorphingTextAnimation
|
||||
text={text}
|
||||
className={isError ? "text-red-500" : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { ResponseType } from "@/app/api/__generated__/models/responseType";
|
||||
import type { ToolUIPart } from "ai";
|
||||
|
||||
interface ClarifyingQuestionPayload {
|
||||
question: string;
|
||||
keyword: string;
|
||||
example?: string;
|
||||
}
|
||||
|
||||
export interface AskQuestionOutput {
|
||||
type: string;
|
||||
message: string;
|
||||
questions: ClarifyingQuestionPayload[];
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
interface ErrorOutput {
|
||||
type: "error";
|
||||
message: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type AskQuestionToolOutput = AskQuestionOutput | ErrorOutput;
|
||||
|
||||
function parseOutput(output: unknown): AskQuestionToolOutput | null {
|
||||
if (!output) return null;
|
||||
if (typeof output === "string") {
|
||||
try {
|
||||
return parseOutput(JSON.parse(output) as unknown);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof output === "object" && output !== null) {
|
||||
const obj = output as Record<string, unknown>;
|
||||
if (
|
||||
obj.type === ResponseType.agent_builder_clarification_needed ||
|
||||
"questions" in obj
|
||||
) {
|
||||
return obj as unknown as AskQuestionOutput;
|
||||
}
|
||||
if (obj.type === "error" || "error" in obj) {
|
||||
return obj as unknown as ErrorOutput;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getAskQuestionOutput(
|
||||
part: ToolUIPart,
|
||||
): AskQuestionToolOutput | null {
|
||||
return parseOutput(part.output);
|
||||
}
|
||||
|
||||
export function isClarificationOutput(
|
||||
output: AskQuestionToolOutput,
|
||||
): output is AskQuestionOutput {
|
||||
return (
|
||||
output.type === ResponseType.agent_builder_clarification_needed ||
|
||||
"questions" in output
|
||||
);
|
||||
}
|
||||
|
||||
export function isErrorOutput(
|
||||
output: AskQuestionToolOutput,
|
||||
): output is ErrorOutput {
|
||||
return output.type === "error";
|
||||
}
|
||||
|
||||
export function getAnimationText(part: ToolUIPart): string {
|
||||
switch (part.state) {
|
||||
case "input-streaming":
|
||||
case "input-available":
|
||||
return "Asking question...";
|
||||
case "output-available": {
|
||||
const output = parseOutput(part.output);
|
||||
if (output && isClarificationOutput(output)) return "Needs your input";
|
||||
if (output && isErrorOutput(output)) return "Failed to ask question";
|
||||
return "Asking question...";
|
||||
}
|
||||
case "output-error":
|
||||
return "Failed to ask question";
|
||||
default:
|
||||
return "Asking question...";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user