1use std::sync::{Arc, Mutex};
4
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7
8use crate::{
9 tool::{Tool, ToolCallExtensions, ToolFailure, ToolFailureKind, ToolReturn, ToolSet},
10 vector_store::{VectorSearchRequest, VectorStoreError, VectorStoreIndex, request::Filter},
11 wasm_compat::WasmCompatSend,
12};
13
14#[derive(Debug, thiserror::Error)]
16#[error("Mock tool error")]
17pub struct MockToolError;
18
19#[derive(Deserialize)]
21pub struct MockOperationArgs {
22 x: i32,
23 y: i32,
24}
25
26#[derive(Deserialize, Serialize)]
28pub struct MockAddTool;
29
30impl Tool for MockAddTool {
31 const NAME: &'static str = "add";
32 type Error = MockToolError;
33 type Args = MockOperationArgs;
34 type Output = i32;
35
36 fn description(&self) -> String {
37 "Add x and y together".to_string()
38 }
39
40 fn parameters(&self) -> serde_json::Value {
41 json!({
42 "type": "object",
43 "properties": {
44 "x": {
45 "type": "number",
46 "description": "The first number to add"
47 },
48 "y": {
49 "type": "number",
50 "description": "The second number to add"
51 }
52 },
53 "required": ["x", "y"],
54 })
55 }
56
57 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
58 Ok(args.x + args.y)
59 }
60}
61
62#[derive(Clone)]
65pub struct SessionId(pub String);
66
67#[derive(Clone, Default)]
76pub struct MockExtensionsProbeTool {
77 seen: Arc<Mutex<Vec<String>>>,
80}
81
82impl MockExtensionsProbeTool {
83 pub fn observed(&self) -> Option<String> {
85 self.seen
86 .lock()
87 .unwrap_or_else(|poisoned| poisoned.into_inner())
88 .last()
89 .cloned()
90 }
91
92 pub fn observations(&self) -> Vec<String> {
94 self.seen
95 .lock()
96 .unwrap_or_else(|poisoned| poisoned.into_inner())
97 .clone()
98 }
99}
100
101impl Tool for MockExtensionsProbeTool {
102 const NAME: &'static str = "context_probe";
103 type Error = MockToolError;
104 type Args = serde_json::Value;
105 type Output = String;
106
107 fn description(&self) -> String {
108 "Records the SessionId observed in its call context".to_string()
109 }
110
111 fn parameters(&self) -> serde_json::Value {
112 json!({"type": "object", "properties": {}})
113 }
114
115 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
116 self.seen
117 .lock()
118 .unwrap_or_else(|poisoned| poisoned.into_inner())
119 .push("call-no-context".to_string());
120 Ok("call-no-context".to_string())
121 }
122
123 async fn call_with_extensions(
124 &self,
125 _args: Self::Args,
126 extensions: &ToolCallExtensions,
127 ) -> Result<Self::Output, Self::Error> {
128 let observed = match extensions.get::<SessionId>() {
129 Some(session) => format!("session:{}", session.0),
130 None => "no-session".to_string(),
131 };
132 self.seen
133 .lock()
134 .unwrap_or_else(|poisoned| poisoned.into_inner())
135 .push(observed.clone());
136 Ok(observed)
137 }
138}
139
140#[derive(Deserialize, Serialize)]
142pub struct MockSubtractTool;
143
144impl Tool for MockSubtractTool {
145 const NAME: &'static str = "subtract";
146 type Error = MockToolError;
147 type Args = MockOperationArgs;
148 type Output = i32;
149
150 fn description(&self) -> String {
151 "Subtract y from x".to_string()
152 }
153
154 fn parameters(&self) -> serde_json::Value {
155 json!({
156 "type": "object",
157 "properties": {
158 "x": {
159 "type": "number",
160 "description": "The number to subtract from"
161 },
162 "y": {
163 "type": "number",
164 "description": "The number to subtract"
165 }
166 },
167 "required": ["x", "y"],
168 })
169 }
170
171 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
172 Ok(args.x - args.y)
173 }
174}
175
176pub fn mock_math_toolset() -> ToolSet {
178 let mut toolset = ToolSet::default();
179 toolset.add_tool(MockAddTool);
180 toolset.add_tool(MockSubtractTool);
181 toolset
182}
183
184#[derive(Deserialize, Serialize)]
186pub struct MockStringOutputTool;
187
188impl Tool for MockStringOutputTool {
189 const NAME: &'static str = "string_output";
190 type Error = MockToolError;
191 type Args = serde_json::Value;
192 type Output = String;
193
194 fn description(&self) -> String {
195 "Returns a multiline string".to_string()
196 }
197
198 fn parameters(&self) -> serde_json::Value {
199 json!({
200 "type": "object",
201 "properties": {}
202 })
203 }
204
205 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
206 Ok("Hello\nWorld".to_string())
207 }
208}
209
210#[derive(Deserialize, Serialize)]
212pub struct MockImageOutputTool;
213
214impl Tool for MockImageOutputTool {
215 const NAME: &'static str = "image_output";
216 type Error = MockToolError;
217 type Args = serde_json::Value;
218 type Output = String;
219
220 fn description(&self) -> String {
221 "Returns image JSON".to_string()
222 }
223
224 fn parameters(&self) -> serde_json::Value {
225 json!({
226 "type": "object",
227 "properties": {}
228 })
229 }
230
231 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
232 Ok(json!({
233 "type": "image",
234 "data": "base64data==",
235 "mimeType": "image/png"
236 })
237 .to_string())
238 }
239}
240
241#[derive(Debug, Deserialize, Serialize)]
243pub struct MockImageGeneratorTool;
244
245impl Tool for MockImageGeneratorTool {
246 const NAME: &'static str = "generate_test_image";
247 type Error = MockToolError;
248 type Args = serde_json::Value;
249 type Output = String;
250
251 fn description(&self) -> String {
252 "Generates a small test image (a 1x1 red pixel). Call this tool when asked to generate or show an image.".to_string()
253 }
254
255 fn parameters(&self) -> serde_json::Value {
256 json!({
257 "type": "object",
258 "properties": {},
259 "required": []
260 })
261 }
262
263 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
264 Ok(json!({
265 "type": "image",
266 "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==",
267 "mimeType": "image/png"
268 })
269 .to_string())
270 }
271}
272
273#[derive(Deserialize, Serialize)]
275pub struct MockObjectOutputTool;
276
277impl Tool for MockObjectOutputTool {
278 const NAME: &'static str = "object_output";
279 type Error = MockToolError;
280 type Args = serde_json::Value;
281 type Output = serde_json::Value;
282
283 fn description(&self) -> String {
284 "Returns an object".to_string()
285 }
286
287 fn parameters(&self) -> serde_json::Value {
288 json!({
289 "type": "object",
290 "properties": {}
291 })
292 }
293
294 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
295 Ok(json!({
296 "status": "ok",
297 "count": 42
298 }))
299 }
300}
301
302pub struct MockExampleTool;
304
305impl Tool for MockExampleTool {
306 const NAME: &'static str = "example_tool";
307 type Error = MockToolError;
308 type Args = ();
309 type Output = String;
310
311 fn description(&self) -> String {
312 "A tool that returns some example text.".to_string()
313 }
314
315 fn parameters(&self) -> serde_json::Value {
316 json!({
317 "type": "object",
318 "properties": {},
319 "required": []
320 })
321 }
322
323 async fn call(&self, _input: Self::Args) -> Result<Self::Output, Self::Error> {
324 Ok("Example answer".to_string())
325 }
326}
327
328#[derive(Clone)]
330pub struct MockBarrierTool {
331 pub barrier: Arc<tokio::sync::Barrier>,
333}
334
335impl MockBarrierTool {
336 pub fn new(barrier: Arc<tokio::sync::Barrier>) -> Self {
338 Self { barrier }
339 }
340}
341
342impl Tool for MockBarrierTool {
343 const NAME: &'static str = "barrier_tool";
344 type Error = MockToolError;
345 type Args = serde_json::Value;
346 type Output = String;
347
348 fn description(&self) -> String {
349 "Waits at a barrier to test concurrency".to_string()
350 }
351
352 fn parameters(&self) -> serde_json::Value {
353 json!({"type": "object", "properties": {}})
354 }
355
356 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
357 self.barrier.wait().await;
358 Ok("done".to_string())
359 }
360}
361
362#[derive(Clone)]
364pub struct MockControlledTool {
365 pub started: Arc<tokio::sync::Notify>,
367 pub allow_finish: Arc<tokio::sync::Notify>,
369}
370
371impl MockControlledTool {
372 pub fn new(started: Arc<tokio::sync::Notify>, allow_finish: Arc<tokio::sync::Notify>) -> Self {
374 Self {
375 started,
376 allow_finish,
377 }
378 }
379}
380
381impl Tool for MockControlledTool {
382 const NAME: &'static str = "controlled";
383 type Error = MockToolError;
384 type Args = serde_json::Value;
385 type Output = i32;
386
387 fn description(&self) -> String {
388 "Test tool".to_string()
389 }
390
391 fn parameters(&self) -> serde_json::Value {
392 json!({"type": "object", "properties": {}})
393 }
394
395 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
396 self.started.notify_one();
397 self.allow_finish.notified().await;
398 Ok(42)
399 }
400}
401
402pub struct MockToolIndex {
404 tool_ids: Vec<String>,
405}
406
407impl MockToolIndex {
408 pub fn new(tool_ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
410 Self {
411 tool_ids: tool_ids.into_iter().map(Into::into).collect(),
412 }
413 }
414}
415
416impl VectorStoreIndex for MockToolIndex {
417 type Filter = Filter<serde_json::Value>;
418
419 async fn top_n<T: for<'a> Deserialize<'a> + WasmCompatSend>(
420 &self,
421 _req: VectorSearchRequest,
422 ) -> Result<Vec<(f64, String, T)>, VectorStoreError> {
423 Ok(vec![])
424 }
425
426 async fn top_n_ids(
427 &self,
428 _req: VectorSearchRequest,
429 ) -> Result<Vec<(f64, String)>, VectorStoreError> {
430 Ok(self
431 .tool_ids
432 .iter()
433 .enumerate()
434 .map(|(i, id)| (1.0 - (i as f64 * 0.1), id.clone()))
435 .collect())
436 }
437}
438
439pub struct BarrierMockToolIndex {
441 barrier: Arc<tokio::sync::Barrier>,
442 tool_id: String,
443}
444
445impl BarrierMockToolIndex {
446 pub fn new(barrier: Arc<tokio::sync::Barrier>, tool_id: impl Into<String>) -> Self {
448 Self {
449 barrier,
450 tool_id: tool_id.into(),
451 }
452 }
453}
454
455impl VectorStoreIndex for BarrierMockToolIndex {
456 type Filter = Filter<serde_json::Value>;
457
458 async fn top_n<T: for<'a> Deserialize<'a> + WasmCompatSend>(
459 &self,
460 _req: VectorSearchRequest,
461 ) -> Result<Vec<(f64, String, T)>, VectorStoreError> {
462 Ok(vec![])
463 }
464
465 async fn top_n_ids(
466 &self,
467 _req: VectorSearchRequest,
468 ) -> Result<Vec<(f64, String)>, VectorStoreError> {
469 self.barrier.wait().await;
470 Ok(vec![(1.0, self.tool_id.clone())])
471 }
472}
473
474#[derive(Debug, thiserror::Error)]
476#[error("mock tool call failed")]
477pub struct MockFailure;
478
479#[derive(Clone)]
484pub struct MockFailingTool {
485 kind: ToolFailureKind,
486}
487
488impl MockFailingTool {
489 pub fn new(kind: ToolFailureKind) -> Self {
491 Self { kind }
492 }
493}
494
495impl Tool for MockFailingTool {
496 const NAME: &'static str = "flaky_tool";
497 type Error = MockFailure;
498 type Args = serde_json::Value;
499 type Output = String;
500
501 fn description(&self) -> String {
502 "A tool that always fails".to_string()
503 }
504
505 fn parameters(&self) -> serde_json::Value {
506 json!({ "type": "object", "properties": {} })
507 }
508
509 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
510 Err(MockFailure)
511 }
512
513 fn classify_error(&self, error: &Self::Error) -> ToolFailure {
514 let message = error.to_string();
515 match self.kind {
516 ToolFailureKind::Timeout => ToolFailure::timeout(message),
517 ToolFailureKind::NotFound => ToolFailure::not_found(message).with_http_status(404),
518 ToolFailureKind::RateLimited => {
519 ToolFailure::rate_limited(message).with_http_status(429)
520 }
521 other => ToolFailure::new(other, message),
522 }
523 }
524}
525
526#[derive(Clone)]
530pub struct MockHandledFailureTool;
531
532impl Tool for MockHandledFailureTool {
533 const NAME: &'static str = "lookup";
534 type Error = MockToolError;
535 type Args = serde_json::Value;
536 type Output = String;
537
538 fn description(&self) -> String {
539 "Looks up a record".to_string()
540 }
541
542 fn parameters(&self) -> serde_json::Value {
543 json!({ "type": "object", "properties": {} })
544 }
545
546 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
547 Ok("no record found for id 42".to_string())
550 }
551
552 async fn call_structured(
553 &self,
554 _args: Self::Args,
555 _extensions: &ToolCallExtensions,
556 ) -> Result<ToolReturn<Self::Output>, Self::Error> {
557 Ok(ToolReturn::failed(
558 "no record found for id 42; try a different id".to_string(),
559 ToolFailure::not_found("record id 42 is missing").with_http_status(404),
560 ))
561 }
562}
563
564#[derive(Clone)]
569pub struct MockDeniedTool;
570
571impl Tool for MockDeniedTool {
572 const NAME: &'static str = "guarded";
573 type Error = MockToolError;
574 type Args = serde_json::Value;
575 type Output = String;
576
577 fn description(&self) -> String {
578 "A tool with an internal authorization check".to_string()
579 }
580
581 fn parameters(&self) -> serde_json::Value {
582 json!({ "type": "object", "properties": {} })
583 }
584
585 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
586 Ok("ok".to_string())
587 }
588
589 async fn call_structured(
590 &self,
591 _args: Self::Args,
592 _extensions: &ToolCallExtensions,
593 ) -> Result<ToolReturn<Self::Output>, Self::Error> {
594 Ok(ToolReturn::denied(
595 "access to this resource is not permitted".to_string(),
596 ))
597 }
598}
599
600#[derive(Clone, Debug, PartialEq, Eq)]
603pub struct MockRequestId(pub String);
604
605#[derive(Clone)]
608pub struct MockMetadataTool;
609
610impl Tool for MockMetadataTool {
611 const NAME: &'static str = "with_meta";
612 type Error = MockToolError;
613 type Args = serde_json::Value;
614 type Output = String;
615
616 fn description(&self) -> String {
617 "Succeeds and attaches request metadata".to_string()
618 }
619
620 fn parameters(&self) -> serde_json::Value {
621 json!({ "type": "object", "properties": {} })
622 }
623
624 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
625 Ok("done".to_string())
626 }
627
628 async fn call_structured(
629 &self,
630 _args: Self::Args,
631 _extensions: &ToolCallExtensions,
632 ) -> Result<ToolReturn<Self::Output>, Self::Error> {
633 Ok(ToolReturn::success("done".to_string())
634 .with_extension(MockRequestId("req-7".to_string())))
635 }
636}