Skip to main content

j_agent/tools/
ask.rs

1use crate::message_types::{AskOption, AskQuestion, AskRequest};
2use crate::tools::{PlanDecision, Tool, ToolResult, parse_tool_args, schema_to_tool_params};
3use schemars::JsonSchema;
4use serde::Deserialize;
5use serde_json::Value;
6use std::borrow::Cow;
7use std::sync::{Arc, atomic::AtomicBool, mpsc};
8
9/// 选项参数
10#[derive(Deserialize, JsonSchema)]
11struct AskOptionParam {
12    /// Option display label shown to user (1-5 words). REQUIRED. Example: "确认无误"
13    label: String,
14    /// Short explanation of what this option means. Example: "继续进入下一阶段"
15    #[serde(default)]
16    description: String,
17}
18
19/// 问题参数
20#[derive(Deserialize, JsonSchema)]
21struct AskQuestionParam {
22    /// Full question text shown to user
23    question: String,
24    /// Short tag (max 12 chars) as column header, e.g. '需求确认'
25    header: String,
26    /// List of 2-4 option OBJECTS. Each element MUST be {"label": "...", "description": "..."}, NOT a plain string.
27    options: Vec<AskOptionParam>,
28    /// Whether to allow multiple selections (default false)
29    #[serde(default)]
30    multi_select: bool,
31}
32
33/// AskTool 参数
34#[derive(Deserialize, JsonSchema)]
35struct AskParams {
36    /// List of questions to ask (1-4)
37    questions: Vec<AskQuestionParam>,
38}
39
40// ========== AskTool ==========
41
42/// 用户提问工具,用于向用户展示结构化问题并等待回复
43#[derive(Debug)]
44pub struct AskTool {
45    /// 用于向主线程发送提问请求的通道发送端
46    pub ask_tx: mpsc::Sender<AskRequest>,
47}
48
49impl AskTool {
50    /// 工具名称常量
51    pub const NAME: &'static str = "Ask";
52}
53
54impl Tool for AskTool {
55    fn name(&self) -> &str {
56        Self::NAME
57    }
58
59    fn description(&self) -> Cow<'_, str> {
60        r#"
61        Present structured questions to the user with single-select or multi-select options. Supports 1-4 questions per call, each with 2-4 options.
62
63        When to use: whenever user input is needed, including but not limited to:
64        - Asking the user to make a choice or confirm an action
65        - Gathering user preferences or configuration
66        - Presenting multiple approaches for the user to decide
67        - Showing intermediate results and requesting feedback
68
69        IMPORTANT — Input format:
70        - `questions` is an array of question OBJECTS (NOT strings).
71        - Each question object has: `question` (full text), `header` (short tag ≤12 chars), `options` (array of OPTION OBJECTS), `multi_select` (bool, optional).
72        - Each option object has: `label` (1-5 words shown to user) and `description` (short explanation). DO NOT pass options as plain strings.
73
74        Concrete example (copy this structure exactly):
75        ```json
76        {
77          "questions": [
78            {
79              "question": "需求文档是否符合预期?",
80              "header": "需求确认",
81              "options": [
82                {"label": "确认无误", "description": "继续进入 API 设计和前端开发阶段"},
83                {"label": "需要调整", "description": "我会指出需要修改的部分"}
84              ],
85              "multi_select": false
86            }
87          ]
88        }
89        ```
90
91        Response format:
92        Returns JSON:
93        ```json
94        {
95            "answers": {
96                "question text": "selected label or free-text input"
97            }
98        }
99        ```
100        For multi-select, multiple labels are comma-separated.
101        "#.into()
102    }
103
104    fn parameters_schema(&self) -> Value {
105        schema_to_tool_params::<AskParams>()
106    }
107
108    fn execute(&self, arguments: &str, _cancelled: &Arc<AtomicBool>) -> ToolResult {
109        let params: AskParams = match parse_tool_args(arguments) {
110            Ok(p) => p,
111            Err(e) => return e,
112        };
113
114        if params.questions.is_empty() || params.questions.len() > 4 {
115            return ToolResult {
116                output: "questions 数量必须为 1-4 个".to_string(),
117                is_error: true,
118                images: vec![],
119                plan_decision: PlanDecision::None,
120            };
121        }
122
123        let mut questions: Vec<AskQuestion> = Vec::new();
124        for q in &params.questions {
125            if q.options.len() < 2 || q.options.len() > 4 {
126                return ToolResult {
127                    output: format!("问题 '{}' 的选项数量必须为 2-4 个", q.question),
128                    is_error: true,
129                    images: vec![],
130                    plan_decision: PlanDecision::None,
131                };
132            }
133
134            questions.push(AskQuestion {
135                question: q.question.clone(),
136                header: q.header.clone(),
137                options: q
138                    .options
139                    .iter()
140                    .map(|o| AskOption {
141                        label: o.label.clone(),
142                        description: o.description.clone(),
143                    })
144                    .collect(),
145                multi_select: q.multi_select,
146            });
147        }
148
149        // 创建响应 channel
150        let (response_tx, response_rx) = mpsc::channel::<String>();
151
152        let ask_request = AskRequest {
153            questions,
154            response_tx,
155        };
156
157        if self.ask_tx.send(ask_request).is_err() {
158            return ToolResult {
159                output: "无法发送提问请求(主线程可能已退出)".to_string(),
160                is_error: true,
161                images: vec![],
162                plan_decision: PlanDecision::None,
163            };
164        }
165
166        // 阻塞等待用户响应
167        match response_rx.recv() {
168            Ok(response) => ToolResult {
169                output: response,
170                is_error: false,
171                images: vec![],
172                plan_decision: PlanDecision::None,
173            },
174            Err(_) => ToolResult {
175                output: "等待用户响应时连接断开".to_string(),
176                is_error: true,
177                images: vec![],
178                plan_decision: PlanDecision::None,
179            },
180        }
181    }
182
183    fn requires_confirmation(&self) -> bool {
184        false
185    }
186}