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_FINANCIAL_CAPABILITY_ID: &str = "fake_financial";
25
26pub 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#[derive(Debug, Clone, Serialize, Deserialize)]
84struct Transaction {
85 id: String,
86 date: String,
87 transaction_type: String, 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
109pub 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 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 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 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
247pub 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 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 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 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
383pub 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
471pub 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}