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