Skip to main content

ai_agent/tools/
ask.rs

1// Source: ~/claudecode/openclaudecode/src/tools/AskUserQuestionTool/prompt.ts
2//! Ask user question tool.
3//!
4//! Provides tool for asking the user for input with multiple choice options.
5
6use crate::error::AgentError;
7use crate::types::*;
8
9pub const ASK_USER_QUESTION_TOOL_NAME: &str = "AskUserQuestion";
10
11/// AskUserQuestion tool - ask the user for input
12pub struct AskUserQuestionTool;
13
14impl AskUserQuestionTool {
15    pub fn new() -> Self {
16        Self
17    }
18
19    pub fn name(&self) -> &str {
20        ASK_USER_QUESTION_TOOL_NAME
21    }
22
23    pub fn description(&self) -> &str {
24        "Ask the user a question with multiple choice options. Use this when you need user input to proceed."
25    }
26
27    pub fn user_facing_name(&self, _input: Option<&serde_json::Value>) -> String {
28        "AskUserQuestion".to_string()
29    }
30
31    pub fn get_tool_use_summary(&self, input: Option<&serde_json::Value>) -> Option<String> {
32        input.and_then(|inp| inp["question"].as_str().map(String::from))
33    }
34
35    pub fn render_tool_result_message(
36        &self,
37        content: &serde_json::Value,
38    ) -> Option<String> {
39        content["content"].as_str().map(|s| s.to_string())
40    }
41
42    pub fn input_schema(&self) -> ToolInputSchema {
43        ToolInputSchema {
44            schema_type: "object".to_string(),
45            properties: serde_json::json!({
46                "question": {
47                    "type": "string",
48                    "description": "The complete question to ask the user"
49                },
50                "header": {
51                    "type": "string",
52                    "description": "Very short label displayed as a chip/tag (max 12 chars)"
53                },
54                "options": {
55                    "type": "array",
56                    "items": {
57                        "type": "object",
58                        "properties": {
59                            "label": { "type": "string", "description": "The display text for this option (1-5 words)" },
60                            "description": { "type": "string", "description": "Explanation of what this option means or what will happen if chosen" }
61                        },
62                        "required": ["label", "description"]
63                    },
64                    "description": "Available choices for this question. Must have 2-4 options."
65                },
66                "multiSelect": {
67                    "type": "boolean",
68                    "description": "Set to true to allow the user to select multiple options instead of just one"
69                },
70                "preview": {
71                    "type": "object",
72                    "properties": {
73                        "type": { "type": "string", "enum": ["html", "markdown"] },
74                        "content": { "type": "string" }
75                    },
76                    "description": "Optional HTML or Markdown preview to show the user alongside the question"
77                }
78            }),
79            required: Some(vec![
80                "question".to_string(),
81                "header".to_string(),
82                "options".to_string(),
83            ]),
84        }
85    }
86
87    pub async fn execute(
88        &self,
89        input: serde_json::Value,
90        _context: &ToolContext,
91    ) -> Result<ToolResult, AgentError> {
92        let question = input["question"]
93            .as_str()
94            .ok_or_else(|| AgentError::Tool("question is required".to_string()))?;
95
96        let header = input["header"]
97            .as_str()
98            .ok_or_else(|| AgentError::Tool("header is required".to_string()))?;
99
100        let options = input["options"]
101            .as_array()
102            .ok_or_else(|| AgentError::Tool("options is required".to_string()))?;
103
104        if options.len() < 2 || options.len() > 4 {
105            return Ok(ToolResult {
106                result_type: "text".to_string(),
107                tool_use_id: "".to_string(),
108                content: "Error: options must have between 2 and 4 choices.".to_string(),
109                is_error: Some(true),
110                was_persisted: None,
111            });
112        }
113
114        let multi_select = input["multiSelect"].as_bool().unwrap_or(false);
115
116        // Format options for display
117        let option_lines: Vec<String> = options
118            .iter()
119            .filter_map(|v| {
120                let label = v.get("label")?.as_str()?;
121                let desc = v.get("description").and_then(|v| v.as_str()).unwrap_or("");
122                Some(format!("  - {}: {}", label, desc))
123            })
124            .collect();
125
126        let multi_select_note = if multi_select {
127            "\n(Note: multiple selections are allowed)"
128        } else {
129            ""
130        };
131
132        let preview_note = if let Some(preview) = input.get("preview") {
133            let preview_type = preview.get("type").and_then(|v| v.as_str()).unwrap_or("");
134            format!("\n[{} preview provided]", preview_type)
135        } else {
136            String::new()
137        };
138
139        let response = format!(
140            "Asking user: {}\n\n\
141            Options: {}\n\n{}\
142            {}\n\
143            {}\n\n\
144            Note: In a full implementation, this would present a UI dialog to the user\n\
145            and wait for their response. The selected option(s) would be returned\n\
146            as the tool result.",
147            question,
148            options.len(),
149            option_lines.join("\n"),
150            multi_select_note,
151            preview_note
152        );
153
154        Ok(ToolResult {
155            result_type: "text".to_string(),
156            tool_use_id: "ask_user_question".to_string(),
157            content: response,
158            is_error: Some(false),
159            was_persisted: None,
160        })
161    }
162}
163
164impl Default for AskUserQuestionTool {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_ask_user_question_name() {
176        let tool = AskUserQuestionTool::new();
177        assert_eq!(tool.name(), ASK_USER_QUESTION_TOOL_NAME);
178    }
179
180    #[test]
181    fn test_ask_user_question_schema() {
182        let tool = AskUserQuestionTool::new();
183        let schema = tool.input_schema();
184        assert!(schema.properties.get("question").is_some());
185        assert!(schema.properties.get("header").is_some());
186        assert!(schema.properties.get("options").is_some());
187        assert!(schema.properties.get("multiSelect").is_some());
188        assert!(schema.properties.get("preview").is_some());
189    }
190
191    #[tokio::test]
192    async fn test_ask_user_question_requires_options() {
193        let tool = AskUserQuestionTool::new();
194        let input = serde_json::json!({
195            "question": "Test?",
196            "header": "Test"
197        });
198        let context = ToolContext::default();
199        let result = tool.execute(input, &context).await;
200        // Missing options returns an Err result
201        assert!(result.is_err());
202        let err_msg = result.unwrap_err().to_string();
203        assert!(err_msg.contains("options is required"));
204    }
205
206    #[tokio::test]
207    async fn test_ask_user_question_valid_options() {
208        let tool = AskUserQuestionTool::new();
209        let input = serde_json::json!({
210            "question": "Which approach?",
211            "header": "Approach",
212            "options": [
213                { "label": "Option A", "description": "First approach" },
214                { "label": "Option B", "description": "Second approach" }
215            ],
216            "multiSelect": false
217        });
218        let context = ToolContext::default();
219        let result = tool.execute(input, &context).await;
220        assert!(result.is_ok());
221        let content = result.unwrap().content;
222        assert!(content.contains("Which approach?"));
223        assert!(content.contains("Option A"));
224        assert!(content.contains("Option B"));
225    }
226
227    #[tokio::test]
228    async fn test_ask_user_question_too_few_options() {
229        let tool = AskUserQuestionTool::new();
230        let input = serde_json::json!({
231            "question": "Which approach?",
232            "header": "Approach",
233            "options": [
234                { "label": "Only One", "description": "Single option" }
235            ]
236        });
237        let context = ToolContext::default();
238        let result = tool.execute(input, &context).await;
239        assert!(result.unwrap().content.contains("2 and 4"));
240    }
241}