Skip to main content

matrixcode_core/tools/
ask.rs

1//! Ask tool: allow the AI to proactively ask the user for clarification or choice.
2//!
3//! When the AI encounters ambiguity, multiple options, or uncertain decisions,
4//! it can use this tool to pause and ask the user instead of guessing.
5
6use anyhow::Result;
7use async_trait::async_trait;
8use serde_json::{Value, json};
9use std::io::{self, BufRead, Write as _};
10
11use super::{Tool, ToolDefinition};
12use crate::approval::RiskLevel;
13
14// ============================================================================
15// Tool Definition
16// ============================================================================
17
18pub struct AskTool;
19
20const ASK_TOOL_DESCRIPTION: &str = 
21    "Ask the user a question when you are uncertain or there are multiple options. \
22     ALWAYS use this tool when: (1) the user's request is ambiguous, \
23     (2) there are multiple viable approaches and you need to choose one, \
24     (3) a decision could significantly impact the project. \
25     Provide your recommendation with reasoning so the user can make an informed choice. \
26     Do NOT guess or proceed without clarification when uncertain.";
27
28#[async_trait]
29impl Tool for AskTool {
30    fn definition(&self) -> ToolDefinition {
31        ToolDefinition {
32            name: "ask".to_string(),
33            description: ASK_TOOL_DESCRIPTION.to_string(),
34            parameters: ask_tool_schema(),
35        }
36    }
37
38    async fn execute(&self, params: Value) -> Result<String> {
39        let question = params["question"]
40            .as_str()
41            .ok_or_else(|| anyhow::anyhow!("missing 'question'"))?;
42
43        let options = params["options"].as_array();
44        let recommendation = params["recommendation"].as_object();
45
46        // Render the question UI
47        render_question_ui(question, options, recommendation);
48
49        // Read user answer
50        let answer = read_user_answer();
51        println!();
52
53        Ok(answer)
54    }
55
56    fn risk_level(&self) -> RiskLevel {
57        RiskLevel::Safe
58    }
59}
60
61/// Get the JSON schema for ask tool parameters.
62fn ask_tool_schema() -> Value {
63    json!({
64        "type": "object",
65        "properties": {
66            "question": {
67                "type": "string",
68                "description": "The question to ask the user. Be specific and clear."
69            },
70            "options": {
71                "type": "array",
72                "description": "List of available options/choices",
73                "items": {
74                    "type": "object",
75                    "properties": {
76                        "id": {
77                            "type": "string",
78                            "description": "Short identifier for the option (e.g., 'A', 'B', '1', '2')"
79                        },
80                        "label": {
81                            "type": "string",
82                            "description": "Human-readable label for the option"
83                        },
84                        "description": {
85                            "type": "string",
86                            "description": "Brief description of what this option entails"
87                        }
88                    },
89                    "required": ["id", "label"]
90                }
91            },
92            "recommendation": {
93                "type": "object",
94                "description": "Your recommended option with reasoning",
95                "properties": {
96                    "option_id": {
97                        "type": "string",
98                        "description": "The id of your recommended option"
99                    },
100                    "reason": {
101                        "type": "string",
102                        "description": "Why you recommend this option"
103                    }
104                },
105                "required": ["option_id", "reason"]
106            }
107        },
108        "required": ["question"]
109    })
110}
111
112// ============================================================================
113// UI Rendering
114// ============================================================================
115
116/// Render the question UI with options and recommendation.
117fn render_question_ui(
118    question: &str,
119    options: Option<&Vec<Value>>,
120    recommendation: Option<&serde_json::Map<String, Value>>,
121) {
122    println!();
123    println!("┌─ AI 询问 ─────────────────────────────────────────");
124
125    // Print the question
126    for line in question.lines() {
127        println!("│ {}", line);
128    }
129    println!("│");
130
131    // Print options if provided
132    if let Some(opts) = options
133        && !opts.is_empty() {
134            render_options(opts);
135        }
136
137    // Print recommendation if provided
138    if let Some(rec) = recommendation {
139        render_recommendation(rec, options);
140    }
141
142    println!("│ 请输入你的选择或补充想法:");
143    println!("└────────────────────────────────────────────────────");
144    print!("> ");
145    let _ = io::stdout().flush();
146}
147
148/// Render the list of options.
149fn render_options(opts: &[Value]) {
150    println!("│ 可选方案:");
151    for opt in opts {
152        let id = opt["id"].as_str().unwrap_or("?");
153        let label = opt["label"].as_str().unwrap_or("未命名");
154        let desc = opt["description"].as_str();
155        if let Some(d) = desc {
156            println!("│   {}) {} - {}", id, label, d);
157        } else {
158            println!("│   {}) {}", id, label);
159        }
160    }
161    println!("│");
162}
163
164/// Render the recommendation with reasoning.
165fn render_recommendation(
166    rec: &serde_json::Map<String, Value>,
167    options: Option<&Vec<Value>>,
168) {
169    let opt_id = rec["option_id"].as_str().unwrap_or("?");
170    let reason = rec["reason"].as_str().unwrap_or("无理由");
171
172    // Find the label for the recommended option
173    let rec_label = options
174        .and_then(|opts| opts.iter().find(|o| o["id"].as_str() == Some(opt_id)))
175        .and_then(|o| o["label"].as_str())
176        .unwrap_or(opt_id);
177
178    println!("│ 💡 推荐方案:{} ({})", rec_label, opt_id);
179    println!("│    理由:{}", reason);
180    println!("│");
181}
182
183// ============================================================================
184// Input Reading
185// ============================================================================
186
187/// Read user's answer from stdin.
188fn read_user_answer() -> String {
189    let stdin = io::stdin();
190    let mut line = String::new();
191    if stdin.lock().read_line(&mut line).is_err() {
192        return "无法读取回答".to_string();
193    }
194    line.trim().to_string()
195}