wabot_testing/
mock_adapter.rs1use std::sync::atomic::{AtomicUsize, Ordering};
5use std::sync::Arc;
6
7use async_trait::async_trait;
8use parking_lot::Mutex;
9use wabot_feature_chat_bot::{
10 ChatAdapter, ChatAdapterError, ChatAdapterRequest, ChatAdapterResponse, ChatItem, ChatMessage,
11 FunctionCall, LanguageModelUsage,
12};
13
14pub enum ScriptedTurn {
18 Items(Vec<ChatItem>),
19 #[allow(clippy::type_complexity)]
20 Responder(Box<dyn Fn(&ChatAdapterRequest) -> Vec<ChatItem> + Send + Sync>),
21}
22
23#[derive(Debug, Clone)]
25pub struct RecordedRequest {
26 pub system_prompt: String,
27 pub tool_names: Vec<String>,
28 pub models: Vec<String>,
29 pub prev_items: Vec<ChatItem>,
30}
31
32impl RecordedRequest {
33 fn of(request: &ChatAdapterRequest) -> Self {
34 Self {
35 system_prompt: request.system_prompt.clone(),
36 tool_names: request.tools.iter().map(|t| t.name.clone()).collect(),
37 models: request.models.iter().map(|m| m.model.clone()).collect(),
38 prev_items: request.prev_items.clone(),
39 }
40 }
41}
42
43pub struct MockChatAdapter {
60 queue: Mutex<std::collections::VecDeque<ScriptedTurn>>,
61 requests: Mutex<Vec<RecordedRequest>>,
62 call_ids: AtomicUsize,
63 fallback_reply: Mutex<Option<String>>,
64}
65
66impl Default for MockChatAdapter {
67 fn default() -> Self {
68 Self::new()
69 }
70}
71
72impl MockChatAdapter {
73 pub fn new() -> Self {
74 Self {
75 queue: Mutex::new(Default::default()),
76 requests: Mutex::new(Vec::new()),
77 call_ids: AtomicUsize::new(0),
78 fallback_reply: Mutex::new(None),
79 }
80 }
81
82 pub fn arc() -> Arc<Self> {
83 Arc::new(Self::new())
84 }
85
86 pub fn with_fallback_reply(self, text: impl Into<String>) -> Self {
92 *self.fallback_reply.lock() = Some(text.into());
93 self
94 }
95
96 pub fn reply(&self, text: impl Into<String>) -> &Self {
98 self.enqueue(ScriptedTurn::Items(vec![ChatItem::bot(ChatMessage::text(
99 text,
100 ))]))
101 }
102
103 pub fn call_tool(&self, name: impl Into<String>, arguments: impl ToArguments) -> &Self {
106 let id = self.call_ids.fetch_add(1, Ordering::SeqCst) + 1;
107 self.enqueue(ScriptedTurn::Items(vec![ChatItem::call(FunctionCall {
108 id: format!("mock-call-{id}"),
109 name: name.into(),
110 arguments: Some(arguments.to_arguments()),
111 result: None,
112 signature: None,
113 })]))
114 }
115
116 pub fn enqueue(&self, turn: ScriptedTurn) -> &Self {
118 self.queue.lock().push_back(turn);
119 self
120 }
121
122 pub fn respond_with(
125 &self,
126 responder: impl Fn(&ChatAdapterRequest) -> Vec<ChatItem> + Send + Sync + 'static,
127 ) -> &Self {
128 self.enqueue(ScriptedTurn::Responder(Box::new(responder)))
129 }
130
131 pub fn requests(&self) -> Vec<RecordedRequest> {
133 self.requests.lock().clone()
134 }
135
136 pub fn last_request(&self) -> Option<RecordedRequest> {
137 self.requests.lock().last().cloned()
138 }
139
140 pub fn call_count(&self) -> usize {
143 self.requests.lock().len()
144 }
145
146 pub fn pending(&self) -> usize {
150 self.queue.lock().len()
151 }
152}
153
154#[async_trait]
155impl ChatAdapter for MockChatAdapter {
156 async fn next_items(
157 &self,
158 request: ChatAdapterRequest,
159 ) -> Result<ChatAdapterResponse, ChatAdapterError> {
160 self.requests.lock().push(RecordedRequest::of(&request));
161
162 let turn = self.queue.lock().pop_front();
163 let next_items = match turn {
164 Some(ScriptedTurn::Items(items)) => items,
165 Some(ScriptedTurn::Responder(responder)) => responder(&request),
166 None => match self.fallback_reply.lock().clone() {
167 Some(text) => vec![ChatItem::bot(ChatMessage::text(text))],
168 None => {
169 return Err(ChatAdapterError::Other(
170 "MockChatAdapter: no scripted turn left. Queue one with reply() / \
171 call_tool() / enqueue(), or set with_fallback_reply(). Remember that \
172 after a call_tool() turn the chat loop calls the adapter again."
173 .into(),
174 ))
175 }
176 },
177 };
178
179 Ok(ChatAdapterResponse {
180 next_items,
181 usage: LanguageModelUsage {
182 input_tokens: 1,
183 output_tokens: 1,
184 provider: Some("mock".into()),
185 model: request.models.first().map(|m| m.model.clone()),
186 ..Default::default()
187 },
188 })
189 }
190}
191
192pub trait ToArguments {
196 fn to_arguments(self) -> String;
197}
198
199impl ToArguments for serde_json::Value {
200 fn to_arguments(self) -> String {
201 self.to_string()
202 }
203}
204
205impl ToArguments for &str {
206 fn to_arguments(self) -> String {
207 self.to_string()
208 }
209}
210
211impl ToArguments for String {
212 fn to_arguments(self) -> String {
213 self
214 }
215}
216
217pub struct NoArgs;
219
220impl ToArguments for NoArgs {
221 fn to_arguments(self) -> String {
222 "{}".into()
223 }
224}