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 = r#"当遇到不确定或多种选择时,向用户提问以获取明确指示。
21
22【何时使用】
23- 用户请求含义模糊
24- 存在多种可行方案需要选择
25- 决策可能对项目产生重大影响
26- 用户说 "怎么处理"、"选哪个"、"你觉得呢" 时
27
28【何时不使用】
29- 简单问题不需要确认(明显最优、低风险、可逆)
30- 任务可以直接执行不需要用户决策
31- 用户说 "开始做"、"直接改" 时 — 直接执行
32
33【Plan Mode 注意】
34- 不要问 "我的计划准备好了吗?" — 用 ExitPlanMode 获取批准
35- 不要问 "你对计划有反馈吗?" — 用户看不到计划直到 ExitPlanMode
36- 不要在问题中引用 "the plan"
37
38【最佳实践】
39- 提供具体选项(不要只给问题描述)
40- 提供推荐方案及理由
41- 如果推荐特定选项,放在列表第一位并在 label 结尾加 "(推荐)" "#;
42
43#[async_trait]
44impl Tool for AskTool {
45    fn definition(&self) -> ToolDefinition {
46        ToolDefinition {
47            name: "ask".to_string(),
48            description: ASK_TOOL_DESCRIPTION.to_string(),
49            parameters: ask_tool_schema(),
50            ..Default::default()
51        }
52    }
53
54    async fn execute(&self, params: Value) -> Result<String> {
55        // Check for multi-question format
56        if let Some(questions) = params.get("questions").and_then(|q| q.as_array()) {
57            // Multi-question mode - render all questions
58            render_multi_questions(questions);
59            let answer = read_user_answer();
60            println!();
61            return Ok(answer);
62        }
63
64        // Single question mode
65        let question = params["question"]
66            .as_str()
67            .ok_or_else(|| anyhow::anyhow!("missing 'question'"))?;
68
69        let options = params
70            .get("options")
71            .and_then(|o| o.get("options"))
72            .and_then(|o| o.as_array())
73            .or_else(|| params["options"].as_array());
74        let recommendation = params["recommendation"].as_object();
75
76        // Render the question UI
77        render_question_ui(question, options, recommendation);
78
79        // Read user answer
80        let answer = read_user_answer();
81        println!();
82
83        Ok(answer)
84    }
85
86    fn risk_level(&self) -> RiskLevel {
87        RiskLevel::Safe
88    }
89}
90
91/// Get the JSON schema for ask tool parameters.
92fn ask_tool_schema() -> Value {
93    json!({
94        "type": "object",
95        "properties": {
96            "question": {
97                "type": "string",
98                "description": "要向用户提问的问题,需具体清晰(单问题模式)"
99            },
100            "options": {
101                "type": "object",
102                "description": "选项配置(单问题模式)",
103                "properties": {
104                    "multiSelect": {
105                        "type": "boolean",
106                        "description": "是否允许多选,默认 false"
107                    },
108                    "options": {
109                        "type": "array",
110                        "description": "可选方案列表",
111                        "items": {
112                            "type": "object",
113                            "properties": {
114                                "id": {
115                                    "type": "string",
116                                    "description": "选项短标识符(如 'A'、'B'、'1'、'2')"
117                                },
118                                "label": {
119                                    "type": "string",
120                                    "description": "选项的可读标签"
121                                },
122                                "description": {
123                                    "type": "string",
124                                    "description": "该选项的简要说明"
125                                }
126                            },
127                            "required": ["id", "label"]
128                        }
129                    }
130                },
131                "required": ["options"]
132            },
133            "recommendation": {
134                "type": "object",
135                "description": "你的推荐方案及理由",
136                "properties": {
137                    "option_id": {
138                        "type": "string",
139                        "description": "推荐选项的标识符"
140                    },
141                    "reason": {
142                        "type": "string",
143                        "description": "推荐该方案的理由"
144                    }
145                },
146                "required": ["option_id", "reason"]
147            },
148            "questions": {
149                "type": "array",
150                "description": "多问题列表(多问题模式,用户可用 Tab 切换问题)",
151                "items": {
152                    "type": "object",
153                    "properties": {
154                        "id": {
155                            "type": "string",
156                            "description": "问题唯一标识符"
157                        },
158                        "question": {
159                            "type": "string",
160                            "description": "问题内容"
161                        },
162                        "options": {
163                            "type": "object",
164                            "description": "选项配置",
165                            "properties": {
166                                "multiSelect": {
167                                    "type": "boolean",
168                                    "description": "是否允许多选,默认 false"
169                                },
170                                "options": {
171                                    "type": "array",
172                                    "description": "可选方案列表",
173                                    "items": {
174                                        "type": "object",
175                                        "properties": {
176                                            "id": { "type": "string", "description": "选项标识符" },
177                                            "label": { "type": "string", "description": "选项标签" },
178                                            "description": { "type": "string", "description": "选项说明" }
179                                        },
180                                        "required": ["id", "label"]
181                                    }
182                                }
183                            },
184                            "required": ["options"]
185                        }
186                    },
187                    "required": ["id", "question"]
188                }
189            }
190        },
191        "oneOf": [
192            { "required": ["question"] },
193            { "required": ["questions"] }
194        ]
195    })
196}
197
198// ============================================================================
199// UI Rendering
200// ============================================================================
201
202/// Render the question UI with options and recommendation.
203fn render_question_ui(
204    question: &str,
205    options: Option<&Vec<Value>>,
206    recommendation: Option<&serde_json::Map<String, Value>>,
207) {
208    println!();
209    println!("┌─ AI 询问 ─────────────────────────────────────────");
210
211    // Print the question
212    for line in question.lines() {
213        println!("│ {}", line);
214    }
215    println!("│");
216
217    // Print options if provided
218    if let Some(opts) = options
219        && !opts.is_empty()
220    {
221        render_options(opts);
222    }
223
224    // Print recommendation if provided
225    if let Some(rec) = recommendation {
226        render_recommendation(rec, options);
227    }
228
229    println!("│ 请输入你的选择或补充想法:");
230    println!("└────────────────────────────────────────────────────");
231    print!("> ");
232    let _ = io::stdout().flush();
233}
234
235/// Render the list of options.
236fn render_options(opts: &[Value]) {
237    println!("│ 可选方案:");
238    for opt in opts {
239        let id = opt["id"].as_str().unwrap_or("?");
240        let label = opt["label"].as_str().unwrap_or("未命名");
241        let desc = opt["description"].as_str();
242        if let Some(d) = desc {
243            println!("│   {}) {} - {}", id, label, d);
244        } else {
245            println!("│   {}) {}", id, label);
246        }
247    }
248    println!("│");
249}
250
251/// Render multiple questions (CLI mode - non-TUI).
252fn render_multi_questions(questions: &[Value]) {
253    println!();
254    println!(
255        "┌─ AI 询问 (共 {} 个问题) ───────────────────────────",
256        questions.len()
257    );
258
259    for (idx, q) in questions.iter().enumerate() {
260        let question = q["question"].as_str().unwrap_or("");
261        println!("│");
262        println!("│ 【问题 {}】", idx + 1);
263        for line in question.lines() {
264            println!("│   {}", line);
265        }
266
267        // Render options if provided
268        if let Some(opts_obj) = q.get("options") {
269            let opts = opts_obj.get("options").and_then(|o| o.as_array());
270            if let Some(opts) = opts {
271                println!("│   可选方案:");
272                for opt in opts {
273                    let id = opt["id"].as_str().unwrap_or("?");
274                    let label = opt["label"].as_str().unwrap_or("未命名");
275                    println!("│     {} - {}", id, label);
276                }
277            }
278        }
279    }
280
281    println!("│");
282    println!("│ 请依次回答所有问题:");
283    println!("└────────────────────────────────────────────────────");
284    print!("> ");
285    let _ = io::stdout().flush();
286}
287
288/// Render the recommendation with reasoning.
289fn render_recommendation(rec: &serde_json::Map<String, Value>, options: Option<&Vec<Value>>) {
290    let opt_id = rec["option_id"].as_str().unwrap_or("?");
291    let reason = rec["reason"].as_str().unwrap_or("无理由");
292
293    // Find the label for the recommended option
294    let rec_label = options
295        .and_then(|opts| opts.iter().find(|o| o["id"].as_str() == Some(opt_id)))
296        .and_then(|o| o["label"].as_str())
297        .unwrap_or(opt_id);
298
299    println!("│ 💡 推荐方案:{} ({})", rec_label, opt_id);
300    println!("│    理由:{}", reason);
301    println!("│");
302}
303
304// ============================================================================
305// Input Reading
306// ============================================================================
307
308/// Read user's answer from stdin.
309fn read_user_answer() -> String {
310    let stdin = io::stdin();
311    let mut line = String::new();
312    if stdin.lock().read_line(&mut line).is_err() {
313        return "stdin_read_error".to_string();
314    }
315    line.trim().to_string()
316}