返回 DeepSeek-TUI-2026
user_input.rs
根目录 / crates / tui / src / tools / user_input.rs
1 //! Tool and types for requesting user input via the TUI.
2
3 use super::spec::{
4 ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
5 };
6 use async_trait::async_trait;
7 use serde::{Deserialize, Serialize};
8 use serde_json::{Value, json};
9
10 #[derive(Debug, Clone, Serialize, Deserialize)]
11 pub struct UserInputOption {
12 pub label: String,
13 pub description: String,
14 }
15
16 #[derive(Debug, Clone, Serialize, Deserialize)]
17 pub struct UserInputQuestion {
18 pub header: String,
19 pub id: String,
20 pub question: String,
21 pub options: Vec<UserInputOption>,
22 }
23
24 #[derive(Debug, Clone, Serialize, Deserialize)]
25 pub struct UserInputRequest {
26 pub questions: Vec<UserInputQuestion>,
27 }
28
29 impl UserInputRequest {
30 pub fn from_value(value: &Value) -> Result<Self, ToolError> {
31 let request: UserInputRequest = serde_json::from_value(value.clone()).map_err(|e| {
32 ToolError::invalid_input(format!("Invalid request_user_input payload: {e}"))
33 })?;
34 request.validate()?;
35 Ok(request)
36 }
37
38 pub fn validate(&self) -> Result<(), ToolError> {
39 if self.questions.is_empty() {
40 return Err(ToolError::invalid_input(
41 "request_user_input.questions must be non-empty",
42 ));
43 }
44 if self.questions.len() > 3 {
45 return Err(ToolError::invalid_input(
46 "request_user_input.questions must contain 1 to 3 items",
47 ));
48 }
49 for q in &self.questions {
50 if q.header.trim().is_empty() {
51 return Err(ToolError::invalid_input(
52 "request_user_input.questions.header cannot be empty",
53 ));
54 }
55 if q.id.trim().is_empty() {
56 return Err(ToolError::invalid_input(
57 "request_user_input.questions.id cannot be empty",
58 ));
59 }
60 if q.question.trim().is_empty() {
61 return Err(ToolError::invalid_input(
62 "request_user_input.questions.question cannot be empty",
63 ));
64 }
65 if q.options.len() < 2 || q.options.len() > 3 {
66 return Err(ToolError::invalid_input(
67 "request_user_input.questions.options must contain 2 or 3 items",
68 ));
69 }
70 for opt in &q.options {
71 if opt.label.trim().is_empty() {
72 return Err(ToolError::invalid_input(
73 "request_user_input option label cannot be empty",
74 ));
75 }
76 if opt.description.trim().is_empty() {
77 return Err(ToolError::invalid_input(
78 "request_user_input option description cannot be empty",
79 ));
80 }
81 }
82 }
83 Ok(())
84 }
85 }
86
87 #[derive(Debug, Clone, Serialize, Deserialize)]
88 pub struct UserInputAnswer {
89 pub id: String,
90 pub label: String,
91 pub value: String,
92 }
93
94 #[derive(Debug, Clone, Serialize, Deserialize)]
95 pub struct UserInputResponse {
96 pub answers: Vec<UserInputAnswer>,
97 }
98
99 pub struct RequestUserInputTool;
100
101 #[async_trait]
102 impl ToolSpec for RequestUserInputTool {
103 fn name(&self) -> &'static str {
104 "request_user_input"
105 }
106
107 fn description(&self) -> &'static str {
108 "Ask the user 1-3 short questions and return their selections."
109 }
110
111 fn input_schema(&self) -> Value {
112 json!({
113 "type": "object",
114 "properties": {
115 "questions": {
116 "type": "array",
117 "items": {
118 "type": "object",
119 "properties": {
120 "header": { "type": "string" },
121 "id": { "type": "string" },
122 "question": { "type": "string" },
123 "options": {
124 "type": "array",
125 "items": {
126 "type": "object",
127 "properties": {
128 "label": { "type": "string" },
129 "description": { "type": "string" }
130 },
131 "required": ["label", "description"]
132 },
133 "minItems": 2,
134 "maxItems": 3
135 }
136 },
137 "required": ["header", "id", "question", "options"]
138 },
139 "minItems": 1,
140 "maxItems": 3
141 }
142 },
143 "required": ["questions"]
144 })
145 }
146
147 fn capabilities(&self) -> Vec<ToolCapability> {
148 vec![ToolCapability::ReadOnly]
149 }
150
151 fn approval_requirement(&self) -> ApprovalRequirement {
152 ApprovalRequirement::Auto
153 }
154
155 async fn execute(
156 &self,
157 _input: Value,
158 _context: &ToolContext,
159 ) -> Result<ToolResult, ToolError> {
160 Err(ToolError::execution_failed(
161 "request_user_input must be handled by the engine",
162 ))
163 }
164 }
165
166 #[cfg(test)]
167 mod tests {
168 use super::*;
169
170 #[test]
171 fn validates_request_shape() {
172 let request = UserInputRequest {
173 questions: vec![UserInputQuestion {
174 header: "Pick".to_string(),
175 id: "choice".to_string(),
176 question: "Which option?".to_string(),
177 options: vec![
178 UserInputOption {
179 label: "A".to_string(),
180 description: "Option A".to_string(),
181 },
182 UserInputOption {
183 label: "B".to_string(),
184 description: "Option B".to_string(),
185 },
186 ],
187 }],
188 };
189 assert!(request.validate().is_ok());
190 }
191
192 #[test]
193 fn rejects_too_many_questions() {
194 let request = UserInputRequest {
195 questions: vec![
196 UserInputQuestion {
197 header: "Q1".to_string(),
198 id: "q1".to_string(),
199 question: "?".to_string(),
200 options: vec![
201 UserInputOption {
202 label: "A".to_string(),
203 description: "A".to_string(),
204 },
205 UserInputOption {
206 label: "B".to_string(),
207 description: "B".to_string(),
208 },
209 ],
210 },
211 UserInputQuestion {
212 header: "Q2".to_string(),
213 id: "q2".to_string(),
214 question: "?".to_string(),
215 options: vec![
216 UserInputOption {
217 label: "A".to_string(),
218 description: "A".to_string(),
219 },
220 UserInputOption {
221 label: "B".to_string(),
222 description: "B".to_string(),
223 },
224 ],
225 },
226 UserInputQuestion {
227 header: "Q3".to_string(),
228 id: "q3".to_string(),
229 question: "?".to_string(),
230 options: vec![
231 UserInputOption {
232 label: "A".to_string(),
233 description: "A".to_string(),
234 },
235 UserInputOption {
236 label: "B".to_string(),
237 description: "B".to_string(),
238 },
239 ],
240 },
241 UserInputQuestion {
242 header: "Q4".to_string(),
243 id: "q4".to_string(),
244 question: "?".to_string(),
245 options: vec![
246 UserInputOption {
247 label: "A".to_string(),
248 description: "A".to_string(),
249 },
250 UserInputOption {
251 label: "B".to_string(),
252 description: "B".to_string(),
253 },
254 ],
255 },
256 ],
257 };
258 assert!(request.validate().is_err());
259 }
260 }
261
261 lines RUST