mermaid_cli/providers/
questions.rs1use std::collections::HashMap;
21use std::sync::{Arc, Mutex};
22
23use tokio::sync::{mpsc, oneshot};
24use tokio_util::sync::CancellationToken;
25
26use crate::domain::{Msg, Question, QuestionResolution, ToolCallId, TurnId};
27
28#[derive(Clone)]
31pub struct QuestionBroker {
32 pending: Arc<Mutex<HashMap<ToolCallId, oneshot::Sender<QuestionResolution>>>>,
33 msg_tx: mpsc::Sender<Msg>,
34}
35
36impl QuestionBroker {
37 pub fn new(msg_tx: mpsc::Sender<Msg>) -> Self {
38 Self {
39 pending: Arc::new(Mutex::new(HashMap::new())),
40 msg_tx,
41 }
42 }
43
44 pub async fn request(
48 &self,
49 token: &CancellationToken,
50 turn: TurnId,
51 call_id: ToolCallId,
52 questions: Vec<Question>,
53 ) -> QuestionResolution {
54 let (tx, rx) = oneshot::channel();
55 self.pending
58 .lock()
59 .unwrap_or_else(|poisoned| poisoned.into_inner())
60 .insert(call_id, tx);
61
62 let sent = self
63 .msg_tx
64 .send(Msg::QuestionAsked {
65 turn,
66 call_id,
67 questions,
68 })
69 .await;
70 if sent.is_err() {
71 self.pending
73 .lock()
74 .unwrap_or_else(|poisoned| poisoned.into_inner())
75 .remove(&call_id);
76 return QuestionResolution::Dismissed;
77 }
78
79 tokio::select! {
80 biased;
81 _ = token.cancelled() => {
82 self.pending.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).remove(&call_id);
83 QuestionResolution::Dismissed
84 }
85 resolution = rx => resolution.unwrap_or(QuestionResolution::Dismissed),
86 }
87 }
88
89 pub fn resolve(&self, call_id: ToolCallId, resolution: QuestionResolution) {
91 let entry = self
92 .pending
93 .lock()
94 .unwrap_or_else(|poisoned| poisoned.into_inner())
95 .remove(&call_id);
96 if let Some(tx) = entry {
97 let _ = tx.send(resolution);
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::domain::{QuestionAnswer, QuestionOption};
106
107 fn sample_questions() -> Vec<Question> {
108 vec![Question {
109 header: "Database".to_string(),
110 question: "Which database?".to_string(),
111 kind: crate::domain::QuestionKind::Select,
112 options: vec![QuestionOption {
113 label: "PostgreSQL".to_string(),
114 description: None,
115 recommended: true,
116 preview: None,
117 }],
118 memory_key: None,
119 }]
120 }
121
122 #[tokio::test]
123 async fn resolve_delivers_answers() {
124 let (tx, _rx) = mpsc::channel::<Msg>(8);
125 let broker = QuestionBroker::new(tx);
126
127 let b2 = broker.clone();
128 let handle = tokio::spawn(async move {
129 b2.request(
130 &CancellationToken::new(),
131 TurnId(1),
132 ToolCallId(1),
133 sample_questions(),
134 )
135 .await
136 });
137 let answers = vec![QuestionAnswer {
139 header: "Database".to_string(),
140 question: "Which database?".to_string(),
141 selected: vec!["PostgreSQL".to_string()],
142 note: None,
143 }];
144 for _ in 0..100 {
145 broker.resolve(
146 ToolCallId(1),
147 QuestionResolution::Answered {
148 answers: answers.clone(),
149 remember: false,
150 },
151 );
152 tokio::task::yield_now().await;
153 if broker
154 .pending
155 .lock()
156 .unwrap_or_else(|poisoned| poisoned.into_inner())
157 .is_empty()
158 {
159 break;
160 }
161 }
162 let resolution = handle.await.unwrap();
163 assert_eq!(
164 resolution,
165 QuestionResolution::Answered {
166 answers,
167 remember: false
168 }
169 );
170 }
171
172 #[tokio::test]
173 async fn cancel_token_dismisses() {
174 let (tx, _rx) = mpsc::channel::<Msg>(8);
175 let broker = QuestionBroker::new(tx);
176 let token = CancellationToken::new();
177 let token2 = token.clone();
178 let handle = tokio::spawn(async move {
179 broker
180 .request(&token2, TurnId(1), ToolCallId(2), sample_questions())
181 .await
182 });
183 tokio::task::yield_now().await;
184 token.cancel();
185 assert_eq!(handle.await.unwrap(), QuestionResolution::Dismissed);
186 }
187}