Skip to main content

everruns_core/capabilities/
fake_financial.rs

1//! Fake Financial Tools Capability - demo tools for financial analysis
2//!
3//! This capability provides mock financial management tools that store state
4//! in the session file system. Perfect for demos and testing financial workflows.
5//!
6//! Tools provided:
7//! - `finance_list_transactions`: List financial transactions
8//! - `finance_create_transaction`: Record a new transaction
9//! - `finance_get_balance`: Get account balances
10//! - `finance_list_budgets`: List budgets
11//! - `finance_create_budget`: Create a new budget
12//! - `finance_get_expense_report`: Generate expense report
13//! - `finance_get_revenue_report`: Generate revenue report
14//! - `finance_forecast_cash_flow`: Forecast cash flow
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_FINANCIAL_CAPABILITY_ID: &str = "fake_financial";
25
26/// Fake Financial Tools capability - mock financial management for demos
27pub struct FakeFinancialCapability;
28
29impl Capability for FakeFinancialCapability {
30    fn id(&self) -> &str {
31        FAKE_FINANCIAL_CAPABILITY_ID
32    }
33
34    fn name(&self) -> &str {
35        "Fake Financial Tools"
36    }
37
38    fn description(&self) -> &str {
39        "Demo capability: financial management tools (transactions, budgets, reports, forecasting). State stored in session filesystem."
40    }
41
42    fn localizations(&self) -> Vec<CapabilityLocalization> {
43        vec![CapabilityLocalization::text(
44            "uk",
45            "Демо-інструменти фінансів",
46            "Демонстраційна можливість: інструменти керування фінансами (транзакції, бюджети, звіти, прогнозування). Стан зберігається у файловій системі сесії.",
47        )]
48    }
49
50    fn status(&self) -> CapabilityStatus {
51        CapabilityStatus::Available
52    }
53
54    fn icon(&self) -> Option<&str> {
55        Some("dollar-sign")
56    }
57
58    fn category(&self) -> Option<&str> {
59        Some("Demo Tools")
60    }
61
62    fn system_prompt_addition(&self) -> Option<&str> {
63        Some(
64            "Financial data is stored in /finance/ (transactions.json, budgets.json, accounts.json).",
65        )
66    }
67
68    fn tools(&self) -> Vec<Box<dyn Tool>> {
69        vec![
70            Box::new(FinanceListTransactionsTool),
71            Box::new(FinanceCreateTransactionTool),
72            Box::new(FinanceGetBalanceTool),
73            Box::new(FinanceListBudgetsTool),
74            Box::new(FinanceCreateBudgetTool),
75            Box::new(FinanceGetExpenseReportTool),
76            Box::new(FinanceGetRevenueReportTool),
77            Box::new(FinanceForecastCashFlowTool),
78        ]
79    }
80}
81
82// Helper structs for financial data
83#[derive(Debug, Clone, Serialize, Deserialize)]
84struct Transaction {
85    id: String,
86    date: String,
87    transaction_type: String, // income, expense
88    category: String,
89    amount: f64,
90    description: String,
91    account: String,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95struct Budget {
96    category: String,
97    monthly_limit: f64,
98    current_spent: f64,
99    period_start: String,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103struct Account {
104    name: String,
105    balance: f64,
106    currency: String,
107}
108
109// ============================================================================
110// Tool: finance_list_transactions
111// ============================================================================
112
113pub struct FinanceListTransactionsTool;
114
115#[async_trait]
116impl Tool for FinanceListTransactionsTool {
117    fn name(&self) -> &str {
118        "finance_list_transactions"
119    }
120
121    fn display_name(&self) -> Option<&str> {
122        Some("List Transactions")
123    }
124
125    fn description(&self) -> &str {
126        "List financial transactions. Filter by type (income/expense) or category."
127    }
128
129    fn parameters_schema(&self) -> Value {
130        json!({
131            "type": "object",
132            "properties": {
133                "transaction_type": {
134                    "type": "string",
135                    "enum": ["income", "expense"],
136                    "description": "Optional: Filter by transaction type"
137                },
138                "category": {
139                    "type": "string",
140                    "description": "Optional: Filter by category"
141                }
142            },
143            "additionalProperties": false
144        })
145    }
146
147    fn hints(&self) -> ToolHints {
148        ToolHints::default()
149            .with_readonly(true)
150            .with_idempotent(true)
151    }
152
153    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
154        ToolExecutionResult::tool_error("finance_list_transactions requires context")
155    }
156
157    async fn execute_with_context(
158        &self,
159        arguments: Value,
160        context: &ToolContext,
161    ) -> ToolExecutionResult {
162        let file_store = match &context.file_store {
163            Some(store) => store,
164            None => return ToolExecutionResult::tool_error("File system not available"),
165        };
166
167        let type_filter = arguments.get("transaction_type").and_then(|v| v.as_str());
168        let category_filter = arguments.get("category").and_then(|v| v.as_str());
169
170        // Read transactions
171        let transactions: Vec<Transaction> = match file_store
172            .read_file(context.session_id, "/finance/transactions.json")
173            .await
174        {
175            Ok(Some(file)) => serde_json::from_str(file.content.as_deref().unwrap_or(""))
176                .unwrap_or_else(|_| {
177                    // Initialize with sample data
178                    vec![
179                        Transaction {
180                            id: "TXN-001".to_string(),
181                            date: "2025-01-05".to_string(),
182                            transaction_type: "income".to_string(),
183                            category: "sales".to_string(),
184                            amount: 15000.0,
185                            description: "Q1 Product Sales".to_string(),
186                            account: "operating".to_string(),
187                        },
188                        Transaction {
189                            id: "TXN-002".to_string(),
190                            date: "2025-01-06".to_string(),
191                            transaction_type: "expense".to_string(),
192                            category: "payroll".to_string(),
193                            amount: 8500.0,
194                            description: "January Payroll".to_string(),
195                            account: "operating".to_string(),
196                        },
197                    ]
198                }),
199            _ => {
200                let initial = vec![Transaction {
201                    id: "TXN-001".to_string(),
202                    date: chrono::Utc::now().format("%Y-%m-%d").to_string(),
203                    transaction_type: "income".to_string(),
204                    category: "sales".to_string(),
205                    amount: 15000.0,
206                    description: "Initial Sale".to_string(),
207                    account: "operating".to_string(),
208                }];
209
210                let content = serde_json::to_string_pretty(&initial).unwrap();
211                let _ = file_store
212                    .write_file(
213                        context.session_id,
214                        "/finance/transactions.json",
215                        &content,
216                        "text",
217                    )
218                    .await;
219
220                initial
221            }
222        };
223
224        // Apply filters
225        let mut filtered = transactions;
226        if let Some(tx_type) = type_filter {
227            filtered.retain(|t| t.transaction_type == tx_type);
228        }
229        if let Some(category) = category_filter {
230            filtered.retain(|t| t.category == category);
231        }
232
233        let total: f64 = filtered.iter().map(|t| t.amount).sum();
234
235        ToolExecutionResult::success(json!({
236            "transactions": filtered,
237            "total_count": filtered.len(),
238            "total_amount": total
239        }))
240    }
241
242    fn requires_context(&self) -> bool {
243        true
244    }
245}
246
247// ============================================================================
248// Tool: finance_create_transaction
249// ============================================================================
250
251pub struct FinanceCreateTransactionTool;
252
253#[async_trait]
254impl Tool for FinanceCreateTransactionTool {
255    fn name(&self) -> &str {
256        "finance_create_transaction"
257    }
258
259    fn display_name(&self) -> Option<&str> {
260        Some("Create Transaction")
261    }
262
263    fn description(&self) -> &str {
264        "Record a new financial transaction (income or expense)."
265    }
266
267    fn parameters_schema(&self) -> Value {
268        json!({
269            "type": "object",
270            "properties": {
271                "transaction_type": {
272                    "type": "string",
273                    "enum": ["income", "expense"]
274                },
275                "category": {"type": "string"},
276                "amount": {"type": "number", "minimum": 0},
277                "description": {"type": "string"},
278                "account": {"type": "string", "default": "operating"}
279            },
280            "required": ["transaction_type", "category", "amount", "description"],
281            "additionalProperties": false
282        })
283    }
284
285    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
286        ToolExecutionResult::tool_error("finance_create_transaction requires context")
287    }
288
289    async fn execute_with_context(
290        &self,
291        arguments: Value,
292        context: &ToolContext,
293    ) -> ToolExecutionResult {
294        let file_store = match &context.file_store {
295            Some(store) => store,
296            None => return ToolExecutionResult::tool_error("File system not available"),
297        };
298
299        let transaction_type = match arguments.get("transaction_type").and_then(|v| v.as_str()) {
300            Some(t) => t,
301            None => {
302                return ToolExecutionResult::tool_error(
303                    "Missing required parameter: transaction_type",
304                );
305            }
306        };
307
308        let category = match arguments.get("category").and_then(|v| v.as_str()) {
309            Some(c) => c,
310            None => return ToolExecutionResult::tool_error("Missing required parameter: category"),
311        };
312
313        let amount = match arguments.get("amount").and_then(|v| v.as_f64()) {
314            Some(a) => a,
315            None => return ToolExecutionResult::tool_error("Missing required parameter: amount"),
316        };
317
318        let description = match arguments.get("description").and_then(|v| v.as_str()) {
319            Some(d) => d,
320            None => {
321                return ToolExecutionResult::tool_error("Missing required parameter: description");
322            }
323        };
324
325        let account = arguments
326            .get("account")
327            .and_then(|v| v.as_str())
328            .unwrap_or("operating");
329
330        // Read current transactions
331        let mut transactions: Vec<Transaction> = match file_store
332            .read_file(context.session_id, "/finance/transactions.json")
333            .await
334        {
335            Ok(Some(file)) => {
336                serde_json::from_str(file.content.as_deref().unwrap_or("")).unwrap_or_default()
337            }
338            _ => vec![],
339        };
340
341        // Create new transaction
342        let transaction_id = format!("TXN-{:03}", transactions.len() + 1);
343        let date = chrono::Utc::now().format("%Y-%m-%d").to_string();
344
345        let transaction = Transaction {
346            id: transaction_id.clone(),
347            date: date.clone(),
348            transaction_type: transaction_type.to_string(),
349            category: category.to_string(),
350            amount,
351            description: description.to_string(),
352            account: account.to_string(),
353        };
354
355        transactions.push(transaction.clone());
356
357        // Save transactions
358        let content = serde_json::to_string_pretty(&transactions).unwrap();
359        match file_store
360            .write_file(
361                context.session_id,
362                "/finance/transactions.json",
363                &content,
364                "text",
365            )
366            .await
367        {
368            Ok(_) => ToolExecutionResult::success(json!({
369                "transaction_id": transaction_id,
370                "amount": amount,
371                "type": transaction_type,
372                "date": date
373            })),
374            Err(e) => ToolExecutionResult::internal_error(e),
375        }
376    }
377
378    fn requires_context(&self) -> bool {
379        true
380    }
381}
382
383// ============================================================================
384// Tool: finance_get_balance
385// ============================================================================
386
387pub struct FinanceGetBalanceTool;
388
389#[async_trait]
390impl Tool for FinanceGetBalanceTool {
391    fn name(&self) -> &str {
392        "finance_get_balance"
393    }
394
395    fn display_name(&self) -> Option<&str> {
396        Some("Get Balance")
397    }
398
399    fn description(&self) -> &str {
400        "Get current account balances."
401    }
402
403    fn parameters_schema(&self) -> Value {
404        json!({
405            "type": "object",
406            "properties": {
407                "account": {
408                    "type": "string",
409                    "description": "Optional: Get balance for specific account"
410                }
411            },
412            "additionalProperties": false
413        })
414    }
415
416    fn hints(&self) -> ToolHints {
417        ToolHints::default()
418            .with_readonly(true)
419            .with_idempotent(true)
420    }
421
422    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
423        ToolExecutionResult::tool_error("finance_get_balance requires context")
424    }
425
426    async fn execute_with_context(
427        &self,
428        arguments: Value,
429        _context: &ToolContext,
430    ) -> ToolExecutionResult {
431        let account_filter = arguments.get("account").and_then(|v| v.as_str());
432
433        let accounts = vec![
434            Account {
435                name: "operating".to_string(),
436                balance: 125000.0,
437                currency: "USD".to_string(),
438            },
439            Account {
440                name: "savings".to_string(),
441                balance: 50000.0,
442                currency: "USD".to_string(),
443            },
444            Account {
445                name: "payroll".to_string(),
446                balance: 75000.0,
447                currency: "USD".to_string(),
448            },
449        ];
450
451        let filtered: Vec<_> = if let Some(account) = account_filter {
452            accounts.into_iter().filter(|a| a.name == account).collect()
453        } else {
454            accounts
455        };
456
457        let total_balance: f64 = filtered.iter().map(|a| a.balance).sum();
458
459        ToolExecutionResult::success(json!({
460            "accounts": filtered,
461            "total_balance": total_balance,
462            "currency": "USD"
463        }))
464    }
465
466    fn requires_context(&self) -> bool {
467        true
468    }
469}
470
471// ============================================================================
472// Remaining tools (simplified implementations)
473// ============================================================================
474
475pub struct FinanceListBudgetsTool;
476
477#[async_trait]
478impl Tool for FinanceListBudgetsTool {
479    fn name(&self) -> &str {
480        "finance_list_budgets"
481    }
482
483    fn display_name(&self) -> Option<&str> {
484        Some("List Budgets")
485    }
486
487    fn description(&self) -> &str {
488        "List all budgets by category."
489    }
490
491    fn parameters_schema(&self) -> Value {
492        json!({"type": "object", "properties": {}, "additionalProperties": false})
493    }
494
495    fn hints(&self) -> ToolHints {
496        ToolHints::default()
497            .with_readonly(true)
498            .with_idempotent(true)
499    }
500
501    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
502        ToolExecutionResult::tool_error("Requires context")
503    }
504
505    async fn execute_with_context(
506        &self,
507        _arguments: Value,
508        _context: &ToolContext,
509    ) -> ToolExecutionResult {
510        let budgets = vec![
511            Budget {
512                category: "payroll".to_string(),
513                monthly_limit: 50000.0,
514                current_spent: 38500.0,
515                period_start: "2025-01-01".to_string(),
516            },
517            Budget {
518                category: "marketing".to_string(),
519                monthly_limit: 15000.0,
520                current_spent: 8200.0,
521                period_start: "2025-01-01".to_string(),
522            },
523        ];
524
525        ToolExecutionResult::success(json!({"budgets": budgets, "total_count": budgets.len()}))
526    }
527
528    fn requires_context(&self) -> bool {
529        true
530    }
531}
532
533pub struct FinanceCreateBudgetTool;
534
535#[async_trait]
536impl Tool for FinanceCreateBudgetTool {
537    fn name(&self) -> &str {
538        "finance_create_budget"
539    }
540
541    fn display_name(&self) -> Option<&str> {
542        Some("Create Budget")
543    }
544
545    fn description(&self) -> &str {
546        "Create or update a budget for a category."
547    }
548
549    fn parameters_schema(&self) -> Value {
550        json!({
551            "type": "object",
552            "properties": {
553                "category": {"type": "string"},
554                "monthly_limit": {"type": "number", "minimum": 0}
555            },
556            "required": ["category", "monthly_limit"],
557            "additionalProperties": false
558        })
559    }
560
561    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
562        ToolExecutionResult::tool_error("Requires context")
563    }
564
565    async fn execute_with_context(
566        &self,
567        arguments: Value,
568        _context: &ToolContext,
569    ) -> ToolExecutionResult {
570        let category = arguments
571            .get("category")
572            .and_then(|v| v.as_str())
573            .unwrap_or("unknown");
574        let monthly_limit = arguments
575            .get("monthly_limit")
576            .and_then(|v| v.as_f64())
577            .unwrap_or(0.0);
578
579        ToolExecutionResult::success(json!({
580            "category": category,
581            "monthly_limit": monthly_limit,
582            "status": "created"
583        }))
584    }
585
586    fn requires_context(&self) -> bool {
587        true
588    }
589}
590
591pub struct FinanceGetExpenseReportTool;
592
593#[async_trait]
594impl Tool for FinanceGetExpenseReportTool {
595    fn name(&self) -> &str {
596        "finance_get_expense_report"
597    }
598
599    fn display_name(&self) -> Option<&str> {
600        Some("Get Expense Report")
601    }
602
603    fn description(&self) -> &str {
604        "Generate expense report for a time period, broken down by category."
605    }
606
607    fn parameters_schema(&self) -> Value {
608        json!({
609            "type": "object",
610            "properties": {
611                "period": {
612                    "type": "string",
613                    "enum": ["current_month", "last_month", "quarter", "year"],
614                    "default": "current_month"
615                }
616            },
617            "additionalProperties": false
618        })
619    }
620
621    fn hints(&self) -> ToolHints {
622        ToolHints::default()
623            .with_readonly(true)
624            .with_idempotent(true)
625    }
626
627    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
628        ToolExecutionResult::tool_error("Requires context")
629    }
630
631    async fn execute_with_context(
632        &self,
633        arguments: Value,
634        _context: &ToolContext,
635    ) -> ToolExecutionResult {
636        let period = arguments
637            .get("period")
638            .and_then(|v| v.as_str())
639            .unwrap_or("current_month");
640
641        let expense_breakdown = json!([
642            {"category": "payroll", "amount": 85000.0, "percentage": 60.0},
643            {"category": "marketing", "amount": 25000.0, "percentage": 18.0},
644            {"category": "operations", "amount": 20000.0, "percentage": 14.0},
645            {"category": "other", "amount": 11500.0, "percentage": 8.0}
646        ]);
647
648        ToolExecutionResult::success(json!({
649            "period": period,
650            "total_expenses": 141500.0,
651            "breakdown": expense_breakdown,
652            "top_category": "payroll"
653        }))
654    }
655
656    fn requires_context(&self) -> bool {
657        true
658    }
659}
660
661pub struct FinanceGetRevenueReportTool;
662
663#[async_trait]
664impl Tool for FinanceGetRevenueReportTool {
665    fn name(&self) -> &str {
666        "finance_get_revenue_report"
667    }
668
669    fn display_name(&self) -> Option<&str> {
670        Some("Get Revenue Report")
671    }
672
673    fn description(&self) -> &str {
674        "Generate revenue report for a time period."
675    }
676
677    fn parameters_schema(&self) -> Value {
678        json!({
679            "type": "object",
680            "properties": {
681                "period": {
682                    "type": "string",
683                    "enum": ["current_month", "last_month", "quarter", "year"],
684                    "default": "current_month"
685                }
686            },
687            "additionalProperties": false
688        })
689    }
690
691    fn hints(&self) -> ToolHints {
692        ToolHints::default()
693            .with_readonly(true)
694            .with_idempotent(true)
695    }
696
697    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
698        ToolExecutionResult::tool_error("Requires context")
699    }
700
701    async fn execute_with_context(
702        &self,
703        arguments: Value,
704        _context: &ToolContext,
705    ) -> ToolExecutionResult {
706        let period = arguments
707            .get("period")
708            .and_then(|v| v.as_str())
709            .unwrap_or("current_month");
710
711        let revenue_breakdown = json!([
712            {"source": "product_sales", "amount": 150000.0, "percentage": 75.0},
713            {"source": "subscriptions", "amount": 35000.0, "percentage": 17.5},
714            {"source": "services", "amount": 15000.0, "percentage": 7.5}
715        ]);
716
717        ToolExecutionResult::success(json!({
718            "period": period,
719            "total_revenue": 200000.0,
720            "breakdown": revenue_breakdown,
721            "growth_vs_last_period": 12.5
722        }))
723    }
724
725    fn requires_context(&self) -> bool {
726        true
727    }
728}
729
730pub struct FinanceForecastCashFlowTool;
731
732#[async_trait]
733impl Tool for FinanceForecastCashFlowTool {
734    fn name(&self) -> &str {
735        "finance_forecast_cash_flow"
736    }
737
738    fn display_name(&self) -> Option<&str> {
739        Some("Forecast Cash Flow")
740    }
741
742    fn description(&self) -> &str {
743        "Forecast cash flow for the next 3-6 months based on historical data."
744    }
745
746    fn parameters_schema(&self) -> Value {
747        json!({
748            "type": "object",
749            "properties": {
750                "months": {
751                    "type": "integer",
752                    "minimum": 1,
753                    "maximum": 12,
754                    "default": 3,
755                    "description": "Number of months to forecast"
756                }
757            },
758            "additionalProperties": false
759        })
760    }
761
762    fn hints(&self) -> ToolHints {
763        ToolHints::default()
764            .with_readonly(true)
765            .with_idempotent(true)
766    }
767
768    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
769        ToolExecutionResult::tool_error("Requires context")
770    }
771
772    async fn execute_with_context(
773        &self,
774        arguments: Value,
775        _context: &ToolContext,
776    ) -> ToolExecutionResult {
777        let months = arguments
778            .get("months")
779            .and_then(|v| v.as_i64())
780            .unwrap_or(3) as i32;
781
782        let forecast: Vec<Value> = (1..=months)
783            .map(|m| {
784                json!({
785                    "month": m,
786                    "projected_revenue": 200000.0 + (m as f64 * 5000.0),
787                    "projected_expenses": 140000.0 + (m as f64 * 2000.0),
788                    "net_cash_flow": 60000.0 + (m as f64 * 3000.0),
789                    "ending_balance": 125000.0 + (m as f64 * 60000.0)
790                })
791            })
792            .collect();
793
794        ToolExecutionResult::success(json!({
795            "forecast_months": months,
796            "forecast": forecast,
797            "assumptions": "Based on 10% monthly growth"
798        }))
799    }
800
801    fn requires_context(&self) -> bool {
802        true
803    }
804}