//! Ask clarification tool — allows the LLM to ask the user for clarification //! instead of guessing when requirements are ambiguous. //! //! Inspired by DeerFlow's `ask_clarification` + ClarificationMiddleware pattern. //! When the LLM encounters an ambiguous or incomplete request, it calls this tool //! to present a structured question to the user and halt execution. use async_trait::async_trait; use serde_json::{json, Value}; use zclaw_types::{Result, ZclawError}; use crate::tool::{Tool, ToolContext, ToolConcurrency}; /// Clarification type — categorizes the reason for asking. #[derive(Debug, Clone, PartialEq)] pub enum ClarificationType { /// Missing information needed to proceed MissingInfo, /// Requirement is ambiguous — multiple interpretations possible AmbiguousRequirement, /// Multiple approaches available — user needs to choose ApproachChoice, /// Risky operation needs explicit confirmation RiskConfirmation, /// Agent has a suggestion but wants user approval Suggestion, } impl ClarificationType { fn from_str(s: &str) -> Self { match s { "missing_info" => Self::MissingInfo, "ambiguous_requirement" => Self::AmbiguousRequirement, "approach_choice" => Self::ApproachChoice, "risk_confirmation" => Self::RiskConfirmation, "suggestion" => Self::Suggestion, _ => Self::MissingInfo, } } fn as_str(&self) -> &'static str { match self { Self::MissingInfo => "missing_info", Self::AmbiguousRequirement => "ambiguous_requirement", Self::ApproachChoice => "approach_choice", Self::RiskConfirmation => "risk_confirmation", Self::Suggestion => "suggestion", } } } pub struct AskClarificationTool; impl AskClarificationTool { pub fn new() -> Self { Self } } #[async_trait] impl Tool for AskClarificationTool { fn name(&self) -> &str { "ask_clarification" } fn description(&self) -> &str { "Ask the user for clarification when the request is ambiguous, incomplete, \ or involves a choice between approaches. Use this instead of guessing. \ The conversation will pause and wait for the user's response." } fn input_schema(&self) -> Value { json!({ "type": "object", "properties": { "question": { "type": "string", "description": "The specific question to ask the user" }, "clarification_type": { "type": "string", "enum": ["missing_info", "ambiguous_requirement", "approach_choice", "risk_confirmation", "suggestion"], "description": "The type of clarification needed" }, "options": { "type": "array", "items": { "type": "string" }, "description": "Suggested options for the user to choose from (optional)" }, "context": { "type": "string", "description": "Brief context about why clarification is needed" } }, "required": ["question", "clarification_type"] }) } fn concurrency(&self) -> ToolConcurrency { ToolConcurrency::Interactive } async fn execute(&self, input: Value, _context: &ToolContext) -> Result { let question = input["question"].as_str() .ok_or_else(|| ZclawError::InvalidInput("Missing 'question' parameter".into()))?; let clarification_type = input["clarification_type"].as_str() .map(ClarificationType::from_str) .unwrap_or(ClarificationType::MissingInfo); let options: Vec = input["options"].as_array() .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect()) .unwrap_or_default(); let context = input["context"].as_str().unwrap_or(""); tracing::info!( "[AskClarification] type={}, question={:?}, options={:?}", clarification_type.as_str(), question, options ); // Return a structured result that the frontend can render as a clarification card. // The LLM will see this result and incorporate the user's answer in the next turn. let mut result = json!({ "status": "clarification_needed", "clarification_type": clarification_type.as_str(), "question": question, }); if !options.is_empty() { result["options"] = json!(options); } if !context.is_empty() { result["context"] = json!(context); } // Include a note telling the LLM to wait for the user's response result["instruction"] = json!("已向用户提问。请等待用户回复后继续。在用户回复前,不要采取任何行动。"); Ok(result) } } impl Default for AskClarificationTool { fn default() -> Self { Self::new() } }