agent_code_lib/tools/
ask_user.rs1use async_trait::async_trait;
8use serde_json::json;
9use std::io::Write;
10
11use super::{Tool, ToolContext, ToolResult};
12use crate::error::ToolError;
13
14pub struct AskUserQuestionTool;
15
16#[async_trait]
17impl Tool for AskUserQuestionTool {
18 fn name(&self) -> &'static str {
19 "AskUserQuestion"
20 }
21
22 fn description(&self) -> &'static str {
23 "Ask the user a question to gather preferences or clarify requirements. \
24 Present 2-4 options for the user to choose from."
25 }
26
27 fn input_schema(&self) -> serde_json::Value {
28 json!({
29 "type": "object",
30 "required": ["questions"],
31 "properties": {
32 "questions": {
33 "type": "array",
34 "description": "Questions to ask (1-4)",
35 "items": {
36 "type": "object",
37 "required": ["question", "options"],
38 "properties": {
39 "question": {
40 "type": "string",
41 "description": "The question to ask"
42 },
43 "options": {
44 "type": "array",
45 "description": "Available choices (2-4)",
46 "items": {
47 "type": "object",
48 "required": ["label", "description"],
49 "properties": {
50 "label": {
51 "type": "string",
52 "description": "Short choice label"
53 },
54 "description": {
55 "type": "string",
56 "description": "What this choice means"
57 }
58 }
59 }
60 }
61 }
62 }
63 }
64 }
65 })
66 }
67
68 fn is_read_only(&self) -> bool {
69 true
70 }
71
72 fn is_concurrency_safe(&self) -> bool {
73 false }
75
76 async fn call(
77 &self,
78 input: serde_json::Value,
79 _ctx: &ToolContext,
80 ) -> Result<ToolResult, ToolError> {
81 let questions = input
82 .get("questions")
83 .and_then(|v| v.as_array())
84 .ok_or_else(|| ToolError::InvalidInput("'questions' array is required".into()))?;
85
86 let mut answers = Vec::new();
87
88 for q in questions {
89 let question_text = q.get("question").and_then(|v| v.as_str()).unwrap_or("?");
90
91 let options = q
92 .get("options")
93 .and_then(|v| v.as_array())
94 .ok_or_else(|| ToolError::InvalidInput("'options' array required".into()))?;
95
96 eprintln!("\n{question_text}");
98 for (i, opt) in options.iter().enumerate() {
99 let label = opt.get("label").and_then(|v| v.as_str()).unwrap_or("?");
100 let desc = opt
101 .get("description")
102 .and_then(|v| v.as_str())
103 .unwrap_or("");
104 let letter = (b'A' + i as u8) as char;
105 eprintln!(" {letter}) {label} — {desc}");
106 }
107 eprint!("Choice: ");
108 let _ = std::io::stderr().flush();
109
110 let mut line = String::new();
112 std::io::stdin()
113 .read_line(&mut line)
114 .map_err(|e| ToolError::ExecutionFailed(format!("Failed to read input: {e}")))?;
115
116 let choice = line.trim().to_uppercase();
117
118 let selected = if choice.len() == 1 {
120 let idx = choice.as_bytes()[0].wrapping_sub(b'A') as usize;
121 if idx < options.len() {
122 options[idx]
123 .get("label")
124 .and_then(|v| v.as_str())
125 .unwrap_or(&choice)
126 .to_string()
127 } else {
128 choice
129 }
130 } else {
131 choice
132 };
133
134 answers.push(format!("{question_text}={selected}"));
135 }
136
137 let result = format!(
138 "User has answered your questions: {}. You can now continue with the user's answers in mind.",
139 answers
140 .iter()
141 .map(|a| format!("\"{a}\""))
142 .collect::<Vec<_>>()
143 .join(", ")
144 );
145
146 Ok(ToolResult::success(result))
147 }
148}