1use crate::error::AgentError;
7use crate::types::*;
8
9pub const ASK_USER_QUESTION_TOOL_NAME: &str = "AskUserQuestion";
10
11pub 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 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 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}