Skip to main content

everruns_core/capabilities/
fake_crm.rs

1//! Fake CRM Tools Capability - demo tools for customer relationship management
2//!
3//! This capability provides mock CRM tools that store state
4//! in the session file system. Perfect for demos and testing customer support workflows.
5//!
6//! Tools provided:
7//! - `crm_list_customers`: List customers
8//! - `crm_get_customer`: Get customer details
9//! - `crm_create_customer`: Create a new customer
10//! - `crm_list_tickets`: List support tickets
11//! - `crm_create_ticket`: Create a new support ticket
12//! - `crm_update_ticket`: Update ticket status
13//! - `crm_add_interaction`: Add customer interaction note
14//! - `crm_search_customers`: Search customers by criteria
15
16use super::{Capability, CapabilityLocalization, CapabilityStatus};
17use crate::tool_types::ToolHints;
18use crate::tools::{Tool, ToolExecutionResult};
19use crate::traits::ToolContext;
20use async_trait::async_trait;
21use serde::{Deserialize, Serialize};
22use serde_json::{Value, json};
23
24pub const FAKE_CRM_CAPABILITY_ID: &str = "fake_crm";
25
26/// Fake CRM Tools capability - mock customer relationship management for demos
27pub struct FakeCrmCapability;
28
29impl Capability for FakeCrmCapability {
30    fn id(&self) -> &str {
31        FAKE_CRM_CAPABILITY_ID
32    }
33
34    fn name(&self) -> &str {
35        "Fake CRM Tools"
36    }
37
38    fn description(&self) -> &str {
39        "Demo capability: CRM and customer support tools (customers, tickets, interactions). State stored in session filesystem."
40    }
41
42    fn localizations(&self) -> Vec<CapabilityLocalization> {
43        vec![CapabilityLocalization::text(
44            "uk",
45            "Демо-інструменти CRM",
46            "Демонстраційна можливість: інструменти CRM і підтримки клієнтів (клієнти, тікети, взаємодії). Стан зберігається у файловій системі сесії.",
47        )]
48    }
49
50    fn status(&self) -> CapabilityStatus {
51        CapabilityStatus::Available
52    }
53
54    fn icon(&self) -> Option<&str> {
55        Some("users")
56    }
57
58    fn category(&self) -> Option<&str> {
59        Some("Demo Tools")
60    }
61
62    fn system_prompt_addition(&self) -> Option<&str> {
63        Some("CRM data is stored in /crm/ (customers.json, tickets.json, interactions.json).")
64    }
65
66    fn tools(&self) -> Vec<Box<dyn Tool>> {
67        vec![
68            Box::new(CrmListCustomersTool),
69            Box::new(CrmGetCustomerTool),
70            Box::new(CrmCreateCustomerTool),
71            Box::new(CrmListTicketsTool),
72            Box::new(CrmCreateTicketTool),
73            Box::new(CrmUpdateTicketTool),
74            Box::new(CrmAddInteractionTool),
75            Box::new(CrmSearchCustomersTool),
76        ]
77    }
78}
79
80// Helper structs for CRM data
81#[derive(Debug, Clone, Serialize, Deserialize)]
82struct Customer {
83    id: String,
84    name: String,
85    email: String,
86    company: String,
87    phone: String,
88    tier: String, // free, pro, enterprise
89    created_at: String,
90    last_contact: String,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94struct Ticket {
95    id: String,
96    customer_id: String,
97    subject: String,
98    description: String,
99    status: String,   // open, in_progress, resolved, closed
100    priority: String, // low, medium, high, urgent
101    assigned_to: Option<String>,
102    created_at: String,
103    updated_at: String,
104}
105
106// ============================================================================
107// Tool: crm_list_customers
108// ============================================================================
109
110pub struct CrmListCustomersTool;
111
112#[async_trait]
113impl Tool for CrmListCustomersTool {
114    fn name(&self) -> &str {
115        "crm_list_customers"
116    }
117
118    fn display_name(&self) -> Option<&str> {
119        Some("List Customers")
120    }
121
122    fn description(&self) -> &str {
123        "List all customers. Optionally filter by customer tier."
124    }
125
126    fn parameters_schema(&self) -> Value {
127        json!({
128            "type": "object",
129            "properties": {
130                "tier": {
131                    "type": "string",
132                    "enum": ["free", "pro", "enterprise"],
133                    "description": "Optional: Filter by customer tier"
134                }
135            },
136            "additionalProperties": false
137        })
138    }
139
140    fn hints(&self) -> ToolHints {
141        ToolHints::default()
142            .with_readonly(true)
143            .with_idempotent(true)
144    }
145
146    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
147        ToolExecutionResult::tool_error("crm_list_customers requires context")
148    }
149
150    async fn execute_with_context(
151        &self,
152        arguments: Value,
153        context: &ToolContext,
154    ) -> ToolExecutionResult {
155        let file_store = match &context.file_store {
156            Some(store) => store,
157            None => return ToolExecutionResult::tool_error("File system not available"),
158        };
159
160        let tier_filter = arguments.get("tier").and_then(|v| v.as_str());
161
162        // Read customers
163        let customers: Vec<Customer> = match file_store
164            .read_file(context.session_id, "/crm/customers.json")
165            .await
166        {
167            Ok(Some(file)) => serde_json::from_str(file.content.as_deref().unwrap_or(""))
168                .unwrap_or_else(|_| {
169                    // Initialize with sample data
170                    vec![
171                        Customer {
172                            id: "CUST-001".to_string(),
173                            name: "Alice Johnson".to_string(),
174                            email: "alice@acmecorp.com".to_string(),
175                            company: "Acme Corp".to_string(),
176                            phone: "+1-555-0101".to_string(),
177                            tier: "enterprise".to_string(),
178                            created_at: "2024-01-15T10:00:00Z".to_string(),
179                            last_contact: "2025-01-05T14:30:00Z".to_string(),
180                        },
181                        Customer {
182                            id: "CUST-002".to_string(),
183                            name: "Bob Smith".to_string(),
184                            email: "bob@techstart.io".to_string(),
185                            company: "TechStart Inc".to_string(),
186                            phone: "+1-555-0102".to_string(),
187                            tier: "pro".to_string(),
188                            created_at: "2024-03-20T09:00:00Z".to_string(),
189                            last_contact: "2025-01-03T11:15:00Z".to_string(),
190                        },
191                    ]
192                }),
193            _ => {
194                let initial = vec![Customer {
195                    id: "CUST-001".to_string(),
196                    name: "Alice Johnson".to_string(),
197                    email: "alice@acmecorp.com".to_string(),
198                    company: "Acme Corp".to_string(),
199                    phone: "+1-555-0101".to_string(),
200                    tier: "enterprise".to_string(),
201                    created_at: chrono::Utc::now().to_rfc3339(),
202                    last_contact: chrono::Utc::now().to_rfc3339(),
203                }];
204
205                let content = serde_json::to_string_pretty(&initial).unwrap();
206                let _ = file_store
207                    .write_file(context.session_id, "/crm/customers.json", &content, "text")
208                    .await;
209
210                initial
211            }
212        };
213
214        // Apply filter
215        let filtered: Vec<_> = if let Some(tier) = tier_filter {
216            customers.into_iter().filter(|c| c.tier == tier).collect()
217        } else {
218            customers
219        };
220
221        ToolExecutionResult::success(json!({
222            "customers": filtered,
223            "total_count": filtered.len()
224        }))
225    }
226
227    fn requires_context(&self) -> bool {
228        true
229    }
230}
231
232// ============================================================================
233// Tool: crm_get_customer
234// ============================================================================
235
236pub struct CrmGetCustomerTool;
237
238#[async_trait]
239impl Tool for CrmGetCustomerTool {
240    fn name(&self) -> &str {
241        "crm_get_customer"
242    }
243
244    fn display_name(&self) -> Option<&str> {
245        Some("Get Customer")
246    }
247
248    fn description(&self) -> &str {
249        "Get detailed customer information by customer ID."
250    }
251
252    fn parameters_schema(&self) -> Value {
253        json!({
254            "type": "object",
255            "properties": {
256                "customer_id": {
257                    "type": "string",
258                    "description": "Customer ID to retrieve"
259                }
260            },
261            "required": ["customer_id"],
262            "additionalProperties": false
263        })
264    }
265
266    fn hints(&self) -> ToolHints {
267        ToolHints::default()
268            .with_readonly(true)
269            .with_idempotent(true)
270    }
271
272    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
273        ToolExecutionResult::tool_error("crm_get_customer requires context")
274    }
275
276    async fn execute_with_context(
277        &self,
278        arguments: Value,
279        context: &ToolContext,
280    ) -> ToolExecutionResult {
281        let file_store = match &context.file_store {
282            Some(store) => store,
283            None => return ToolExecutionResult::tool_error("File system not available"),
284        };
285
286        let customer_id = match arguments.get("customer_id").and_then(|v| v.as_str()) {
287            Some(id) => id,
288            None => {
289                return ToolExecutionResult::tool_error("Missing required parameter: customer_id");
290            }
291        };
292
293        // Read customers
294        let customers: Vec<Customer> = match file_store
295            .read_file(context.session_id, "/crm/customers.json")
296            .await
297        {
298            Ok(Some(file)) => {
299                serde_json::from_str(file.content.as_deref().unwrap_or("")).unwrap_or_default()
300            }
301            _ => vec![],
302        };
303
304        // Find customer
305        match customers.iter().find(|c| c.id == customer_id) {
306            Some(customer) => ToolExecutionResult::success(json!({"customer": customer})),
307            None => ToolExecutionResult::tool_error(format!("Customer not found: {}", customer_id)),
308        }
309    }
310
311    fn requires_context(&self) -> bool {
312        true
313    }
314}
315
316// ============================================================================
317// Tool: crm_create_customer
318// ============================================================================
319
320pub struct CrmCreateCustomerTool;
321
322#[async_trait]
323impl Tool for CrmCreateCustomerTool {
324    fn name(&self) -> &str {
325        "crm_create_customer"
326    }
327
328    fn display_name(&self) -> Option<&str> {
329        Some("Create Customer")
330    }
331
332    fn description(&self) -> &str {
333        "Create a new customer record in the CRM."
334    }
335
336    fn parameters_schema(&self) -> Value {
337        json!({
338            "type": "object",
339            "properties": {
340                "name": {"type": "string"},
341                "email": {"type": "string"},
342                "company": {"type": "string"},
343                "phone": {"type": "string"},
344                "tier": {
345                    "type": "string",
346                    "enum": ["free", "pro", "enterprise"],
347                    "default": "free"
348                }
349            },
350            "required": ["name", "email"],
351            "additionalProperties": false
352        })
353    }
354
355    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
356        ToolExecutionResult::tool_error("crm_create_customer requires context")
357    }
358
359    async fn execute_with_context(
360        &self,
361        arguments: Value,
362        context: &ToolContext,
363    ) -> ToolExecutionResult {
364        let file_store = match &context.file_store {
365            Some(store) => store,
366            None => return ToolExecutionResult::tool_error("File system not available"),
367        };
368
369        let name = match arguments.get("name").and_then(|v| v.as_str()) {
370            Some(n) => n,
371            None => return ToolExecutionResult::tool_error("Missing required parameter: name"),
372        };
373
374        let email = match arguments.get("email").and_then(|v| v.as_str()) {
375            Some(e) => e,
376            None => return ToolExecutionResult::tool_error("Missing required parameter: email"),
377        };
378
379        let company = arguments
380            .get("company")
381            .and_then(|v| v.as_str())
382            .unwrap_or("");
383        let phone = arguments
384            .get("phone")
385            .and_then(|v| v.as_str())
386            .unwrap_or("");
387        let tier = arguments
388            .get("tier")
389            .and_then(|v| v.as_str())
390            .unwrap_or("free");
391
392        // Read current customers
393        let mut customers: Vec<Customer> = match file_store
394            .read_file(context.session_id, "/crm/customers.json")
395            .await
396        {
397            Ok(Some(file)) => {
398                serde_json::from_str(file.content.as_deref().unwrap_or("")).unwrap_or_default()
399            }
400            _ => vec![],
401        };
402
403        // Create new customer
404        let customer_id = format!("CUST-{:03}", customers.len() + 1);
405        let now = chrono::Utc::now().to_rfc3339();
406
407        let customer = Customer {
408            id: customer_id.clone(),
409            name: name.to_string(),
410            email: email.to_string(),
411            company: company.to_string(),
412            phone: phone.to_string(),
413            tier: tier.to_string(),
414            created_at: now.clone(),
415            last_contact: now,
416        };
417
418        customers.push(customer.clone());
419
420        // Save customers
421        let content = serde_json::to_string_pretty(&customers).unwrap();
422        match file_store
423            .write_file(context.session_id, "/crm/customers.json", &content, "text")
424            .await
425        {
426            Ok(_) => ToolExecutionResult::success(json!({
427                "customer_id": customer_id,
428                "customer": customer
429            })),
430            Err(e) => ToolExecutionResult::internal_error(e),
431        }
432    }
433
434    fn requires_context(&self) -> bool {
435        true
436    }
437}
438
439// ============================================================================
440// Tool: crm_list_tickets
441// ============================================================================
442
443pub struct CrmListTicketsTool;
444
445#[async_trait]
446impl Tool for CrmListTicketsTool {
447    fn name(&self) -> &str {
448        "crm_list_tickets"
449    }
450
451    fn display_name(&self) -> Option<&str> {
452        Some("List Tickets")
453    }
454
455    fn description(&self) -> &str {
456        "List support tickets. Filter by status or priority."
457    }
458
459    fn parameters_schema(&self) -> Value {
460        json!({
461            "type": "object",
462            "properties": {
463                "status": {
464                    "type": "string",
465                    "enum": ["open", "in_progress", "resolved", "closed"]
466                },
467                "priority": {
468                    "type": "string",
469                    "enum": ["low", "medium", "high", "urgent"]
470                }
471            },
472            "additionalProperties": false
473        })
474    }
475
476    fn hints(&self) -> ToolHints {
477        ToolHints::default()
478            .with_readonly(true)
479            .with_idempotent(true)
480    }
481
482    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
483        ToolExecutionResult::tool_error("crm_list_tickets requires context")
484    }
485
486    async fn execute_with_context(
487        &self,
488        arguments: Value,
489        context: &ToolContext,
490    ) -> ToolExecutionResult {
491        let file_store = match &context.file_store {
492            Some(store) => store,
493            None => return ToolExecutionResult::tool_error("File system not available"),
494        };
495
496        let status_filter = arguments.get("status").and_then(|v| v.as_str());
497        let priority_filter = arguments.get("priority").and_then(|v| v.as_str());
498
499        // Read tickets
500        let tickets: Vec<Ticket> = match file_store
501            .read_file(context.session_id, "/crm/tickets.json")
502            .await
503        {
504            Ok(Some(file)) => {
505                serde_json::from_str(file.content.as_deref().unwrap_or("")).unwrap_or_default()
506            }
507            _ => vec![],
508        };
509
510        // Apply filters
511        let mut filtered = tickets;
512        if let Some(status) = status_filter {
513            filtered.retain(|t| t.status == status);
514        }
515        if let Some(priority) = priority_filter {
516            filtered.retain(|t| t.priority == priority);
517        }
518
519        ToolExecutionResult::success(json!({
520            "tickets": filtered,
521            "total_count": filtered.len()
522        }))
523    }
524
525    fn requires_context(&self) -> bool {
526        true
527    }
528}
529
530// ============================================================================
531// Tool: crm_create_ticket
532// ============================================================================
533
534pub struct CrmCreateTicketTool;
535
536#[async_trait]
537impl Tool for CrmCreateTicketTool {
538    fn name(&self) -> &str {
539        "crm_create_ticket"
540    }
541
542    fn display_name(&self) -> Option<&str> {
543        Some("Create Ticket")
544    }
545
546    fn description(&self) -> &str {
547        "Create a new support ticket for a customer."
548    }
549
550    fn parameters_schema(&self) -> Value {
551        json!({
552            "type": "object",
553            "properties": {
554                "customer_id": {"type": "string"},
555                "subject": {"type": "string"},
556                "description": {"type": "string"},
557                "priority": {
558                    "type": "string",
559                    "enum": ["low", "medium", "high", "urgent"],
560                    "default": "medium"
561                }
562            },
563            "required": ["customer_id", "subject", "description"],
564            "additionalProperties": false
565        })
566    }
567
568    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
569        ToolExecutionResult::tool_error("crm_create_ticket requires context")
570    }
571
572    async fn execute_with_context(
573        &self,
574        arguments: Value,
575        context: &ToolContext,
576    ) -> ToolExecutionResult {
577        let file_store = match &context.file_store {
578            Some(store) => store,
579            None => return ToolExecutionResult::tool_error("File system not available"),
580        };
581
582        let customer_id = match arguments.get("customer_id").and_then(|v| v.as_str()) {
583            Some(id) => id,
584            None => {
585                return ToolExecutionResult::tool_error("Missing required parameter: customer_id");
586            }
587        };
588
589        let subject = match arguments.get("subject").and_then(|v| v.as_str()) {
590            Some(s) => s,
591            None => return ToolExecutionResult::tool_error("Missing required parameter: subject"),
592        };
593
594        let description = match arguments.get("description").and_then(|v| v.as_str()) {
595            Some(d) => d,
596            None => {
597                return ToolExecutionResult::tool_error("Missing required parameter: description");
598            }
599        };
600
601        let priority = arguments
602            .get("priority")
603            .and_then(|v| v.as_str())
604            .unwrap_or("medium");
605
606        // Read current tickets
607        let mut tickets: Vec<Ticket> = match file_store
608            .read_file(context.session_id, "/crm/tickets.json")
609            .await
610        {
611            Ok(Some(file)) => {
612                serde_json::from_str(file.content.as_deref().unwrap_or("")).unwrap_or_default()
613            }
614            _ => vec![],
615        };
616
617        // Create new ticket
618        let ticket_id = format!("TKT-{:05}", tickets.len() + 1);
619        let now = chrono::Utc::now().to_rfc3339();
620
621        let ticket = Ticket {
622            id: ticket_id.clone(),
623            customer_id: customer_id.to_string(),
624            subject: subject.to_string(),
625            description: description.to_string(),
626            status: "open".to_string(),
627            priority: priority.to_string(),
628            assigned_to: None,
629            created_at: now.clone(),
630            updated_at: now,
631        };
632
633        tickets.push(ticket.clone());
634
635        // Save tickets
636        let content = serde_json::to_string_pretty(&tickets).unwrap();
637        match file_store
638            .write_file(context.session_id, "/crm/tickets.json", &content, "text")
639            .await
640        {
641            Ok(_) => ToolExecutionResult::success(json!({
642                "ticket_id": ticket_id,
643                "status": "open",
644                "priority": priority
645            })),
646            Err(e) => ToolExecutionResult::internal_error(e),
647        }
648    }
649
650    fn requires_context(&self) -> bool {
651        true
652    }
653}
654
655// ============================================================================
656// Remaining tools (simplified implementations)
657// ============================================================================
658
659pub struct CrmUpdateTicketTool;
660
661#[async_trait]
662impl Tool for CrmUpdateTicketTool {
663    fn name(&self) -> &str {
664        "crm_update_ticket"
665    }
666
667    fn display_name(&self) -> Option<&str> {
668        Some("Update Ticket")
669    }
670
671    fn description(&self) -> &str {
672        "Update ticket status or assignment."
673    }
674
675    fn parameters_schema(&self) -> Value {
676        json!({
677            "type": "object",
678            "properties": {
679                "ticket_id": {"type": "string"},
680                "status": {"type": "string", "enum": ["open", "in_progress", "resolved", "closed"]},
681                "assigned_to": {"type": "string"}
682            },
683            "required": ["ticket_id"],
684            "additionalProperties": false
685        })
686    }
687
688    fn hints(&self) -> ToolHints {
689        ToolHints::default().with_idempotent(true)
690    }
691
692    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
693        ToolExecutionResult::tool_error("Requires context")
694    }
695
696    async fn execute_with_context(
697        &self,
698        arguments: Value,
699        context: &ToolContext,
700    ) -> ToolExecutionResult {
701        let file_store = match &context.file_store {
702            Some(store) => store,
703            None => return ToolExecutionResult::tool_error("File system not available"),
704        };
705
706        let ticket_id = arguments
707            .get("ticket_id")
708            .and_then(|v| v.as_str())
709            .unwrap_or("unknown");
710
711        let new_status = arguments.get("status").and_then(|v| v.as_str());
712        let assigned_to = arguments.get("assigned_to").and_then(|v| v.as_str());
713
714        // Read tickets
715        let mut tickets: Vec<Ticket> = match file_store
716            .read_file(context.session_id, "/crm/tickets.json")
717            .await
718        {
719            Ok(Some(file)) => {
720                serde_json::from_str(file.content.as_deref().unwrap_or("")).unwrap_or_default()
721            }
722            _ => vec![],
723        };
724
725        // Find and update ticket
726        if let Some(ticket) = tickets.iter_mut().find(|t| t.id == ticket_id) {
727            if let Some(status) = new_status {
728                ticket.status = status.to_string();
729            }
730            if let Some(agent) = assigned_to {
731                ticket.assigned_to = Some(agent.to_string());
732            }
733            ticket.updated_at = chrono::Utc::now().to_rfc3339();
734            let updated_status = ticket.status.clone();
735
736            let content = serde_json::to_string_pretty(&tickets).unwrap();
737            let _ = file_store
738                .write_file(context.session_id, "/crm/tickets.json", &content, "text")
739                .await;
740
741            ToolExecutionResult::success(json!({"ticket_id": ticket_id, "status": updated_status}))
742        } else {
743            ToolExecutionResult::tool_error(format!("Ticket not found: {}", ticket_id))
744        }
745    }
746
747    fn requires_context(&self) -> bool {
748        true
749    }
750}
751
752pub struct CrmAddInteractionTool;
753
754#[async_trait]
755impl Tool for CrmAddInteractionTool {
756    fn name(&self) -> &str {
757        "crm_add_interaction"
758    }
759
760    fn display_name(&self) -> Option<&str> {
761        Some("Add Interaction")
762    }
763
764    fn description(&self) -> &str {
765        "Add a customer interaction note (call, email, meeting, chat)."
766    }
767
768    fn parameters_schema(&self) -> Value {
769        json!({
770            "type": "object",
771            "properties": {
772                "customer_id": {"type": "string"},
773                "interaction_type": {"type": "string", "enum": ["call", "email", "meeting", "chat"]},
774                "summary": {"type": "string"},
775                "agent": {"type": "string"}
776            },
777            "required": ["customer_id", "interaction_type", "summary", "agent"],
778            "additionalProperties": false
779        })
780    }
781
782    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
783        ToolExecutionResult::tool_error("Requires context")
784    }
785
786    async fn execute_with_context(
787        &self,
788        _arguments: Value,
789        _context: &ToolContext,
790    ) -> ToolExecutionResult {
791        let interaction_id = format!("INT-{:05}", chrono::Utc::now().timestamp() % 100000);
792
793        ToolExecutionResult::success(json!({
794            "interaction_id": interaction_id,
795            "status": "recorded"
796        }))
797    }
798
799    fn requires_context(&self) -> bool {
800        true
801    }
802}
803
804pub struct CrmSearchCustomersTool;
805
806#[async_trait]
807impl Tool for CrmSearchCustomersTool {
808    fn name(&self) -> &str {
809        "crm_search_customers"
810    }
811
812    fn display_name(&self) -> Option<&str> {
813        Some("Search Customers")
814    }
815
816    fn description(&self) -> &str {
817        "Search customers by name, email, or company."
818    }
819
820    fn parameters_schema(&self) -> Value {
821        json!({
822            "type": "object",
823            "properties": {
824                "query": {"type": "string", "description": "Search query"}
825            },
826            "required": ["query"],
827            "additionalProperties": false
828        })
829    }
830
831    fn hints(&self) -> ToolHints {
832        ToolHints::default()
833            .with_readonly(true)
834            .with_idempotent(true)
835    }
836
837    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
838        ToolExecutionResult::tool_error("Requires context")
839    }
840
841    async fn execute_with_context(
842        &self,
843        arguments: Value,
844        context: &ToolContext,
845    ) -> ToolExecutionResult {
846        let file_store = match &context.file_store {
847            Some(store) => store,
848            None => return ToolExecutionResult::tool_error("File system not available"),
849        };
850
851        let query = arguments
852            .get("query")
853            .and_then(|v| v.as_str())
854            .unwrap_or("")
855            .to_lowercase();
856
857        // Read customers
858        let customers: Vec<Customer> = match file_store
859            .read_file(context.session_id, "/crm/customers.json")
860            .await
861        {
862            Ok(Some(file)) => {
863                serde_json::from_str(file.content.as_deref().unwrap_or("")).unwrap_or_default()
864            }
865            _ => vec![],
866        };
867
868        // Search
869        let results: Vec<_> = customers
870            .into_iter()
871            .filter(|c| {
872                c.name.to_lowercase().contains(&query)
873                    || c.email.to_lowercase().contains(&query)
874                    || c.company.to_lowercase().contains(&query)
875            })
876            .collect();
877
878        ToolExecutionResult::success(json!({
879            "results": results,
880            "count": results.len()
881        }))
882    }
883
884    fn requires_context(&self) -> bool {
885        true
886    }
887}