1use 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
26pub 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#[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, 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, priority: String, assigned_to: Option<String>,
102 created_at: String,
103 updated_at: String,
104}
105
106pub 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 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 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 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
232pub 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 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 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
316pub 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 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 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 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
439pub 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 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 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
530pub 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 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 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 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
655pub 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 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 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 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 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}