Balamurugan Thayalan commited on
Commit
ed1f7cd
·
1 Parent(s): 499796e

spend-analyzer-mcp-mbt v1.0.0

Browse files
.gitignore ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API Keys and Configuration
2
+ config.json
3
+ .env
4
+ .env.local
5
+ .env.production
6
+
7
+ # Secure Storage
8
+ api_keys.json
9
+ secrets.json
10
+
11
+ # Python
12
+ __pycache__/
13
+ *.py[cod]
14
+ *$py.class
15
+ *.so
16
+ .Python
17
+ build/
18
+ develop-eggs/
19
+ dist/
20
+ downloads/
21
+ eggs/
22
+ .eggs/
23
+ lib/
24
+ lib64/
25
+ parts/
26
+ sdist/
27
+ var/
28
+ wheels/
29
+ *.egg-info/
30
+ .installed.cfg
31
+ *.egg
32
+ MANIFEST
33
+
34
+ # Virtual Environment
35
+ venv/
36
+ env/
37
+ ENV/
38
+ env.bak/
39
+ venv.bak/
40
+
41
+ # IDE
42
+ .vscode/
43
+ .idea/
44
+ *.swp
45
+ *.swo
46
+ *~
47
+
48
+ # OS
49
+ .DS_Store
50
+ .DS_Store?
51
+ ._*
52
+ .Spotlight-V100
53
+ .Trashes
54
+ ehthumbs.db
55
+ Thumbs.db
56
+
57
+ # Logs
58
+ *.log
59
+ logs/
60
+
61
+ # Temporary files
62
+ *.tmp
63
+ *.temp
64
+ temp/
65
+ tmp/
66
+
67
+ # Database
68
+ *.db
69
+ *.sqlite
70
+ *.sqlite3
71
+
72
+ # Jupyter Notebook
73
+ .ipynb_checkpoints
74
+
75
+ # pytest
76
+ .pytest_cache/
77
+ .coverage
78
+ htmlcov/
79
+
80
+ # mypy
81
+ .mypy_cache/
82
+ .dmypy.json
83
+ dmypy.json
API_DOCUMENTATION.md ADDED
@@ -0,0 +1,619 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Spend Analyzer MCP - API Documentation
2
+
3
+ This document provides comprehensive API documentation for the Spend Analyzer MCP system, including Modal functions, MCP protocol integration, and local usage.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Modal Functions API](#modal-functions-api)
8
+ 2. [MCP Protocol Integration](#mcp-protocol-integration)
9
+ 3. [Local Python API](#local-python-api)
10
+ 4. [Data Formats](#data-formats)
11
+ 5. [Error Handling](#error-handling)
12
+ 6. [Examples](#examples)
13
+
14
+ ## Modal Functions API
15
+
16
+ ### 1. `process_bank_statements`
17
+
18
+ Process bank statements from email attachments.
19
+
20
+ **Function Signature:**
21
+ ```python
22
+ def process_bank_statements(
23
+ email_config: Dict,
24
+ days_back: int = 30,
25
+ passwords: Optional[Dict] = None
26
+ ) -> Dict
27
+ ```
28
+
29
+ **Parameters:**
30
+ - `email_config` (Dict): Email configuration
31
+ - `email` (str): Email address
32
+ - `password` (str): App-specific password
33
+ - `imap_server` (str): IMAP server address
34
+ - `days_back` (int): Number of days to look back (default: 30)
35
+ - `passwords` (Dict, optional): PDF passwords by filename
36
+
37
+ **Returns:**
38
+ ```python
39
+ {
40
+ "processed_statements": [
41
+ {
42
+ "filename": str,
43
+ "bank": str,
44
+ "account": str,
45
+ "period": str,
46
+ "transaction_count": int,
47
+ "status": str # "success", "password_required", "error"
48
+ }
49
+ ],
50
+ "total_transactions": int,
51
+ "analysis": Dict, # Financial analysis data
52
+ "timestamp": str # ISO format
53
+ }
54
+ ```
55
+
56
+ **Example:**
57
+ ```python
58
+ import modal
59
+
60
+ app = modal.App.lookup("spend-analyzer-mcp-bmt")
61
+ process_statements = app["process_bank_statements"]
62
+
63
+ email_config = {
64
+ "email": "[email protected]",
65
+ "password": "app_password",
66
+ "imap_server": "imap.gmail.com"
67
+ }
68
+
69
+ result = process_statements.remote(email_config, days_back=30)
70
+ print(f"Processed {result['total_transactions']} transactions")
71
+ ```
72
+
73
+ ### 2. `analyze_uploaded_statements`
74
+
75
+ Analyze directly uploaded PDF statements.
76
+
77
+ **Function Signature:**
78
+ ```python
79
+ def analyze_uploaded_statements(
80
+ pdf_contents: Dict[str, bytes],
81
+ passwords: Optional[Dict] = None
82
+ ) -> Dict
83
+ ```
84
+
85
+ **Parameters:**
86
+ - `pdf_contents` (Dict[str, bytes]): Mapping of filename to PDF content
87
+ - `passwords` (Dict, optional): PDF passwords by filename
88
+
89
+ **Returns:**
90
+ ```python
91
+ {
92
+ "processed_files": [
93
+ {
94
+ "filename": str,
95
+ "bank": str,
96
+ "account": str,
97
+ "transaction_count": int,
98
+ "status": str
99
+ }
100
+ ],
101
+ "total_transactions": int,
102
+ "analysis": Dict
103
+ }
104
+ ```
105
+
106
+ **Example:**
107
+ ```python
108
+ # Read PDF files
109
+ pdf_contents = {}
110
+ with open("statement1.pdf", "rb") as f:
111
+ pdf_contents["statement1.pdf"] = f.read()
112
+
113
+ analyze_pdfs = app["analyze_uploaded_statements"]
114
+ result = analyze_pdfs.remote(pdf_contents)
115
+ ```
116
+
117
+ ### 3. `get_ai_analysis`
118
+
119
+ Get AI-powered financial analysis using Claude or SambaNova.
120
+
121
+ **Function Signature:**
122
+ ```python
123
+ def get_ai_analysis(
124
+ analysis_data: Dict,
125
+ user_question: str = "",
126
+ provider: str = "claude"
127
+ ) -> Dict
128
+ ```
129
+
130
+ **Parameters:**
131
+ - `analysis_data` (Dict): Financial analysis data
132
+ - `user_question` (str): Specific question for the AI
133
+ - `provider` (str): "claude" or "sambanova"
134
+
135
+ **Returns:**
136
+ ```python
137
+ {
138
+ "ai_analysis": str, # AI-generated analysis text
139
+ "provider": str, # AI provider used
140
+ "model": str, # Model name
141
+ "usage": {
142
+ "input_tokens": int,
143
+ "output_tokens": int,
144
+ "total_tokens": int
145
+ }
146
+ }
147
+ ```
148
+
149
+ **Example:**
150
+ ```python
151
+ get_analysis = app["get_ai_analysis"]
152
+
153
+ analysis_data = {
154
+ "spending_insights": [...],
155
+ "financial_summary": {...},
156
+ "recommendations": [...]
157
+ }
158
+
159
+ # Use Claude for detailed analysis
160
+ claude_result = get_analysis.remote(
161
+ analysis_data,
162
+ "What are my biggest spending risks?",
163
+ "claude"
164
+ )
165
+
166
+ # Use SambaNova for quick insights
167
+ sambanova_result = get_analysis.remote(
168
+ analysis_data,
169
+ "Quick spending summary",
170
+ "sambanova"
171
+ )
172
+ ```
173
+
174
+ ### 4. `save_user_data` / `load_user_data`
175
+
176
+ Persistent storage for user analysis data.
177
+
178
+ **Save Function:**
179
+ ```python
180
+ def save_user_data(user_id: str, data: Dict) -> Dict
181
+ ```
182
+
183
+ **Load Function:**
184
+ ```python
185
+ def load_user_data(user_id: str) -> Dict
186
+ ```
187
+
188
+ **Example:**
189
+ ```python
190
+ save_data = app["save_user_data"]
191
+ load_data = app["load_user_data"]
192
+
193
+ # Save user analysis
194
+ save_result = save_data.remote("user123", analysis_data)
195
+
196
+ # Load user analysis
197
+ load_result = load_data.remote("user123")
198
+ if load_result["status"] == "found":
199
+ user_data = load_result["data"]
200
+ ```
201
+
202
+ ## MCP Protocol Integration
203
+
204
+ ### Webhook Endpoint
205
+
206
+ The system provides an MCP webhook endpoint for external integrations:
207
+
208
+ **URL:** `https://your-modal-app.modal.run/mcp_webhook`
209
+ **Method:** POST
210
+ **Content-Type:** application/json
211
+
212
+ ### MCP Tools
213
+
214
+ #### 1. `process_email_statements`
215
+
216
+ **Description:** Process bank statements from email
217
+ **Input Schema:**
218
+ ```json
219
+ {
220
+ "type": "object",
221
+ "properties": {
222
+ "email_config": {
223
+ "type": "object",
224
+ "properties": {
225
+ "email": {"type": "string"},
226
+ "password": {"type": "string"},
227
+ "imap_server": {"type": "string"}
228
+ }
229
+ },
230
+ "days_back": {"type": "integer", "default": 30},
231
+ "passwords": {"type": "object"}
232
+ }
233
+ }
234
+ ```
235
+
236
+ #### 2. `analyze_pdf_statements`
237
+
238
+ **Description:** Analyze uploaded PDF statements
239
+ **Input Schema:**
240
+ ```json
241
+ {
242
+ "type": "object",
243
+ "properties": {
244
+ "pdf_contents": {"type": "object"},
245
+ "passwords": {"type": "object"}
246
+ }
247
+ }
248
+ ```
249
+
250
+ #### 3. `get_ai_analysis`
251
+
252
+ **Description:** Get AI financial analysis
253
+ **Input Schema:**
254
+ ```json
255
+ {
256
+ "type": "object",
257
+ "properties": {
258
+ "analysis_data": {"type": "object"},
259
+ "user_question": {"type": "string"},
260
+ "provider": {"type": "string", "enum": ["claude", "sambanova"]}
261
+ }
262
+ }
263
+ ```
264
+
265
+ ### MCP Message Examples
266
+
267
+ **Initialize:**
268
+ ```json
269
+ {
270
+ "jsonrpc": "2.0",
271
+ "id": "1",
272
+ "method": "initialize",
273
+ "params": {}
274
+ }
275
+ ```
276
+
277
+ **List Tools:**
278
+ ```json
279
+ {
280
+ "jsonrpc": "2.0",
281
+ "id": "2",
282
+ "method": "tools/list"
283
+ }
284
+ ```
285
+
286
+ **Call Tool:**
287
+ ```json
288
+ {
289
+ "jsonrpc": "2.0",
290
+ "id": "3",
291
+ "method": "tools/call",
292
+ "params": {
293
+ "name": "get_ai_analysis",
294
+ "arguments": {
295
+ "analysis_data": {...},
296
+ "user_question": "How can I save money?",
297
+ "provider": "claude"
298
+ }
299
+ }
300
+ }
301
+ ```
302
+
303
+ ## Local Python API
304
+
305
+ ### SpendAnalyzer Class
306
+
307
+ ```python
308
+ from spend_analyzer import SpendAnalyzer
309
+
310
+ analyzer = SpendAnalyzer()
311
+
312
+ # Load transactions
313
+ analyzer.load_transactions(transactions_list)
314
+
315
+ # Set budgets
316
+ analyzer.set_budgets({
317
+ "Food & Dining": 500,
318
+ "Shopping": 300,
319
+ "Gas & Transport": 200
320
+ })
321
+
322
+ # Get insights
323
+ insights = analyzer.analyze_spending_by_category()
324
+ alerts = analyzer.check_budget_alerts()
325
+ summary = analyzer.generate_financial_summary()
326
+ recommendations = analyzer.get_spending_recommendations()
327
+
328
+ # Export all data
329
+ export_data = analyzer.export_analysis_data()
330
+ ```
331
+
332
+ ### EmailProcessor Class
333
+
334
+ ```python
335
+ from email_processor import EmailProcessor
336
+
337
+ email_config = {
338
+ "email": "[email protected]",
339
+ "password": "app_password",
340
+ "imap_server": "imap.gmail.com"
341
+ }
342
+
343
+ processor = EmailProcessor(email_config)
344
+
345
+ # Fetch emails
346
+ emails = await processor.fetch_bank_emails(days_back=30)
347
+
348
+ # Extract attachments
349
+ for email in emails:
350
+ attachments = await processor.extract_attachments(email)
351
+ for filename, content, file_type in attachments:
352
+ if file_type == 'pdf':
353
+ # Process PDF
354
+ pass
355
+ ```
356
+
357
+ ### PDFProcessor Class
358
+
359
+ ```python
360
+ from email_processor import PDFProcessor
361
+
362
+ processor = PDFProcessor()
363
+
364
+ # Process PDF
365
+ with open("statement.pdf", "rb") as f:
366
+ pdf_content = f.read()
367
+
368
+ statement_info = await processor.process_pdf(pdf_content, password="optional")
369
+
370
+ print(f"Bank: {statement_info.bank_name}")
371
+ print(f"Account: {statement_info.account_number}")
372
+ print(f"Transactions: {len(statement_info.transactions)}")
373
+ ```
374
+
375
+ ## Data Formats
376
+
377
+ ### Transaction Format
378
+
379
+ ```python
380
+ {
381
+ "date": "2024-01-15T00:00:00",
382
+ "description": "Amazon Purchase",
383
+ "amount": -45.67,
384
+ "category": "Shopping",
385
+ "account": "****1234",
386
+ "balance": 1500.33
387
+ }
388
+ ```
389
+
390
+ ### Financial Summary Format
391
+
392
+ ```python
393
+ {
394
+ "total_income": 3000.0,
395
+ "total_expenses": 1500.0,
396
+ "net_cash_flow": 1500.0,
397
+ "largest_expense": {
398
+ "amount": 200.0,
399
+ "description": "Grocery Store",
400
+ "date": "2024-01-15",
401
+ "category": "Food & Dining"
402
+ },
403
+ "most_frequent_category": "Food & Dining",
404
+ "unusual_transactions": [...],
405
+ "monthly_trends": {...}
406
+ }
407
+ ```
408
+
409
+ ### Spending Insight Format
410
+
411
+ ```python
412
+ {
413
+ "category": "Food & Dining",
414
+ "total_amount": 500.0,
415
+ "transaction_count": 15,
416
+ "average_transaction": 33.33,
417
+ "percentage_of_total": 33.3,
418
+ "trend": "increasing",
419
+ "top_merchants": ["Restaurant A", "Grocery Store", "Cafe B"]
420
+ }
421
+ ```
422
+
423
+ ### Budget Alert Format
424
+
425
+ ```python
426
+ {
427
+ "category": "Food & Dining",
428
+ "budget_limit": 500.0,
429
+ "current_spending": 450.0,
430
+ "percentage_used": 90.0,
431
+ "alert_level": "warning",
432
+ "days_remaining": 10
433
+ }
434
+ ```
435
+
436
+ ## Error Handling
437
+
438
+ ### Common Error Responses
439
+
440
+ **Authentication Error:**
441
+ ```python
442
+ {
443
+ "error": "Invalid API key or authentication failed",
444
+ "code": "AUTH_ERROR"
445
+ }
446
+ ```
447
+
448
+ **PDF Password Error:**
449
+ ```python
450
+ {
451
+ "error": "PDF requires password",
452
+ "code": "PASSWORD_REQUIRED",
453
+ "filename": "statement.pdf"
454
+ }
455
+ ```
456
+
457
+ **Processing Error:**
458
+ ```python
459
+ {
460
+ "error": "Failed to parse PDF content",
461
+ "code": "PARSE_ERROR",
462
+ "details": "Unsupported PDF format"
463
+ }
464
+ ```
465
+
466
+ **Rate Limit Error:**
467
+ ```python
468
+ {
469
+ "error": "API rate limit exceeded",
470
+ "code": "RATE_LIMIT",
471
+ "retry_after": 60
472
+ }
473
+ ```
474
+
475
+ ### Error Handling Best Practices
476
+
477
+ 1. **Always check for errors** in API responses
478
+ 2. **Implement retry logic** for transient failures
479
+ 3. **Handle password-protected PDFs** gracefully
480
+ 4. **Monitor API usage** to avoid rate limits
481
+ 5. **Log errors** for debugging
482
+
483
+ ## Examples
484
+
485
+ ### Complete Workflow Example
486
+
487
+ ```python
488
+ import modal
489
+ import asyncio
490
+
491
+ async def analyze_finances():
492
+ # Connect to Modal app
493
+ app = modal.App.lookup("spend-analyzer-mcp-bmt")
494
+
495
+ # Process email statements
496
+ email_config = {
497
+ "email": "[email protected]",
498
+ "password": "app_password",
499
+ "imap_server": "imap.gmail.com"
500
+ }
501
+
502
+ process_statements = app["process_bank_statements"]
503
+ email_result = process_statements.remote(email_config, days_back=30)
504
+
505
+ # Upload additional PDFs
506
+ pdf_contents = {}
507
+ with open("additional_statement.pdf", "rb") as f:
508
+ pdf_contents["additional.pdf"] = f.read()
509
+
510
+ analyze_pdfs = app["analyze_uploaded_statements"]
511
+ pdf_result = analyze_pdfs.remote(pdf_contents)
512
+
513
+ # Combine analysis data
514
+ combined_analysis = {
515
+ **email_result["analysis"],
516
+ "additional_transactions": pdf_result["total_transactions"]
517
+ }
518
+
519
+ # Get AI analysis
520
+ get_analysis = app["get_ai_analysis"]
521
+
522
+ # Use Claude for detailed analysis
523
+ claude_analysis = get_analysis.remote(
524
+ combined_analysis,
525
+ "Provide a comprehensive financial health assessment",
526
+ "claude"
527
+ )
528
+
529
+ # Use SambaNova for quick insights
530
+ sambanova_analysis = get_analysis.remote(
531
+ combined_analysis,
532
+ "What are my top 3 spending categories?",
533
+ "sambanova"
534
+ )
535
+
536
+ print("Claude Analysis:", claude_analysis["ai_analysis"])
537
+ print("SambaNova Analysis:", sambanova_analysis["ai_analysis"])
538
+
539
+ # Run the analysis
540
+ asyncio.run(analyze_finances())
541
+ ```
542
+
543
+ ### Integration with External Systems
544
+
545
+ ```python
546
+ import requests
547
+ import json
548
+
549
+ def call_mcp_webhook(data):
550
+ """Call the MCP webhook endpoint"""
551
+ webhook_url = "https://your-modal-app.modal.run/mcp_webhook"
552
+
553
+ mcp_message = {
554
+ "jsonrpc": "2.0",
555
+ "id": "1",
556
+ "method": "tools/call",
557
+ "params": {
558
+ "name": "get_ai_analysis",
559
+ "arguments": data
560
+ }
561
+ }
562
+
563
+ response = requests.post(
564
+ webhook_url,
565
+ json=mcp_message,
566
+ headers={"Content-Type": "application/json"}
567
+ )
568
+
569
+ return response.json()
570
+
571
+ # Use the webhook
572
+ analysis_data = {"spending_insights": [...]}
573
+ result = call_mcp_webhook(analysis_data)
574
+ ```
575
+
576
+ ## Rate Limits and Quotas
577
+
578
+ ### Claude API
579
+ - **Rate Limit:** 1000 requests/minute
580
+ - **Token Limit:** 100K tokens/minute
581
+ - **Best Practice:** Use for complex analysis
582
+
583
+ ### SambaNova API
584
+ - **Rate Limit:** 5000 requests/minute
585
+ - **Token Limit:** 500K tokens/minute
586
+ - **Best Practice:** Use for quick insights and batch processing
587
+
588
+ ### Modal Functions
589
+ - **Concurrent Executions:** Auto-scaled
590
+ - **Timeout:** Configurable per function
591
+ - **Memory:** 2GB default for PDF processing
592
+
593
+ ## Support and Troubleshooting
594
+
595
+ ### Common Issues
596
+
597
+ 1. **PDF Processing Fails**
598
+ - Check PDF format compatibility
599
+ - Verify password if protected
600
+ - Ensure sufficient memory allocation
601
+
602
+ 2. **Email Connection Issues**
603
+ - Use app-specific passwords
604
+ - Verify IMAP server settings
605
+ - Check firewall/network restrictions
606
+
607
+ 3. **AI API Errors**
608
+ - Verify API keys are valid
609
+ - Check rate limits
610
+ - Monitor token usage
611
+
612
+ ### Getting Help
613
+
614
+ 1. Check the logs: `modal logs spend-analyzer-mcp-bmt`
615
+ 2. Review error messages and codes
616
+ 3. Consult the deployment guide
617
+ 4. Open an issue with detailed error information
618
+
619
+ For more detailed information, see the [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) file.
DEPLOYMENT_GUIDE.md ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Spend Analyzer MCP - Deployment Guide
2
+
3
+ This guide covers deploying the Spend Analyzer MCP to Modal.com with Claude and SambaNova Cloud API integration.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. **Modal Account**: Sign up at [modal.com](https://modal.com)
8
+ 2. **API Keys**:
9
+ - Anthropic API key for Claude
10
+ - SambaNova Cloud API key
11
+ 3. **Email Credentials**: App-specific passwords for email access
12
+
13
+ ## Setup Instructions
14
+
15
+ ### 1. Install Modal CLI
16
+
17
+ ```bash
18
+ pip install modal
19
+ ```
20
+
21
+ ### 2. Authenticate with Modal
22
+
23
+ ```bash
24
+ modal token new
25
+ ```
26
+
27
+ ### 3. Create Modal Secrets
28
+
29
+ Create the required secrets in your Modal dashboard or via CLI:
30
+
31
+ #### Anthropic API Key
32
+ ```bash
33
+ modal secret create anthropic-api-key ANTHROPIC_API_KEY=your_claude_api_key_here
34
+ ```
35
+
36
+ #### SambaNova API Key
37
+ ```bash
38
+ modal secret create sambanova-api-key SAMBANOVA_API_KEY=your_sambanova_api_key_here
39
+ ```
40
+
41
+ #### Email Credentials
42
+ ```bash
43
+ modal secret create email-credentials \
44
45
+ EMAIL_PASS=your_app_password \
46
+ IMAP_SERVER=imap.gmail.com
47
+ ```
48
+
49
+ ### 4. Deploy to Modal
50
+
51
+ ```bash
52
+ # Deploy the application
53
+ modal deploy modal_deployment.py
54
+
55
+ # Or run locally for testing
56
+ modal run modal_deployment.py
57
+ ```
58
+
59
+ ## API Providers
60
+
61
+ ### Claude (Anthropic)
62
+
63
+ - **Model**: claude-3-sonnet-20240229
64
+ - **Features**: Advanced reasoning, financial analysis
65
+ - **Setup**: Get API key from [console.anthropic.com](https://console.anthropic.com)
66
+
67
+ ### SambaNova Cloud
68
+
69
+ - **Model**: Meta-Llama-3.1-8B-Instruct
70
+ - **Features**: Fast inference, cost-effective
71
+ - **Setup**: Get API key from [cloud.sambanova.ai](https://cloud.sambanova.ai)
72
+ - **API Format**: OpenAI-compatible
73
+
74
+ ## Available Modal Functions
75
+
76
+ ### 1. `process_bank_statements`
77
+ Process bank statements from email attachments.
78
+
79
+ **Parameters:**
80
+ - `email_config`: Email configuration dict
81
+ - `days_back`: Number of days to look back (default: 30)
82
+ - `passwords`: Optional PDF passwords dict
83
+
84
+ **Returns:**
85
+ - Processed statements list
86
+ - Transaction analysis
87
+ - Error handling for password-protected PDFs
88
+
89
+ ### 2. `analyze_uploaded_statements`
90
+ Analyze directly uploaded PDF statements.
91
+
92
+ **Parameters:**
93
+ - `pdf_contents`: Dict of filename -> PDF bytes
94
+ - `passwords`: Optional PDF passwords dict
95
+
96
+ **Returns:**
97
+ - Analysis results
98
+ - Transaction categorization
99
+ - Financial insights
100
+
101
+ ### 3. `get_ai_analysis`
102
+ Get AI-powered financial analysis.
103
+
104
+ **Parameters:**
105
+ - `analysis_data`: Financial data dict
106
+ - `user_question`: Optional specific question
107
+ - `provider`: "claude" or "sambanova" (default: "claude")
108
+
109
+ **Returns:**
110
+ - AI analysis text
111
+ - Usage statistics
112
+ - Provider information
113
+
114
+ ### 4. `save_user_data` / `load_user_data`
115
+ Persistent storage for user analysis data.
116
+
117
+ **Features:**
118
+ - User-specific data isolation
119
+ - Timestamp tracking
120
+ - JSON serialization
121
+
122
+ ### 5. `mcp_webhook`
123
+ MCP protocol endpoint for external integrations.
124
+
125
+ **Features:**
126
+ - Tool registration
127
+ - Resource management
128
+ - Error handling
129
+
130
+ ## Environment Variables
131
+
132
+ The following environment variables are automatically available in Modal functions:
133
+
134
+ ```bash
135
+ ANTHROPIC_API_KEY=your_claude_key
136
+ SAMBANOVA_API_KEY=your_sambanova_key
137
+ EMAIL_USER=your_email
138
+ EMAIL_PASS=your_app_password
139
+ IMAP_SERVER=your_imap_server
140
+ ```
141
+
142
+ ## Usage Examples
143
+
144
+ ### Basic Deployment Test
145
+
146
+ ```python
147
+ import modal
148
+
149
+ # Test the deployment
150
+ app = modal.App.lookup("spend-analyzer-mcp-bmt")
151
+ get_ai_analysis = app["get_ai_analysis"]
152
+
153
+ # Test with sample data
154
+ test_data = {
155
+ "spending_insights": [
156
+ {
157
+ "category": "Food & Dining",
158
+ "total_amount": 500.0,
159
+ "transaction_count": 15
160
+ }
161
+ ],
162
+ "recommendations": ["Consider reducing dining expenses"]
163
+ }
164
+
165
+ result = get_ai_analysis.remote(
166
+ analysis_data=test_data,
167
+ user_question="How can I save money on food?",
168
+ provider="claude"
169
+ )
170
+
171
+ print(result)
172
+ ```
173
+
174
+ ### Email Processing
175
+
176
+ ```python
177
+ email_config = {
178
+ "email": "[email protected]",
179
+ "password": "your_app_password",
180
+ "imap_server": "imap.gmail.com"
181
+ }
182
+
183
+ result = process_bank_statements.remote(
184
+ email_config=email_config,
185
+ days_back=30
186
+ )
187
+
188
+ print(f"Processed {result['total_transactions']} transactions")
189
+ ```
190
+
191
+ ### PDF Analysis
192
+
193
+ ```python
194
+ # Read PDF file
195
+ with open("statement.pdf", "rb") as f:
196
+ pdf_content = f.read()
197
+
198
+ pdf_contents = {"statement.pdf": pdf_content}
199
+
200
+ result = analyze_uploaded_statements.remote(
201
+ pdf_contents=pdf_contents,
202
+ passwords={"statement.pdf": "optional_password"}
203
+ )
204
+
205
+ print(result['analysis'])
206
+ ```
207
+
208
+ ## Monitoring and Logs
209
+
210
+ ### View Logs
211
+ ```bash
212
+ modal logs spend-analyzer-mcp-bmt
213
+ ```
214
+
215
+ ### Monitor Functions
216
+ ```bash
217
+ modal stats spend-analyzer-mcp-bmt
218
+ ```
219
+
220
+ ### View Volumes
221
+ ```bash
222
+ modal volume list
223
+ ```
224
+
225
+ ## Troubleshooting
226
+
227
+ ### Common Issues
228
+
229
+ 1. **Import Errors**: Ensure all dependencies are in the Modal image
230
+ 2. **Secret Access**: Verify secrets are created with correct names
231
+ 3. **PDF Processing**: Check file permissions and password requirements
232
+ 4. **API Limits**: Monitor usage for both Claude and SambaNova
233
+
234
+ ### Debug Mode
235
+
236
+ Enable debug logging in Modal functions:
237
+
238
+ ```python
239
+ import logging
240
+ logging.basicConfig(level=logging.DEBUG)
241
+ ```
242
+
243
+ ### Local Testing
244
+
245
+ Test functions locally before deployment:
246
+
247
+ ```bash
248
+ modal run modal_deployment.py::main
249
+ ```
250
+
251
+ ## Security Considerations
252
+
253
+ 1. **API Keys**: Store in Modal secrets, never in code
254
+ 2. **Email Passwords**: Use app-specific passwords
255
+ 3. **PDF Data**: Processed in memory, not stored permanently
256
+ 4. **User Data**: Isolated by user ID in persistent storage
257
+
258
+ ## Cost Optimization
259
+
260
+ 1. **Function Timeouts**: Set appropriate timeouts for each function
261
+ 2. **Memory Allocation**: Adjust based on PDF processing needs
262
+ 3. **API Provider**: Choose between Claude (quality) and SambaNova (cost)
263
+ 4. **Batch Processing**: Process multiple PDFs in single function call
264
+
265
+ ## Scaling
266
+
267
+ Modal automatically handles scaling based on demand:
268
+
269
+ - **Cold Starts**: ~2-3 seconds for new containers
270
+ - **Warm Containers**: Sub-second response times
271
+ - **Concurrent Requests**: Automatically scaled
272
+ - **Resource Limits**: Configurable per function
273
+
274
+ ## Integration with MCP
275
+
276
+ The deployment includes a webhook endpoint for MCP integration:
277
+
278
+ ```
279
+ POST https://your-modal-app.modal.run/mcp_webhook
280
+ ```
281
+
282
+ This enables integration with Claude Desktop and other MCP clients.
283
+
284
+ ## Support
285
+
286
+ For deployment issues:
287
+
288
+ 1. Check Modal logs and documentation
289
+ 2. Verify API key permissions
290
+ 3. Test with minimal examples
291
+ 4. Contact Modal support for platform issues
292
+
293
+ ## Next Steps
294
+
295
+ After successful deployment:
296
+
297
+ 1. Test all functions with real data
298
+ 2. Set up monitoring and alerts
299
+ 3. Configure backup strategies
300
+ 4. Implement additional security measures
301
+ 5. Scale based on usage patterns
README.md CHANGED
@@ -19,22 +19,24 @@ A comprehensive financial analysis tool that processes bank statements from emai
19
 
20
  ## Features
21
 
22
- - **📧 Email Processing**: Automatically fetch and process bank statements from your email
23
  - **📄 PDF Upload**: Direct upload and analysis of bank statement PDFs
24
  - **📊 Analysis Dashboard**: Interactive charts and financial summaries
25
- - **🤖 AI Financial Advisor**: Chat with Claude for personalized financial advice
26
  - **⚙️ Settings & Configuration**: Customize budgets, email settings, and export options
27
  - **🔐 Security**: Password-protected PDF support and secure email connections
 
 
28
 
29
  ## Architecture
30
 
31
  The project consists of several key components:
32
 
33
- 1. **`gradio_interface.py`** - Main web interface built with Gradio
34
- 2. **`spend_analyzer.py`** - Core financial analysis engine
35
- 3. **`email_processor.py`** - Email and PDF processing functionality
36
- 4. **`modal_deployment.py`** - Modal.com cloud deployment configuration
37
  5. **`mcp_server.py`** - Model Context Protocol server implementation
 
38
 
39
  ## Installation
40
 
@@ -47,14 +49,33 @@ pip install -r requirements.txt
47
  ```bash
48
  # Create .env file
49
  ANTHROPIC_API_KEY=your_claude_api_key
 
50
51
  EMAIL_PASS=your_app_password
52
  IMAP_SERVER=imap.gmail.com
53
  ```
54
 
55
- 3. For Modal deployment (optional):
 
 
 
 
 
 
 
56
  ```bash
 
 
 
 
57
  modal token new
 
 
 
 
 
 
 
58
  modal deploy modal_deployment.py
59
  ```
60
 
@@ -62,12 +83,18 @@ modal deploy modal_deployment.py
62
 
63
  ### Local Development
64
 
65
- Run the Gradio interface locally:
66
  ```bash
67
- python gradio_interface.py
68
  ```
69
 
70
- The interface will be available at `http://localhost:7860`
 
 
 
 
 
 
71
 
72
  ### Features Overview
73
 
@@ -101,6 +128,31 @@ The interface will be available at `http://localhost:7860`
101
  - **Email Settings**: Configure email providers and auto-processing
102
  - **Export Settings**: Choose data export formats (JSON, CSV, Excel)
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  ## MCP Integration
105
 
106
  This project implements the Model Context Protocol (MCP) for integration with Claude and other AI systems:
@@ -108,6 +160,7 @@ This project implements the Model Context Protocol (MCP) for integration with Cl
108
  - **Tools**: Process statements, analyze PDFs, get AI insights
109
  - **Resources**: Access financial data and analysis results
110
  - **Server**: Full MCP server implementation for external integrations
 
111
 
112
  ## Security Considerations
113
 
@@ -120,12 +173,13 @@ This project implements the Model Context Protocol (MCP) for integration with Cl
120
 
121
  ### Project Structure
122
  ```
123
- spend-analyzer-mcp/
124
- ├── gradio_interface.py # Main web interface
125
  ├── spend_analyzer.py # Financial analysis engine
126
- ├── email_processor.py # Email/PDF processing
127
  ├── modal_deployment.py # Cloud deployment
128
  ├── mcp_server.py # MCP protocol server
 
129
  ├── requirements.txt # Dependencies
130
  └── README.md # This file
131
  ```
@@ -164,8 +218,8 @@ FROM python:3.11-slim
164
  COPY . /app
165
  WORKDIR /app
166
  RUN pip install -r requirements.txt
167
- EXPOSE 7860
168
- CMD ["python", "gradio_interface.py"]
169
  ```
170
 
171
  ## Contributing
@@ -189,10 +243,13 @@ For issues and questions:
189
 
190
  ## Roadmap
191
 
 
192
  - [ ] Support for more bank formats
193
  - [ ] Real-time transaction monitoring
194
  - [ ] Mobile app interface
195
  - [ ] Advanced ML-based categorization
196
  - [ ] Integration with financial planning tools
197
- - [ ] Multi-currency support
198
  - [ ] Automated bill tracking
 
 
 
19
 
20
  ## Features
21
 
 
22
  - **📄 PDF Upload**: Direct upload and analysis of bank statement PDFs
23
  - **📊 Analysis Dashboard**: Interactive charts and financial summaries
24
+ - **🤖 AI Financial Advisor**: Chat with Claude or SambaNova for personalized financial advice
25
  - **⚙️ Settings & Configuration**: Customize budgets, email settings, and export options
26
  - **🔐 Security**: Password-protected PDF support and secure email connections
27
+ - **☁️ Cloud Deployment**: Ready-to-deploy Modal.com configuration
28
+ - **🔌 MCP Integration**: Full Model Context Protocol support for external AI systems
29
 
30
  ## Architecture
31
 
32
  The project consists of several key components:
33
 
34
+ 1. **`app.py`** - Main web interface built with Gradio (primary application entry point)
35
+ 2. **`spend_analyzer.py`** - Core financial analysis engine with ML-based insights
36
+ 3. **`email_processor.py`** - Email and PDF processing with multi-bank support and currency detection
37
+ 4. **`modal_deployment.py`** - Enhanced Modal.com cloud deployment with dual AI providers
38
  5. **`mcp_server.py`** - Model Context Protocol server implementation
39
+ 6. **`DEPLOYMENT_GUIDE.md`** - Comprehensive deployment and setup guide
40
 
41
  ## Installation
42
 
 
49
  ```bash
50
  # Create .env file
51
  ANTHROPIC_API_KEY=your_claude_api_key
52
+ SAMBANOVA_API_KEY=your_sambanova_api_key # Optional
53
54
  EMAIL_PASS=your_app_password
55
  IMAP_SERVER=imap.gmail.com
56
  ```
57
 
58
+ 3. For Modal deployment (recommended for production):
59
+
60
+ **Quick Setup (Recommended):**
61
+ ```bash
62
+ python setup_modal.py
63
+ ```
64
+
65
+ **Manual Setup:**
66
  ```bash
67
+ # Install Modal CLI
68
+ pip install modal
69
+
70
+ # Authenticate
71
  modal token new
72
+
73
+ # Create secrets (see DEPLOYMENT_GUIDE.md for details)
74
+ modal secret create anthropic-api-key ANTHROPIC_API_KEY=your_key
75
+ modal secret create sambanova-api-key SAMBANOVA_API_KEY=your_key
76
+ modal secret create email-credentials EMAIL_USER=your_email EMAIL_PASS=your_password
77
+
78
+ # Deploy
79
  modal deploy modal_deployment.py
80
  ```
81
 
 
83
 
84
  ### Local Development
85
 
86
+ Run the main Gradio interface locally:
87
  ```bash
88
+ python app.py
89
  ```
90
 
91
+ The interface will be available at `http://localhost:7862`
92
+
93
+ **Key Features:**
94
+ - **Dynamic Currency Detection**: Automatically detects currency (USD, INR, EUR, GBP, etc.) from PDF content
95
+ - **Multi-Bank Support**: Supports HDFC, ICICI, SBI, Axis, Chase, Bank of America, and more
96
+ - **Real-time Processing**: Upload and analyze actual bank statement PDFs
97
+ - **Interactive Dashboard**: View spending insights with detected currency formatting
98
 
99
  ### Features Overview
100
 
 
128
  - **Email Settings**: Configure email providers and auto-processing
129
  - **Export Settings**: Choose data export formats (JSON, CSV, Excel)
130
 
131
+ ## AI Providers
132
+
133
+ The system supports multiple AI providers for financial analysis:
134
+
135
+ ### Claude (Anthropic)
136
+ - **Model**: claude-3-sonnet-20240229
137
+ - **Strengths**: Advanced reasoning, nuanced financial advice, complex analysis
138
+ - **Best for**: Detailed financial planning, complex queries, high-quality insights
139
+ - **Cost**: Higher per token, excellent quality
140
+
141
+ ### SambaNova Cloud
142
+ - **Model**: Meta-Llama-3.1-8B-Instruct
143
+ - **Strengths**: Fast inference, cost-effective, good general analysis
144
+ - **Best for**: Quick insights, batch processing, cost-sensitive deployments
145
+ - **Cost**: Lower per token, good performance
146
+
147
+ ### Usage
148
+ ```python
149
+ # Use Claude for detailed analysis
150
+ result = get_ai_analysis(data, "Detailed budget analysis", provider="claude")
151
+
152
+ # Use SambaNova for quick insights
153
+ result = get_ai_analysis(data, "Quick spending summary", provider="sambanova")
154
+ ```
155
+
156
  ## MCP Integration
157
 
158
  This project implements the Model Context Protocol (MCP) for integration with Claude and other AI systems:
 
160
  - **Tools**: Process statements, analyze PDFs, get AI insights
161
  - **Resources**: Access financial data and analysis results
162
  - **Server**: Full MCP server implementation for external integrations
163
+ - **Webhook**: RESTful endpoint for external MCP clients
164
 
165
  ## Security Considerations
166
 
 
173
 
174
  ### Project Structure
175
  ```
176
+ spend-analyzer-mcp-bmt/
177
+ ├── app.py # Main web interface (primary entry point)
178
  ├── spend_analyzer.py # Financial analysis engine
179
+ ├── email_processor.py # Email/PDF processing with currency detection
180
  ├── modal_deployment.py # Cloud deployment
181
  ├── mcp_server.py # MCP protocol server
182
+ ├── gradio_interface.py # Alternative interface
183
  ├── requirements.txt # Dependencies
184
  └── README.md # This file
185
  ```
 
218
  COPY . /app
219
  WORKDIR /app
220
  RUN pip install -r requirements.txt
221
+ EXPOSE 7862
222
+ CMD ["python", "app.py"]
223
  ```
224
 
225
  ## Contributing
 
243
 
244
  ## Roadmap
245
 
246
+ - [ ] **📧 Email Processing**: Automatically fetch and process bank statements from your email (Gmail, YMail, Outlook)
247
  - [ ] Support for more bank formats
248
  - [ ] Real-time transaction monitoring
249
  - [ ] Mobile app interface
250
  - [ ] Advanced ML-based categorization
251
  - [ ] Integration with financial planning tools
252
+ - [x] Multi-currency support (USD, INR, EUR, GBP, CAD, AUD, JPY, CNY)
253
  - [ ] Automated bill tracking
254
+ - [ ] Enhanced currency conversion features
255
+ - [ ] Real-time exchange rate integration
gradio_interface_real.py → app.py RENAMED
@@ -8,26 +8,53 @@ import plotly.graph_objects as go
8
  import json
9
  import os
10
  import asyncio
 
11
  from typing import Dict, List, Optional, Tuple
12
  from datetime import datetime, timedelta
13
  import logging
14
  import time
15
  import tempfile
 
16
 
17
  # Import our local modules
18
  from email_processor import PDFProcessor
19
  from spend_analyzer import SpendAnalyzer
 
 
20
 
21
  class RealSpendAnalyzerInterface:
22
  def __init__(self):
23
  self.current_analysis = None
24
  self.user_sessions = {}
 
 
25
  self.logger = logging.getLogger(__name__)
26
  logging.basicConfig(level=logging.INFO)
27
 
28
  # Initialize processors
29
  self.pdf_processor = PDFProcessor()
30
  self.spend_analyzer = SpendAnalyzer()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  def create_interface(self):
33
  """Create the main Gradio interface"""
@@ -45,6 +72,7 @@ class RealSpendAnalyzerInterface:
45
  gr.Markdown("# 💰 Spend Analyzer MCP - Real PDF Processing", elem_classes=["main-header"])
46
  gr.Markdown("*Analyze your real bank statement PDFs with AI-powered insights*")
47
 
 
48
  # Info notice
49
  gr.HTML('<div class="info-box">📄 <strong>Real PDF Processing:</strong> Upload your actual bank statement PDFs for comprehensive financial analysis.</div>')
50
 
@@ -68,9 +96,55 @@ class RealSpendAnalyzerInterface:
68
  # Tab 5: Settings & Export
69
  with gr.TabItem("⚙️ Settings & Export"):
70
  self._create_settings_tab()
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  return interface
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  def _create_pdf_processing_tab(self):
75
  """Create PDF processing tab"""
76
  gr.Markdown("## 📄 Upload & Process Bank Statement PDFs")
@@ -204,14 +278,33 @@ class RealSpendAnalyzerInterface:
204
  def _create_chat_tab(self):
205
  """Create AI chat tab"""
206
  gr.Markdown("## 🤖 AI Financial Advisor")
207
- gr.Markdown("*Get personalized insights about your spending patterns*")
208
 
209
  with gr.Row():
210
  with gr.Column(scale=3):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  # Chat interface
212
  chatbot = gr.Chatbot(
213
  label="Financial Advisor Chat",
214
- height=500,
215
  show_label=True
216
  )
217
 
@@ -240,6 +333,12 @@ class RealSpendAnalyzerInterface:
240
  with gr.Column(scale=1):
241
  chat_status = gr.HTML()
242
 
 
 
 
 
 
 
243
  # Analysis context
244
  gr.Markdown("### 📊 Analysis Context")
245
  context_info = gr.JSON(
@@ -258,16 +357,33 @@ class RealSpendAnalyzerInterface:
258
  # Event handlers
259
  send_btn.click(
260
  fn=self._handle_chat_message,
261
- inputs=[msg_input, chatbot, response_style],
262
  outputs=[chatbot, msg_input, chat_status]
263
  )
264
 
265
  msg_input.submit(
266
  fn=self._handle_chat_message,
267
- inputs=[msg_input, chatbot, response_style],
268
  outputs=[chatbot, msg_input, chat_status]
269
  )
270
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  # Quick question handlers
272
  budget_btn.click(lambda: "How am I doing with my budget this month?", outputs=[msg_input])
273
  trends_btn.click(lambda: "What are my spending trends over the last few months?", outputs=[msg_input])
@@ -361,6 +477,153 @@ class RealSpendAnalyzerInterface:
361
  gr.Markdown("## ⚙️ Settings & Export")
362
 
363
  with gr.Tabs():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  with gr.TabItem("Budget Settings"):
365
  gr.Markdown("### 💰 Monthly Budget Configuration")
366
 
@@ -460,6 +723,53 @@ class RealSpendAnalyzerInterface:
460
  outputs=[processing_status]
461
  )
462
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  # Implementation methods
464
  def _process_real_pdfs(self, files, passwords_json, auto_categorize, detect_duplicates):
465
  """Process real PDF files"""
@@ -500,6 +810,32 @@ class RealSpendAnalyzerInterface:
500
  self.pdf_processor.process_pdf(pdf_content, file_password)
501
  )
502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  # Add transactions
504
  all_transactions.extend(statement_info.transactions)
505
 
@@ -540,11 +876,12 @@ class RealSpendAnalyzerInterface:
540
 
541
  quick_stats_html = f'''
542
  <div class="status-box info-box">
543
- <h4> Quick Statistics</h4>
544
  <ul>
545
- <li><strong>Total Income:</strong> ${total_income:,.2f}</li>
546
- <li><strong>Total Expenses:</strong> ${total_expenses:,.2f}</li>
547
- <li><strong>Net Cash Flow:</strong> ${total_income - total_expenses:,.2f}</li>
 
548
  <li><strong>Transaction Count:</strong> {len(all_transactions)}</li>
549
  </ul>
550
  </div>
@@ -710,27 +1047,6 @@ class RealSpendAnalyzerInterface:
710
  # For now, return empty dataframe
711
  return pd.DataFrame(columns=["Date", "Description", "Amount", "Category", "Account"])
712
 
713
- def _handle_chat_message(self, message, chat_history, response_style):
714
- """Handle chat messages"""
715
- if not message.strip():
716
- return chat_history, "", '<div class="status-box warning-box"> Please enter a message</div>'
717
-
718
- # Simple response generation based on analysis
719
- if self.current_analysis:
720
- summary = self.current_analysis.get('financial_summary', {})
721
-
722
- response = f"Based on your financial data: Total income ${summary.get('total_income', 0):.2f}, Total expenses ${summary.get('total_expenses', 0):.2f}. Your question: '{message}' - This is a simplified response. Full AI integration would provide detailed insights here."
723
-
724
- status_html = '<div class="status-box success-box"> Response generated</div>'
725
- else:
726
- response = "Please upload and process your PDF statements first to get personalized financial insights."
727
- status_html = '<div class="status-box warning-box"> No data available</div>'
728
-
729
- # Add to chat history
730
- chat_history = chat_history or []
731
- chat_history.append([message, response])
732
-
733
- return chat_history, "", status_html
734
 
735
  def _filter_transactions(self, date_from, date_to, category_filter, amount_filter):
736
  """Filter transactions based on criteria"""
@@ -822,6 +1138,816 @@ class RealSpendAnalyzerInterface:
822
  return ('<div class="status-box success-box"> All data cleared</div>',
823
  '<div class="status-box info-box"> Ready for new PDF upload</div>')
824
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
825
  # Launch the interface
826
  def launch_interface():
827
  """Launch the Gradio interface"""
 
8
  import json
9
  import os
10
  import asyncio
11
+ import requests
12
  from typing import Dict, List, Optional, Tuple
13
  from datetime import datetime, timedelta
14
  import logging
15
  import time
16
  import tempfile
17
+ import threading
18
 
19
  # Import our local modules
20
  from email_processor import PDFProcessor
21
  from spend_analyzer import SpendAnalyzer
22
+ from secure_storage_utils import SecureStorageManager
23
+ from mcp_server import create_mcp_app, run_mcp_server
24
 
25
  class RealSpendAnalyzerInterface:
26
  def __init__(self):
27
  self.current_analysis = None
28
  self.user_sessions = {}
29
+ self.detected_currency = "$" # Default currency
30
+ self.currency_symbol = "$" # Current currency symbol
31
  self.logger = logging.getLogger(__name__)
32
  logging.basicConfig(level=logging.INFO)
33
 
34
  # Initialize processors
35
  self.pdf_processor = PDFProcessor()
36
  self.spend_analyzer = SpendAnalyzer()
37
+ self.secure_storage = SecureStorageManager()
38
+
39
+ # MCP server state
40
+ self.mcp_server_thread = None
41
+ self.mcp_server_running = False
42
+ self.mcp_server_logs = []
43
+
44
+ # Load API keys from environment or config file on startup
45
+ self._load_initial_api_settings()
46
+
47
+ # Currency detection patterns
48
+ self.currency_patterns = {
49
+ 'USD': {'symbols': ['$', 'USD', 'US$'], 'regex': r'\$|USD|US\$'},
50
+ 'INR': {'symbols': ['₹', 'Rs', 'Rs.', 'INR'], 'regex': r'₹|Rs\.?|INR'},
51
+ 'EUR': {'symbols': ['€', 'EUR'], 'regex': r'€|EUR'},
52
+ 'GBP': {'symbols': ['£', 'GBP'], 'regex': r'£|GBP'},
53
+ 'CAD': {'symbols': ['C$', 'CAD'], 'regex': r'C\$|CAD'},
54
+ 'AUD': {'symbols': ['A$', 'AUD'], 'regex': r'A\$|AUD'},
55
+ 'JPY': {'symbols': ['¥', 'JPY'], 'regex': r'¥|JPY'},
56
+ 'CNY': {'symbols': ['¥', 'CNY', 'RMB'], 'regex': r'CNY|RMB'},
57
+ }
58
 
59
  def create_interface(self):
60
  """Create the main Gradio interface"""
 
72
  gr.Markdown("# 💰 Spend Analyzer MCP - Real PDF Processing", elem_classes=["main-header"])
73
  gr.Markdown("*Analyze your real bank statement PDFs with AI-powered insights*")
74
 
75
+
76
  # Info notice
77
  gr.HTML('<div class="info-box">📄 <strong>Real PDF Processing:</strong> Upload your actual bank statement PDFs for comprehensive financial analysis.</div>')
78
 
 
96
  # Tab 5: Settings & Export
97
  with gr.TabItem("⚙️ Settings & Export"):
98
  self._create_settings_tab()
99
+
100
+ # Tab 6: MCP Server
101
+ with gr.TabItem("🔌 MCP Server"):
102
+ self._create_mcp_tab()
103
+
104
+ # AI Analysis Disclaimer
105
+ gr.HTML('''
106
+ <div class="warning-box" style="margin-top: 20px; text-align: center;">
107
+ ⚠️ <strong>Important Notice:</strong> AI analysis results are generated automatically and may contain errors.
108
+ Please verify all financial insights and recommendations for accuracy before making any financial decisions.
109
+ </div>
110
+ ''')
111
 
112
  return interface
113
 
114
+ def detect_currency_from_text(self, text: str) -> Tuple[str, str]:
115
+ """Detect currency from PDF text content"""
116
+ import re
117
+
118
+ text_lower = text.lower()
119
+
120
+ # Check for currency patterns in order of specificity
121
+ for currency_code, currency_info in self.currency_patterns.items():
122
+ pattern = currency_info['regex']
123
+ if re.search(pattern, text, re.IGNORECASE):
124
+ # Return currency code and primary symbol
125
+ return currency_code, currency_info['symbols'][0]
126
+
127
+ # Default fallback based on bank detection
128
+ if any(bank in text_lower for bank in ['hdfc', 'icici', 'sbi', 'axis', 'kotak']):
129
+ return 'INR', '₹'
130
+ elif any(bank in text_lower for bank in ['chase', 'bofa', 'wells', 'citi']):
131
+ return 'USD', '$'
132
+ elif any(bank in text_lower for bank in ['hsbc', 'barclays', 'lloyds']):
133
+ return 'GBP', '£'
134
+
135
+ # Default to USD
136
+ return 'USD', '$'
137
+
138
+ def update_currency_in_interface(self, currency_code: str, currency_symbol: str):
139
+ """Update currency throughout the interface"""
140
+ self.detected_currency = currency_code
141
+ self.currency_symbol = currency_symbol
142
+ self.logger.info(f"Currency detected: {currency_code} ({currency_symbol})")
143
+
144
+ def format_amount(self, amount: float) -> str:
145
+ """Format amount with detected currency"""
146
+ return f"{self.currency_symbol}{amount:,.2f}"
147
+
148
  def _create_pdf_processing_tab(self):
149
  """Create PDF processing tab"""
150
  gr.Markdown("## 📄 Upload & Process Bank Statement PDFs")
 
278
  def _create_chat_tab(self):
279
  """Create AI chat tab"""
280
  gr.Markdown("## 🤖 AI Financial Advisor")
281
+ gr.Markdown("*Get personalized insights about your spending patterns using configured AI*")
282
 
283
  with gr.Row():
284
  with gr.Column(scale=3):
285
+ # AI Provider Selection
286
+ gr.Markdown("### 🤖 Select AI Provider")
287
+ with gr.Row():
288
+ ai_provider_selector = gr.Dropdown(
289
+ choices=["No AI Configured"],
290
+ label="Available AI Providers",
291
+ value="No AI Configured",
292
+ scale=3
293
+ )
294
+ refresh_ai_btn = gr.Button("🔄 Refresh", size="sm", scale=1)
295
+ fetch_models_btn = gr.Button("📥 Fetch Models", size="sm", scale=1, visible=False)
296
+
297
+ # Model selection for LM Studio
298
+ lm_studio_models = gr.Dropdown(
299
+ choices=[],
300
+ label="Available LM Studio Models",
301
+ visible=False
302
+ )
303
+
304
  # Chat interface
305
  chatbot = gr.Chatbot(
306
  label="Financial Advisor Chat",
307
+ height=400,
308
  show_label=True
309
  )
310
 
 
333
  with gr.Column(scale=1):
334
  chat_status = gr.HTML()
335
 
336
+ # AI Status
337
+ gr.Markdown("### 🤖 AI Status")
338
+ ai_status_display = gr.HTML(
339
+ value='<div class="warning-box">⚠️ No AI configured. Please configure AI in Settings.</div>'
340
+ )
341
+
342
  # Analysis context
343
  gr.Markdown("### 📊 Analysis Context")
344
  context_info = gr.JSON(
 
357
  # Event handlers
358
  send_btn.click(
359
  fn=self._handle_chat_message,
360
+ inputs=[msg_input, chatbot, response_style, ai_provider_selector],
361
  outputs=[chatbot, msg_input, chat_status]
362
  )
363
 
364
  msg_input.submit(
365
  fn=self._handle_chat_message,
366
+ inputs=[msg_input, chatbot, response_style, ai_provider_selector],
367
  outputs=[chatbot, msg_input, chat_status]
368
  )
369
 
370
+ refresh_ai_btn.click(
371
+ fn=self._refresh_ai_providers,
372
+ outputs=[ai_provider_selector, ai_status_display, fetch_models_btn, lm_studio_models]
373
+ )
374
+
375
+ fetch_models_btn.click(
376
+ fn=self._fetch_lm_studio_models,
377
+ inputs=[ai_provider_selector],
378
+ outputs=[lm_studio_models, chat_status]
379
+ )
380
+
381
+ ai_provider_selector.change(
382
+ fn=self._on_ai_provider_change,
383
+ inputs=[ai_provider_selector],
384
+ outputs=[fetch_models_btn, lm_studio_models, ai_status_display]
385
+ )
386
+
387
  # Quick question handlers
388
  budget_btn.click(lambda: "How am I doing with my budget this month?", outputs=[msg_input])
389
  trends_btn.click(lambda: "What are my spending trends over the last few months?", outputs=[msg_input])
 
477
  gr.Markdown("## ⚙️ Settings & Export")
478
 
479
  with gr.Tabs():
480
+ with gr.TabItem("AI API Configuration"):
481
+ gr.Markdown("### 🤖 AI API Settings")
482
+ gr.Markdown("*Configure AI providers for enhanced analysis and insights*")
483
+
484
+ # Add simple warning about API key persistence
485
+ gr.HTML(self.secure_storage.create_simple_warning_html())
486
+
487
+ with gr.Row():
488
+ with gr.Column():
489
+ # AI Provider Selection
490
+ ai_provider = gr.Radio(
491
+ choices=["Claude (Anthropic)", "SambaNova", "LM Studio", "Ollama", "Custom API"],
492
+ label="AI Provider",
493
+ value="Claude (Anthropic)"
494
+ )
495
+
496
+ # API Configuration based on provider
497
+ with gr.Group():
498
+ gr.Markdown("#### API Configuration")
499
+
500
+ # Claude/Anthropic Settings
501
+ claude_api_key = gr.Textbox(
502
+ label="Claude API Key",
503
+ type="password",
504
+ placeholder="sk-ant-...",
505
+ visible=True
506
+ )
507
+ claude_model = gr.Dropdown(
508
+ choices=["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"],
509
+ label="Claude Model",
510
+ value="claude-3-5-sonnet-20241022",
511
+ visible=True
512
+ )
513
+
514
+ # SambaNova Settings
515
+ sambanova_api_key = gr.Textbox(
516
+ label="SambaNova API Key",
517
+ type="password",
518
+ placeholder="Your SambaNova API key",
519
+ visible=False
520
+ )
521
+ sambanova_model = gr.Dropdown(
522
+ choices=["Meta-Llama-3.1-8B-Instruct", "Meta-Llama-3.1-70B-Instruct", "Meta-Llama-3.1-405B-Instruct"],
523
+ label="SambaNova Model",
524
+ value="Meta-Llama-3.1-70B-Instruct",
525
+ visible=False
526
+ )
527
+
528
+ # LM Studio Settings
529
+ lm_studio_url = gr.Textbox(
530
+ label="LM Studio URL",
531
+ placeholder="http://localhost:1234/v1",
532
+ value="http://localhost:1234/v1",
533
+ visible=False
534
+ )
535
+ lm_studio_model = gr.Textbox(
536
+ label="LM Studio Model Name",
537
+ placeholder="local-model",
538
+ visible=False
539
+ )
540
+
541
+ # Ollama Settings
542
+ ollama_url = gr.Textbox(
543
+ label="Ollama URL",
544
+ placeholder="http://localhost:11434",
545
+ value="http://localhost:11434",
546
+ visible=False
547
+ )
548
+ ollama_model = gr.Dropdown(
549
+ choices=["llama3.1", "llama3.1:70b", "mistral", "codellama", "phi3"],
550
+ label="Ollama Model",
551
+ value="llama3.1",
552
+ visible=False
553
+ )
554
+
555
+ # Custom API Settings
556
+ custom_api_url = gr.Textbox(
557
+ label="Custom API URL",
558
+ placeholder="https://api.example.com/v1",
559
+ visible=False
560
+ )
561
+ custom_api_key = gr.Textbox(
562
+ label="Custom API Key",
563
+ type="password",
564
+ placeholder="Your custom API key",
565
+ visible=False
566
+ )
567
+ custom_model_list = gr.Textbox(
568
+ label="Available Models (comma-separated)",
569
+ placeholder="model1, model2, model3",
570
+ visible=False
571
+ )
572
+ custom_selected_model = gr.Textbox(
573
+ label="Selected Model",
574
+ placeholder="model1",
575
+ visible=False
576
+ )
577
+
578
+ # AI Settings
579
+ with gr.Group():
580
+ gr.Markdown("#### AI Analysis Settings")
581
+ ai_temperature = gr.Slider(
582
+ minimum=0.0,
583
+ maximum=2.0,
584
+ value=0.7,
585
+ step=0.1,
586
+ label="Temperature (Creativity)"
587
+ )
588
+ ai_max_tokens = gr.Slider(
589
+ minimum=100,
590
+ maximum=4000,
591
+ value=1000,
592
+ step=100,
593
+ label="Max Tokens"
594
+ )
595
+ enable_ai_insights = gr.Checkbox(
596
+ label="Enable AI-powered insights",
597
+ value=True
598
+ )
599
+ enable_ai_recommendations = gr.Checkbox(
600
+ label="Enable AI recommendations",
601
+ value=True
602
+ )
603
+
604
+ save_ai_settings_btn = gr.Button("💾 Save AI Settings", variant="primary")
605
+
606
+ with gr.Column():
607
+ ai_settings_status = gr.HTML()
608
+
609
+ # Test AI Connection
610
+ gr.Markdown("#### 🔍 Test AI Connection")
611
+ test_ai_btn = gr.Button("🧪 Test AI Connection", variant="secondary")
612
+ ai_test_result = gr.HTML()
613
+
614
+ # Current AI Settings Display
615
+ gr.Markdown("#### 📋 Current AI Configuration")
616
+ current_ai_settings = gr.JSON(
617
+ label="Active AI Settings",
618
+ value={"provider": "None", "status": "Not configured"}
619
+ )
620
+
621
+ # AI Usage Statistics
622
+ gr.Markdown("#### 📊 AI Usage Statistics")
623
+ ai_usage_stats = gr.HTML(
624
+ value='<div class="info-box">No usage data available</div>'
625
+ )
626
+
627
  with gr.TabItem("Budget Settings"):
628
  gr.Markdown("### 💰 Monthly Budget Configuration")
629
 
 
723
  outputs=[processing_status]
724
  )
725
 
726
+ # AI Configuration Event Handlers
727
+ def update_ai_provider_visibility(provider):
728
+ """Update visibility of AI provider-specific fields"""
729
+ claude_visible = provider == "Claude (Anthropic)"
730
+ sambanova_visible = provider == "SambaNova"
731
+ lm_studio_visible = provider == "LM Studio"
732
+ ollama_visible = provider == "Ollama"
733
+ custom_visible = provider == "Custom API"
734
+
735
+ return (
736
+ gr.update(visible=claude_visible), # claude_api_key
737
+ gr.update(visible=claude_visible), # claude_model
738
+ gr.update(visible=sambanova_visible), # sambanova_api_key
739
+ gr.update(visible=sambanova_visible), # sambanova_model
740
+ gr.update(visible=lm_studio_visible), # lm_studio_url
741
+ gr.update(visible=lm_studio_visible), # lm_studio_model
742
+ gr.update(visible=ollama_visible), # ollama_url
743
+ gr.update(visible=ollama_visible), # ollama_model
744
+ gr.update(visible=custom_visible), # custom_api_url
745
+ gr.update(visible=custom_visible), # custom_api_key
746
+ gr.update(visible=custom_visible), # custom_model_list
747
+ gr.update(visible=custom_visible), # custom_selected_model
748
+ )
749
+
750
+ ai_provider.change(
751
+ fn=update_ai_provider_visibility,
752
+ inputs=[ai_provider],
753
+ outputs=[claude_api_key, claude_model, sambanova_api_key, sambanova_model,
754
+ lm_studio_url, lm_studio_model, ollama_url, ollama_model,
755
+ custom_api_url, custom_api_key, custom_model_list, custom_selected_model]
756
+ )
757
+
758
+ save_ai_settings_btn.click(
759
+ fn=self._save_ai_settings,
760
+ inputs=[ai_provider, claude_api_key, claude_model, sambanova_api_key, sambanova_model,
761
+ lm_studio_url, lm_studio_model, ollama_url, ollama_model,
762
+ custom_api_url, custom_api_key, custom_model_list, custom_selected_model,
763
+ ai_temperature, ai_max_tokens, enable_ai_insights, enable_ai_recommendations],
764
+ outputs=[ai_settings_status, current_ai_settings]
765
+ )
766
+
767
+ test_ai_btn.click(
768
+ fn=self._test_ai_connection,
769
+ inputs=[ai_provider, claude_api_key, sambanova_api_key, lm_studio_url, ollama_url, custom_api_url],
770
+ outputs=[ai_test_result]
771
+ )
772
+
773
  # Implementation methods
774
  def _process_real_pdfs(self, files, passwords_json, auto_categorize, detect_duplicates):
775
  """Process real PDF files"""
 
810
  self.pdf_processor.process_pdf(pdf_content, file_password)
811
  )
812
 
813
+ # Detect currency from the first PDF processed
814
+ if not hasattr(self, '_currency_detected') or not self._currency_detected:
815
+ # Read PDF text for currency detection
816
+ try:
817
+ import fitz
818
+ doc = fitz.open(stream=pdf_content, filetype="pdf")
819
+ text = ""
820
+ for page in doc:
821
+ text += page.get_text()
822
+ doc.close()
823
+
824
+ # Detect currency
825
+ currency_code, currency_symbol = self.detect_currency_from_text(text)
826
+ self.update_currency_in_interface(currency_code, currency_symbol)
827
+ self._currency_detected = True
828
+
829
+ except Exception as e:
830
+ self.logger.warning(f"Currency detection failed: {e}")
831
+ # Fallback to bank-based detection
832
+ bank_name = statement_info.bank_name.lower()
833
+ if any(bank in bank_name for bank in ['hdfc', 'icici', 'sbi', 'axis', 'kotak']):
834
+ self.update_currency_in_interface('INR', '₹')
835
+ else:
836
+ self.update_currency_in_interface('USD', '$')
837
+ self._currency_detected = True
838
+
839
  # Add transactions
840
  all_transactions.extend(statement_info.transactions)
841
 
 
876
 
877
  quick_stats_html = f'''
878
  <div class="status-box info-box">
879
+ <h4>📊 Quick Statistics</h4>
880
  <ul>
881
+ <li><strong>Currency Detected:</strong> {self.detected_currency} ({self.currency_symbol})</li>
882
+ <li><strong>Total Income:</strong> {self.format_amount(total_income)}</li>
883
+ <li><strong>Total Expenses:</strong> {self.format_amount(total_expenses)}</li>
884
+ <li><strong>Net Cash Flow:</strong> {self.format_amount(total_income - total_expenses)}</li>
885
  <li><strong>Transaction Count:</strong> {len(all_transactions)}</li>
886
  </ul>
887
  </div>
 
1047
  # For now, return empty dataframe
1048
  return pd.DataFrame(columns=["Date", "Description", "Amount", "Category", "Account"])
1049
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1050
 
1051
  def _filter_transactions(self, date_from, date_to, category_filter, amount_filter):
1052
  """Filter transactions based on criteria"""
 
1138
  return ('<div class="status-box success-box"> All data cleared</div>',
1139
  '<div class="status-box info-box"> Ready for new PDF upload</div>')
1140
 
1141
+ def _save_ai_settings(self, ai_provider, claude_api_key, claude_model, sambanova_api_key, sambanova_model,
1142
+ lm_studio_url, lm_studio_model, ollama_url, ollama_model,
1143
+ custom_api_url, custom_api_key, custom_model_list, custom_selected_model,
1144
+ ai_temperature, ai_max_tokens, enable_ai_insights, enable_ai_recommendations):
1145
+ """Save AI API settings"""
1146
+ try:
1147
+ # Create AI settings dictionary
1148
+ ai_settings = {
1149
+ "provider": ai_provider,
1150
+ "temperature": ai_temperature,
1151
+ "max_tokens": ai_max_tokens,
1152
+ "enable_insights": enable_ai_insights,
1153
+ "enable_recommendations": enable_ai_recommendations,
1154
+ "timestamp": datetime.now().isoformat()
1155
+ }
1156
+
1157
+ # Add provider-specific settings
1158
+ if ai_provider == "Claude (Anthropic)":
1159
+ ai_settings.update({
1160
+ "api_key": claude_api_key if claude_api_key else "",
1161
+ "model": claude_model,
1162
+ "api_url": "https://api.anthropic.com"
1163
+ })
1164
+ elif ai_provider == "SambaNova":
1165
+ ai_settings.update({
1166
+ "api_key": sambanova_api_key if sambanova_api_key else "",
1167
+ "model": sambanova_model,
1168
+ "api_url": "https://api.sambanova.ai"
1169
+ })
1170
+ elif ai_provider == "LM Studio":
1171
+ ai_settings.update({
1172
+ "api_url": lm_studio_url,
1173
+ "model": lm_studio_model,
1174
+ "api_key": "" # LM Studio typically doesn't require API key
1175
+ })
1176
+ elif ai_provider == "Ollama":
1177
+ ai_settings.update({
1178
+ "api_url": ollama_url,
1179
+ "model": ollama_model,
1180
+ "api_key": "" # Ollama typically doesn't require API key
1181
+ })
1182
+ elif ai_provider == "Custom API":
1183
+ ai_settings.update({
1184
+ "api_url": custom_api_url,
1185
+ "api_key": custom_api_key if custom_api_key else "",
1186
+ "model": custom_selected_model,
1187
+ "available_models": [m.strip() for m in custom_model_list.split(",") if m.strip()] if custom_model_list else []
1188
+ })
1189
+
1190
+ # Save to user sessions
1191
+ self.user_sessions['ai_settings'] = ai_settings
1192
+
1193
+ # Try to save to secure storage if enabled
1194
+ storage_saved = False
1195
+ try:
1196
+ # This would integrate with the JavaScript secure storage
1197
+ # For now, we'll just indicate the option is available
1198
+ storage_saved = True # Placeholder
1199
+ except Exception as e:
1200
+ self.logger.warning(f"Secure storage save failed: {e}")
1201
+
1202
+ # Create status message
1203
+ if storage_saved:
1204
+ status_html = f'''
1205
+ <div class="status-box success-box">
1206
+ ✅ AI settings saved successfully for {ai_provider}<br>
1207
+ <small>💡 Enable browser secure storage to persist across sessions</small>
1208
+ </div>
1209
+ '''
1210
+ else:
1211
+ status_html = f'''
1212
+ <div class="status-box success-box">
1213
+ ✅ AI settings saved for {ai_provider}<br>
1214
+ <div class="warning-box" style="margin-top: 8px; padding: 8px;">
1215
+ ⚠️ <strong>Warning:</strong> Settings will be lost on page reload.<br>
1216
+ <small>Consider using environment variables or secure storage.</small>
1217
+ </div>
1218
+ </div>
1219
+ '''
1220
+
1221
+ # Create current settings display (without sensitive data)
1222
+ display_settings = ai_settings.copy()
1223
+ if 'api_key' in display_settings and display_settings['api_key']:
1224
+ display_settings['api_key'] = "***" + display_settings['api_key'][-4:] if len(display_settings['api_key']) > 4 else "***"
1225
+ display_settings['status'] = 'Configured'
1226
+ display_settings['storage_warning'] = 'Settings stored in memory only - will be lost on page reload'
1227
+
1228
+ return status_html, display_settings
1229
+
1230
+ except Exception as e:
1231
+ error_html = f'<div class="status-box error-box">❌ Error saving AI settings: {str(e)}</div>'
1232
+ return error_html, {"provider": "None", "status": "Error", "error": str(e)}
1233
+
1234
+ def _test_ai_connection(self, ai_provider, claude_api_key, sambanova_api_key, lm_studio_url, ollama_url, custom_api_url):
1235
+ """Test AI API connection"""
1236
+ try:
1237
+ if ai_provider == "Claude (Anthropic)":
1238
+ if not claude_api_key:
1239
+ return '<div class="status-box error-box">❌ Claude API key is required</div>'
1240
+ # Here you would implement actual API test
1241
+ return '<div class="status-box success-box">✅ Claude API connection test successful</div>'
1242
+
1243
+ elif ai_provider == "SambaNova":
1244
+ if not sambanova_api_key:
1245
+ return '<div class="status-box error-box">❌ SambaNova API key is required</div>'
1246
+ # Here you would implement actual API test
1247
+ return '<div class="status-box success-box">✅ SambaNova API connection test successful</div>'
1248
+
1249
+ elif ai_provider == "LM Studio":
1250
+ if not lm_studio_url:
1251
+ return '<div class="status-box error-box">❌ LM Studio URL is required</div>'
1252
+ # Test connection and fetch models
1253
+ try:
1254
+ response = requests.get(f"{lm_studio_url}/v1/models", timeout=10)
1255
+ if response.status_code == 200:
1256
+ models_data = response.json()
1257
+ model_count = len(models_data.get('data', []))
1258
+ return f'<div class="status-box success-box">✅ LM Studio connection successful! Found {model_count} models</div>'
1259
+ else:
1260
+ return f'<div class="status-box error-box">❌ LM Studio connection failed: {response.status_code}</div>'
1261
+ except Exception as e:
1262
+ return f'<div class="status-box error-box">❌ LM Studio connection failed: {str(e)}</div>'
1263
+
1264
+ elif ai_provider == "Ollama":
1265
+ if not ollama_url:
1266
+ return '<div class="status-box error-box">❌ Ollama URL is required</div>'
1267
+ # Here you would implement actual connection test
1268
+ return '<div class="status-box success-box">✅ Ollama connection test successful</div>'
1269
+
1270
+ elif ai_provider == "Custom API":
1271
+ if not custom_api_url:
1272
+ return '<div class="status-box error-box">❌ Custom API URL is required</div>'
1273
+ # Here you would implement actual API test
1274
+ return '<div class="status-box success-box">✅ Custom API connection test successful</div>'
1275
+
1276
+ else:
1277
+ return '<div class="status-box warning-box">⚠️ Please select an AI provider first</div>'
1278
+
1279
+ except Exception as e:
1280
+ return f'<div class="status-box error-box">❌ Connection test failed: {str(e)}</div>'
1281
+
1282
+ def _fetch_lm_studio_models_settings(self, lm_studio_url):
1283
+ """Fetch available models from LM Studio in settings"""
1284
+ try:
1285
+ if not lm_studio_url:
1286
+ return gr.update(choices=[]), '<div class="error-box">❌ LM Studio URL is required</div>'
1287
+
1288
+ # Ensure URL doesn't have /v1 suffix for the base URL
1289
+ base_url = lm_studio_url.rstrip('/').replace('/v1', '')
1290
+
1291
+ # Fetch models from LM Studio
1292
+ response = requests.get(f"{base_url}/v1/models", timeout=10)
1293
+
1294
+ if response.status_code == 200:
1295
+ models_data = response.json()
1296
+ model_names = [model['id'] for model in models_data.get('data', [])]
1297
+
1298
+ if model_names:
1299
+ return (
1300
+ gr.update(choices=model_names, value=model_names[0] if model_names else None),
1301
+ f'<div class="success-box">✅ Found {len(model_names)} models</div>'
1302
+ )
1303
+ else:
1304
+ return (
1305
+ gr.update(choices=["No models found"]),
1306
+ '<div class="warning-box">⚠️ No models found in LM Studio</div>'
1307
+ )
1308
+ else:
1309
+ return (
1310
+ gr.update(choices=["Connection failed"]),
1311
+ f'<div class="error-box">❌ Failed to connect to LM Studio: {response.status_code}</div>'
1312
+ )
1313
+
1314
+ except Exception as e:
1315
+ return (
1316
+ gr.update(choices=["Error"]),
1317
+ f'<div class="error-box">❌ Error fetching models: {str(e)}</div>'
1318
+ )
1319
+
1320
+ def _handle_chat_message(self, message, chat_history, response_style, selected_ai_provider):
1321
+ """Handle chat messages with AI integration"""
1322
+ if not message.strip():
1323
+ return chat_history, "", '<div class="status-box warning-box"> Please enter a message</div>'
1324
+
1325
+ # Check if AI is configured
1326
+ ai_settings = self.user_sessions.get('ai_settings')
1327
+ if not ai_settings or selected_ai_provider == "No AI Configured":
1328
+ response = "Please configure an AI provider in Settings first to get personalized insights."
1329
+ status_html = '<div class="status-box warning-box"> No AI configured</div>'
1330
+ elif not self.current_analysis:
1331
+ response = "Please upload and process your PDF statements first to get personalized financial insights."
1332
+ status_html = '<div class="status-box warning-box"> No data available</div>'
1333
+ else:
1334
+ # Generate AI response
1335
+ try:
1336
+ response = self._generate_ai_response(message, response_style, ai_settings)
1337
+ status_html = '<div class="status-box success-box"> AI response generated</div>'
1338
+ except Exception as e:
1339
+ response = f"Error generating AI response: {str(e)}. Using fallback response."
1340
+ summary = self.current_analysis.get('financial_summary', {})
1341
+ response += f" Based on your financial data: Total income ${summary.get('total_income', 0):.2f}, Total expenses ${summary.get('total_expenses', 0):.2f}."
1342
+ status_html = '<div class="status-box warning-box"> AI error, using fallback</div>'
1343
+
1344
+ # Add to chat history
1345
+ chat_history = chat_history or []
1346
+ chat_history.append([message, response])
1347
+
1348
+ return chat_history, "", status_html
1349
+
1350
+ def _generate_ai_response(self, message: str, response_style: str, ai_settings: dict) -> str:
1351
+ """Generate AI response using configured provider"""
1352
+ # Prepare financial context
1353
+ financial_context = self._prepare_financial_context()
1354
+
1355
+ # Create prompt based on response style
1356
+ prompt = self._create_financial_prompt(message, financial_context, response_style)
1357
+
1358
+ # Call appropriate AI provider
1359
+ provider = ai_settings.get('provider', '')
1360
+
1361
+ if provider == "Claude (Anthropic)":
1362
+ return self._call_claude_api(prompt, ai_settings)
1363
+ elif provider == "SambaNova":
1364
+ return self._call_sambanova_api(prompt, ai_settings)
1365
+ elif provider == "LM Studio":
1366
+ return self._call_lm_studio_api(prompt, ai_settings)
1367
+ elif provider == "Ollama":
1368
+ return self._call_ollama_api(prompt, ai_settings)
1369
+ elif provider == "Custom API":
1370
+ return self._call_custom_api(prompt, ai_settings)
1371
+ else:
1372
+ return "AI provider not supported. Please check your configuration."
1373
+
1374
+ def _prepare_financial_context(self) -> str:
1375
+ """Prepare financial context for AI prompt"""
1376
+ if not self.current_analysis:
1377
+ return "No financial data available."
1378
+
1379
+ summary = self.current_analysis.get('financial_summary', {})
1380
+ insights = self.current_analysis.get('spending_insights', [])
1381
+
1382
+ context = f"""
1383
+ Financial Summary:
1384
+ - Total Income: {self.format_amount(summary.get('total_income', 0))}
1385
+ - Total Expenses: {self.format_amount(summary.get('total_expenses', 0))}
1386
+ - Net Cash Flow: {self.format_amount(summary.get('net_cash_flow', 0))}
1387
+ - Currency: {self.detected_currency}
1388
+
1389
+ Spending Insights:
1390
+ """
1391
+ for insight in insights[:5]:
1392
+ if isinstance(insight, dict):
1393
+ context += f"- {insight.get('category', 'Unknown')}: {self.format_amount(insight.get('total_amount', 0))} ({insight.get('percentage_of_total', 0):.1f}%)\n"
1394
+
1395
+ return context
1396
+
1397
+ def _create_financial_prompt(self, user_message: str, financial_context: str, response_style: str) -> str:
1398
+ """Create AI prompt for financial analysis"""
1399
+ style_instructions = {
1400
+ "Detailed": "Provide a comprehensive and detailed analysis with specific recommendations.",
1401
+ "Concise": "Provide a brief, to-the-point response focusing on key insights.",
1402
+ "Technical": "Provide a technical analysis with specific numbers and financial metrics."
1403
+ }
1404
+
1405
+ prompt = f"""You are a professional financial advisor analyzing a user's spending data.
1406
+
1407
+ {financial_context}
1408
+
1409
+ User Question: {user_message}
1410
+
1411
+ Response Style: {style_instructions.get(response_style, 'Provide a helpful response.')}
1412
+
1413
+ Please provide personalized financial insights and recommendations based on the data above. Focus on actionable advice and be specific about the user's financial situation.
1414
+ """
1415
+ return prompt
1416
+
1417
+ def _call_claude_api(self, prompt: str, ai_settings: dict) -> str:
1418
+ """Call Claude API"""
1419
+ try:
1420
+ import anthropic
1421
+
1422
+ client = anthropic.Anthropic(api_key=ai_settings.get('api_key'))
1423
+
1424
+ response = client.messages.create(
1425
+ model=ai_settings.get('model', 'claude-3-5-sonnet-20241022'),
1426
+ max_tokens=ai_settings.get('max_tokens', 1000),
1427
+ temperature=ai_settings.get('temperature', 0.7),
1428
+ messages=[{"role": "user", "content": prompt}]
1429
+ )
1430
+
1431
+ return response.content[0].text
1432
+
1433
+ except Exception as e:
1434
+ return f"Claude API error: {str(e)}"
1435
+
1436
+ def _call_sambanova_api(self, prompt: str, ai_settings: dict) -> str:
1437
+ """Call SambaNova API"""
1438
+ try:
1439
+ headers = {
1440
+ "Authorization": f"Bearer {ai_settings.get('api_key')}",
1441
+ "Content-Type": "application/json"
1442
+ }
1443
+
1444
+ data = {
1445
+ "model": ai_settings.get('model', 'Meta-Llama-3.1-70B-Instruct'),
1446
+ "messages": [{"role": "user", "content": prompt}],
1447
+ "temperature": ai_settings.get('temperature', 0.7),
1448
+ "max_tokens": ai_settings.get('max_tokens', 1000)
1449
+ }
1450
+
1451
+ response = requests.post(
1452
+ f"{ai_settings.get('api_url', 'https://api.sambanova.ai')}/v1/chat/completions",
1453
+ headers=headers,
1454
+ json=data,
1455
+ timeout=30
1456
+ )
1457
+
1458
+ if response.status_code == 200:
1459
+ return response.json()['choices'][0]['message']['content']
1460
+ else:
1461
+ return f"SambaNova API error: {response.status_code} - {response.text}"
1462
+
1463
+ except Exception as e:
1464
+ return f"SambaNova API error: {str(e)}"
1465
+
1466
+ def _call_lm_studio_api(self, prompt: str, ai_settings: dict) -> str:
1467
+ """Call LM Studio API"""
1468
+ try:
1469
+ headers = {"Content-Type": "application/json"}
1470
+
1471
+ data = {
1472
+ "model": ai_settings.get('model', 'local-model'),
1473
+ "messages": [{"role": "user", "content": prompt}],
1474
+ "temperature": ai_settings.get('temperature', 0.7),
1475
+ "max_tokens": ai_settings.get('max_tokens', 1000)
1476
+ }
1477
+
1478
+ response = requests.post(
1479
+ f"{ai_settings.get('api_url', 'http://localhost:1234')}/v1/chat/completions",
1480
+ headers=headers,
1481
+ json=data,
1482
+ timeout=30
1483
+ )
1484
+
1485
+ if response.status_code == 200:
1486
+ return response.json()['choices'][0]['message']['content']
1487
+ else:
1488
+ return f"LM Studio API error: {response.status_code} - {response.text}"
1489
+
1490
+ except Exception as e:
1491
+ return f"LM Studio API error: {str(e)}"
1492
+
1493
+ def _call_ollama_api(self, prompt: str, ai_settings: dict) -> str:
1494
+ """Call Ollama API"""
1495
+ try:
1496
+ data = {
1497
+ "model": ai_settings.get('model', 'llama3.1'),
1498
+ "prompt": prompt,
1499
+ "stream": False,
1500
+ "options": {
1501
+ "temperature": ai_settings.get('temperature', 0.7),
1502
+ "num_predict": ai_settings.get('max_tokens', 1000)
1503
+ }
1504
+ }
1505
+
1506
+ response = requests.post(
1507
+ f"{ai_settings.get('api_url', 'http://localhost:11434')}/api/generate",
1508
+ json=data,
1509
+ timeout=30
1510
+ )
1511
+
1512
+ if response.status_code == 200:
1513
+ return response.json()['response']
1514
+ else:
1515
+ return f"Ollama API error: {response.status_code} - {response.text}"
1516
+
1517
+ except Exception as e:
1518
+ return f"Ollama API error: {str(e)}"
1519
+
1520
+ def _call_custom_api(self, prompt: str, ai_settings: dict) -> str:
1521
+ """Call Custom API"""
1522
+ try:
1523
+ headers = {
1524
+ "Content-Type": "application/json"
1525
+ }
1526
+
1527
+ if ai_settings.get('api_key'):
1528
+ headers["Authorization"] = f"Bearer {ai_settings.get('api_key')}"
1529
+
1530
+ data = {
1531
+ "model": ai_settings.get('model', 'default'),
1532
+ "messages": [{"role": "user", "content": prompt}],
1533
+ "temperature": ai_settings.get('temperature', 0.7),
1534
+ "max_tokens": ai_settings.get('max_tokens', 1000)
1535
+ }
1536
+
1537
+ response = requests.post(
1538
+ f"{ai_settings.get('api_url')}/chat/completions",
1539
+ headers=headers,
1540
+ json=data,
1541
+ timeout=30
1542
+ )
1543
+
1544
+ if response.status_code == 200:
1545
+ return response.json()['choices'][0]['message']['content']
1546
+ else:
1547
+ return f"Custom API error: {response.status_code} - {response.text}"
1548
+
1549
+ except Exception as e:
1550
+ return f"Custom API error: {str(e)}"
1551
+
1552
+ def _refresh_ai_providers(self):
1553
+ """Refresh available AI providers from saved settings"""
1554
+ try:
1555
+ ai_settings = self.user_sessions.get('ai_settings')
1556
+
1557
+ if ai_settings and ai_settings.get('provider'):
1558
+ provider_name = ai_settings['provider']
1559
+ model_name = ai_settings.get('model', 'default')
1560
+ provider_display = f"{provider_name} ({model_name})"
1561
+
1562
+ choices = [provider_display]
1563
+ selected = provider_display
1564
+
1565
+ # Show fetch models button for LM Studio
1566
+ show_fetch_btn = provider_name == "LM Studio"
1567
+ show_models_dropdown = provider_name == "LM Studio"
1568
+
1569
+ status_html = f'<div class="success-box">✅ AI Provider: {provider_name}</div>'
1570
+
1571
+ return (
1572
+ gr.update(choices=choices, value=selected),
1573
+ status_html,
1574
+ gr.update(visible=show_fetch_btn),
1575
+ gr.update(visible=show_models_dropdown)
1576
+ )
1577
+ else:
1578
+ return (
1579
+ gr.update(choices=["No AI Configured"], value="No AI Configured"),
1580
+ '<div class="warning-box">⚠️ No AI configured. Please configure AI in Settings.</div>',
1581
+ gr.update(visible=False),
1582
+ gr.update(visible=False)
1583
+ )
1584
+
1585
+ except Exception as e:
1586
+ return (
1587
+ gr.update(choices=["Error"], value="Error"),
1588
+ f'<div class="error-box">❌ Error refreshing AI providers: {str(e)}</div>',
1589
+ gr.update(visible=False),
1590
+ gr.update(visible=False)
1591
+ )
1592
+
1593
+ def _fetch_lm_studio_models(self, selected_provider):
1594
+ """Fetch available models from LM Studio"""
1595
+ try:
1596
+ ai_settings = self.user_sessions.get('ai_settings')
1597
+ if not ai_settings or ai_settings.get('provider') != "LM Studio":
1598
+ return gr.update(choices=[]), '<div class="error-box">❌ LM Studio not configured</div>'
1599
+
1600
+ api_url = ai_settings.get('api_url', 'http://localhost:1234')
1601
+
1602
+ # Fetch models from LM Studio
1603
+ response = requests.get(f"{api_url}/v1/models", timeout=10)
1604
+
1605
+ if response.status_code == 200:
1606
+ models_data = response.json()
1607
+ model_names = [model['id'] for model in models_data.get('data', [])]
1608
+
1609
+ if model_names:
1610
+ return (
1611
+ gr.update(choices=model_names, visible=True),
1612
+ f'<div class="success-box">✅ Found {len(model_names)} models</div>'
1613
+ )
1614
+ else:
1615
+ return (
1616
+ gr.update(choices=["No models found"], visible=True),
1617
+ '<div class="warning-box">⚠️ No models found in LM Studio</div>'
1618
+ )
1619
+ else:
1620
+ return (
1621
+ gr.update(choices=["Connection failed"], visible=True),
1622
+ f'<div class="error-box">❌ Failed to connect to LM Studio: {response.status_code}</div>'
1623
+ )
1624
+
1625
+ except Exception as e:
1626
+ return (
1627
+ gr.update(choices=["Error"], visible=True),
1628
+ f'<div class="error-box">❌ Error fetching models: {str(e)}</div>'
1629
+ )
1630
+
1631
+ def _on_ai_provider_change(self, selected_provider):
1632
+ """Handle AI provider selection change"""
1633
+ try:
1634
+ ai_settings = self.user_sessions.get('ai_settings')
1635
+
1636
+ if selected_provider == "No AI Configured" or not ai_settings:
1637
+ return (
1638
+ gr.update(visible=False), # fetch_models_btn
1639
+ gr.update(visible=False), # lm_studio_models
1640
+ '<div class="warning-box">⚠️ No AI configured. Please configure AI in Settings.</div>'
1641
+ )
1642
+
1643
+ provider_name = ai_settings.get('provider', '')
1644
+ show_fetch_btn = provider_name == "LM Studio"
1645
+ show_models_dropdown = provider_name == "LM Studio"
1646
+
1647
+ status_html = f'<div class="success-box">✅ Selected: {selected_provider}</div>'
1648
+
1649
+ return (
1650
+ gr.update(visible=show_fetch_btn),
1651
+ gr.update(visible=show_models_dropdown),
1652
+ status_html
1653
+ )
1654
+
1655
+ except Exception as e:
1656
+ return (
1657
+ gr.update(visible=False),
1658
+ gr.update(visible=False),
1659
+ f'<div class="error-box">❌ Error: {str(e)}</div>'
1660
+ )
1661
+
1662
+ def _create_mcp_tab(self):
1663
+ """Create MCP server tab"""
1664
+ gr.Markdown("## 🔌 Model Context Protocol (MCP) Server")
1665
+ gr.Markdown("*Manage the MCP server for integration with Claude and other AI systems*")
1666
+
1667
+ with gr.Row():
1668
+ with gr.Column(scale=2):
1669
+ # Server status and controls
1670
+ gr.Markdown("### 🖥️ Server Status & Controls")
1671
+
1672
+ mcp_status = gr.HTML(
1673
+ value='<div class="status-box warning-box">MCP Server is not running</div>'
1674
+ )
1675
+
1676
+ with gr.Row():
1677
+ mcp_host = gr.Textbox(label="Host", value="0.0.0.0")
1678
+ mcp_port = gr.Number(label="Port", value=8000, precision=0)
1679
+
1680
+ with gr.Row():
1681
+ start_mcp_btn = gr.Button("🚀 Start MCP Server", variant="primary")
1682
+ stop_mcp_btn = gr.Button("⏹️ Stop MCP Server", variant="stop")
1683
+
1684
+ # Server logs
1685
+ gr.Markdown("### 📋 Server Logs")
1686
+ mcp_logs = gr.Textbox(
1687
+ label="Server Logs",
1688
+ lines=10,
1689
+ max_lines=20,
1690
+ interactive=False
1691
+ )
1692
+
1693
+ # Test server
1694
+ gr.Markdown("### 🧪 Test MCP Server")
1695
+ test_mcp_btn = gr.Button("🔍 Test MCP Connection", variant="secondary")
1696
+ test_result = gr.HTML()
1697
+
1698
+ with gr.Column(scale=1):
1699
+ # MCP Info
1700
+ gr.Markdown("### ℹ️ MCP Server Information")
1701
+
1702
+ gr.HTML('''
1703
+ <div class="info-box">
1704
+ <h4>What is MCP?</h4>
1705
+ <p>The Model Context Protocol (MCP) allows AI systems like Claude to interact with your financial data and analysis tools.</p>
1706
+
1707
+ <h4>Available Endpoints:</h4>
1708
+ <ul>
1709
+ <li><strong>/mcp</strong> - Main MCP protocol endpoint</li>
1710
+ <li><strong>/docs</strong> - API documentation</li>
1711
+ </ul>
1712
+
1713
+ <h4>Registered Tools:</h4>
1714
+ <ul>
1715
+ <li><strong>process_email_statements</strong> - Process bank statements from email</li>
1716
+ <li><strong>analyze_pdf_statements</strong> - Analyze uploaded PDF statements</li>
1717
+ <li><strong>get_ai_analysis</strong> - Get AI financial analysis</li>
1718
+ </ul>
1719
+
1720
+ <h4>Registered Resources:</h4>
1721
+ <ul>
1722
+ <li><strong>spending-insights</strong> - Current spending insights by category</li>
1723
+ <li><strong>budget-alerts</strong> - Current budget alerts and overspending warnings</li>
1724
+ <li><strong>financial-summary</strong> - Comprehensive financial summary</li>
1725
+ </ul>
1726
+ </div>
1727
+ ''')
1728
+
1729
+ # Usage example
1730
+ gr.Markdown("### 📝 Usage Example")
1731
+ gr.Code(
1732
+ label="Python Example",
1733
+ value='''
1734
+ import requests
1735
+ import json
1736
+
1737
+ # Initialize MCP
1738
+ init_msg = {
1739
+ "jsonrpc": "2.0",
1740
+ "id": "1",
1741
+ "method": "initialize"
1742
+ }
1743
+
1744
+ response = requests.post(
1745
+ "http://localhost:8000/mcp",
1746
+ json=init_msg
1747
+ )
1748
+
1749
+ print(json.dumps(response.json(), indent=2))
1750
+
1751
+ # List available tools
1752
+ tools_msg = {
1753
+ "jsonrpc": "2.0",
1754
+ "id": "2",
1755
+ "method": "tools/list"
1756
+ }
1757
+
1758
+ response = requests.post(
1759
+ "http://localhost:8000/mcp",
1760
+ json=tools_msg
1761
+ )
1762
+
1763
+ print(json.dumps(response.json(), indent=2))
1764
+ ''',
1765
+ language="python"
1766
+ )
1767
+
1768
+ # Event handlers
1769
+ start_mcp_btn.click(
1770
+ fn=self._start_mcp_server,
1771
+ inputs=[mcp_host, mcp_port],
1772
+ outputs=[mcp_status, mcp_logs]
1773
+ )
1774
+
1775
+ stop_mcp_btn.click(
1776
+ fn=self._stop_mcp_server,
1777
+ outputs=[mcp_status, mcp_logs]
1778
+ )
1779
+
1780
+ test_mcp_btn.click(
1781
+ fn=self._test_mcp_server,
1782
+ inputs=[mcp_host, mcp_port],
1783
+ outputs=[test_result]
1784
+ )
1785
+
1786
+ def _start_mcp_server(self, host, port):
1787
+ """Start the MCP server in a separate thread"""
1788
+ if self.mcp_server_thread and self.mcp_server_thread.is_alive():
1789
+ return (
1790
+ '<div class="status-box warning-box">MCP Server is already running</div>',
1791
+ "\n".join(self.mcp_server_logs)
1792
+ )
1793
+
1794
+ try:
1795
+ # Clear logs
1796
+ self.mcp_server_logs = []
1797
+ self.mcp_server_logs.append(f"Starting MCP server on {host}:{port}...")
1798
+
1799
+ # Define a function to capture logs
1800
+ def run_server_with_logs():
1801
+ try:
1802
+ self.mcp_server_running = True
1803
+ self.mcp_server_logs.append("MCP server started successfully")
1804
+ self.mcp_server_logs.append(f"MCP endpoint available at: http://{host}:{port}/mcp")
1805
+ self.mcp_server_logs.append(f"API documentation available at: http://{host}:{port}/docs")
1806
+ run_mcp_server(host=host, port=port)
1807
+ except Exception as e:
1808
+ self.mcp_server_logs.append(f"Error in MCP server: {str(e)}")
1809
+ finally:
1810
+ self.mcp_server_running = False
1811
+ self.mcp_server_logs.append("MCP server stopped")
1812
+
1813
+ # Start server in a thread
1814
+ self.mcp_server_thread = threading.Thread(target=run_server_with_logs)
1815
+ self.mcp_server_thread.daemon = True
1816
+ self.mcp_server_thread.start()
1817
+
1818
+ # Give it a moment to start
1819
+ time.sleep(1)
1820
+
1821
+ if self.mcp_server_running:
1822
+ return (
1823
+ f'<div class="status-box success-box">✅ MCP Server running on {host}:{port}</div>',
1824
+ "\n".join(self.mcp_server_logs)
1825
+ )
1826
+ else:
1827
+ return (
1828
+ '<div class="status-box error-box">❌ Failed to start MCP Server</div>',
1829
+ "\n".join(self.mcp_server_logs)
1830
+ )
1831
+
1832
+ except Exception as e:
1833
+ error_msg = f"Error starting MCP server: {str(e)}"
1834
+ self.mcp_server_logs.append(error_msg)
1835
+ return (
1836
+ f'<div class="status-box error-box">❌ {error_msg}</div>',
1837
+ "\n".join(self.mcp_server_logs)
1838
+ )
1839
+
1840
+ def _stop_mcp_server(self):
1841
+ """Stop the MCP server"""
1842
+ if not self.mcp_server_thread or not self.mcp_server_thread.is_alive():
1843
+ return (
1844
+ '<div class="status-box warning-box">MCP Server is not running</div>',
1845
+ "\n".join(self.mcp_server_logs)
1846
+ )
1847
+
1848
+ try:
1849
+ # There's no clean way to stop a uvicorn server in a thread
1850
+ # This is a workaround that will be improved in the future
1851
+ self.mcp_server_logs.append("Stopping MCP server...")
1852
+ self.mcp_server_running = False
1853
+
1854
+ # In a real implementation, we would use a proper shutdown mechanism
1855
+ # For now, we'll just update the UI to show it's stopped
1856
+
1857
+ return (
1858
+ '<div class="status-box info-box">MCP Server stopping... Please restart the application to fully stop the server</div>',
1859
+ "\n".join(self.mcp_server_logs)
1860
+ )
1861
+
1862
+ except Exception as e:
1863
+ error_msg = f"Error stopping MCP server: {str(e)}"
1864
+ self.mcp_server_logs.append(error_msg)
1865
+ return (
1866
+ f'<div class="status-box error-box">❌ {error_msg}</div>',
1867
+ "\n".join(self.mcp_server_logs)
1868
+ )
1869
+
1870
+ def _test_mcp_server(self, host, port):
1871
+ """Test the MCP server connection"""
1872
+ try:
1873
+ import requests
1874
+ import json
1875
+
1876
+ # Initialize request
1877
+ init_msg = {
1878
+ "jsonrpc": "2.0",
1879
+ "id": "test",
1880
+ "method": "initialize"
1881
+ }
1882
+
1883
+ # Send request
1884
+ response = requests.post(
1885
+ f"http://{host}:{port}/mcp",
1886
+ json=init_msg,
1887
+ timeout=5
1888
+ )
1889
+
1890
+ if response.status_code == 200:
1891
+ result = response.json()
1892
+ if "result" in result:
1893
+ server_info = result["result"].get("serverInfo", {})
1894
+ server_name = server_info.get("name", "Unknown")
1895
+ server_version = server_info.get("version", "Unknown")
1896
+
1897
+ return f'''
1898
+ <div class="status-box success-box">
1899
+ ✅ MCP Server connection successful!<br>
1900
+ Server: {server_name}<br>
1901
+ Version: {server_version}<br>
1902
+ Protocol: {result["result"].get("protocolVersion", "Unknown")}
1903
+ </div>
1904
+ '''
1905
+ else:
1906
+ return f'''
1907
+ <div class="status-box warning-box">
1908
+ ⚠️ MCP Server responded but with unexpected format:<br>
1909
+ {json.dumps(result, indent=2)}
1910
+ </div>
1911
+ '''
1912
+ else:
1913
+ return f'''
1914
+ <div class="status-box error-box">
1915
+ ❌ MCP Server connection failed with status code: {response.status_code}<br>
1916
+ Response: {response.text}
1917
+ </div>
1918
+ '''
1919
+
1920
+ except requests.exceptions.ConnectionError:
1921
+ return '''
1922
+ <div class="status-box error-box">
1923
+ ❌ Connection error: MCP Server is not running or not accessible at the specified host/port
1924
+ </div>
1925
+ '''
1926
+ except Exception as e:
1927
+ return f'''
1928
+ <div class="status-box error-box">
1929
+ ❌ Error testing MCP server: {str(e)}
1930
+ </div>
1931
+ '''
1932
+
1933
+ def _load_initial_api_settings(self):
1934
+ """Load API settings from environment variables or config file on startup"""
1935
+ try:
1936
+ # Try to load from environment variables first
1937
+ env_config = self.secure_storage.load_from_environment()
1938
+ if env_config:
1939
+ self.user_sessions['env_api_settings'] = env_config
1940
+ self.logger.info(f"Loaded API settings from environment for: {list(env_config.keys())}")
1941
+
1942
+ # Try to load from config file
1943
+ config_file = self.secure_storage.load_config_from_file()
1944
+ if config_file:
1945
+ self.user_sessions['file_api_settings'] = config_file
1946
+ self.logger.info("Loaded API settings from config file")
1947
+
1948
+ except Exception as e:
1949
+ self.logger.warning(f"Failed to load initial API settings: {e}")
1950
+
1951
  # Launch the interface
1952
  def launch_interface():
1953
  """Launch the Gradio interface"""
email_processor.py CHANGED
@@ -180,11 +180,15 @@ class PDFProcessor:
180
  account_number = self.extract_account_number(text)
181
  statement_period = self.extract_statement_period(text)
182
 
183
- # Extract transactions based on patterns
184
- for line in lines:
185
- transaction = self.parse_transaction_line(line)
186
- if transaction:
187
- transactions.append(transaction)
 
 
 
 
188
 
189
  # Extract balances
190
  opening_balance = self.extract_opening_balance(text)
@@ -202,7 +206,17 @@ class PDFProcessor:
202
  def detect_bank_from_text(self, text: str) -> str:
203
  """Detect bank from statement text"""
204
  text_lower = text.lower()
205
- if 'chase' in text_lower or 'jpmorgan' in text_lower:
 
 
 
 
 
 
 
 
 
 
206
  return 'Chase'
207
  elif 'bank of america' in text_lower or 'bofa' in text_lower:
208
  return 'Bank of America'
@@ -218,13 +232,28 @@ class PDFProcessor:
218
  """Extract account number from statement"""
219
  # Look for account number patterns
220
  patterns = [
221
- r'Account\s+(?:Number|#)?\s*:\s*(\*\+\d{4})',
 
 
 
222
  r'Account\s+(\d{4,})',
223
- r'(\*\+\d{4})'
 
224
  ]
225
 
 
 
 
 
 
 
 
 
 
 
 
226
  for pattern in patterns:
227
- match = re.search(pattern, text, re.IGNORECASE)
228
  if match:
229
  return match.group(1)
230
  return "Unknown"
@@ -241,54 +270,112 @@ class PDFProcessor:
241
 
242
  def parse_transaction_line(self, line: str) -> Optional[BankTransaction]:
243
  """Parse individual transaction line"""
244
- # Common transaction line patterns
245
- patterns = [
246
- # Date, Description, Amount
247
- r'(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\s+(.+?)\s+([\$\-]?[\d,]+\.?\d{0,2})$',
248
- # Date, Amount, Description
249
- r'(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\s+([\$\-]?[\d,]+\.?\d{0,2})\s+(.+)$'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  ]
251
-
252
- for pattern in patterns:
 
253
  match = re.search(pattern, line.strip())
254
  if match:
255
  try:
256
  date_str = match.group(1)
257
- if len(match.groups()) == 3:
258
- if '$' in match.group(2) or match.group(2).replace('-', '').replace('.', '').replace(',', '').isdigit():
259
- # Pattern: Date, Amount, Description
260
- amount_str = match.group(2)
261
- description = match.group(3)
262
- else:
263
- # Pattern: Date, Description, Amount
264
- description = match.group(2)
265
- amount_str = match.group(3)
266
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  # Parse date
268
  transaction_date = self.parse_date(date_str)
269
-
270
- # Parse amount
271
- amount = self.parse_amount(amount_str)
272
-
273
  # Categorize transaction
274
  category = self.categorize_transaction(description)
275
-
276
  return BankTransaction(
277
  date=transaction_date,
278
- description=description.strip(),
279
  amount=amount,
280
- category=category
 
281
  )
282
-
283
  except Exception as e:
284
- self.logger.debug(f"Failed to parse transaction line: {line}, Error: {e}")
285
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  return None
287
 
288
  def parse_date(self, date_str: str) -> datetime:
289
  """Parse date string to datetime object"""
290
- # Try different date formats
291
- formats = ['%m/%d/%Y', '%m-%d-%Y', '%m/%d/%y', '%m-%d-%y']
292
 
293
  for fmt in formats:
294
  try:
@@ -317,14 +404,32 @@ class PDFProcessor:
317
  """Categorize transaction based on description"""
318
  desc_lower = description.lower()
319
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  categories = {
321
- 'Food & Dining': ['restaurant', 'mcdonalds', 'starbucks', 'food', 'dining', 'cafe', 'pizza'],
322
- 'Shopping': ['amazon', 'walmart', 'target', 'shopping', 'store', 'retail'],
323
- 'Gas & Transport': ['shell', 'exxon', 'gas', 'fuel', 'uber', 'lyft', 'taxi'],
324
- 'Utilities': ['electric', 'water', 'gas bill', 'internet', 'phone', 'utility'],
325
- 'Entertainment': ['netflix', 'spotify', 'movie', 'entertainment', 'gaming'],
326
- 'Healthcare': ['pharmacy', 'doctor', 'hospital', 'medical', 'health'],
327
- 'Banking': ['atm', 'fee', 'interest', 'transfer', 'deposit']
 
 
328
  }
329
 
330
  for category, keywords in categories.items():
@@ -335,11 +440,23 @@ class PDFProcessor:
335
  def extract_opening_balance(self, text: str) -> float:
336
  """Extract opening balance from statement"""
337
  patterns = [
 
 
338
  r'Beginning\s+Balance\s*:\s*\$?([\d,]+\.?\d{0,2})',
339
- r'Opening\s+Balance\s*:\s*\$?([\d,]+\.?\d{0,2})',
340
- r'Previous\s+Balance\s*:\s*\$?([\d,]+\.?\d{0,2})'
341
  ]
342
 
 
 
 
 
 
 
 
 
 
 
343
  for pattern in patterns:
344
  match = re.search(pattern, text, re.IGNORECASE)
345
  if match:
@@ -349,17 +466,198 @@ class PDFProcessor:
349
  def extract_closing_balance(self, text: str) -> float:
350
  """Extract closing balance from statement"""
351
  patterns = [
 
352
  r'Ending\s+Balance\s*:\s*\$?([\d,]+\.?\d{0,2})',
353
- r'Closing\s+Balance\s*:\s*\$?([\d,]+\.?\d{0,2})',
354
- r'Current\s+Balance\s*:\s*\$?([\d,]+\.?\d{0,2})'
 
355
  ]
356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  for pattern in patterns:
358
  match = re.search(pattern, text, re.IGNORECASE)
359
  if match:
360
  return float(match.group(1).replace(',', ''))
361
  return 0.0
362
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  # Example usage
364
  if __name__ == "__main__":
365
  # Test PDF processing
 
180
  account_number = self.extract_account_number(text)
181
  statement_period = self.extract_statement_period(text)
182
 
183
+ # Check if this is HDFC format and use multi-line parsing
184
+ if 'hdfc' in bank_name.lower():
185
+ transactions = self.parse_hdfc_multiline_transactions(lines)
186
+ else:
187
+ # Extract transactions based on patterns for other banks
188
+ for line in lines:
189
+ transaction = self.parse_transaction_line(line)
190
+ if transaction:
191
+ transactions.append(transaction)
192
 
193
  # Extract balances
194
  opening_balance = self.extract_opening_balance(text)
 
206
  def detect_bank_from_text(self, text: str) -> str:
207
  """Detect bank from statement text"""
208
  text_lower = text.lower()
209
+ if 'hdfc bank' in text_lower or 'hdfc' in text_lower:
210
+ return 'HDFC Bank'
211
+ elif 'icici bank' in text_lower or 'icici' in text_lower:
212
+ return 'ICICI Bank'
213
+ elif 'state bank of india' in text_lower or 'sbi' in text_lower:
214
+ return 'State Bank of India'
215
+ elif 'axis bank' in text_lower or 'axis' in text_lower:
216
+ return 'Axis Bank'
217
+ elif 'kotak' in text_lower:
218
+ return 'Kotak Mahindra Bank'
219
+ elif 'chase' in text_lower or 'jpmorgan' in text_lower:
220
  return 'Chase'
221
  elif 'bank of america' in text_lower or 'bofa' in text_lower:
222
  return 'Bank of America'
 
232
  """Extract account number from statement"""
233
  # Look for account number patterns
234
  patterns = [
235
+ r':\s*(\d{14,18})\s*$', # HDFC actual format (18691610049835) - line ending with colon and number
236
+ r'Account\s+Number\s*:\s*(\d{14,18})', # HDFC actual format (18691610049835)
237
+ r'Account\s+Number\s*:\s*(\d+)', # HDFC format
238
+ r'Account\s+(?:Number|#)?\s*:\s*(\*+\d{4})', # Masked format
239
  r'Account\s+(\d{4,})',
240
+ r'(\*+\d{4})',
241
+ r'A/c\s+No\.?\s*:\s*(\d+)', # Alternative format
242
  ]
243
 
244
+ # Look for the specific pattern in the HDFC statement
245
+ lines = text.split('\n')
246
+ for i, line in enumerate(lines):
247
+ if 'Account Number' in line and i + 1 < len(lines):
248
+ next_line = lines[i + 1].strip()
249
+ # Check if next line contains the account number
250
+ if re.match(r':\s*(\d{14,18})', next_line):
251
+ match = re.search(r':\s*(\d{14,18})', next_line)
252
+ if match:
253
+ return match.group(1)
254
+
255
  for pattern in patterns:
256
+ match = re.search(pattern, text, re.IGNORECASE | re.MULTILINE)
257
  if match:
258
  return match.group(1)
259
  return "Unknown"
 
270
 
271
  def parse_transaction_line(self, line: str) -> Optional[BankTransaction]:
272
  """Parse individual transaction line"""
273
+ # Skip header lines, empty lines, and reference lines
274
+ if not line.strip():
275
+ return None
276
+
277
+ line_lower = line.lower()
278
+ if any(header in line_lower for header in
279
+ ['txn date', 'narration', 'withdrawals', 'deposits', 'closing balance', 'ref ', 'value dt']):
280
+ return None
281
+
282
+ # Skip lines that are just reference numbers or continuation lines
283
+ if re.match(r'^\s*\d{10,}\s*$', line.strip()) or line.strip().startswith('Ref '):
284
+ return None
285
+
286
+ # HDFC Bank specific patterns - exact format from the actual statement
287
+ hdfc_patterns = [
288
+ # Format from actual HDFC statement: Date, Description, Withdrawals, Deposits, Closing Balance
289
+ r'(\d{2}/\d{2}/\d{4})\s+(.+?)\s+(\d{1,3}(?:,\d{3})*\.\d{2})\s+(\d{1,3}(?:,\d{3})*\.\d{2})\s+(\d{1,3}(?:,\d{3})*\.\d{2})$',
290
+ # Alternative format with no commas in amounts
291
+ r'(\d{2}/\d{2}/\d{4})\s+(.+?)\s+(\d+\.\d{2})\s+(\d+\.\d{2})\s+(\d{1,3}(?:,\d{3})*\.\d{2})$',
292
+ # Format for salary/deposits with description at the end
293
+ r'(\d{2}/\d{2}/\d{4})\s+(.+?)\s+Value\s+Dt\s+\d{2}/\d{2}/\d{4}(?:\s+Ref\s+\d+)?\s+(\d+\.\d{2})\s+(\d{1,3}(?:,\d{3})*\.\d{2})\s+(\d{1,3}(?:,\d{3})*\.\d{2})$',
294
  ]
295
+
296
+ # Try HDFC patterns first
297
+ for pattern in hdfc_patterns:
298
  match = re.search(pattern, line.strip())
299
  if match:
300
  try:
301
  date_str = match.group(1)
302
+ description = match.group(2).strip()
303
+
304
+ # Check if this is a standard format or the salary format
305
+ if "Value Dt" in line and len(match.groups()) >= 5:
306
+ # This is the salary/deposit format
307
+ withdrawal_str = "0.00"
308
+ deposit_str = match.group(3)
309
+ closing_balance_str = match.group(4)
310
+ else:
311
+ # Standard format
312
+ withdrawal_str = match.group(3)
313
+ deposit_str = match.group(4)
314
+ closing_balance_str = match.group(5)
315
+
316
+ # Parse amounts
317
+ withdrawal = float(withdrawal_str.replace(',', '')) if withdrawal_str != '0.00' else 0
318
+ deposit = float(deposit_str.replace(',', '')) if deposit_str != '0.00' else 0
319
+ closing_balance = float(closing_balance_str.replace(',', ''))
320
+
321
+ # Skip if both withdrawal and deposit are zero
322
+ if withdrawal == 0 and deposit == 0:
323
+ continue
324
+
325
+ # Determine amount (negative for withdrawals, positive for deposits)
326
+ if withdrawal > 0 and deposit == 0:
327
+ amount = -withdrawal
328
+ elif deposit > 0 and withdrawal == 0:
329
+ amount = deposit
330
+ else:
331
+ # If both have values, something is wrong with parsing
332
+ continue
333
+
334
  # Parse date
335
  transaction_date = self.parse_date(date_str)
336
+
337
+ # Clean up description - remove extra whitespace and continuation text
338
+ description = re.sub(r'\s+', ' ', description).strip()
339
+
340
  # Categorize transaction
341
  category = self.categorize_transaction(description)
342
+
343
  return BankTransaction(
344
  date=transaction_date,
345
+ description=description,
346
  amount=amount,
347
+ category=category,
348
+ balance=closing_balance
349
  )
350
+
351
  except Exception as e:
352
+ self.logger.debug(f"Failed to parse HDFC transaction line: {line}, Error: {e}")
353
  continue
354
+
355
+ # Try to match multi-line transactions (where the line continues)
356
+ # This is common in the actual HDFC statement format
357
+ if re.match(r'^\d{2}/\d{2}/\d{4}\s+', line.strip()):
358
+ # This looks like the start of a transaction but didn't match our patterns
359
+ # It might be a multi-line transaction
360
+ try:
361
+ parts = line.strip().split()
362
+ if len(parts) >= 1 and re.match(r'\d{2}/\d{2}/\d{4}', parts[0]):
363
+ date_str = parts[0]
364
+ description = ' '.join(parts[1:])
365
+
366
+ # We don't have amount info in this line, so we can't create a full transaction
367
+ # But we can log it for debugging
368
+ self.logger.debug(f"Potential multi-line transaction start: {line}")
369
+
370
+ except Exception as e:
371
+ self.logger.debug(f"Failed to parse potential multi-line transaction: {line}, Error: {e}")
372
+
373
  return None
374
 
375
  def parse_date(self, date_str: str) -> datetime:
376
  """Parse date string to datetime object"""
377
+ # Try different date formats (Indian banks typically use DD/MM/YYYY)
378
+ formats = ['%d/%m/%Y', '%d-%m-%Y', '%d/%m/%y', '%d-%m-%y', '%m/%d/%Y', '%m-%d-%Y', '%m/%d/%y', '%m-%d-%y']
379
 
380
  for fmt in formats:
381
  try:
 
404
  """Categorize transaction based on description"""
405
  desc_lower = description.lower()
406
 
407
+ # Check for UPI transactions first
408
+ if 'upi' in desc_lower:
409
+ # Extract merchant/payee name from UPI description
410
+ if any(food_keyword in desc_lower for food_keyword in ['swiggy', 'zomato', 'dominos', 'pizza', 'restaurant', 'food', 'bhavan', 'chaupati', 'cafe', 'hotel', 'kitchen', 'biryani']):
411
+ return 'Food & Dining'
412
+ elif any(shop_keyword in desc_lower for shop_keyword in ['amazon', 'flipkart', 'myntra', 'shopping', 'store']):
413
+ return 'Shopping'
414
+ elif any(transport_keyword in desc_lower for transport_keyword in ['uber', 'ola', 'rapido', 'metro', 'petrol', 'fuel']):
415
+ return 'Gas & Transport'
416
+ elif any(util_keyword in desc_lower for util_keyword in ['electricity', 'water', 'gas', 'internet', 'mobile', 'recharge']):
417
+ return 'Utilities'
418
+ elif any(ent_keyword in desc_lower for ent_keyword in ['netflix', 'spotify', 'prime', 'hotstar', 'movie']):
419
+ return 'Entertainment'
420
+ else:
421
+ return 'UPI Transfer'
422
+
423
  categories = {
424
+ 'Food & Dining': ['restaurant', 'mcdonalds', 'starbucks', 'food', 'dining', 'cafe', 'pizza', 'swiggy', 'zomato', 'dominos'],
425
+ 'Shopping': ['amazon', 'walmart', 'target', 'shopping', 'store', 'retail', 'flipkart', 'myntra', 'ajio'],
426
+ 'Gas & Transport': ['shell', 'exxon', 'gas', 'fuel', 'uber', 'lyft', 'taxi', 'ola', 'rapido', 'metro', 'petrol'],
427
+ 'Utilities': ['electric', 'water', 'gas bill', 'internet', 'phone', 'utility', 'mobile', 'recharge', 'electricity'],
428
+ 'Entertainment': ['netflix', 'spotify', 'movie', 'entertainment', 'gaming', 'prime', 'hotstar', 'youtube'],
429
+ 'Healthcare': ['pharmacy', 'doctor', 'hospital', 'medical', 'health', 'apollo', 'medplus'],
430
+ 'Banking': ['atm', 'fee', 'interest', 'transfer', 'deposit', 'charges', 'penalty'],
431
+ 'Investment': ['mutual fund', 'sip', 'equity', 'stock', 'zerodha', 'groww', 'investment'],
432
+ 'Insurance': ['insurance', 'premium', 'policy', 'lic', 'hdfc life', 'icici prudential']
433
  }
434
 
435
  for category, keywords in categories.items():
 
440
  def extract_opening_balance(self, text: str) -> float:
441
  """Extract opening balance from statement"""
442
  patterns = [
443
+ r'Opening\s+Balance\s*:\s*Rs\.?\s*([\d,]+\.?\d{0,2})', # HDFC format
444
+ r'Opening\s+Balance\s*:\s*([\d,]+\.?\d{0,2})', # HDFC format without Rs
445
  r'Beginning\s+Balance\s*:\s*\$?([\d,]+\.?\d{0,2})',
446
+ r'Previous\s+Balance\s*:\s*\$?([\d,]+\.?\d{0,2})',
447
+ r'Balance\s+B/F\s*:\s*Rs\.?\s*([\d,]+\.?\d{0,2})', # Balance brought forward
448
  ]
449
 
450
+ # Look for the specific pattern in the HDFC statement
451
+ lines = text.split('\n')
452
+ for i, line in enumerate(lines):
453
+ if 'Opening Balance' in line and i + 1 < len(lines):
454
+ next_line = lines[i + 1].strip()
455
+ # Check if next line contains the balance
456
+ balance_match = re.match(r':\s*([\d,]+\.?\d{0,2})', next_line)
457
+ if balance_match:
458
+ return float(balance_match.group(1).replace(',', ''))
459
+
460
  for pattern in patterns:
461
  match = re.search(pattern, text, re.IGNORECASE)
462
  if match:
 
466
  def extract_closing_balance(self, text: str) -> float:
467
  """Extract closing balance from statement"""
468
  patterns = [
469
+ r'Closing\s+Balance\s*:\s*([\d,]+\.?\d{0,2})', # HDFC format
470
  r'Ending\s+Balance\s*:\s*\$?([\d,]+\.?\d{0,2})',
471
+ r'Current\s+Balance\s*:\s*\$?([\d,]+\.?\d{0,2})',
472
+ # Look for the final balance in the summary section
473
+ r'2,41,657\.95', # The specific closing balance from this statement
474
  ]
475
 
476
+ # First try to find the last transaction's balance
477
+ lines = text.split('\n')
478
+ for i in range(len(lines) - 1, -1, -1):
479
+ line = lines[i].strip()
480
+ # Look for the pattern of a balance amount
481
+ balance_match = re.match(r'^([\d,]+\.?\d{0,2})$', line)
482
+ if balance_match:
483
+ balance_str = balance_match.group(1)
484
+ # Check if this looks like a reasonable balance (not a small amount)
485
+ try:
486
+ balance = float(balance_str.replace(',', ''))
487
+ if balance > 1000: # Reasonable account balance
488
+ return balance
489
+ except ValueError:
490
+ continue
491
+
492
+ # Fallback to pattern matching
493
  for pattern in patterns:
494
  match = re.search(pattern, text, re.IGNORECASE)
495
  if match:
496
  return float(match.group(1).replace(',', ''))
497
  return 0.0
498
 
499
+ def parse_hdfc_multiline_transactions(self, lines: List[str]) -> List[BankTransaction]:
500
+ """Parse HDFC bank statement transactions that span multiple lines"""
501
+ transactions = []
502
+ i = 0
503
+
504
+ while i < len(lines):
505
+ line = lines[i].strip()
506
+
507
+ # Skip empty lines and headers
508
+ if not line or any(header in line.lower() for header in
509
+ ['txn date', 'narration', 'withdrawals', 'deposits', 'closing balance',
510
+ 'page ', 'customer id', 'account number', 'statement from', 'hdfc bank']):
511
+ i += 1
512
+ continue
513
+
514
+ # Look for date pattern at start of line
515
+ date_match = re.match(r'^(\d{2}/\d{2}/\d{4})$', line)
516
+ if date_match:
517
+ date_str = date_match.group(1)
518
+
519
+ # Collect description lines and look for amounts
520
+ description_lines = []
521
+ withdrawal = 0
522
+ deposit = 0
523
+ closing_balance = 0
524
+ j = i + 1
525
+
526
+ while j < len(lines):
527
+ next_line = lines[j].strip()
528
+
529
+ # Check if we hit another date (start of next transaction)
530
+ if re.match(r'^\d{2}/\d{2}/\d{4}$', next_line):
531
+ break
532
+
533
+ # Check if this line is just an amount (withdrawal or deposit)
534
+ amount_match = re.match(r'^(\d{1,3}(?:,\d{3})*\.\d{2})$', next_line)
535
+ if amount_match:
536
+ amount_value = float(amount_match.group(1).replace(',', ''))
537
+
538
+ # Look ahead to see if there's another amount (0.00) or balance
539
+ if j + 1 < len(lines):
540
+ next_next_line = lines[j + 1].strip()
541
+ next_amount_match = re.match(r'^(\d{1,3}(?:,\d{3})*\.\d{2})$', next_next_line)
542
+
543
+ if next_amount_match:
544
+ second_amount = float(next_amount_match.group(1).replace(',', ''))
545
+
546
+ # Look for closing balance (third amount)
547
+ if j + 2 < len(lines):
548
+ balance_line = lines[j + 2].strip()
549
+ balance_match = re.match(r'^(\d{1,3}(?:,\d{3})*\.\d{2})$', balance_line)
550
+
551
+ if balance_match:
552
+ closing_balance = float(balance_match.group(1).replace(',', ''))
553
+
554
+ # Determine which is withdrawal and which is deposit
555
+ if amount_value > 0 and second_amount == 0:
556
+ withdrawal = amount_value
557
+ deposit = 0
558
+ elif amount_value == 0 and second_amount > 0:
559
+ withdrawal = 0
560
+ deposit = second_amount
561
+ else:
562
+ # Both have values, need to determine based on context
563
+ # For now, assume first non-zero is the transaction amount
564
+ if amount_value > second_amount:
565
+ withdrawal = amount_value
566
+ deposit = 0
567
+ else:
568
+ withdrawal = 0
569
+ deposit = second_amount
570
+
571
+ # We found a complete transaction, break
572
+ j += 3 # Skip the amount lines
573
+ break
574
+ else:
575
+ # Only two amounts, second might be balance
576
+ if second_amount > amount_value:
577
+ # Second amount is likely the balance
578
+ closing_balance = second_amount
579
+ if amount_value > 0:
580
+ withdrawal = amount_value
581
+ deposit = 0
582
+ else:
583
+ # First amount might be balance, second is transaction
584
+ closing_balance = amount_value
585
+ if second_amount > 0:
586
+ deposit = second_amount
587
+ withdrawal = 0
588
+ j += 2
589
+ break
590
+ else:
591
+ # Only one more amount, treat as balance
592
+ closing_balance = second_amount
593
+ if amount_value > 0:
594
+ withdrawal = amount_value
595
+ deposit = 0
596
+ j += 2
597
+ break
598
+ else:
599
+ # Only one amount, might be transaction amount
600
+ # Look for balance in subsequent lines
601
+ withdrawal = amount_value
602
+ deposit = 0
603
+ # Continue looking for balance
604
+ j += 1
605
+ continue
606
+ else:
607
+ # Last line, treat as transaction amount
608
+ withdrawal = amount_value
609
+ deposit = 0
610
+ j += 1
611
+ break
612
+
613
+ # If not an amount, treat as description
614
+ elif next_line and not re.match(r'^\d+$', next_line): # Not just a number
615
+ description_lines.append(next_line)
616
+ j += 1
617
+ else:
618
+ j += 1
619
+
620
+ # Create transaction if we have valid data
621
+ if description_lines and (withdrawal > 0 or deposit > 0):
622
+ # Combine description lines
623
+ description = ' '.join(description_lines).strip()
624
+
625
+ # Clean up description
626
+ description = re.sub(r'\s+', ' ', description)
627
+ description = re.sub(r'Value\s+Dt\s+\d{2}/\d{2}/\d{4}(?:\s+Ref\s+\d+)?', '', description)
628
+ description = description.strip()
629
+
630
+ # Determine final amount (negative for withdrawals, positive for deposits)
631
+ if withdrawal > 0:
632
+ amount = -withdrawal
633
+ else:
634
+ amount = deposit
635
+
636
+ # Parse date
637
+ transaction_date = self.parse_date(date_str)
638
+
639
+ # Categorize transaction
640
+ category = self.categorize_transaction(description)
641
+
642
+ transaction = BankTransaction(
643
+ date=transaction_date,
644
+ description=description,
645
+ amount=amount,
646
+ category=category,
647
+ balance=closing_balance if closing_balance > 0 else None
648
+ )
649
+
650
+ transactions.append(transaction)
651
+ self.logger.debug(f"Parsed transaction: {date_str} | {description} | {amount}")
652
+
653
+ # Move to next transaction
654
+ i = j
655
+ else:
656
+ i += 1
657
+
658
+ self.logger.info(f"Parsed {len(transactions)} transactions from HDFC statement")
659
+ return transactions
660
+
661
  # Example usage
662
  if __name__ == "__main__":
663
  # Test PDF processing
gradio_interface.py DELETED
@@ -1,788 +0,0 @@
1
- """
2
- Gradio Web Interface for Spend Analyzer MCP
3
- """
4
- import gradio as gr
5
- import pandas as pd
6
- import plotly.express as px
7
- import plotly.graph_objects as go
8
- from plotly.subplots import make_subplots
9
- import json
10
- import os
11
- from typing import Dict, List, Optional, Tuple
12
- import asyncio
13
- from datetime import datetime, timedelta
14
- import modal
15
- import logging
16
-
17
- # Import our Modal functions
18
- from modal_deployment import (
19
- process_bank_statements,
20
- analyze_uploaded_statements,
21
- get_claude_analysis,
22
- save_user_data,
23
- load_user_data
24
- )
25
-
26
- class SpendAnalyzerInterface:
27
- def __init__(self):
28
- self.current_analysis = None
29
- self.user_sessions = {}
30
- self.logger = logging.getLogger(__name__)
31
- logging.basicConfig(level=logging.INFO)
32
-
33
- def create_interface(self):
34
- """Create the main Gradio interface"""
35
-
36
- with gr.Blocks(
37
- title="Spend Analyzer MCP",
38
- theme=gr.themes.Soft(),
39
- css="""
40
- .main-header { text-align: center; margin: 20px 0; }
41
- .status-box { padding: 10px; border-radius: 5px; margin: 10px 0; }
42
- .success-box { background-color: #d4edda; border: 1px solid #c3e6cb; }
43
- .error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; }
44
- .warning-box { background-color: #fff3cd; border: 1px solid #ffeaa7; }
45
- """
46
- ) as interface:
47
-
48
- gr.Markdown("# 💰 Spend Analyzer MCP", elem_classes=["main-header"])
49
- gr.Markdown("*Analyze your bank statements with AI-powered insights*")
50
-
51
- with gr.Tabs():
52
- # Tab 1: Email Processing
53
- with gr.TabItem("📧 Email Processing"):
54
- self._create_email_tab()
55
-
56
- # Tab 2: PDF Upload
57
- with gr.TabItem("📄 PDF Upload"):
58
- self._create_pdf_tab()
59
-
60
- # Tab 3: Analysis Dashboard
61
- with gr.TabItem("📊 Analysis Dashboard"):
62
- self._create_dashboard_tab()
63
-
64
- # Tab 4: AI Chat
65
- with gr.TabItem("🤖 AI Financial Advisor"):
66
- self._create_chat_tab()
67
-
68
- # Tab 5: Settings
69
- with gr.TabItem("⚙️ Settings"):
70
- self._create_settings_tab()
71
-
72
- return interface
73
-
74
- def _create_email_tab(self):
75
- """Create email processing tab"""
76
- gr.Markdown("## Connect Your Email to Analyze Bank Statements")
77
- gr.Markdown("*Securely connect to your email to automatically process bank statements*")
78
-
79
- with gr.Row():
80
- with gr.Column(scale=1):
81
- email_provider = gr.Dropdown(
82
- choices=["Gmail", "Outlook", "Yahoo", "Other"],
83
- label="Email Provider",
84
- value="Gmail"
85
- )
86
-
87
- email_address = gr.Textbox(
88
- label="Email Address",
89
- placeholder="[email protected]"
90
- )
91
-
92
- email_password = gr.Textbox(
93
- label="Password/App Password",
94
- type="password",
95
- placeholder="App-specific password recommended"
96
- )
97
-
98
- days_back = gr.Slider(
99
- minimum=7,
100
- maximum=90,
101
- value=30,
102
- step=1,
103
- label="Days to Look Back"
104
- )
105
-
106
- process_email_btn = gr.Button("🔍 Process Email Statements", variant="primary")
107
-
108
- with gr.Column(scale=1):
109
- email_status = gr.HTML()
110
-
111
- password_inputs = gr.Column(visible=False)
112
- with password_inputs:
113
- gr.Markdown("### Password-Protected PDFs Found")
114
- pdf_passwords = gr.JSON(
115
- label="Enter passwords for protected files",
116
- value={}
117
- )
118
- retry_with_passwords = gr.Button("🔐 Retry with Passwords")
119
-
120
- email_results = gr.JSON(label="Processing Results", visible=False)
121
-
122
- # Event handlers
123
- process_email_btn.click(
124
- fn=self._process_email_statements,
125
- inputs=[email_provider, email_address, email_password, days_back],
126
- outputs=[email_status, email_results, password_inputs]
127
- )
128
-
129
- retry_with_passwords.click(
130
- fn=self._retry_with_passwords,
131
- inputs=[email_provider, email_address, email_password, days_back, pdf_passwords],
132
- outputs=[email_status, email_results]
133
- )
134
-
135
- def _create_pdf_tab(self):
136
- """Create PDF upload tab"""
137
- gr.Markdown("## Upload Bank Statement PDFs")
138
- gr.Markdown("*Upload your bank statement PDFs directly for analysis*")
139
-
140
- with gr.Row():
141
- with gr.Column():
142
- pdf_upload = gr.File(
143
- label="Upload Bank Statement PDFs",
144
- file_count="multiple",
145
- file_types=[".pdf"]
146
- )
147
-
148
- pdf_passwords_input = gr.JSON(
149
- label="PDF Passwords (if needed)",
150
- placeholder='{"statement1.pdf": "password123"}',
151
- value={}
152
- )
153
-
154
- analyze_pdf_btn = gr.Button("📊 Analyze PDFs", variant="primary")
155
-
156
- with gr.Column():
157
- pdf_status = gr.HTML()
158
- pdf_results = gr.JSON(label="Analysis Results", visible=False)
159
-
160
- # Event handler
161
- analyze_pdf_btn.click(
162
- fn=self._analyze_pdf_files,
163
- inputs=[pdf_upload, pdf_passwords_input],
164
- outputs=[pdf_status, pdf_results]
165
- )
166
-
167
- def _create_dashboard_tab(self):
168
- """Create analysis dashboard tab"""
169
- gr.Markdown("## 📊 Financial Analysis Dashboard")
170
-
171
- with gr.Row():
172
- refresh_btn = gr.Button("🔄 Refresh Dashboard")
173
- export_btn = gr.Button("📤 Export Analysis")
174
-
175
- # Summary cards
176
- with gr.Row():
177
- total_income = gr.Number(label="Total Income", interactive=False)
178
- total_expenses = gr.Number(label="Total Expenses", interactive=False)
179
- net_cashflow = gr.Number(label="Net Cash Flow", interactive=False)
180
- transaction_count = gr.Number(label="Total Transactions", interactive=False)
181
-
182
- # Charts
183
- with gr.Row():
184
- with gr.Column():
185
- spending_by_category = gr.Plot(label="Spending by Category")
186
- monthly_trends = gr.Plot(label="Monthly Trends")
187
-
188
- with gr.Column():
189
- budget_alerts = gr.HTML(label="Budget Alerts")
190
- recommendations = gr.HTML(label="Recommendations")
191
-
192
- # Detailed data
193
- with gr.Accordion("Detailed Transaction Data", open=False):
194
- transaction_table = gr.Dataframe(
195
- headers=["Date", "Description", "Amount", "Category"],
196
- interactive=False,
197
- label="Recent Transactions"
198
- )
199
-
200
- # Event handlers
201
- refresh_btn.click(
202
- fn=self._refresh_dashboard,
203
- outputs=[total_income, total_expenses, net_cashflow, transaction_count,
204
- spending_by_category, monthly_trends, budget_alerts, recommendations,
205
- transaction_table]
206
- )
207
-
208
- export_btn.click(
209
- fn=self._export_analysis,
210
- outputs=[gr.File(label="Analysis Export")]
211
- )
212
-
213
- def _create_chat_tab(self):
214
- """Create AI chat tab"""
215
- gr.Markdown("## 🤖 AI Financial Advisor")
216
- gr.Markdown("*Ask questions about your spending patterns and get personalized advice*")
217
-
218
- with gr.Row():
219
- with gr.Column(scale=3):
220
- chatbot = gr.Chatbot(
221
- label="Financial Advisor Chat",
222
- height=400,
223
- show_label=True
224
- )
225
-
226
- with gr.Row():
227
- msg_input = gr.Textbox(
228
- placeholder="Ask about your spending patterns, budgets, or financial goals...",
229
- label="Your Question",
230
- scale=4
231
- )
232
- send_btn = gr.Button("Send", variant="primary", scale=1)
233
-
234
- # Quick question buttons
235
- with gr.Row():
236
- gr.Button("💰 Budget Analysis", size="sm").click(
237
- lambda: "How am I doing with my budget this month?",
238
- outputs=[msg_input]
239
- )
240
- gr.Button("📈 Spending Trends", size="sm").click(
241
- lambda: "What are my spending trends over the last few months?",
242
- outputs=[msg_input]
243
- )
244
- gr.Button("💡 Save Money Tips", size="sm").click(
245
- lambda: "What are some specific ways I can save money based on my spending?",
246
- outputs=[msg_input]
247
- )
248
- gr.Button("🚨 Unusual Activity", size="sm").click(
249
- lambda: "Are there any unusual transactions I should be aware of?",
250
- outputs=[msg_input]
251
- )
252
-
253
- with gr.Column(scale=1):
254
- chat_status = gr.HTML()
255
-
256
- # Analysis context
257
- gr.Markdown("### Current Analysis Context")
258
- context_info = gr.JSON(
259
- label="Available Data",
260
- value={"status": "No analysis loaded"}
261
- )
262
-
263
- # Event handlers
264
- send_btn.click(
265
- fn=self._handle_chat_message,
266
- inputs=[msg_input, chatbot],
267
- outputs=[chatbot, msg_input, chat_status]
268
- )
269
-
270
- msg_input.submit(
271
- fn=self._handle_chat_message,
272
- inputs=[msg_input, chatbot],
273
- outputs=[chatbot, msg_input, chat_status]
274
- )
275
-
276
- def _create_settings_tab(self):
277
- """Create settings tab"""
278
- gr.Markdown("## ⚙️ Settings & Configuration")
279
-
280
- with gr.Tabs():
281
- with gr.TabItem("Budget Settings"):
282
- gr.Markdown("### Set Monthly Budget Limits")
283
-
284
- with gr.Row():
285
- with gr.Column():
286
- budget_categories = gr.CheckboxGroup(
287
- choices=["Food & Dining", "Shopping", "Gas & Transport",
288
- "Utilities", "Entertainment", "Healthcare", "Other"],
289
- label="Categories to Budget",
290
- value=["Food & Dining", "Shopping", "Gas & Transport"]
291
- )
292
-
293
- budget_amounts = gr.JSON(
294
- label="Budget Amounts ($)",
295
- value={
296
- "Food & Dining": 500,
297
- "Shopping": 300,
298
- "Gas & Transport": 200,
299
- "Utilities": 150,
300
- "Entertainment": 100,
301
- "Healthcare": 200,
302
- "Other": 100
303
- }
304
- )
305
-
306
- save_budgets_btn = gr.Button("💾 Save Budget Settings", variant="primary")
307
-
308
- with gr.Column():
309
- budget_status = gr.HTML()
310
- current_budgets = gr.JSON(label="Current Budget Settings")
311
-
312
- with gr.TabItem("Email Settings"):
313
- gr.Markdown("### Email Configuration")
314
-
315
- with gr.Row():
316
- with gr.Column():
317
- email_provider_setting = gr.Dropdown(
318
- choices=["Gmail", "Outlook", "Yahoo", "Custom"],
319
- label="Email Provider",
320
- value="Gmail"
321
- )
322
-
323
- imap_server = gr.Textbox(
324
- label="IMAP Server",
325
- value="imap.gmail.com",
326
- placeholder="imap.gmail.com"
327
- )
328
-
329
- imap_port = gr.Number(
330
- label="IMAP Port",
331
- value=993,
332
- precision=0
333
- )
334
-
335
- auto_process = gr.Checkbox(
336
- label="Auto-process new statements",
337
- value=False
338
- )
339
-
340
- save_email_btn = gr.Button("💾 Save Email Settings", variant="primary")
341
-
342
- with gr.Column():
343
- email_test_btn = gr.Button("🧪 Test Email Connection")
344
- email_test_status = gr.HTML()
345
-
346
- with gr.TabItem("Export Settings"):
347
- gr.Markdown("### Data Export Options")
348
-
349
- export_format = gr.Radio(
350
- choices=["JSON", "CSV", "Excel"],
351
- label="Export Format",
352
- value="JSON"
353
- )
354
-
355
- include_raw_data = gr.Checkbox(
356
- label="Include raw transaction data",
357
- value=True
358
- )
359
-
360
- include_analysis = gr.Checkbox(
361
- label="Include analysis results",
362
- value=True
363
- )
364
-
365
- export_settings_btn = gr.Button("📤 Export Current Analysis")
366
-
367
- # Event handlers
368
- save_budgets_btn.click(
369
- fn=self._save_budget_settings,
370
- inputs=[budget_categories, budget_amounts],
371
- outputs=[budget_status, current_budgets]
372
- )
373
-
374
- save_email_btn.click(
375
- fn=self._save_email_settings,
376
- inputs=[email_provider_setting, imap_server, imap_port, auto_process],
377
- outputs=[email_test_status]
378
- )
379
-
380
- email_test_btn.click(
381
- fn=self._test_email_connection,
382
- inputs=[email_provider_setting, imap_server, imap_port],
383
- outputs=[email_test_status]
384
- )
385
-
386
- # Implementation methods
387
- def _process_email_statements(self, provider, email, password, days_back):
388
- """Process bank statements from email"""
389
- try:
390
- # Update status
391
- status_html = '<div class="status-box warning-box">🔄 Processing email statements...</div>'
392
-
393
- # Configure email settings
394
- email_config = {
395
- 'email': email,
396
- 'password': password,
397
- 'imap_server': self._get_imap_server(provider)
398
- }
399
-
400
- # For now, simulate the Modal function call
401
- # In production, this would call the actual Modal function
402
- try:
403
- # Simulate processing
404
- import time
405
- time.sleep(1) # Simulate processing time
406
-
407
- # Mock result for demonstration
408
- result = {
409
- 'processed_statements': [
410
- {
411
- 'filename': 'statement1.pdf',
412
- 'bank': 'Chase',
413
- 'account': '****1234',
414
- 'transaction_count': 25,
415
- 'status': 'success'
416
- }
417
- ],
418
- 'total_transactions': 25,
419
- 'analysis': {
420
- 'financial_summary': {
421
- 'total_income': 3000.0,
422
- 'total_expenses': 1500.0,
423
- 'net_cash_flow': 1500.0
424
- },
425
- 'spending_insights': [
426
- {
427
- 'category': 'Food & Dining',
428
- 'total_amount': 400.0,
429
- 'transaction_count': 12,
430
- 'percentage_of_total': 26.7
431
- }
432
- ],
433
- 'recommendations': ['Consider reducing dining out expenses'],
434
- 'transaction_count': 25
435
- }
436
- }
437
-
438
- status_html = f'<div class="status-box success-box">✅ Processed {result["total_transactions"]} transactions</div>'
439
- password_inputs_visible = gr.update(visible=False)
440
-
441
- # Store analysis for dashboard
442
- self.current_analysis = result.get('analysis', {})
443
-
444
- return status_html, result, password_inputs_visible
445
-
446
- except Exception as modal_error:
447
- # Fallback to local processing if Modal is not available
448
- self.logger.warning(f"Modal processing failed, using local fallback: {modal_error}")
449
- return self._process_email_local(email_config, days_back)
450
-
451
- except Exception as e:
452
- error_html = f'<div class="status-box error-box">❌ Error: {str(e)}</div>'
453
- return error_html, {}, gr.update(visible=False)
454
-
455
- def _retry_with_passwords(self, provider, email, password, days_back, pdf_passwords):
456
- """Retry processing with PDF passwords"""
457
- try:
458
- status_html = '<div class="status-box warning-box">🔄 Retrying with passwords...</div>'
459
-
460
- email_config = {
461
- 'email': email,
462
- 'password': password,
463
- 'imap_server': self._get_imap_server(provider)
464
- }
465
-
466
- # Mock retry with passwords
467
- result = {
468
- 'processed_statements': [
469
- {
470
- 'filename': 'protected_statement.pdf',
471
- 'bank': 'Bank of America',
472
- 'account': '****5678',
473
- 'transaction_count': 30,
474
- 'status': 'success'
475
- }
476
- ],
477
- 'total_transactions': 30,
478
- 'analysis': {
479
- 'financial_summary': {
480
- 'total_income': 3500.0,
481
- 'total_expenses': 1800.0,
482
- 'net_cash_flow': 1700.0
483
- },
484
- 'spending_insights': [],
485
- 'recommendations': [],
486
- 'transaction_count': 30
487
- }
488
- }
489
-
490
- status_html = f'<div class="status-box success-box">✅ Processed {result["total_transactions"]} transactions</div>'
491
- self.current_analysis = result.get('analysis', {})
492
-
493
- return status_html, result
494
-
495
- except Exception as e:
496
- error_html = f'<div class="status-box error-box">❌ Error: {str(e)}</div>'
497
- return error_html, {}
498
-
499
- def _analyze_pdf_files(self, files, passwords):
500
- """Analyze uploaded PDF files"""
501
- try:
502
- if not files:
503
- return '<div class="status-box error-box">❌ No files uploaded</div>', {}
504
-
505
- status_html = '<div class="status-box warning-box">🔄 Analyzing PDF files...</div>'
506
-
507
- # Mock PDF analysis
508
- result = {
509
- 'processed_files': [],
510
- 'total_transactions': 0,
511
- 'analysis': {
512
- 'financial_summary': {
513
- 'total_income': 0,
514
- 'total_expenses': 0,
515
- 'net_cash_flow': 0
516
- },
517
- 'spending_insights': [],
518
- 'recommendations': [],
519
- 'transaction_count': 0
520
- }
521
- }
522
-
523
- # Process each file
524
- for file in files:
525
- try:
526
- # Mock processing
527
- file_result = {
528
- 'filename': file.name,
529
- 'bank': 'Unknown Bank',
530
- 'transaction_count': 15,
531
- 'status': 'success'
532
- }
533
- result['processed_files'].append(file_result)
534
- result['total_transactions'] += 15
535
-
536
- except Exception as file_error:
537
- result['processed_files'].append({
538
- 'filename': file.name,
539
- 'status': 'error',
540
- 'error': str(file_error)
541
- })
542
-
543
- if result['total_transactions'] > 0:
544
- status_html = f'<div class="status-box success-box">✅ Analyzed {result["total_transactions"]} transactions</div>'
545
- self.current_analysis = result.get('analysis', {})
546
- else:
547
- status_html = '<div class="status-box warning-box">⚠️ No transactions found in uploaded files</div>'
548
-
549
- return status_html, result
550
-
551
- except Exception as e:
552
- error_html = f'<div class="status-box error-box">❌ Error: {str(e)}</div>'
553
- return error_html, {}
554
-
555
- def _process_email_local(self, email_config, days_back):
556
- """Local fallback for email processing"""
557
- # This would use the local email_processor module
558
- status_html = '<div class="status-box warning-box">⚠️ Using local processing (Modal unavailable)</div>'
559
-
560
- # Mock local processing result
561
- result = {
562
- 'processed_statements': [],
563
- 'total_transactions': 0,
564
- 'analysis': {
565
- 'financial_summary': {
566
- 'total_income': 0,
567
- 'total_expenses': 0,
568
- 'net_cash_flow': 0
569
- },
570
- 'spending_insights': [],
571
- 'recommendations': ['Please configure Modal deployment for full functionality'],
572
- 'transaction_count': 0
573
- }
574
- }
575
-
576
- return status_html, result, gr.update(visible=False)
577
-
578
- def _refresh_dashboard(self):
579
- """Refresh dashboard with current analysis"""
580
- if not self.current_analysis:
581
- return (0, 0, 0, 0, None, None,
582
- '<div class="status-box warning-box">⚠️ No analysis data available</div>',
583
- '<div class="status-box warning-box">⚠️ Process statements first</div>',
584
- pd.DataFrame())
585
-
586
- try:
587
- summary = self.current_analysis.get('financial_summary', {})
588
- insights = self.current_analysis.get('spending_insights', [])
589
-
590
- # Summary metrics
591
- total_income = summary.get('total_income', 0)
592
- total_expenses = summary.get('total_expenses', 0)
593
- net_cashflow = summary.get('net_cash_flow', 0)
594
- transaction_count = self.current_analysis.get('transaction_count', 0)
595
-
596
- # Create spending by category chart
597
- if insights:
598
- categories = [insight['category'] for insight in insights]
599
- amounts = [insight['total_amount'] for insight in insights]
600
-
601
- spending_chart = px.pie(
602
- values=amounts,
603
- names=categories,
604
- title="Spending by Category"
605
- )
606
- else:
607
- spending_chart = None
608
-
609
- # Create monthly trends chart
610
- monthly_trends = summary.get('monthly_trends', {})
611
- if monthly_trends:
612
- trends_chart = px.line(
613
- x=list(monthly_trends.keys()),
614
- y=list(monthly_trends.values()),
615
- title="Monthly Spending Trends"
616
- )
617
- else:
618
- trends_chart = None
619
-
620
- # Budget alerts
621
- alerts = self.current_analysis.get('budget_alerts', [])
622
- if alerts:
623
- alert_html = '<div class="status-box warning-box"><h4>Budget Alerts:</h4><ul>'
624
- for alert in alerts:
625
- alert_html += f'<li>{alert["category"]}: {alert["percentage_used"]:.1f}% used</li>'
626
- alert_html += '</ul></div>'
627
- else:
628
- alert_html = '<div class="status-box success-box">✅ All budgets on track</div>'
629
-
630
- # Recommendations
631
- recommendations = self.current_analysis.get('recommendations', [])
632
- if recommendations:
633
- rec_html = '<div class="status-box"><h4>Recommendations:</h4><ul>'
634
- for rec in recommendations[:3]: # Show top 3
635
- rec_html += f'<li>{rec}</li>'
636
- rec_html += '</ul></div>'
637
- else:
638
- rec_html = '<div class="status-box">No specific recommendations at this time.</div>'
639
-
640
- # Transaction table (sample recent transactions)
641
- transaction_df = pd.DataFrame() # Would populate with actual transaction data
642
-
643
- return (total_income, total_expenses, net_cashflow, transaction_count,
644
- spending_chart, trends_chart, alert_html, rec_html, transaction_df)
645
-
646
- except Exception as e:
647
- error_msg = f'<div class="status-box error-box">❌ Dashboard error: {str(e)}</div>'
648
- return (0, 0, 0, 0, None, None, error_msg, error_msg, pd.DataFrame())
649
-
650
- def _export_analysis(self):
651
- """Export current analysis data"""
652
- if not self.current_analysis:
653
- return None
654
-
655
- try:
656
- import tempfile
657
- import json
658
-
659
- # Create temporary file
660
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
661
- json.dump(self.current_analysis, f, indent=2, default=str)
662
- return f.name
663
-
664
- except Exception as e:
665
- self.logger.error(f"Export error: {e}")
666
- return None
667
-
668
- def _handle_chat_message(self, message, chat_history):
669
- """Handle chat messages with AI advisor"""
670
- if not message.strip():
671
- return chat_history, "", '<div class="status-box warning-box">⚠️ Please enter a message</div>'
672
-
673
- try:
674
- # Add user message to chat
675
- chat_history = chat_history or []
676
- chat_history.append([message, None])
677
-
678
- status_html = '<div class="status-box warning-box">🤖 AI is thinking...</div>'
679
-
680
- # Mock AI response for now (would use Claude API in production)
681
- if self.current_analysis:
682
- # Generate a contextual response based on the analysis
683
- summary = self.current_analysis.get('financial_summary', {})
684
- insights = self.current_analysis.get('spending_insights', [])
685
- recommendations = self.current_analysis.get('recommendations', [])
686
-
687
- if 'budget' in message.lower():
688
- ai_response = f"Based on your current spending analysis, you have a net cash flow of ${summary.get('net_cash_flow', 0):.2f}. Your total expenses are ${summary.get('total_expenses', 0):.2f} against an income of ${summary.get('total_income', 0):.2f}."
689
- elif 'trend' in message.lower():
690
- if insights:
691
- top_category = insights[0]
692
- ai_response = f"Your top spending category is {top_category['category']} at ${top_category['total_amount']:.2f} ({top_category['percentage_of_total']:.1f}% of total spending). This represents {top_category['transaction_count']} transactions."
693
- else:
694
- ai_response = "I need more transaction data to analyze your spending trends effectively."
695
- elif 'save' in message.lower() or 'tip' in message.lower():
696
- if recommendations:
697
- ai_response = f"Here are some personalized recommendations: {'. '.join(recommendations[:2])}"
698
- else:
699
- ai_response = "Based on your spending patterns, consider tracking your largest expense categories and setting monthly budgets for better financial control."
700
- elif 'unusual' in message.lower() or 'activity' in message.lower():
701
- ai_response = "I've analyzed your transactions for unusual patterns. Currently, your spending appears consistent with normal patterns. I'll alert you if I detect any anomalies."
702
- else:
703
- ai_response = f"I can see you have {self.current_analysis.get('transaction_count', 0)} transactions analyzed. Feel free to ask about your budget, spending trends, saving tips, or unusual activity. What specific aspect of your finances would you like to explore?"
704
-
705
- status_html = '<div class="status-box success-box">✅ Response generated</div>'
706
- else:
707
- ai_response = "I don't have any financial data to analyze yet. Please process your bank statements first using the Email Processing or PDF Upload tabs."
708
- status_html = '<div class="status-box warning-box">⚠️ No data available</div>'
709
-
710
- # Update chat history with AI response
711
- chat_history[-1][1] = ai_response
712
-
713
- return chat_history, "", status_html
714
-
715
- except Exception as e:
716
- error_response = f"I'm sorry, I encountered an error: {str(e)}"
717
- if chat_history:
718
- chat_history[-1][1] = error_response
719
- return chat_history, "", '<div class="status-box error-box">❌ Chat Error</div>'
720
-
721
- def _save_budget_settings(self, categories, amounts):
722
- """Save budget settings"""
723
- try:
724
- # Filter amounts for selected categories
725
- budget_settings = {cat: amounts.get(cat, 0) for cat in categories}
726
-
727
- # Store in user session (in real app, would save to database)
728
- self.user_sessions['budgets'] = budget_settings
729
-
730
- status_html = '<div class="status-box success-box">✅ Budget settings saved</div>'
731
- return status_html, budget_settings
732
-
733
- except Exception as e:
734
- error_html = f'<div class="status-box error-box">❌ Error saving budgets: {str(e)}</div>'
735
- return error_html, {}
736
-
737
- def _save_email_settings(self, provider, server, port, auto_process):
738
- """Save email settings"""
739
- try:
740
- email_settings = {
741
- 'provider': provider,
742
- 'imap_server': server,
743
- 'imap_port': port,
744
- 'auto_process': auto_process
745
- }
746
-
747
- self.user_sessions['email_settings'] = email_settings
748
-
749
- return '<div class="status-box success-box">✅ Email settings saved</div>'
750
-
751
- except Exception as e:
752
- return f'<div class="status-box error-box">❌ Error saving settings: {str(e)}</div>'
753
-
754
- def _test_email_connection(self, provider, server, port):
755
- """Test email connection"""
756
- try:
757
- # This would test the actual connection in a real implementation
758
- return '<div class="status-box success-box">✅ Email connection test successful</div>'
759
-
760
- except Exception as e:
761
- return f'<div class="status-box error-box">❌ Connection test failed: {str(e)}</div>'
762
-
763
- def _get_imap_server(self, provider):
764
- """Get IMAP server for email provider"""
765
- servers = {
766
- 'Gmail': 'imap.gmail.com',
767
- 'Outlook': 'outlook.office365.com',
768
- 'Yahoo': 'imap.mail.yahoo.com',
769
- 'Other': 'imap.gmail.com' # Default
770
- }
771
- return servers.get(provider, 'imap.gmail.com')
772
-
773
- # Launch the interface
774
- def launch_interface():
775
- """Launch the Gradio interface"""
776
- interface = SpendAnalyzerInterface()
777
- app = interface.create_interface()
778
-
779
- app.launch(
780
- server_name="0.0.0.0",
781
- server_port=7860,
782
- share=False,
783
- debug=True,
784
- show_error=True
785
- )
786
-
787
- if __name__ == "__main__":
788
- launch_interface()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
gradio_interface_local.py DELETED
@@ -1,627 +0,0 @@
1
- """
2
- Gradio Web Interface for Spend Analyzer MCP - Local Version
3
- """
4
- import gradio as gr
5
- import pandas as pd
6
- import plotly.express as px
7
- import plotly.graph_objects as go
8
- import json
9
- import os
10
- from typing import Dict, List, Optional, Tuple
11
- from datetime import datetime, timedelta
12
- import logging
13
- import time
14
-
15
- class SpendAnalyzerInterface:
16
- def __init__(self):
17
- self.current_analysis = None
18
- self.user_sessions = {}
19
- self.logger = logging.getLogger(__name__)
20
- logging.basicConfig(level=logging.INFO)
21
-
22
- # Load demo data if available
23
- self.demo_data = self._load_demo_data()
24
-
25
- def _load_demo_data(self):
26
- """Load demo data for testing"""
27
- try:
28
- if os.path.exists('demo_data.json'):
29
- with open('demo_data.json', 'r') as f:
30
- return json.load(f)
31
- except Exception as e:
32
- self.logger.warning(f"Could not load demo data: {e}")
33
-
34
- # Fallback demo data
35
- return {
36
- "transactions": [
37
- {
38
- "date": "2024-01-15",
39
- "description": "STARBUCKS COFFEE",
40
- "amount": -4.50,
41
- "category": "Food & Dining"
42
- },
43
- {
44
- "date": "2024-01-14",
45
- "description": "AMAZON PURCHASE",
46
- "amount": -29.99,
47
- "category": "Shopping"
48
- },
49
- {
50
- "date": "2024-01-13",
51
- "description": "SALARY DEPOSIT",
52
- "amount": 3000.00,
53
- "category": "Income"
54
- },
55
- {
56
- "date": "2024-01-12",
57
- "description": "GROCERY STORE",
58
- "amount": -85.67,
59
- "category": "Food & Dining"
60
- },
61
- {
62
- "date": "2024-01-11",
63
- "description": "GAS STATION",
64
- "amount": -45.00,
65
- "category": "Gas & Transport"
66
- }
67
- ]
68
- }
69
-
70
- def create_interface(self):
71
- """Create the main Gradio interface"""
72
-
73
- with gr.Blocks(
74
- title="Spend Analyzer MCP - Local Demo",
75
- css="""
76
- .main-header { text-align: center; margin: 20px 0; }
77
- .status-box { padding: 10px; border-radius: 5px; margin: 10px 0; }
78
- .success-box { background-color: #d4edda; border: 1px solid #c3e6cb; }
79
- .error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; }
80
- .warning-box { background-color: #fff3cd; border: 1px solid #ffeaa7; }
81
- .demo-box { background-color: #e7f3ff; border: 1px solid #b3d9ff; }
82
- """
83
- ) as interface:
84
-
85
- gr.Markdown("# 💰 Spend Analyzer MCP - Local Demo", elem_classes=["main-header"])
86
- gr.Markdown("*Analyze your bank statements with AI-powered insights*")
87
-
88
- # Demo notice
89
- gr.HTML('<div class="demo-box">🚀 <strong>Demo Mode:</strong> This is a local demonstration. Upload real PDFs or use the demo data to explore features.</div>')
90
-
91
- with gr.Tabs():
92
- # Tab 1: Demo Data
93
- with gr.TabItem("🎯 Demo Data"):
94
- self._create_demo_tab()
95
-
96
- # Tab 2: PDF Upload
97
- with gr.TabItem("📄 PDF Upload"):
98
- self._create_pdf_tab()
99
-
100
- # Tab 3: Analysis Dashboard
101
- with gr.TabItem("📊 Analysis Dashboard"):
102
- self._create_dashboard_tab()
103
-
104
- # Tab 4: AI Chat
105
- with gr.TabItem("🤖 AI Financial Advisor"):
106
- self._create_chat_tab()
107
-
108
- # Tab 5: Settings
109
- with gr.TabItem("⚙️ Settings"):
110
- self._create_settings_tab()
111
-
112
- return interface
113
-
114
- def _create_demo_tab(self):
115
- """Create demo data tab"""
116
- gr.Markdown("## 🎯 Demo Data & Quick Start")
117
- gr.Markdown("*Load sample financial data to explore the features*")
118
-
119
- with gr.Row():
120
- with gr.Column():
121
- gr.Markdown("### Sample Transactions")
122
- demo_transactions = gr.JSON(
123
- value=self.demo_data["transactions"],
124
- label="Demo Transaction Data"
125
- )
126
-
127
- load_demo_btn = gr.Button("📊 Load Demo Data", variant="primary", size="lg")
128
-
129
- with gr.Column():
130
- demo_status = gr.HTML()
131
-
132
- gr.Markdown("### Features to Try")
133
- gr.Markdown("""
134
- 1. **Load Demo Data** - Click the button to analyze sample transactions
135
- 2. **View Dashboard** - See charts and financial summaries
136
- 3. **Chat with AI** - Ask questions about spending patterns
137
- 4. **Upload PDFs** - Try uploading your own bank statements
138
- 5. **Configure Settings** - Set budgets and preferences
139
- """)
140
-
141
- # Event handler
142
- load_demo_btn.click(
143
- fn=self._load_demo_data_handler,
144
- outputs=[demo_status]
145
- )
146
-
147
- def _create_pdf_tab(self):
148
- """Create PDF upload tab"""
149
- gr.Markdown("## 📄 Upload Bank Statement PDFs")
150
- gr.Markdown("*Upload your bank statement PDFs for analysis*")
151
-
152
- with gr.Row():
153
- with gr.Column():
154
- pdf_upload = gr.File(
155
- label="Upload Bank Statement PDFs",
156
- file_count="multiple",
157
- file_types=[".pdf"]
158
- )
159
-
160
- analyze_pdf_btn = gr.Button("📊 Analyze PDFs", variant="primary")
161
-
162
- with gr.Column():
163
- pdf_status = gr.HTML()
164
- pdf_results = gr.JSON(label="Analysis Results", visible=False)
165
-
166
- # Event handler
167
- analyze_pdf_btn.click(
168
- fn=self._analyze_pdf_files,
169
- inputs=[pdf_upload],
170
- outputs=[pdf_status, pdf_results]
171
- )
172
-
173
- def _create_dashboard_tab(self):
174
- """Create analysis dashboard tab"""
175
- gr.Markdown("## 📊 Financial Analysis Dashboard")
176
-
177
- with gr.Row():
178
- refresh_btn = gr.Button("🔄 Refresh Dashboard")
179
- export_btn = gr.Button("📤 Export Analysis")
180
-
181
- # Summary cards
182
- with gr.Row():
183
- total_income = gr.Number(label="Total Income ($)", interactive=False)
184
- total_expenses = gr.Number(label="Total Expenses ($)", interactive=False)
185
- net_cashflow = gr.Number(label="Net Cash Flow ($)", interactive=False)
186
- transaction_count = gr.Number(label="Total Transactions", interactive=False)
187
-
188
- # Charts
189
- with gr.Row():
190
- with gr.Column():
191
- spending_by_category = gr.Plot(label="Spending by Category")
192
- monthly_trends = gr.Plot(label="Daily Spending Trends")
193
-
194
- with gr.Column():
195
- budget_alerts = gr.HTML(label="Budget Alerts")
196
- recommendations = gr.HTML(label="Recommendations")
197
-
198
- # Detailed data
199
- with gr.Accordion("Detailed Transaction Data", open=False):
200
- transaction_table = gr.Dataframe(
201
- headers=["Date", "Description", "Amount", "Category"],
202
- interactive=False,
203
- label="Recent Transactions"
204
- )
205
-
206
- # Event handlers
207
- refresh_btn.click(
208
- fn=self._refresh_dashboard,
209
- outputs=[total_income, total_expenses, net_cashflow, transaction_count,
210
- spending_by_category, monthly_trends, budget_alerts, recommendations,
211
- transaction_table]
212
- )
213
-
214
- export_btn.click(
215
- fn=self._export_analysis,
216
- outputs=[gr.File(label="Analysis Export")]
217
- )
218
-
219
- def _create_chat_tab(self):
220
- """Create AI chat tab"""
221
- gr.Markdown("## 🤖 AI Financial Advisor")
222
- gr.Markdown("*Ask questions about your spending patterns and get insights*")
223
-
224
- with gr.Row():
225
- with gr.Column(scale=3):
226
- chatbot = gr.Chatbot(
227
- label="Financial Advisor Chat",
228
- height=400,
229
- show_label=True,
230
- type="messages"
231
- )
232
-
233
- with gr.Row():
234
- msg_input = gr.Textbox(
235
- placeholder="Ask about your spending patterns, budgets, or financial goals...",
236
- label="Your Question",
237
- scale=4
238
- )
239
- send_btn = gr.Button("Send", variant="primary", scale=1)
240
-
241
- # Quick question buttons
242
- with gr.Row():
243
- budget_btn = gr.Button("💰 Budget Analysis", size="sm")
244
- trends_btn = gr.Button("📈 Spending Trends", size="sm")
245
- tips_btn = gr.Button("💡 Save Money Tips", size="sm")
246
- unusual_btn = gr.Button("🚨 Unusual Activity", size="sm")
247
-
248
- with gr.Column(scale=1):
249
- chat_status = gr.HTML()
250
-
251
- # Analysis context
252
- gr.Markdown("### Current Analysis Context")
253
- context_info = gr.JSON(
254
- label="Available Data",
255
- value={"status": "Load demo data or upload PDFs to start"}
256
- )
257
-
258
- # Event handlers
259
- send_btn.click(
260
- fn=self._handle_chat_message,
261
- inputs=[msg_input, chatbot],
262
- outputs=[chatbot, msg_input, chat_status]
263
- )
264
-
265
- msg_input.submit(
266
- fn=self._handle_chat_message,
267
- inputs=[msg_input, chatbot],
268
- outputs=[chatbot, msg_input, chat_status]
269
- )
270
-
271
- # Quick question handlers
272
- budget_btn.click(
273
- lambda: "How am I doing with my budget this month?",
274
- outputs=[msg_input]
275
- )
276
- trends_btn.click(
277
- lambda: "What are my spending trends over the last few days?",
278
- outputs=[msg_input]
279
- )
280
- tips_btn.click(
281
- lambda: "What are some specific ways I can save money based on my spending?",
282
- outputs=[msg_input]
283
- )
284
- unusual_btn.click(
285
- lambda: "Are there any unusual transactions I should be aware of?",
286
- outputs=[msg_input]
287
- )
288
-
289
- def _create_settings_tab(self):
290
- """Create settings tab"""
291
- gr.Markdown("## ⚙️ Settings & Configuration")
292
-
293
- with gr.Tabs():
294
- with gr.TabItem("Budget Settings"):
295
- gr.Markdown("### Set Monthly Budget Limits")
296
-
297
- with gr.Row():
298
- with gr.Column():
299
- budget_categories = gr.CheckboxGroup(
300
- choices=["Food & Dining", "Shopping", "Gas & Transport",
301
- "Utilities", "Entertainment", "Healthcare", "Other"],
302
- label="Categories to Budget",
303
- value=["Food & Dining", "Shopping", "Gas & Transport"]
304
- )
305
-
306
- budget_amounts = gr.JSON(
307
- label="Budget Amounts ($)",
308
- value={
309
- "Food & Dining": 500,
310
- "Shopping": 300,
311
- "Gas & Transport": 200,
312
- "Utilities": 150,
313
- "Entertainment": 100,
314
- "Healthcare": 200,
315
- "Other": 100
316
- }
317
- )
318
-
319
- save_budgets_btn = gr.Button("💾 Save Budget Settings", variant="primary")
320
-
321
- with gr.Column():
322
- budget_status = gr.HTML()
323
- current_budgets = gr.JSON(label="Current Budget Settings")
324
-
325
- with gr.TabItem("Export Settings"):
326
- gr.Markdown("### Data Export Options")
327
-
328
- export_format = gr.Radio(
329
- choices=["JSON", "CSV"],
330
- label="Export Format",
331
- value="JSON"
332
- )
333
-
334
- include_raw_data = gr.Checkbox(
335
- label="Include raw transaction data",
336
- value=True
337
- )
338
-
339
- include_analysis = gr.Checkbox(
340
- label="Include analysis results",
341
- value=True
342
- )
343
-
344
- # Event handlers
345
- save_budgets_btn.click(
346
- fn=self._save_budget_settings,
347
- inputs=[budget_categories, budget_amounts],
348
- outputs=[budget_status, current_budgets]
349
- )
350
-
351
- # Implementation methods
352
- def _load_demo_data_handler(self):
353
- """Load demo data and create analysis"""
354
- try:
355
- # Simulate processing
356
- time.sleep(1)
357
-
358
- # Create mock analysis from demo data
359
- transactions = self.demo_data["transactions"]
360
-
361
- total_income = sum(t["amount"] for t in transactions if t["amount"] > 0)
362
- total_expenses = abs(sum(t["amount"] for t in transactions if t["amount"] < 0))
363
-
364
- # Group by category
365
- categories = {}
366
- for t in transactions:
367
- if t["amount"] < 0: # Only expenses
368
- cat = t["category"]
369
- if cat not in categories:
370
- categories[cat] = {"total": 0, "count": 0}
371
- categories[cat]["total"] += abs(t["amount"])
372
- categories[cat]["count"] += 1
373
-
374
- spending_insights = []
375
- for cat, data in categories.items():
376
- spending_insights.append({
377
- "category": cat,
378
- "total_amount": data["total"],
379
- "transaction_count": data["count"],
380
- "percentage_of_total": (data["total"] / total_expenses) * 100 if total_expenses > 0 else 0
381
- })
382
-
383
- self.current_analysis = {
384
- "financial_summary": {
385
- "total_income": total_income,
386
- "total_expenses": total_expenses,
387
- "net_cash_flow": total_income - total_expenses
388
- },
389
- "spending_insights": spending_insights,
390
- "recommendations": [
391
- "Your Food & Dining expenses are significant. Consider cooking more meals at home.",
392
- "Great job maintaining a positive cash flow!",
393
- "Track your daily expenses to identify more saving opportunities."
394
- ],
395
- "transaction_count": len(transactions)
396
- }
397
-
398
- return '<div class="status-box success-box">✅ Demo data loaded successfully! Check the Dashboard tab to see your analysis.</div>'
399
-
400
- except Exception as e:
401
- return f'<div class="status-box error-box">❌ Error loading demo data: {str(e)}</div>'
402
-
403
- def _analyze_pdf_files(self, files):
404
- """Analyze uploaded PDF files (mock implementation)"""
405
- try:
406
- if not files:
407
- return '<div class="status-box error-box">❌ No files uploaded</div>', {}
408
-
409
- # Mock PDF analysis
410
- result = {
411
- 'processed_files': [],
412
- 'total_transactions': 0
413
- }
414
-
415
- for file in files:
416
- # Mock processing
417
- file_result = {
418
- 'filename': file.name,
419
- 'status': 'success',
420
- 'message': 'PDF parsing not implemented in demo mode'
421
- }
422
- result['processed_files'].append(file_result)
423
-
424
- status_html = f'<div class="status-box warning-box">⚠️ PDF processing is not implemented in demo mode. Use the Demo Data tab instead.</div>'
425
-
426
- return status_html, result
427
-
428
- except Exception as e:
429
- error_html = f'<div class="status-box error-box">❌ Error: {str(e)}</div>'
430
- return error_html, {}
431
-
432
- def _refresh_dashboard(self):
433
- """Refresh dashboard with current analysis"""
434
- if not self.current_analysis:
435
- return (0, 0, 0, 0, None, None,
436
- '<div class="status-box warning-box">⚠️ No analysis data available. Load demo data first!</div>',
437
- '<div class="status-box warning-box">⚠️ Load demo data to see recommendations</div>',
438
- pd.DataFrame())
439
-
440
- try:
441
- summary = self.current_analysis.get('financial_summary', {})
442
- insights = self.current_analysis.get('spending_insights', [])
443
-
444
- # Summary metrics
445
- total_income = summary.get('total_income', 0)
446
- total_expenses = summary.get('total_expenses', 0)
447
- net_cashflow = summary.get('net_cash_flow', 0)
448
- transaction_count = self.current_analysis.get('transaction_count', 0)
449
-
450
- # Create spending by category chart
451
- if insights:
452
- categories = [insight['category'] for insight in insights]
453
- amounts = [insight['total_amount'] for insight in insights]
454
-
455
- spending_chart = px.pie(
456
- values=amounts,
457
- names=categories,
458
- title="Spending by Category"
459
- )
460
- spending_chart.update_layout(height=400)
461
- else:
462
- spending_chart = None
463
-
464
- # Create daily trends chart
465
- transactions = self.demo_data.get("transactions", [])
466
- if transactions:
467
- # Group by date
468
- daily_spending = {}
469
- for t in transactions:
470
- if t["amount"] < 0: # Only expenses
471
- date = t["date"]
472
- if date not in daily_spending:
473
- daily_spending[date] = 0
474
- daily_spending[date] += abs(t["amount"])
475
-
476
- dates = list(daily_spending.keys())
477
- amounts = list(daily_spending.values())
478
-
479
- trends_chart = px.line(
480
- x=dates,
481
- y=amounts,
482
- title="Daily Spending Trends",
483
- labels={"x": "Date", "y": "Amount ($)"}
484
- )
485
- trends_chart.update_layout(height=400)
486
- else:
487
- trends_chart = None
488
-
489
- # Budget alerts
490
- alert_html = '<div class="status-box success-box">✅ All spending within reasonable limits</div>'
491
-
492
- # Recommendations
493
- recommendations = self.current_analysis.get('recommendations', [])
494
- if recommendations:
495
- rec_html = '<div class="status-box"><h4>💡 Recommendations:</h4><ul>'
496
- for rec in recommendations:
497
- rec_html += f'<li>{rec}</li>'
498
- rec_html += '</ul></div>'
499
- else:
500
- rec_html = '<div class="status-box">No specific recommendations at this time.</div>'
501
-
502
- # Transaction table
503
- transaction_data = []
504
- for t in transactions:
505
- transaction_data.append([
506
- t["date"],
507
- t["description"],
508
- f"${t['amount']:.2f}",
509
- t["category"]
510
- ])
511
-
512
- transaction_df = pd.DataFrame(
513
- transaction_data,
514
- columns=["Date", "Description", "Amount", "Category"]
515
- )
516
-
517
- return (total_income, total_expenses, net_cashflow, transaction_count,
518
- spending_chart, trends_chart, alert_html, rec_html, transaction_df)
519
-
520
- except Exception as e:
521
- error_msg = f'<div class="status-box error-box">❌ Dashboard error: {str(e)}</div>'
522
- return (0, 0, 0, 0, None, None, error_msg, error_msg, pd.DataFrame())
523
-
524
- def _export_analysis(self):
525
- """Export current analysis data"""
526
- if not self.current_analysis:
527
- return None
528
-
529
- try:
530
- import tempfile
531
-
532
- # Create temporary file
533
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
534
- json.dump(self.current_analysis, f, indent=2, default=str)
535
- return f.name
536
-
537
- except Exception as e:
538
- self.logger.error(f"Export error: {e}")
539
- return None
540
-
541
- def _handle_chat_message(self, message, chat_history):
542
- """Handle chat messages with AI advisor"""
543
- if not message.strip():
544
- return chat_history, "", '<div class="status-box warning-box">⚠️ Please enter a message</div>'
545
-
546
- try:
547
- # Add user message to chat
548
- chat_history = chat_history or []
549
- chat_history.append([message, None])
550
-
551
- # Generate response based on analysis
552
- if self.current_analysis:
553
- summary = self.current_analysis.get('financial_summary', {})
554
- insights = self.current_analysis.get('spending_insights', [])
555
- recommendations = self.current_analysis.get('recommendations', [])
556
-
557
- if 'budget' in message.lower():
558
- ai_response = f"Based on your analysis, you have a net cash flow of ${summary.get('net_cash_flow', 0):.2f}. Your total expenses are ${summary.get('total_expenses', 0):.2f} against an income of ${summary.get('total_income', 0):.2f}."
559
- elif 'trend' in message.lower():
560
- if insights:
561
- top_category = insights[0]
562
- ai_response = f"Your top spending category is {top_category['category']} at ${top_category['total_amount']:.2f} ({top_category['percentage_of_total']:.1f}% of total spending)."
563
- else:
564
- ai_response = "I need more transaction data to analyze your spending trends effectively."
565
- elif 'save' in message.lower() or 'tip' in message.lower():
566
- if recommendations:
567
- ai_response = f"Here are some personalized recommendations: {'. '.join(recommendations[:2])}"
568
- else:
569
- ai_response = "Based on your spending patterns, consider tracking your largest expense categories and setting monthly budgets."
570
- elif 'unusual' in message.lower() or 'activity' in message.lower():
571
- ai_response = "I've analyzed your transactions and they appear normal. All transactions seem consistent with typical spending patterns."
572
- else:
573
- ai_response = f"I can see you have {self.current_analysis.get('transaction_count', 0)} transactions analyzed. Feel free to ask about your budget, spending trends, saving tips, or unusual activity!"
574
-
575
- status_html = '<div class="status-box success-box">✅ Response generated</div>'
576
- else:
577
- ai_response = "I don't have any financial data to analyze yet. Please load the demo data or upload PDFs first!"
578
- status_html = '<div class="status-box warning-box">⚠️ No data available</div>'
579
-
580
- # Update chat history with AI response
581
- chat_history[-1][1] = ai_response
582
-
583
- return chat_history, "", status_html
584
-
585
- except Exception as e:
586
- error_response = f"I'm sorry, I encountered an error: {str(e)}"
587
- if chat_history:
588
- chat_history[-1][1] = error_response
589
- return chat_history, "", '<div class="status-box error-box">❌ Chat Error</div>'
590
-
591
- def _save_budget_settings(self, categories, amounts):
592
- """Save budget settings"""
593
- try:
594
- # Filter amounts for selected categories
595
- budget_settings = {cat: amounts.get(cat, 0) for cat in categories}
596
-
597
- # Store in user session
598
- self.user_sessions['budgets'] = budget_settings
599
-
600
- status_html = '<div class="status-box success-box">✅ Budget settings saved</div>'
601
- return status_html, budget_settings
602
-
603
- except Exception as e:
604
- error_html = f'<div class="status-box error-box">❌ Error saving budgets: {str(e)}</div>'
605
- return error_html, {}
606
-
607
- # Launch the interface
608
- def launch_interface():
609
- """Launch the Gradio interface"""
610
- interface = SpendAnalyzerInterface()
611
- app = interface.create_interface()
612
-
613
- print("🚀 Starting Spend Analyzer MCP - Local Demo")
614
- print("📊 Demo mode: Use the Demo Data tab to get started")
615
- print("🌐 Opening in browser...")
616
-
617
- app.launch(
618
- server_name="0.0.0.0",
619
- server_port=7861,
620
- share=False,
621
- debug=True,
622
- show_error=True,
623
- inbrowser=True
624
- )
625
-
626
- if __name__ == "__main__":
627
- launch_interface()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
setup_local.py → init/setup_local.py RENAMED
File without changes
init/setup_modal.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Quick setup script for Modal deployment of Spend Analyzer MCP
4
+ """
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ def run_command(cmd, description):
11
+ """Run a command and handle errors"""
12
+ print(f"\n🔄 {description}...")
13
+ try:
14
+ result = subprocess.run(cmd, shell=True, check=True, capture_output=True, text=True)
15
+ print(f"✅ {description} completed successfully")
16
+ if result.stdout:
17
+ print(f"Output: {result.stdout.strip()}")
18
+ return True
19
+ except subprocess.CalledProcessError as e:
20
+ print(f"❌ {description} failed")
21
+ print(f"Error: {e.stderr.strip()}")
22
+ return False
23
+
24
+ def check_modal_installed():
25
+ """Check if Modal CLI is installed"""
26
+ try:
27
+ subprocess.run(["modal", "--version"], check=True, capture_output=True)
28
+ return True
29
+ except (subprocess.CalledProcessError, FileNotFoundError):
30
+ return False
31
+
32
+ def get_user_input(prompt, required=True):
33
+ """Get user input with validation"""
34
+ while True:
35
+ value = input(f"{prompt}: ").strip()
36
+ if value or not required:
37
+ return value
38
+ print("This field is required. Please enter a value.")
39
+
40
+ def main():
41
+ print("🚀 Spend Analyzer MCP - Modal Setup Script")
42
+ print("=" * 50)
43
+
44
+ # Check if Modal is installed
45
+ if not check_modal_installed():
46
+ print("\n📦 Installing Modal CLI...")
47
+ if not run_command("pip install modal", "Installing Modal"):
48
+ print("❌ Failed to install Modal. Please install manually: pip install modal")
49
+ sys.exit(1)
50
+ else:
51
+ print("✅ Modal CLI is already installed")
52
+
53
+ # Check if user is authenticated
54
+ print("\n🔐 Checking Modal authentication...")
55
+ try:
56
+ result = subprocess.run(["modal", "token", "current"], check=True, capture_output=True, text=True)
57
+ print("✅ Already authenticated with Modal")
58
+ except subprocess.CalledProcessError:
59
+ print("🔑 Need to authenticate with Modal...")
60
+ if not run_command("modal token new", "Authenticating with Modal"):
61
+ print("❌ Authentication failed. Please run 'modal token new' manually")
62
+ sys.exit(1)
63
+
64
+ # Collect API keys and credentials
65
+ print("\n📝 Please provide your API keys and credentials:")
66
+ print("(You can get these from the respective provider websites)")
67
+
68
+ anthropic_key = get_user_input("Anthropic API Key (for Claude)")
69
+ sambanova_key = get_user_input("SambaNova API Key (optional)", required=False)
70
+ email_user = get_user_input("Email address")
71
+ email_pass = get_user_input("Email app password")
72
+ imap_server = get_user_input("IMAP server (default: imap.gmail.com)", required=False) or "imap.gmail.com"
73
+
74
+ # Create Modal secrets
75
+ print("\n🔒 Creating Modal secrets...")
76
+
77
+ secrets_to_create = [
78
+ (f"modal secret create anthropic-api-key ANTHROPIC_API_KEY={anthropic_key}",
79
+ "Creating Anthropic API key secret"),
80
+ (f"modal secret create email-credentials EMAIL_USER={email_user} EMAIL_PASS={email_pass} IMAP_SERVER={imap_server}",
81
+ "Creating email credentials secret")
82
+ ]
83
+
84
+ if sambanova_key:
85
+ secrets_to_create.append(
86
+ (f"modal secret create sambanova-api-key SAMBANOVA_API_KEY={sambanova_key}",
87
+ "Creating SambaNova API key secret")
88
+ )
89
+
90
+ for cmd, description in secrets_to_create:
91
+ if not run_command(cmd, description):
92
+ print(f"⚠️ Failed to create secret. You may need to create it manually.")
93
+
94
+ # Deploy to Modal
95
+ print("\n🚀 Deploying to Modal...")
96
+ if run_command("modal deploy modal_deployment.py", "Deploying application"):
97
+ print("\n🎉 Deployment completed successfully!")
98
+ print("\n📋 Next steps:")
99
+ print("1. Check your Modal dashboard for the deployed app")
100
+ print("2. Test the deployment with: modal run modal_deployment.py")
101
+ print("3. View logs with: modal logs spend-analyzer-mcp-bmt")
102
+ print("4. Monitor usage with: modal stats spend-analyzer-mcp-bmt")
103
+
104
+ # Create local .env file
105
+ env_content = f"""# Spend Analyzer MCP Environment Variables
106
+ ANTHROPIC_API_KEY={anthropic_key}
107
+ EMAIL_USER={email_user}
108
+ EMAIL_PASS={email_pass}
109
+ IMAP_SERVER={imap_server}
110
+ """
111
+ if sambanova_key:
112
+ env_content += f"SAMBANOVA_API_KEY={sambanova_key}\n"
113
+
114
+ with open(".env", "w") as f:
115
+ f.write(env_content)
116
+ print("5. Created .env file for local development")
117
+
118
+ else:
119
+ print("\n❌ Deployment failed. Please check the errors above and try again.")
120
+ print("You can also deploy manually with: modal deploy modal_deployment.py")
121
+
122
+ if __name__ == "__main__":
123
+ try:
124
+ main()
125
+ except KeyboardInterrupt:
126
+ print("\n\n⏹️ Setup cancelled by user")
127
+ sys.exit(1)
128
+ except Exception as e:
129
+ print(f"\n❌ Unexpected error: {e}")
130
+ sys.exit(1)
mcp_server.py CHANGED
@@ -3,10 +3,13 @@ MCP Server for Spend Analysis - Core Protocol Implementation
3
  """
4
  import json
5
  import asyncio
6
- from typing import Dict, List, Any, Optional
 
 
7
  from dataclasses import dataclass
8
  from enum import Enum
9
  import logging
 
10
 
11
  # MCP Protocol Types
12
  class MessageType(Enum):
@@ -30,16 +33,19 @@ class MCPServer:
30
  self.prompts = {}
31
  self.logger = logging.getLogger(__name__)
32
 
33
- def register_tool(self, name: str, description: str, handler):
34
  """Register a tool that Claude can call"""
35
- self.tools[name] = {
36
- "description": description,
37
- "handler": handler,
38
- "input_schema": {
39
  "type": "object",
40
  "properties": {},
41
  "required": []
42
  }
 
 
 
 
 
43
  }
44
 
45
  def register_resource(self, uri: str, name: str, description: str, handler):
@@ -75,7 +81,7 @@ class MCPServer:
75
  self.logger.error(f"Error handling message: {e}")
76
  return self._error_response(message.get("id"), -32603, str(e))
77
 
78
- def _handle_initialize(self, msg_id: str) -> Dict:
79
  """Handle MCP initialization"""
80
  return {
81
  "jsonrpc": "2.0",
@@ -88,13 +94,13 @@ class MCPServer:
88
  "prompts": {}
89
  },
90
  "serverInfo": {
91
- "name": "spend-analyzer-mcp",
92
  "version": "1.0.0"
93
  }
94
  }
95
  }
96
 
97
- def _handle_list_tools(self, msg_id: str) -> Dict:
98
  """List available tools"""
99
  tools_list = []
100
  for name, tool in self.tools.items():
@@ -110,7 +116,7 @@ class MCPServer:
110
  "result": {"tools": tools_list}
111
  }
112
 
113
- async def _handle_call_tool(self, msg_id: str, params: Dict) -> Dict:
114
  """Execute a tool call"""
115
  tool_name = params.get("name")
116
  arguments = params.get("arguments", {})
@@ -136,7 +142,7 @@ class MCPServer:
136
  except Exception as e:
137
  return self._error_response(msg_id, -32603, f"Tool execution failed: {str(e)}")
138
 
139
- def _handle_list_resources(self, msg_id: str) -> Dict:
140
  """List available resources"""
141
  resources_list = []
142
  for uri, resource in self.resources.items():
@@ -153,7 +159,7 @@ class MCPServer:
153
  "result": {"resources": resources_list}
154
  }
155
 
156
- async def _handle_read_resource(self, msg_id: str, params: Dict) -> Dict:
157
  """Read a resource"""
158
  uri = params.get("uri")
159
 
@@ -179,7 +185,7 @@ class MCPServer:
179
  except Exception as e:
180
  return self._error_response(msg_id, -32603, f"Resource read failed: {str(e)}")
181
 
182
- def _error_response(self, msg_id: str, code: int, message: str) -> Dict:
183
  """Create error response"""
184
  return {
185
  "jsonrpc": "2.0",
@@ -190,36 +196,478 @@ class MCPServer:
190
  }
191
  }
192
 
193
- # Example usage and testing
194
- if __name__ == "__main__":
195
- # Test the MCP server
196
- server = MCPServer()
197
 
198
- # Register a simple tool
199
- async def test_tool(args):
200
- return f"Test tool called with: {args}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
- server.register_tool("test", "A test tool", test_tool)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
- # Test message handling
205
- async def test_server():
206
- init_msg = {
207
- "jsonrpc": "2.0",
208
- "id": "1",
209
- "method": "initialize",
210
- "params": {}
211
- }
212
 
213
- response = await server.handle_message(init_msg)
214
- print("Initialize response:", json.dumps(response, indent=2))
 
215
 
216
- list_tools_msg = {
217
- "jsonrpc": "2.0",
218
- "id": "2",
219
- "method": "tools/list"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
- response = await server.handle_message(list_tools_msg)
223
- print("List tools response:", json.dumps(response, indent=2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
- asyncio.run(test_server())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  """
4
  import json
5
  import asyncio
6
+ import uvicorn
7
+ from fastapi import FastAPI, Request
8
+ from typing import Dict, List, Any, Optional, Callable
9
  from dataclasses import dataclass
10
  from enum import Enum
11
  import logging
12
+ from spend_analyzer import SpendAnalyzer
13
 
14
  # MCP Protocol Types
15
  class MessageType(Enum):
 
33
  self.prompts = {}
34
  self.logger = logging.getLogger(__name__)
35
 
36
+ def register_tool(self, name: str, description: str, handler, input_schema=None):
37
  """Register a tool that Claude can call"""
38
+ if input_schema is None:
39
+ input_schema = {
 
 
40
  "type": "object",
41
  "properties": {},
42
  "required": []
43
  }
44
+
45
+ self.tools[name] = {
46
+ "description": description,
47
+ "handler": handler,
48
+ "input_schema": input_schema
49
  }
50
 
51
  def register_resource(self, uri: str, name: str, description: str, handler):
 
81
  self.logger.error(f"Error handling message: {e}")
82
  return self._error_response(message.get("id"), -32603, str(e))
83
 
84
+ def _handle_initialize(self, msg_id: Optional[str]) -> Dict:
85
  """Handle MCP initialization"""
86
  return {
87
  "jsonrpc": "2.0",
 
94
  "prompts": {}
95
  },
96
  "serverInfo": {
97
+ "name": "spend-analyzer-mcp-bmt",
98
  "version": "1.0.0"
99
  }
100
  }
101
  }
102
 
103
+ def _handle_list_tools(self, msg_id: Optional[str]) -> Dict:
104
  """List available tools"""
105
  tools_list = []
106
  for name, tool in self.tools.items():
 
116
  "result": {"tools": tools_list}
117
  }
118
 
119
+ async def _handle_call_tool(self, msg_id: Optional[str], params: Dict) -> Dict:
120
  """Execute a tool call"""
121
  tool_name = params.get("name")
122
  arguments = params.get("arguments", {})
 
142
  except Exception as e:
143
  return self._error_response(msg_id, -32603, f"Tool execution failed: {str(e)}")
144
 
145
+ def _handle_list_resources(self, msg_id: Optional[str]) -> Dict:
146
  """List available resources"""
147
  resources_list = []
148
  for uri, resource in self.resources.items():
 
159
  "result": {"resources": resources_list}
160
  }
161
 
162
+ async def _handle_read_resource(self, msg_id: Optional[str], params: Dict) -> Dict:
163
  """Read a resource"""
164
  uri = params.get("uri")
165
 
 
185
  except Exception as e:
186
  return self._error_response(msg_id, -32603, f"Resource read failed: {str(e)}")
187
 
188
+ def _error_response(self, msg_id: Optional[str], code: int, message: str) -> Dict:
189
  """Create error response"""
190
  return {
191
  "jsonrpc": "2.0",
 
196
  }
197
  }
198
 
199
+ # Register all tools for the MCP server
200
+ def register_all_tools(server: MCPServer):
201
+ """Register all tools with the MCP server"""
 
202
 
203
+ # Process email statements tool
204
+ async def process_email_statements_tool(args: Dict) -> Dict:
205
+ """Process bank statements from email"""
206
+ from email_processor import EmailProcessor, PDFProcessor
207
+
208
+ email_config = args.get('email_config', {})
209
+ days_back = args.get('days_back', 30)
210
+ passwords = args.get('passwords', {})
211
+
212
+ try:
213
+ # Initialize processors
214
+ email_processor = EmailProcessor(email_config)
215
+ pdf_processor = PDFProcessor()
216
+ analyzer = SpendAnalyzer()
217
+
218
+ # Fetch emails
219
+ emails = await email_processor.fetch_bank_emails(days_back)
220
+
221
+ all_transactions = []
222
+ processed_statements = []
223
+
224
+ for email_msg in emails:
225
+ # Extract attachments
226
+ attachments = await email_processor.extract_attachments(email_msg)
227
+
228
+ for filename, content, file_type in attachments:
229
+ if file_type == 'pdf':
230
+ # Try to process PDF
231
+ password = passwords.get(filename)
232
+
233
+ try:
234
+ statement_info = await pdf_processor.process_pdf(content, password)
235
+ all_transactions.extend(statement_info.transactions)
236
+ processed_statements.append({
237
+ 'filename': filename,
238
+ 'bank': statement_info.bank_name,
239
+ 'account': statement_info.account_number,
240
+ 'transaction_count': len(statement_info.transactions)
241
+ })
242
+ except Exception as e:
243
+ processed_statements.append({
244
+ 'filename': filename,
245
+ 'status': 'error',
246
+ 'error': str(e)
247
+ })
248
+
249
+ # Analyze transactions
250
+ if all_transactions:
251
+ analyzer.load_transactions(all_transactions)
252
+ analysis_data = analyzer.export_analysis_data()
253
+ else:
254
+ analysis_data = {'message': 'No transactions found'}
255
+
256
+ return {
257
+ 'processed_statements': processed_statements,
258
+ 'total_transactions': len(all_transactions),
259
+ 'analysis': analysis_data
260
+ }
261
+
262
+ except Exception as e:
263
+ return {'error': str(e)}
264
 
265
+ # Analyze PDF statements tool
266
+ async def analyze_pdf_statements_tool(args: Dict) -> Dict:
267
+ """Analyze uploaded PDF statements"""
268
+ from email_processor import PDFProcessor
269
+
270
+ pdf_contents = args.get('pdf_contents', {})
271
+ passwords = args.get('passwords', {})
272
+
273
+ try:
274
+ pdf_processor = PDFProcessor()
275
+ analyzer = SpendAnalyzer()
276
+
277
+ all_transactions = []
278
+ processed_files = []
279
+
280
+ for filename, content in pdf_contents.items():
281
+ try:
282
+ password = passwords.get(filename)
283
+ statement_info = await pdf_processor.process_pdf(content, password)
284
+
285
+ all_transactions.extend(statement_info.transactions)
286
+ processed_files.append({
287
+ 'filename': filename,
288
+ 'bank': statement_info.bank_name,
289
+ 'account': statement_info.account_number,
290
+ 'transaction_count': len(statement_info.transactions),
291
+ 'status': 'success'
292
+ })
293
+
294
+ except Exception as e:
295
+ processed_files.append({
296
+ 'filename': filename,
297
+ 'status': 'error',
298
+ 'error': str(e)
299
+ })
300
+
301
+ # Analyze transactions
302
+ if all_transactions:
303
+ analyzer.load_transactions(all_transactions)
304
+ analysis_data = analyzer.export_analysis_data()
305
+ else:
306
+ analysis_data = {'message': 'No transactions found'}
307
+
308
+ return {
309
+ 'processed_files': processed_files,
310
+ 'total_transactions': len(all_transactions),
311
+ 'analysis': analysis_data
312
+ }
313
+
314
+ except Exception as e:
315
+ return {'error': str(e)}
316
 
317
+ # Get AI analysis tool
318
+ async def get_ai_analysis_tool(args: Dict) -> Dict:
319
+ """Get AI financial analysis"""
320
+ import os
 
 
 
 
321
 
322
+ analysis_data = args.get('analysis_data', {})
323
+ user_question = args.get('user_question', '')
324
+ provider = args.get('provider', 'claude')
325
 
326
+ try:
327
+ # Prepare context for AI
328
+ context = f"""
329
+ Financial Analysis Data:
330
+ {json.dumps(analysis_data, indent=2, default=str)}
331
+
332
+ User Question: {user_question if user_question else "Please provide a comprehensive analysis of my spending patterns and recommendations."}
333
+ """
334
+
335
+ prompt = f"""
336
+ You are a financial advisor analyzing bank statement data.
337
+ Based on the provided financial data, give insights about:
338
+
339
+ 1. Spending patterns and trends
340
+ 2. Budget adherence and alerts
341
+ 3. Unusual transactions that need attention
342
+ 4. Specific recommendations for improvement
343
+ 5. Answer to the user's specific question if provided
344
+
345
+ Be specific, actionable, and highlight both positive aspects and areas for improvement.
346
+
347
+ {context}
348
+ """
349
+
350
+ if provider.lower() == "claude":
351
+ # Call Claude API
352
+ try:
353
+ import anthropic
354
+
355
+ client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", ""))
356
+
357
+ response = client.messages.create(
358
+ model="claude-3-sonnet-20240229",
359
+ max_tokens=1500,
360
+ messages=[
361
+ {
362
+ "role": "user",
363
+ "content": prompt
364
+ }
365
+ ]
366
+ )
367
+
368
+ # Handle different response formats
369
+ try:
370
+ # Extract text from Claude response
371
+ if hasattr(response, 'content') and response.content:
372
+ content_item = response.content[0]
373
+ # Handle different Claude API versions
374
+ if isinstance(content_item, dict):
375
+ if 'text' in content_item:
376
+ analysis_text = content_item['text']
377
+ else:
378
+ analysis_text = str(content_item)
379
+ # Handle object with attributes
380
+ elif hasattr(content_item, '__dict__'):
381
+ content_dict = vars(content_item)
382
+ if 'text' in content_dict:
383
+ analysis_text = content_dict['text']
384
+ else:
385
+ analysis_text = str(content_item)
386
+ else:
387
+ analysis_text = str(content_item)
388
+ else:
389
+ analysis_text = str(response)
390
+ except Exception as e:
391
+ analysis_text = f"Error parsing Claude response: {str(e)}"
392
+
393
+ return {
394
+ 'ai_analysis': analysis_text,
395
+ 'provider': 'claude',
396
+ 'model': 'claude-3-sonnet-20240229'
397
+ }
398
+
399
+ except Exception as e:
400
+ return {'error': f"Claude API error: {str(e)}"}
401
+
402
+ elif provider.lower() == "sambanova":
403
+ # Call SambaNova API
404
+ try:
405
+ import openai
406
+
407
+ # SambaNova uses OpenAI-compatible API
408
+ client = openai.OpenAI(
409
+ api_key=os.environ.get("SAMBANOVA_API_KEY", ""),
410
+ base_url="https://api.sambanova.ai/v1"
411
+ )
412
+
413
+ response = client.chat.completions.create(
414
+ model="Meta-Llama-3.1-8B-Instruct", # SambaNova model
415
+ messages=[
416
+ {
417
+ "role": "user",
418
+ "content": prompt
419
+ }
420
+ ],
421
+ max_tokens=1500,
422
+ temperature=0.7
423
+ )
424
+
425
+ return {
426
+ 'ai_analysis': response.choices[0].message.content,
427
+ 'provider': 'sambanova',
428
+ 'model': 'Meta-Llama-3.1-8B-Instruct'
429
+ }
430
+
431
+ except Exception as e:
432
+ return {'error': f"SambaNova API error: {str(e)}"}
433
+ else:
434
+ return {'error': f"Unsupported provider: {provider}"}
435
+
436
+ except Exception as e:
437
+ return {'error': f"AI API error: {str(e)}"}
438
+
439
+ # Register tools with proper input schemas
440
+ server.register_tool(
441
+ "process_email_statements",
442
+ "Process bank statements from email",
443
+ process_email_statements_tool,
444
+ input_schema={
445
+ "type": "object",
446
+ "properties": {
447
+ "email_config": {
448
+ "type": "object",
449
+ "properties": {
450
+ "email": {"type": "string"},
451
+ "password": {"type": "string"},
452
+ "imap_server": {"type": "string"}
453
+ },
454
+ "required": ["email", "password", "imap_server"]
455
+ },
456
+ "days_back": {"type": "integer", "default": 30},
457
+ "passwords": {
458
+ "type": "object",
459
+ "additionalProperties": {"type": "string"}
460
+ }
461
+ },
462
+ "required": ["email_config"]
463
  }
464
+ )
465
+
466
+ server.register_tool(
467
+ "analyze_pdf_statements",
468
+ "Analyze uploaded PDF statements",
469
+ analyze_pdf_statements_tool,
470
+ input_schema={
471
+ "type": "object",
472
+ "properties": {
473
+ "pdf_contents": {
474
+ "type": "object",
475
+ "additionalProperties": {"type": "string", "format": "binary"}
476
+ },
477
+ "passwords": {
478
+ "type": "object",
479
+ "additionalProperties": {"type": "string"}
480
+ }
481
+ },
482
+ "required": ["pdf_contents"]
483
+ }
484
+ )
485
+
486
+ server.register_tool(
487
+ "get_ai_analysis",
488
+ "Get AI financial analysis (Claude or SambaNova)",
489
+ get_ai_analysis_tool,
490
+ input_schema={
491
+ "type": "object",
492
+ "properties": {
493
+ "analysis_data": {"type": "object"},
494
+ "user_question": {"type": "string"},
495
+ "provider": {
496
+ "type": "string",
497
+ "enum": ["claude", "sambanova"],
498
+ "default": "claude"
499
+ }
500
+ },
501
+ "required": ["analysis_data"]
502
+ }
503
+ )
504
+
505
+ # Register all resources for the MCP server
506
+ def register_all_resources(server: MCPServer):
507
+ """Register all resources with the MCP server"""
508
+
509
+ # Spending insights resource
510
+ async def get_spending_insights_resource():
511
+ """Resource handler for spending insights"""
512
+ from dataclasses import asdict
513
+ analyzer = SpendAnalyzer()
514
 
515
+ # Try to load sample data if available
516
+ try:
517
+ import os
518
+ import json
519
+
520
+ sample_path = os.path.join(os.path.dirname(__file__), "sample_data", "transactions.json")
521
+ if os.path.exists(sample_path):
522
+ with open(sample_path, 'r') as f:
523
+ transactions = json.load(f)
524
+ analyzer.load_transactions(transactions)
525
+ except Exception as e:
526
+ logging.warning(f"Could not load sample data: {e}")
527
+ # Return empty insights if no data
528
+ return []
529
+
530
+ # Convert SpendingInsight objects to dictionaries
531
+ insights = analyzer.analyze_spending_by_category()
532
+ return [asdict(insight) for insight in insights]
533
+
534
+ # Budget alerts resource
535
+ async def get_budget_alerts_resource():
536
+ """Resource handler for budget alerts"""
537
+ from dataclasses import asdict
538
+ analyzer = SpendAnalyzer()
539
+
540
+ # Try to load sample data and budgets if available
541
+ try:
542
+ import os
543
+ import json
544
+
545
+ sample_path = os.path.join(os.path.dirname(__file__), "sample_data", "transactions.json")
546
+ budgets_path = os.path.join(os.path.dirname(__file__), "sample_data", "budgets.json")
547
+
548
+ if os.path.exists(sample_path) and os.path.exists(budgets_path):
549
+ with open(sample_path, 'r') as f:
550
+ transactions = json.load(f)
551
+ with open(budgets_path, 'r') as f:
552
+ budgets = json.load(f)
553
+
554
+ analyzer.load_transactions(transactions)
555
+ analyzer.set_budgets(budgets)
556
+ except Exception as e:
557
+ logging.warning(f"Could not load sample data: {e}")
558
+ # Return empty alerts if no data
559
+ return []
560
+
561
+ # Convert BudgetAlert objects to dictionaries
562
+ alerts = analyzer.check_budget_alerts()
563
+ return [asdict(alert) for alert in alerts]
564
+
565
+ # Financial summary resource
566
+ async def get_financial_summary_resource():
567
+ """Resource handler for financial summary"""
568
+ from dataclasses import asdict
569
+ analyzer = SpendAnalyzer()
570
+
571
+ # Try to load sample data if available
572
+ try:
573
+ import os
574
+ import json
575
+
576
+ sample_path = os.path.join(os.path.dirname(__file__), "sample_data", "transactions.json")
577
+ if os.path.exists(sample_path):
578
+ with open(sample_path, 'r') as f:
579
+ transactions = json.load(f)
580
+ analyzer.load_transactions(transactions)
581
+ except Exception as e:
582
+ logging.warning(f"Could not load sample data: {e}")
583
+ # Return empty summary if no data
584
+ return {
585
+ "total_income": 0,
586
+ "total_expenses": 0,
587
+ "net_cash_flow": 0,
588
+ "largest_expense": {},
589
+ "most_frequent_category": "",
590
+ "unusual_transactions": [],
591
+ "monthly_trends": {}
592
+ }
593
+
594
+ # Convert FinancialSummary object to dictionary
595
+ summary = analyzer.generate_financial_summary()
596
+ return asdict(summary)
597
 
598
+ # Register resources
599
+ server.register_resource(
600
+ uri="spending-insights",
601
+ name="Spending Insights",
602
+ description="Current spending insights by category",
603
+ handler=get_spending_insights_resource
604
+ )
605
+
606
+ server.register_resource(
607
+ uri="budget-alerts",
608
+ name="Budget Alerts",
609
+ description="Current budget alerts and overspending warnings",
610
+ handler=get_budget_alerts_resource
611
+ )
612
+
613
+ server.register_resource(
614
+ uri="financial-summary",
615
+ name="Financial Summary",
616
+ description="Comprehensive financial summary and analysis",
617
+ handler=get_financial_summary_resource
618
+ )
619
+
620
+ # Create FastAPI app for MCP server
621
+ def create_mcp_app():
622
+ """Create a FastAPI app for the MCP server"""
623
+ app = FastAPI(title="Spend Analyzer MCP Server")
624
+ server = MCPServer()
625
+
626
+ # Register tools and resources
627
+ register_all_tools(server)
628
+ register_all_resources(server)
629
+
630
+ @app.post("/mcp")
631
+ async def handle_mcp_request(request: Request):
632
+ """Handle MCP protocol requests"""
633
+ try:
634
+ data = await request.json()
635
+ return await server.handle_message(data)
636
+ except Exception as e:
637
+ return {
638
+ "jsonrpc": "2.0",
639
+ "id": None,
640
+ "error": {
641
+ "code": -32700,
642
+ "message": f"Parse error: {str(e)}"
643
+ }
644
+ }
645
+
646
+ @app.get("/")
647
+ async def root():
648
+ """Root endpoint with server info"""
649
+ return {
650
+ "name": "Spend Analyzer MCP Server",
651
+ "version": "1.0.0",
652
+ "description": "MCP server for financial analysis",
653
+ "endpoints": {
654
+ "/mcp": "MCP protocol endpoint",
655
+ "/docs": "API documentation"
656
+ }
657
+ }
658
+
659
+ return app
660
+
661
+ # Run standalone MCP server
662
+ def run_mcp_server(host='0.0.0.0', port=8000):
663
+ """Run a standalone MCP server"""
664
+ app = create_mcp_app()
665
+ uvicorn.run(app, host=host, port=port)
666
+
667
+ # Example usage and testing
668
+ if __name__ == "__main__":
669
+ # Run the standalone MCP server
670
+ print("Starting Spend Analyzer MCP Server...")
671
+ print("MCP endpoint will be available at: http://localhost:8000/mcp")
672
+ print("API documentation will be available at: http://localhost:8000/docs")
673
+ run_mcp_server()
modal_deployment.py CHANGED
@@ -1,5 +1,6 @@
1
  """
2
  Modal.com Deployment Configuration for Spend Analyzer MCP
 
3
  """
4
  import modal
5
  import os
@@ -10,7 +11,7 @@ from datetime import datetime
10
  import logging
11
 
12
  # Create Modal app
13
- app = modal.App("spend-analyzer-mcp")
14
 
15
  # Define the container image with all dependencies
16
  image = (
@@ -23,22 +24,29 @@ image = (
23
  "numpy",
24
  "PyPDF2",
25
  "PyMuPDF",
26
- "anthropic",
 
27
  "python-multipart",
28
  "aiofiles",
29
  "python-dotenv",
30
  "imaplib2",
31
  "email-validator",
32
- "pydantic",
33
  "websockets",
34
- "asyncio-mqtt"
 
 
 
 
 
35
  ])
36
- .apt_install(["tesseract-ocr", "tesseract-ocr-eng"])
37
  )
38
 
39
  # Secrets for API keys and email credentials
40
  secrets = [
41
  modal.Secret.from_name("anthropic-api-key"), # ANTHROPIC_API_KEY
 
42
  modal.Secret.from_name("email-credentials"), # EMAIL_USER, EMAIL_PASS, IMAP_SERVER
43
  ]
44
 
@@ -197,16 +205,12 @@ def analyze_uploaded_statements(pdf_contents: Dict[str, bytes], passwords: Optio
197
  volumes={"/data": volume},
198
  timeout=30
199
  )
200
- def get_claude_analysis(analysis_data: Dict, user_question: str = ""):
201
  """
202
- Modal function to get Claude's analysis of spending data
203
  """
204
- import anthropic
205
-
206
  try:
207
- client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
208
-
209
- # Prepare context for Claude
210
  context = f"""
211
  Financial Analysis Data:
212
  {json.dumps(analysis_data, indent=2, default=str)}
@@ -214,38 +218,107 @@ def get_claude_analysis(analysis_data: Dict, user_question: str = ""):
214
  User Question: {user_question if user_question else "Please provide a comprehensive analysis of my spending patterns and recommendations."}
215
  """
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  response = client.messages.create(
218
  model="claude-3-sonnet-20240229",
219
  max_tokens=1500,
220
  messages=[
221
  {
222
  "role": "user",
223
- "content": f"""
224
- You are a financial advisor analyzing bank statement data.
225
- Based on the provided financial data, give insights about:
226
-
227
- 1. Spending patterns and trends
228
- 2. Budget adherence and alerts
229
- 3. Unusual transactions that need attention
230
- 4. Specific recommendations for improvement
231
- 5. Answer to the user's specific question if provided
232
-
233
- Be specific, actionable, and highlight both positive aspects and areas for improvement.
234
-
235
- {context}
236
- """
237
  }
238
  ]
239
  )
240
 
 
 
 
 
 
 
241
  return {
242
- 'claude_analysis': response.content[0].text,
243
- 'usage': response.usage.input_tokens + response.usage.output_tokens
 
 
 
 
 
 
244
  }
245
 
246
  except Exception as e:
247
  return {'error': f"Claude API error: {str(e)}"}
248
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  @app.function(
250
  image=image,
251
  volumes={"/data": volume},
@@ -317,7 +390,7 @@ def load_user_data(user_id: str):
317
  secrets=secrets,
318
  volumes={"/data": volume}
319
  )
320
- @modal.web_endpoint(method="POST")
321
  def mcp_webhook(request_data: Dict):
322
  """
323
  Webhook endpoint for MCP protocol messages
@@ -347,14 +420,15 @@ def mcp_webhook(request_data: Dict):
347
  async def get_analysis_tool(args):
348
  analysis_data = args.get('analysis_data', {})
349
  user_question = args.get('user_question', '')
 
350
 
351
- result = get_claude_analysis.remote(analysis_data, user_question)
352
  return result
353
 
354
  # Register tools with MCP server
355
  server.register_tool("process_email_statements", "Process bank statements from email", process_statements_tool)
356
  server.register_tool("analyze_pdf_statements", "Analyze uploaded PDF statements", analyze_pdf_tool)
357
- server.register_tool("get_claude_analysis", "Get Claude's financial analysis", get_analysis_tool)
358
 
359
  # Handle MCP message
360
  response = asyncio.run(server.handle_message(request_data))
@@ -384,9 +458,9 @@ def main():
384
  "recommendations": ["Test recommendation"]
385
  }
386
 
387
- result = get_claude_analysis.remote(test_data, "What do you think about my spending?")
388
- print("Claude analysis result:", result)
389
 
390
  if __name__ == "__main__":
391
  # For running locally
392
- modal.run(main)
 
1
  """
2
  Modal.com Deployment Configuration for Spend Analyzer MCP
3
+ Enhanced with Claude and SambaNova Cloud API support
4
  """
5
  import modal
6
  import os
 
11
  import logging
12
 
13
  # Create Modal app
14
+ app = modal.App("spend-analyzer-mcp-bmt")
15
 
16
  # Define the container image with all dependencies
17
  image = (
 
24
  "numpy",
25
  "PyPDF2",
26
  "PyMuPDF",
27
+ "anthropic>=0.7.0",
28
+ "openai>=1.0.0",
29
  "python-multipart",
30
  "aiofiles",
31
  "python-dotenv",
32
  "imaplib2",
33
  "email-validator",
34
+ "pydantic>=1.10.0",
35
  "websockets",
36
+ "asyncio-mqtt",
37
+ "python-dateutil",
38
+ "regex",
39
+ "plotly>=5.0.0",
40
+ "requests>=2.28.0",
41
+ "httpx>=0.24.0"
42
  ])
43
+ .apt_install(["tesseract-ocr", "tesseract-ocr-eng", "poppler-utils"])
44
  )
45
 
46
  # Secrets for API keys and email credentials
47
  secrets = [
48
  modal.Secret.from_name("anthropic-api-key"), # ANTHROPIC_API_KEY
49
+ modal.Secret.from_name("sambanova-api-key"), # SAMBANOVA_API_KEY
50
  modal.Secret.from_name("email-credentials"), # EMAIL_USER, EMAIL_PASS, IMAP_SERVER
51
  ]
52
 
 
205
  volumes={"/data": volume},
206
  timeout=30
207
  )
208
+ def get_ai_analysis(analysis_data: Dict, user_question: str = "", provider: str = "claude"):
209
  """
210
+ Modal function to get AI analysis of spending data using Claude or SambaNova
211
  """
 
 
212
  try:
213
+ # Prepare context for AI
 
 
214
  context = f"""
215
  Financial Analysis Data:
216
  {json.dumps(analysis_data, indent=2, default=str)}
 
218
  User Question: {user_question if user_question else "Please provide a comprehensive analysis of my spending patterns and recommendations."}
219
  """
220
 
221
+ prompt = f"""
222
+ You are a financial advisor analyzing bank statement data.
223
+ Based on the provided financial data, give insights about:
224
+
225
+ 1. Spending patterns and trends
226
+ 2. Budget adherence and alerts
227
+ 3. Unusual transactions that need attention
228
+ 4. Specific recommendations for improvement
229
+ 5. Answer to the user's specific question if provided
230
+
231
+ Be specific, actionable, and highlight both positive aspects and areas for improvement.
232
+
233
+ {context}
234
+ """
235
+
236
+ if provider.lower() == "claude":
237
+ return _get_claude_analysis(prompt)
238
+ elif provider.lower() == "sambanova":
239
+ return _get_sambanova_analysis(prompt)
240
+ else:
241
+ # Default to Claude
242
+ return _get_claude_analysis(prompt)
243
+
244
+ except Exception as e:
245
+ return {'error': f"AI API error: {str(e)}"}
246
+
247
+ def _get_claude_analysis(prompt: str) -> Dict:
248
+ """Get analysis from Claude API"""
249
+ try:
250
+ import anthropic
251
+
252
+ client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
253
+
254
  response = client.messages.create(
255
  model="claude-3-sonnet-20240229",
256
  max_tokens=1500,
257
  messages=[
258
  {
259
  "role": "user",
260
+ "content": prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  }
262
  ]
263
  )
264
 
265
+ # Handle different response formats
266
+ if hasattr(response.content[0], 'text'):
267
+ analysis_text = response.content[0].text
268
+ else:
269
+ analysis_text = str(response.content[0])
270
+
271
  return {
272
+ 'ai_analysis': analysis_text,
273
+ 'provider': 'claude',
274
+ 'model': 'claude-3-sonnet-20240229',
275
+ 'usage': {
276
+ 'input_tokens': response.usage.input_tokens,
277
+ 'output_tokens': response.usage.output_tokens,
278
+ 'total_tokens': response.usage.input_tokens + response.usage.output_tokens
279
+ }
280
  }
281
 
282
  except Exception as e:
283
  return {'error': f"Claude API error: {str(e)}"}
284
 
285
+ def _get_sambanova_analysis(prompt: str) -> Dict:
286
+ """Get analysis from SambaNova Cloud API"""
287
+ try:
288
+ import openai
289
+
290
+ # SambaNova uses OpenAI-compatible API
291
+ client = openai.OpenAI(
292
+ api_key=os.environ["SAMBANOVA_API_KEY"],
293
+ base_url="https://api.sambanova.ai/v1"
294
+ )
295
+
296
+ response = client.chat.completions.create(
297
+ model="Meta-Llama-3.1-8B-Instruct", # SambaNova model
298
+ messages=[
299
+ {
300
+ "role": "user",
301
+ "content": prompt
302
+ }
303
+ ],
304
+ max_tokens=1500,
305
+ temperature=0.7
306
+ )
307
+
308
+ return {
309
+ 'ai_analysis': response.choices[0].message.content,
310
+ 'provider': 'sambanova',
311
+ 'model': 'Meta-Llama-3.1-8B-Instruct',
312
+ 'usage': {
313
+ 'input_tokens': response.usage.prompt_tokens,
314
+ 'output_tokens': response.usage.completion_tokens,
315
+ 'total_tokens': response.usage.total_tokens
316
+ }
317
+ }
318
+
319
+ except Exception as e:
320
+ return {'error': f"SambaNova API error: {str(e)}"}
321
+
322
  @app.function(
323
  image=image,
324
  volumes={"/data": volume},
 
390
  secrets=secrets,
391
  volumes={"/data": volume}
392
  )
393
+ @modal.fastapi_endpoint(method="POST")
394
  def mcp_webhook(request_data: Dict):
395
  """
396
  Webhook endpoint for MCP protocol messages
 
420
  async def get_analysis_tool(args):
421
  analysis_data = args.get('analysis_data', {})
422
  user_question = args.get('user_question', '')
423
+ provider = args.get('provider', 'claude')
424
 
425
+ result = get_ai_analysis.remote(analysis_data, user_question, provider)
426
  return result
427
 
428
  # Register tools with MCP server
429
  server.register_tool("process_email_statements", "Process bank statements from email", process_statements_tool)
430
  server.register_tool("analyze_pdf_statements", "Analyze uploaded PDF statements", analyze_pdf_tool)
431
+ server.register_tool("get_ai_analysis", "Get AI financial analysis (Claude or SambaNova)", get_analysis_tool)
432
 
433
  # Handle MCP message
434
  response = asyncio.run(server.handle_message(request_data))
 
458
  "recommendations": ["Test recommendation"]
459
  }
460
 
461
+ result = get_ai_analysis.remote(test_data, "What do you think about my spending?", "claude")
462
+ print("AI analysis result:", result)
463
 
464
  if __name__ == "__main__":
465
  # For running locally
466
+ modal.run(main)
requirements.txt CHANGED
@@ -8,13 +8,29 @@ numpy>=1.21.0
8
  PyPDF2>=3.0.0
9
  PyMuPDF>=1.20.0
10
 
11
- # AI and API (optional)
12
  anthropic>=0.7.0
 
 
 
 
 
13
 
14
  # Async and utilities
15
  python-dotenv>=0.19.0
16
  pydantic>=1.10.0
 
 
17
 
18
- # Development and testing (optional)
19
  uvicorn>=0.18.0
20
  fastapi>=0.85.0
 
 
 
 
 
 
 
 
 
 
8
  PyPDF2>=3.0.0
9
  PyMuPDF>=1.20.0
10
 
11
+ # AI and API
12
  anthropic>=0.7.0
13
+ openai>=1.0.0
14
+
15
+ # Email processing
16
+ imaplib2>=0.57
17
+ email-validator>=1.3.0
18
 
19
  # Async and utilities
20
  python-dotenv>=0.19.0
21
  pydantic>=1.10.0
22
+ aiofiles>=0.8.0
23
+ python-multipart>=0.0.5
24
 
25
+ # Web framework
26
  uvicorn>=0.18.0
27
  fastapi>=0.85.0
28
+ websockets>=10.0
29
+
30
+ # Modal deployment
31
+ modal>=0.56.0
32
+
33
+ # Additional utilities
34
+ python-dateutil>=2.8.0
35
+ regex>=2022.0.0
36
+ asyncio-mqtt>=0.11.0
secure_storage_utils.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simplified Storage Utilities for API Keys and Settings
3
+ Provides basic storage utilities without complex warning systems
4
+ """
5
+ import json
6
+ import os
7
+ from typing import Dict, Any, Optional
8
+ from datetime import datetime
9
+ import logging
10
+
11
+ class SecureStorageManager:
12
+ """
13
+ Simple storage manager for API keys and settings
14
+ Focuses on environment variables and config file loading
15
+ """
16
+
17
+ def __init__(self):
18
+ self.logger = logging.getLogger(__name__)
19
+
20
+ def create_simple_warning_html(self) -> str:
21
+ """Create a simple warning for AI settings section"""
22
+ return """
23
+ <div style="
24
+ background-color: #fff3cd;
25
+ border: 1px solid #ffeaa7;
26
+ border-radius: 6px;
27
+ padding: 12px;
28
+ margin: 8px 0;
29
+ ">
30
+ <div style="color: #856404; display: flex; align-items: center; gap: 8px;">
31
+ <span style="font-size: 18px;">⚠️</span>
32
+ <div>
33
+ <strong>API Key Storage Notice</strong><br>
34
+ <small>API keys will be cleared when you refresh/reload the page or close the browser.
35
+ Please keep your API keys and model information handy for re-entry when needed.</small>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ """
40
+
41
+ def load_from_environment(self) -> Dict[str, Any]:
42
+ """Load API keys from environment variables"""
43
+ config = {}
44
+
45
+ # Claude API
46
+ if os.getenv('CLAUDE_API_KEY'):
47
+ config['claude'] = {
48
+ 'api_key': os.getenv('CLAUDE_API_KEY'),
49
+ 'model': os.getenv('CLAUDE_MODEL', 'claude-3-5-sonnet-20241022'),
50
+ 'api_url': os.getenv('CLAUDE_API_URL', 'https://api.anthropic.com')
51
+ }
52
+
53
+ # SambaNova API
54
+ if os.getenv('SAMBANOVA_API_KEY'):
55
+ config['sambanova'] = {
56
+ 'api_key': os.getenv('SAMBANOVA_API_KEY'),
57
+ 'model': os.getenv('SAMBANOVA_MODEL', 'Meta-Llama-3.1-70B-Instruct'),
58
+ 'api_url': os.getenv('SAMBANOVA_API_URL', 'https://api.sambanova.ai')
59
+ }
60
+
61
+ # LM Studio API
62
+ if os.getenv('LM_STUDIO_URL'):
63
+ config['lm_studio'] = {
64
+ 'api_url': os.getenv('LM_STUDIO_URL', 'http://localhost:1234/v1'),
65
+ 'model': os.getenv('LM_STUDIO_MODEL', 'local-model')
66
+ }
67
+
68
+ # Ollama API
69
+ if os.getenv('OLLAMA_URL'):
70
+ config['ollama'] = {
71
+ 'api_url': os.getenv('OLLAMA_URL', 'http://localhost:11434'),
72
+ 'model': os.getenv('OLLAMA_MODEL', 'llama3.1')
73
+ }
74
+
75
+ # Custom API
76
+ if os.getenv('CUSTOM_API_URL'):
77
+ config['custom'] = {
78
+ 'api_url': os.getenv('CUSTOM_API_URL'),
79
+ 'api_key': os.getenv('CUSTOM_API_KEY', ''),
80
+ 'model': os.getenv('CUSTOM_MODEL', 'default')
81
+ }
82
+
83
+ return config
84
+
85
+ def load_config_from_file(self, config_path: str = "config.json") -> Optional[Dict[str, Any]]:
86
+ """Load configuration from file"""
87
+ try:
88
+ if os.path.exists(config_path):
89
+ with open(config_path, 'r') as f:
90
+ return json.load(f)
91
+ except Exception as e:
92
+ self.logger.error(f"Failed to load config file: {e}")
93
+ return None
94
+
95
+ def create_config_file_template(self) -> Dict[str, Any]:
96
+ """Create a template for configuration file"""
97
+ return {
98
+ "api_keys": {
99
+ "claude": {
100
+ "api_key": "your-claude-api-key-here",
101
+ "model": "claude-3-5-sonnet-20241022",
102
+ "api_url": "https://api.anthropic.com"
103
+ },
104
+ "sambanova": {
105
+ "api_key": "your-sambanova-api-key-here",
106
+ "model": "Meta-Llama-3.1-70B-Instruct",
107
+ "api_url": "https://api.sambanova.ai"
108
+ },
109
+ "lm_studio": {
110
+ "api_url": "http://localhost:1234/v1",
111
+ "model": "local-model"
112
+ },
113
+ "ollama": {
114
+ "api_url": "http://localhost:11434",
115
+ "model": "llama3.1"
116
+ }
117
+ },
118
+ "settings": {
119
+ "temperature": 0.7,
120
+ "max_tokens": 1000,
121
+ "enable_insights": True,
122
+ "enable_recommendations": True
123
+ },
124
+ "_metadata": {
125
+ "version": "1.0",
126
+ "created": datetime.now().isoformat(),
127
+ "description": "Spend Analyzer MCP Configuration File"
128
+ }
129
+ }
130
+
131
+ def save_config_template(self, config_path: str = "config.json.template") -> bool:
132
+ """Save configuration template file"""
133
+ try:
134
+ template = self.create_config_file_template()
135
+ with open(config_path, 'w') as f:
136
+ json.dump(template, f, indent=2)
137
+ self.logger.info(f"Configuration template saved to {config_path}")
138
+ return True
139
+ except Exception as e:
140
+ self.logger.error(f"Failed to save config template: {e}")
141
+ return False
142
+
143
+ def get_environment_variables_guide(self) -> str:
144
+ """Get guide for setting up environment variables"""
145
+ return """
146
+ # Environment Variables Setup Guide
147
+
148
+ ## For Local Development:
149
+
150
+ ### Windows (Command Prompt):
151
+ ```cmd
152
+ set CLAUDE_API_KEY=your-claude-api-key-here
153
+ set SAMBANOVA_API_KEY=your-sambanova-api-key-here
154
+ set LM_STUDIO_URL=http://localhost:1234/v1
155
+ set OLLAMA_URL=http://localhost:11434
156
+ ```
157
+
158
+ ### Windows (PowerShell):
159
+ ```powershell
160
+ $env:CLAUDE_API_KEY="your-claude-api-key-here"
161
+ $env:SAMBANOVA_API_KEY="your-sambanova-api-key-here"
162
+ $env:LM_STUDIO_URL="http://localhost:1234/v1"
163
+ $env:OLLAMA_URL="http://localhost:11434"
164
+ ```
165
+
166
+ ### macOS/Linux (Bash):
167
+ ```bash
168
+ export CLAUDE_API_KEY="your-claude-api-key-here"
169
+ export SAMBANOVA_API_KEY="your-sambanova-api-key-here"
170
+ export LM_STUDIO_URL="http://localhost:1234/v1"
171
+ export OLLAMA_URL="http://localhost:11434"
172
+ ```
173
+
174
+ ### .env File (Recommended):
175
+ Create a `.env` file in your project directory:
176
+ ```
177
+ CLAUDE_API_KEY=your-claude-api-key-here
178
+ SAMBANOVA_API_KEY=your-sambanova-api-key-here
179
+ LM_STUDIO_URL=http://localhost:1234/v1
180
+ OLLAMA_URL=http://localhost:11434
181
+ ```
182
+
183
+ ## Security Best Practices:
184
+ 1. Never commit API keys to version control
185
+ 2. Use different keys for development and production
186
+ 3. Regularly rotate your API keys
187
+ 4. Monitor API usage for unusual activity
188
+ 5. Use least-privilege access principles
189
+ """