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

Initial Commit

Browse files
README.md CHANGED
@@ -12,3 +12,187 @@ short_description: Finance MCP to Analyze Finance Statement (Email, PDF)
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
15
+
16
+ # Spend Analyzer MCP
17
+
18
+ A comprehensive financial analysis tool that processes bank statements from emails or uploaded PDFs, analyzes spending patterns, and provides AI-powered financial insights through a Model Context Protocol (MCP) interface.
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
+
41
+ 1. Install dependencies:
42
+ ```bash
43
+ pip install -r requirements.txt
44
+ ```
45
+
46
+ 2. Set up environment variables:
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
+
61
+ ## Usage
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
+
74
+ #### Email Processing Tab
75
+ - Connect to Gmail, Outlook, or Yahoo email
76
+ - Automatically scan for bank statement PDFs
77
+ - Handle password-protected documents
78
+ - Process statements from the last 7-90 days
79
+
80
+ #### PDF Upload Tab
81
+ - Direct upload of bank statement PDFs
82
+ - Support for multiple files
83
+ - Password protection handling
84
+ - Instant analysis and processing
85
+
86
+ #### Analysis Dashboard
87
+ - Financial summary cards (income, expenses, cash flow)
88
+ - Interactive spending category charts
89
+ - Monthly trend analysis
90
+ - Budget alerts and recommendations
91
+ - Detailed transaction tables
92
+
93
+ #### AI Financial Advisor
94
+ - Chat with Claude about your spending patterns
95
+ - Quick question buttons for common queries
96
+ - Contextual responses based on your data
97
+ - Personalized financial recommendations
98
+
99
+ #### Settings
100
+ - **Budget Settings**: Set monthly limits by category
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:
107
+
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
+
114
+ - Email passwords should use app-specific passwords
115
+ - PDF passwords are handled securely in memory
116
+ - No financial data is stored permanently by default
117
+ - All processing can be done locally or in secure cloud environments
118
+
119
+ ## Development
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
+ ```
132
+
133
+ ### Key Classes
134
+
135
+ - **`SpendAnalyzerInterface`**: Main Gradio interface controller
136
+ - **`SpendAnalyzer`**: Core financial analysis and insights
137
+ - **`EmailProcessor`**: Email connection and PDF extraction
138
+ - **`PDFProcessor`**: Bank statement PDF parsing
139
+ - **`MCPServer`**: Model Context Protocol implementation
140
+
141
+ ### Extending the System
142
+
143
+ 1. **Add New Banks**: Extend parsing patterns in `PDFProcessor`
144
+ 2. **Custom Categories**: Modify categorization logic in `SpendAnalyzer`
145
+ 3. **New Charts**: Add visualization functions to the dashboard
146
+ 4. **AI Prompts**: Enhance Claude integration in `modal_deployment.py`
147
+
148
+ ## Deployment Options
149
+
150
+ ### Local Deployment
151
+ - Run directly with Python
152
+ - All processing happens locally
153
+ - Suitable for development and testing
154
+
155
+ ### Modal.com Deployment
156
+ - Serverless cloud deployment
157
+ - Scalable processing
158
+ - Integrated with Claude API
159
+ - Production-ready
160
+
161
+ ### Docker Deployment
162
+ ```dockerfile
163
+ 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
172
+
173
+ 1. Fork the repository
174
+ 2. Create a feature branch
175
+ 3. Make your changes
176
+ 4. Add tests if applicable
177
+ 5. Submit a pull request
178
+
179
+ ## License
180
+
181
+ This project is open source. Please ensure you comply with your bank's terms of service when processing financial data.
182
+
183
+ ## Support
184
+
185
+ For issues and questions:
186
+ 1. Check the existing issues
187
+ 2. Create a new issue with detailed information
188
+ 3. Include error logs and system information
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
email_processor.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Email and PDF Processing Module for Bank Statement Analysis
3
+ """
4
+ import imaplib
5
+ from email.message import Message
6
+ import os
7
+ import io
8
+ import re
9
+ import pandas as pd
10
+ from typing import List, Dict, Optional, Tuple
11
+ from dataclasses import dataclass
12
+ from datetime import datetime, timedelta
13
+ import PyPDF2
14
+ import fitz # PyMuPDF
15
+ from email.mime.multipart import MIMEMultipart
16
+ from email.mime.text import MIMEText
17
+ import logging
18
+
19
+ @dataclass
20
+ class BankTransaction:
21
+ date: datetime
22
+ description: str
23
+ amount: float
24
+ category: str = "Unknown"
25
+ account: str = ""
26
+ balance: Optional[float] = None
27
+
28
+ @dataclass
29
+ class StatementInfo:
30
+ bank_name: str
31
+ account_number: str
32
+ statement_period: str
33
+ transactions: List[BankTransaction]
34
+ opening_balance: float
35
+ closing_balance: float
36
+
37
+ class EmailProcessor:
38
+ def __init__(self, email_config: Dict):
39
+ self.email_config = email_config
40
+ self.logger = logging.getLogger(__name__)
41
+ self.bank_patterns = {
42
+ 'chase': r'chase\.com|jpmorgan',
43
+ 'bofa': r'bankofamerica\.com|bofa',
44
+ 'wells': r'wellsfargo\.com',
45
+ 'citi': r'citi\.com|citibank',
46
+ 'amex': r'americanexpress\.com|amex',
47
+ 'hdfc': r'hdfcbank\.com',
48
+ 'icici': r'icicibank\.com',
49
+ 'sbi': r'sbi\.co\.in',
50
+ 'axis': r'axisbank\.com',
51
+ }
52
+
53
+ async def connect_to_email(self) -> imaplib.IMAP4_SSL:
54
+ """Connect to email server"""
55
+ try:
56
+ mail = imaplib.IMAP4_SSL(self.email_config['imap_server'])
57
+ mail.login(self.email_config['email'], self.email_config['password'])
58
+ return mail
59
+ except Exception as e:
60
+ self.logger.error(f"Failed to connect to email: {e}")
61
+ raise
62
+
63
+ async def fetch_bank_emails(self, days_back: int = 30) -> List[Message]:
64
+ """Fetch emails from banks containing statements"""
65
+ mail = await self.connect_to_email()
66
+ mail.select('inbox')
67
+
68
+ # Calculate date range
69
+ end_date = datetime.now()
70
+ start_date = end_date - timedelta(days=days_back)
71
+
72
+ # Search for bank emails
73
+ bank_domains = '|'.join(self.bank_patterns.values())
74
+ search_criteria = f'(FROM "{bank_domains}" SINCE "{start_date.strftime("%d-%b-%Y")}")'
75
+
76
+ try:
77
+ status, messages = mail.search(None, search_criteria)
78
+ email_ids = messages[0].split()
79
+
80
+ emails = []
81
+ for email_id in email_ids[-50:]: # Limit to recent 50 emails
82
+ status, msg_data = mail.fetch(email_id, '(RFC822)')
83
+ msg = Message.from_bytes(msg_data[0][1])
84
+ emails.append(msg)
85
+
86
+ return emails
87
+ finally:
88
+ mail.close()
89
+ mail.logout()
90
+
91
+ def identify_bank(self, sender_email: str) -> str:
92
+ """Identify bank from sender email"""
93
+ sender_lower = sender_email.lower()
94
+ for bank, pattern in self.bank_patterns.items():
95
+ if re.search(pattern, sender_lower):
96
+ return bank
97
+ return "unknown"
98
+
99
+ async def extract_attachments(self, msg: Message) -> List[Tuple[str, bytes, str]]:
100
+ """Extract PDF attachments from email"""
101
+ attachments = []
102
+ self.logger.debug(f"Processing message with type: {type(msg)}")
103
+
104
+ for part in msg.walk():
105
+ self.logger.debug(f"Processing part with type: {type(part)}")
106
+ try:
107
+ if part.get_content_disposition() == 'attachment':
108
+ filename = part.get_filename()
109
+ if filename and filename.lower().endswith('.pdf'):
110
+ content = part.get_payload(decode=True)
111
+ attachments.append((filename, content, 'pdf'))
112
+ except Exception as e:
113
+ self.logger.error(f"Error processing part: {e}, Part type: {type(part)}")
114
+ continue
115
+
116
+ return attachments
117
+
118
+ class PDFProcessor:
119
+ def __init__(self):
120
+ self.logger = logging.getLogger(__name__)
121
+ self.transaction_patterns = {
122
+ 'date': r'(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})',
123
+ 'amount': r'([\$\-]?[\d,]+\.?\d{0,2})',
124
+ 'description': r'([A-Za-z0-9\s\*\#\-_]+)'
125
+ }
126
+
127
+ async def process_pdf(self, pdf_content: bytes, password: Optional[str] = None) -> StatementInfo:
128
+ """Process PDF bank statement"""
129
+ try:
130
+ # Try PyMuPDF first
131
+ doc = fitz.open(stream=pdf_content, filetype="pdf")
132
+
133
+ if doc.needs_pass and password:
134
+ if not doc.authenticate(password):
135
+ raise ValueError("Invalid PDF password")
136
+ elif doc.needs_pass and not password:
137
+ raise ValueError("PDF requires password")
138
+
139
+ text = ""
140
+ for page in doc:
141
+ text += page.get_text()
142
+
143
+ doc.close()
144
+
145
+ return await self.parse_statement_text(text)
146
+
147
+ except Exception as e:
148
+ self.logger.error(f"Error processing PDF: {e}")
149
+ # Fallback to PyPDF2
150
+ return await self.process_pdf_fallback(pdf_content, password)
151
+
152
+ async def process_pdf_fallback(self, pdf_content: bytes, password: Optional[str] = None) -> StatementInfo:
153
+ """Fallback PDF processing with PyPDF2"""
154
+ try:
155
+ pdf_reader = PyPDF2.PdfReader(io.BytesIO(pdf_content))
156
+
157
+ if pdf_reader.is_encrypted:
158
+ if password:
159
+ pdf_reader.decrypt(password)
160
+ else:
161
+ raise ValueError("PDF requires password")
162
+
163
+ text = ""
164
+ for page in pdf_reader.pages:
165
+ text += page.extract_text()
166
+
167
+ return await self.parse_statement_text(text)
168
+
169
+ except Exception as e:
170
+ self.logger.error(f"Fallback PDF processing failed: {e}")
171
+ raise
172
+
173
+ async def parse_statement_text(self, text: str) -> StatementInfo:
174
+ """Parse bank statement text to extract transactions"""
175
+ lines = text.split('\n')
176
+ transactions = []
177
+
178
+ # Bank-specific parsing logic
179
+ bank_name = self.detect_bank_from_text(text)
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)
191
+ closing_balance = self.extract_closing_balance(text)
192
+
193
+ return StatementInfo(
194
+ bank_name=bank_name,
195
+ account_number=account_number,
196
+ statement_period=statement_period,
197
+ transactions=transactions,
198
+ opening_balance=opening_balance,
199
+ closing_balance=closing_balance
200
+ )
201
+
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'
209
+ elif 'wells fargo' in text_lower:
210
+ return 'Wells Fargo'
211
+ elif 'citibank' in text_lower or 'citi' in text_lower:
212
+ return 'Citibank'
213
+ elif 'american express' in text_lower or 'amex' in text_lower:
214
+ return 'American Express'
215
+ return 'Unknown Bank'
216
+
217
+ def extract_account_number(self, text: str) -> str:
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"
231
+
232
+ def extract_statement_period(self, text: str) -> str:
233
+ """Extract statement period"""
234
+ # Look for date ranges
235
+ pattern = r'(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\s*(?:to|through|-)\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})'
236
+ match = re.search(pattern, text, re.IGNORECASE)
237
+
238
+ if match:
239
+ return f"{match.group(1)} to {match.group(2)}"
240
+ return "Unknown Period"
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:
295
+ return datetime.strptime(date_str, fmt)
296
+ except ValueError:
297
+ continue
298
+ # If all fails, return current date
299
+ return datetime.now()
300
+
301
+ def parse_amount(self, amount_str: str) -> float:
302
+ """Parse amount string to float"""
303
+ # Clean amount string
304
+ clean_amount = amount_str.replace('$', '').replace(',', '').strip()
305
+
306
+ # Handle negative amounts
307
+ is_negative = clean_amount.startswith('-') or clean_amount.startswith('(')
308
+ clean_amount = clean_amount.replace('-', '').replace('(', '').replace(')', '')
309
+
310
+ try:
311
+ amount = float(clean_amount)
312
+ return -amount if is_negative else amount
313
+ except ValueError:
314
+ return 0.0
315
+
316
+ def categorize_transaction(self, description: str) -> str:
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():
331
+ if any(keyword in desc_lower for keyword in keywords):
332
+ return category
333
+ return 'Other'
334
+
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:
346
+ return float(match.group(1).replace(',', ''))
347
+ return 0.0
348
+
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
366
+ pdf_processor = PDFProcessor()
367
+
368
+ # Example test with sample PDF content
369
+ print("PDF Processor initialized successfully")
gradio_interface.py ADDED
@@ -0,0 +1,788 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,627 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
gradio_interface_real.py ADDED
@@ -0,0 +1,845 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio Web Interface for Spend Analyzer MCP - Real PDF Processing
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
+ 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"""
34
+ with gr.Blocks(
35
+ title="Spend Analyzer MCP - Real PDF Processing",
36
+ css="""
37
+ .main-header { text-align: center; margin: 20px 0; }
38
+ .status-box { padding: 10px; border-radius: 5px; margin: 10px 0; }
39
+ .success-box { background-color: #d4edda; border: 1px solid #c3e6cb; }
40
+ .error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; }
41
+ .warning-box { background-color: #fff3cd; border: 1px solid #ffeaa7; }
42
+ .info-box { background-color: #e7f3ff; border: 1px solid #b3d9ff; }
43
+ """
44
+ ) as interface:
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
+
51
+ with gr.Tabs():
52
+ # Tab 1: PDF Upload & Processing
53
+ with gr.TabItem("📄 PDF Upload & Analysis"):
54
+ self._create_pdf_processing_tab()
55
+
56
+ # Tab 2: Analysis Dashboard
57
+ with gr.TabItem("📊 Analysis Dashboard"):
58
+ self._create_dashboard_tab()
59
+
60
+ # Tab 3: AI Financial Advisor
61
+ with gr.TabItem("🤖 AI Financial Advisor"):
62
+ self._create_chat_tab()
63
+
64
+ # Tab 4: Transaction Management
65
+ with gr.TabItem("📋 Transaction Management"):
66
+ self._create_transaction_tab()
67
+
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")
77
+ gr.Markdown("*Upload your bank statement PDFs for real financial analysis*")
78
+
79
+ with gr.Row():
80
+ with gr.Column(scale=2):
81
+ # File upload section
82
+ gr.Markdown("### 📁 File Upload")
83
+ pdf_upload = gr.File(
84
+ label="Upload Bank Statement PDFs",
85
+ file_count="multiple",
86
+ file_types=[".pdf"],
87
+ height=150
88
+ )
89
+
90
+ # Password section
91
+ gr.Markdown("### 🔐 PDF Passwords (if needed)")
92
+ pdf_passwords_input = gr.Textbox(
93
+ label="PDF Passwords (JSON format)",
94
+ placeholder='{"statement1.pdf": "password123", "statement2.pdf": "password456"}',
95
+ lines=3
96
+ )
97
+
98
+ # Processing options
99
+ gr.Markdown("### ⚙️ Processing Options")
100
+ with gr.Row():
101
+ auto_categorize = gr.Checkbox(
102
+ label="Auto-categorize transactions",
103
+ value=True
104
+ )
105
+ detect_duplicates = gr.Checkbox(
106
+ label="Detect duplicate transactions",
107
+ value=True
108
+ )
109
+
110
+ # Process button
111
+ process_pdf_btn = gr.Button("🚀 Process PDFs", variant="primary", size="lg")
112
+
113
+ with gr.Column(scale=1):
114
+ # Status and results
115
+ processing_status = gr.HTML()
116
+
117
+ # Processing progress
118
+ gr.Markdown("### 📊 Processing Results")
119
+ processing_results = gr.JSON(
120
+ label="Detailed Results",
121
+ visible=False
122
+ )
123
+
124
+ # Quick stats
125
+ quick_stats = gr.HTML()
126
+
127
+ # Event handler
128
+ process_pdf_btn.click(
129
+ fn=self._process_real_pdfs,
130
+ inputs=[pdf_upload, pdf_passwords_input, auto_categorize, detect_duplicates],
131
+ outputs=[processing_status, processing_results, quick_stats]
132
+ )
133
+
134
+ def _create_dashboard_tab(self):
135
+ """Create analysis dashboard tab"""
136
+ gr.Markdown("## 📊 Financial Analysis Dashboard")
137
+
138
+ with gr.Row():
139
+ refresh_btn = gr.Button("🔄 Refresh Dashboard")
140
+ export_btn = gr.Button("📤 Export Analysis")
141
+ clear_btn = gr.Button("🗑️ Clear Data", variant="stop")
142
+
143
+ # Summary cards
144
+ gr.Markdown("### 💰 Financial Summary")
145
+ with gr.Row():
146
+ total_income = gr.Number(label="Total Income ($)", interactive=False)
147
+ total_expenses = gr.Number(label="Total Expenses ($)", interactive=False)
148
+ net_cashflow = gr.Number(label="Net Cash Flow ($)", interactive=False)
149
+ transaction_count = gr.Number(label="Total Transactions", interactive=False)
150
+
151
+ # Charts section
152
+ gr.Markdown("### 📈 Visual Analysis")
153
+ with gr.Row():
154
+ with gr.Column():
155
+ spending_by_category = gr.Plot(label="Spending by Category")
156
+ monthly_trends = gr.Plot(label="Monthly Spending Trends")
157
+
158
+ with gr.Column():
159
+ income_vs_expenses = gr.Plot(label="Income vs Expenses")
160
+ top_merchants = gr.Plot(label="Top Merchants")
161
+
162
+ # Insights section
163
+ gr.Markdown("### 🎯 Financial Insights")
164
+ with gr.Row():
165
+ with gr.Column():
166
+ budget_alerts = gr.HTML(label="Budget Alerts")
167
+ spending_insights = gr.HTML(label="Spending Insights")
168
+
169
+ with gr.Column():
170
+ recommendations = gr.HTML(label="AI Recommendations")
171
+ unusual_transactions = gr.HTML(label="Unusual Transactions")
172
+
173
+ # Detailed data
174
+ with gr.Accordion("📋 Detailed Transaction Data", open=False):
175
+ transaction_table = gr.Dataframe(
176
+ headers=["Date", "Description", "Amount", "Category", "Account"],
177
+ interactive=True,
178
+ label="All Transactions"
179
+ )
180
+
181
+ # Status displays for clear function
182
+ clear_status = gr.HTML()
183
+ clear_info = gr.HTML()
184
+
185
+ # Event handlers
186
+ refresh_btn.click(
187
+ fn=self._refresh_dashboard,
188
+ outputs=[total_income, total_expenses, net_cashflow, transaction_count,
189
+ spending_by_category, monthly_trends, income_vs_expenses, top_merchants,
190
+ budget_alerts, spending_insights, recommendations, unusual_transactions,
191
+ transaction_table]
192
+ )
193
+
194
+ export_btn.click(
195
+ fn=self._export_analysis,
196
+ outputs=[gr.File(label="Analysis Export")]
197
+ )
198
+
199
+ clear_btn.click(
200
+ fn=self._clear_data,
201
+ outputs=[clear_status, clear_info]
202
+ )
203
+
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
+
218
+ with gr.Row():
219
+ msg_input = gr.Textbox(
220
+ placeholder="Ask about your spending patterns, budgets, or financial goals...",
221
+ label="Your Question",
222
+ scale=4
223
+ )
224
+ send_btn = gr.Button("Send", variant="primary", scale=1)
225
+
226
+ # Quick question buttons
227
+ gr.Markdown("### 🎯 Quick Questions")
228
+ with gr.Row():
229
+ budget_btn = gr.Button("💰 Budget Analysis", size="sm")
230
+ trends_btn = gr.Button("📈 Spending Trends", size="sm")
231
+ tips_btn = gr.Button("💡 Save Money Tips", size="sm")
232
+ unusual_btn = gr.Button("🚨 Unusual Activity", size="sm")
233
+
234
+ with gr.Row():
235
+ categories_btn = gr.Button("📊 Category Breakdown", size="sm")
236
+ merchants_btn = gr.Button("🏪 Top Merchants", size="sm")
237
+ monthly_btn = gr.Button("📅 Monthly Analysis", size="sm")
238
+ goals_btn = gr.Button("🎯 Financial Goals", size="sm")
239
+
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(
246
+ label="Available Data",
247
+ value={"status": "Upload PDFs to start analysis"}
248
+ )
249
+
250
+ # Chat settings
251
+ gr.Markdown("### ⚙️ Chat Settings")
252
+ response_style = gr.Radio(
253
+ choices=["Detailed", "Concise", "Technical"],
254
+ label="Response Style",
255
+ value="Detailed"
256
+ )
257
+
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])
274
+ tips_btn.click(lambda: "What are specific ways I can save money based on my spending?", outputs=[msg_input])
275
+ unusual_btn.click(lambda: "Are there any unusual transactions I should be aware of?", outputs=[msg_input])
276
+ categories_btn.click(lambda: "Break down my spending by category", outputs=[msg_input])
277
+ merchants_btn.click(lambda: "Who are my top merchants and how much do I spend with them?", outputs=[msg_input])
278
+ monthly_btn.click(lambda: "Analyze my monthly spending patterns", outputs=[msg_input])
279
+ goals_btn.click(lambda: "Help me set realistic financial goals based on my spending", outputs=[msg_input])
280
+
281
+ def _create_transaction_tab(self):
282
+ """Create transaction management tab"""
283
+ gr.Markdown("## 📋 Transaction Management")
284
+ gr.Markdown("*Review, edit, and categorize your transactions*")
285
+
286
+ with gr.Row():
287
+ with gr.Column(scale=2):
288
+ # Transaction filters
289
+ gr.Markdown("### 🔍 Filter Transactions")
290
+ with gr.Row():
291
+ date_from = gr.Textbox(label="From Date (YYYY-MM-DD)", placeholder="2024-01-01")
292
+ date_to = gr.Textbox(label="To Date (YYYY-MM-DD)", placeholder="2024-12-31")
293
+
294
+ with gr.Row():
295
+ category_filter = gr.Dropdown(
296
+ choices=["All", "Food & Dining", "Shopping", "Gas & Transport",
297
+ "Utilities", "Entertainment", "Healthcare", "Other"],
298
+ label="Category Filter",
299
+ value="All"
300
+ )
301
+ amount_filter = gr.Radio(
302
+ choices=["All", "Income Only", "Expenses Only", "> $100", "> $500"],
303
+ label="Amount Filter",
304
+ value="All"
305
+ )
306
+
307
+ filter_btn = gr.Button("🔍 Apply Filters", variant="secondary")
308
+
309
+ # Transaction editing
310
+ gr.Markdown("### ✏️ Edit Transaction")
311
+ with gr.Row():
312
+ edit_transaction_id = gr.Number(label="Transaction ID", precision=0)
313
+ edit_category = gr.Dropdown(
314
+ choices=["Food & Dining", "Shopping", "Gas & Transport",
315
+ "Utilities", "Entertainment", "Healthcare", "Other"],
316
+ label="New Category"
317
+ )
318
+
319
+ update_btn = gr.Button("💾 Update Transaction", variant="primary")
320
+
321
+ with gr.Column(scale=1):
322
+ # Transaction stats
323
+ gr.Markdown("### 📊 Transaction Statistics")
324
+ transaction_stats = gr.HTML()
325
+
326
+ # Category management
327
+ gr.Markdown("### 🏷️ Category Management")
328
+ add_category = gr.Textbox(label="Add New Category")
329
+ add_category_btn = gr.Button("➕ Add Category")
330
+
331
+ category_status = gr.HTML()
332
+
333
+ # Filtered transactions table
334
+ filtered_transactions = gr.Dataframe(
335
+ headers=["ID", "Date", "Description", "Amount", "Category", "Account"],
336
+ interactive=False,
337
+ label="Filtered Transactions"
338
+ )
339
+
340
+ # Event handlers
341
+ filter_btn.click(
342
+ fn=self._filter_transactions,
343
+ inputs=[date_from, date_to, category_filter, amount_filter],
344
+ outputs=[filtered_transactions, transaction_stats]
345
+ )
346
+
347
+ update_btn.click(
348
+ fn=self._update_transaction,
349
+ inputs=[edit_transaction_id, edit_category],
350
+ outputs=[category_status, filtered_transactions]
351
+ )
352
+
353
+ add_category_btn.click(
354
+ fn=self._add_category,
355
+ inputs=[add_category],
356
+ outputs=[category_status, edit_category, category_filter]
357
+ )
358
+
359
+ def _create_settings_tab(self):
360
+ """Create settings and export tab"""
361
+ gr.Markdown("## ⚙️ Settings & Export")
362
+
363
+ with gr.Tabs():
364
+ with gr.TabItem("Budget Settings"):
365
+ gr.Markdown("### 💰 Monthly Budget Configuration")
366
+
367
+ with gr.Row():
368
+ with gr.Column():
369
+ budget_categories = gr.CheckboxGroup(
370
+ choices=["Food & Dining", "Shopping", "Gas & Transport",
371
+ "Utilities", "Entertainment", "Healthcare", "Other"],
372
+ label="Categories to Budget",
373
+ value=["Food & Dining", "Shopping", "Gas & Transport"]
374
+ )
375
+
376
+ budget_amounts = gr.JSON(
377
+ label="Budget Amounts ($)",
378
+ value={
379
+ "Food & Dining": 500,
380
+ "Shopping": 300,
381
+ "Gas & Transport": 200,
382
+ "Utilities": 150,
383
+ "Entertainment": 100,
384
+ "Healthcare": 200,
385
+ "Other": 100
386
+ }
387
+ )
388
+
389
+ save_budgets_btn = gr.Button("💾 Save Budget Settings", variant="primary")
390
+
391
+ with gr.Column():
392
+ budget_status = gr.HTML()
393
+ current_budgets = gr.JSON(label="Current Budget Settings")
394
+
395
+ with gr.TabItem("Export Options"):
396
+ gr.Markdown("### 📤 Data Export")
397
+
398
+ with gr.Row():
399
+ with gr.Column():
400
+ export_format = gr.Radio(
401
+ choices=["JSON", "CSV", "Excel"],
402
+ label="Export Format",
403
+ value="CSV"
404
+ )
405
+
406
+ export_options = gr.CheckboxGroup(
407
+ choices=["Raw Transactions", "Analysis Summary", "Charts Data", "Recommendations"],
408
+ label="Include in Export",
409
+ value=["Raw Transactions", "Analysis Summary"]
410
+ )
411
+
412
+ date_range_export = gr.CheckboxGroup(
413
+ choices=["Last 30 days", "Last 90 days", "Last 6 months", "All data"],
414
+ label="Date Range",
415
+ value=["All data"]
416
+ )
417
+
418
+ export_data_btn = gr.Button("📤 Export Data", variant="primary")
419
+
420
+ with gr.Column():
421
+ export_status = gr.HTML()
422
+
423
+ gr.Markdown("### 📊 Export Preview")
424
+ export_preview = gr.JSON(label="Export Preview")
425
+
426
+ with gr.TabItem("Processing Settings"):
427
+ gr.Markdown("### ⚙️ PDF Processing Configuration")
428
+
429
+ processing_settings = gr.JSON(
430
+ label="Processing Settings",
431
+ value={
432
+ "auto_categorize": True,
433
+ "detect_duplicates": True,
434
+ "merge_similar_transactions": False,
435
+ "confidence_threshold": 0.8,
436
+ "date_format": "auto",
437
+ "amount_format": "auto"
438
+ }
439
+ )
440
+
441
+ save_processing_btn = gr.Button("💾 Save Processing Settings", variant="primary")
442
+ processing_status = gr.HTML()
443
+
444
+ # Event handlers
445
+ save_budgets_btn.click(
446
+ fn=self._save_budget_settings,
447
+ inputs=[budget_categories, budget_amounts],
448
+ outputs=[budget_status, current_budgets]
449
+ )
450
+
451
+ export_data_btn.click(
452
+ fn=self._export_data,
453
+ inputs=[export_format, export_options, date_range_export],
454
+ outputs=[export_status, export_preview, gr.File(label="Export File")]
455
+ )
456
+
457
+ save_processing_btn.click(
458
+ fn=self._save_processing_settings,
459
+ inputs=[processing_settings],
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"""
466
+ try:
467
+ if not files:
468
+ return ('<div class="status-box error-box"> No files uploaded</div>',
469
+ gr.update(visible=False), "")
470
+
471
+ # Update status
472
+ status_html = '<div class="status-box warning-box"> Processing PDF files...</div>'
473
+
474
+ # Parse passwords if provided
475
+ passwords = {}
476
+ if isinstance(passwords_json, dict):
477
+ passwords = passwords_json
478
+ elif passwords_json.strip():
479
+ try:
480
+ passwords = json.loads(passwords_json)
481
+ except json.JSONDecodeError:
482
+ return ('<div class="status-box error-box"> Invalid JSON format for passwords</div>',
483
+ gr.update(visible=False), "")
484
+
485
+ all_transactions = []
486
+ processed_files = []
487
+
488
+ # Process each PDF
489
+ for file in files:
490
+ try:
491
+ # Read file content
492
+ with open(file.name, 'rb') as f:
493
+ pdf_content = f.read()
494
+
495
+ # Get password for this file
496
+ file_password = passwords.get(os.path.basename(file.name))
497
+
498
+ # Process PDF
499
+ statement_info = asyncio.run(
500
+ self.pdf_processor.process_pdf(pdf_content, file_password)
501
+ )
502
+
503
+ # Add transactions
504
+ all_transactions.extend(statement_info.transactions)
505
+
506
+ processed_files.append({
507
+ 'filename': os.path.basename(file.name),
508
+ 'bank': statement_info.bank_name,
509
+ 'account': statement_info.account_number,
510
+ 'period': statement_info.statement_period,
511
+ 'transaction_count': len(statement_info.transactions),
512
+ 'opening_balance': statement_info.opening_balance,
513
+ 'closing_balance': statement_info.closing_balance,
514
+ 'status': 'success'
515
+ })
516
+
517
+ except Exception as e:
518
+ processed_files.append({
519
+ 'filename': os.path.basename(file.name),
520
+ 'status': 'error',
521
+ 'error': str(e)
522
+ })
523
+
524
+ if not all_transactions:
525
+ return ('<div class="status-box warning-box"> No transactions found in uploaded files</div>',
526
+ gr.update(value={"processed_files": processed_files}, visible=True), "")
527
+
528
+ # Load transactions into analyzer
529
+ self.spend_analyzer.load_transactions(all_transactions)
530
+
531
+ # Generate analysis
532
+ self.current_analysis = self.spend_analyzer.export_analysis_data()
533
+
534
+ # Create success status
535
+ status_html = f'<div class="status-box success-box"> Successfully processed {len(processed_files)} files with {len(all_transactions)} transactions</div>'
536
+
537
+ # Create quick stats
538
+ total_income = sum(t.amount for t in all_transactions if t.amount > 0)
539
+ total_expenses = abs(sum(t.amount for t in all_transactions if t.amount < 0))
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>
551
+ '''
552
+
553
+ results = {
554
+ "processed_files": processed_files,
555
+ "total_transactions": len(all_transactions),
556
+ "analysis_summary": {
557
+ "total_income": total_income,
558
+ "total_expenses": total_expenses,
559
+ "net_cash_flow": total_income - total_expenses
560
+ }
561
+ }
562
+
563
+ return (status_html,
564
+ gr.update(value=results, visible=True),
565
+ quick_stats_html)
566
+
567
+ except Exception as e:
568
+ error_html = f'<div class="status-box error-box"> Processing error: {str(e)}</div>'
569
+ return error_html, gr.update(visible=False), ""
570
+
571
+ def _refresh_dashboard(self):
572
+ """Refresh dashboard with current analysis"""
573
+ if not self.current_analysis:
574
+ empty_return = (0, 0, 0, 0, None, None, None, None,
575
+ '<div class="status-box warning-box"> No analysis data available</div>',
576
+ '<div class="status-box warning-box"> Process PDFs first</div>',
577
+ '<div class="status-box warning-box"> No recommendations available</div>',
578
+ '<div class="status-box warning-box"> No unusual transactions detected</div>',
579
+ pd.DataFrame())
580
+ return empty_return
581
+
582
+ try:
583
+ summary = self.current_analysis.get('financial_summary', {})
584
+ insights = self.current_analysis.get('spending_insights', [])
585
+
586
+ # Summary metrics
587
+ total_income = summary.get('total_income', 0)
588
+ total_expenses = summary.get('total_expenses', 0)
589
+ net_cashflow = summary.get('net_cash_flow', 0)
590
+ transaction_count = self.current_analysis.get('transaction_count', 0)
591
+
592
+ # Create charts
593
+ charts = self._create_charts(insights, summary)
594
+
595
+ # Create insights HTML
596
+ insights_html = self._create_insights_html()
597
+
598
+ # Create transaction table
599
+ transaction_df = self._create_transaction_dataframe()
600
+
601
+ return (total_income, total_expenses, net_cashflow, transaction_count,
602
+ charts['spending_by_category'], charts['monthly_trends'],
603
+ charts['income_vs_expenses'], charts['top_merchants'],
604
+ insights_html['budget_alerts'], insights_html['spending_insights'],
605
+ insights_html['recommendations'], insights_html['unusual_transactions'],
606
+ transaction_df)
607
+
608
+ except Exception as e:
609
+ error_msg = f'<div class="status-box error-box"> Dashboard error: {str(e)}</div>'
610
+ empty_return = (0, 0, 0, 0, None, None, None, None,
611
+ error_msg, error_msg, error_msg, error_msg, pd.DataFrame())
612
+ return empty_return
613
+
614
+ def _create_charts(self, insights, summary):
615
+ """Create visualization charts"""
616
+ charts = {}
617
+
618
+ # Spending by category chart
619
+ if insights:
620
+ categories = [insight['category'] for insight in insights]
621
+ amounts = [insight['total_amount'] for insight in insights]
622
+
623
+ charts['spending_by_category'] = px.pie(
624
+ values=amounts,
625
+ names=categories,
626
+ title="Spending by Category"
627
+ )
628
+ else:
629
+ charts['spending_by_category'] = None
630
+
631
+ # Monthly trends (placeholder)
632
+ charts['monthly_trends'] = None
633
+ charts['income_vs_expenses'] = None
634
+ charts['top_merchants'] = None
635
+
636
+ return charts
637
+
638
+ def _create_insights_html(self):
639
+ """Create insights HTML sections"""
640
+ insights = {}
641
+
642
+ if not self.current_analysis:
643
+ # Return empty insights if no analysis available
644
+ insights['budget_alerts'] = '<div class="status-box warning-box"> No analysis data available</div>'
645
+ insights['spending_insights'] = '<div class="status-box warning-box"> No analysis data available</div>'
646
+ insights['recommendations'] = '<div class="status-box warning-box"> No analysis data available</div>'
647
+ insights['unusual_transactions'] = '<div class="status-box warning-box"> No analysis data available</div>'
648
+ return insights
649
+
650
+ # Budget alerts
651
+ budget_alerts = self.current_analysis.get('budget_alerts', [])
652
+ if budget_alerts:
653
+ alerts_html = '<div class="status-box warning-box"><h4> Budget Alerts:</h4><ul>'
654
+ for alert in budget_alerts:
655
+ if isinstance(alert, dict):
656
+ alerts_html += f'<li>{alert.get("category", "Unknown")}: {alert.get("percentage_used", 0):.1f}% used</li>'
657
+ alerts_html += '</ul></div>'
658
+ else:
659
+ alerts_html = '<div class="status-box success-box"> All budgets on track</div>'
660
+
661
+ insights['budget_alerts'] = alerts_html
662
+
663
+ # Spending insights
664
+ spending_insights = self.current_analysis.get('spending_insights', [])
665
+ if spending_insights:
666
+ insights_html = '<div class="status-box info-box"><h4> Spending Insights:</h4><ul>'
667
+ for insight in spending_insights[:3]:
668
+ if isinstance(insight, dict):
669
+ insights_html += f'<li><strong>{insight.get("category", "Unknown")}:</strong> ${insight.get("total_amount", 0):.2f} ({insight.get("percentage_of_total", 0):.1f}%)</li>'
670
+ insights_html += '</ul></div>'
671
+ else:
672
+ insights_html = '<div class="status-box">No spending insights available</div>'
673
+
674
+ insights['spending_insights'] = insights_html
675
+
676
+ # Recommendations
677
+ recommendations = self.current_analysis.get('recommendations', [])
678
+ if recommendations:
679
+ rec_html = '<div class="status-box info-box"><h4> Recommendations:</h4><ul>'
680
+ for rec in recommendations[:3]:
681
+ if rec: # Check if recommendation is not None/empty
682
+ rec_html += f'<li>{rec}</li>'
683
+ rec_html += '</ul></div>'
684
+ else:
685
+ rec_html = '<div class="status-box">No specific recommendations available</div>'
686
+
687
+ insights['recommendations'] = rec_html
688
+
689
+ # Unusual transactions
690
+ financial_summary = self.current_analysis.get('financial_summary', {})
691
+ unusual = financial_summary.get('unusual_transactions', []) if financial_summary else []
692
+ if unusual:
693
+ unusual_html = '<div class="status-box warning-box"><h4> Unusual Transactions:</h4><ul>'
694
+ for trans in unusual[:3]:
695
+ if isinstance(trans, dict):
696
+ desc = trans.get("description", "Unknown")
697
+ amount = trans.get("amount", 0)
698
+ unusual_html += f'<li>{desc}: ${amount:.2f}</li>'
699
+ unusual_html += '</ul></div>'
700
+ else:
701
+ unusual_html = '<div class="status-box success-box"> No unusual transactions detected</div>'
702
+
703
+ insights['unusual_transactions'] = unusual_html
704
+
705
+ return insights
706
+
707
+ def _create_transaction_dataframe(self):
708
+ """Create transaction dataframe for display"""
709
+ # This would create a dataframe from the actual transactions
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"""
737
+ # Placeholder implementation
738
+ return pd.DataFrame(), '<div class="status-box info-box">Filtering functionality would be implemented here</div>'
739
+
740
+ def _update_transaction(self, transaction_id, new_category):
741
+ """Update transaction category"""
742
+ return '<div class="status-box success-box"> Transaction updated</div>', pd.DataFrame()
743
+
744
+ def _add_category(self, new_category):
745
+ """Add new transaction category"""
746
+ return '<div class="status-box success-box"> Category added</div>', gr.update(), gr.update()
747
+
748
+ def _save_budget_settings(self, categories, amounts):
749
+ """Save budget settings"""
750
+ try:
751
+ budget_settings = {cat: amounts.get(cat, 0) for cat in categories}
752
+ self.user_sessions['budgets'] = budget_settings
753
+
754
+ # Apply budgets to analyzer
755
+ self.spend_analyzer.set_budgets(budget_settings)
756
+
757
+ status_html = '<div class="status-box success-box"> Budget settings saved and applied</div>'
758
+ return status_html, budget_settings
759
+
760
+ except Exception as e:
761
+ error_html = f'<div class="status-box error-box"> Error saving budgets: {str(e)}</div>'
762
+ return error_html, {}
763
+
764
+ def _export_data(self, export_format, export_options, date_range):
765
+ """Export analysis data"""
766
+ if not self.current_analysis:
767
+ return '<div class="status-box error-box"> No data to export</div>', {}, None
768
+
769
+ try:
770
+ # Create export data
771
+ export_data = {}
772
+
773
+ if "Analysis Summary" in export_options:
774
+ export_data['summary'] = self.current_analysis.get('financial_summary', {})
775
+
776
+ if "Raw Transactions" in export_options:
777
+ export_data['transactions'] = [] # Would populate with actual transaction data
778
+
779
+ # Create temporary file
780
+ with tempfile.NamedTemporaryFile(mode='w', suffix=f'.{export_format.lower()}', delete=False) as f:
781
+ if export_format == "JSON":
782
+ json.dump(export_data, f, indent=2, default=str)
783
+ elif export_format == "CSV":
784
+ # Would create CSV format
785
+ f.write("Export functionality would create CSV here")
786
+
787
+ file_path = f.name
788
+
789
+ status_html = '<div class="status-box success-box"> Data exported successfully</div>'
790
+ return status_html, export_data, file_path
791
+
792
+ except Exception as e:
793
+ error_html = f'<div class="status-box error-box"> Export error: {str(e)}</div>'
794
+ return error_html, {}, None
795
+
796
+ def _save_processing_settings(self, settings):
797
+ """Save processing settings"""
798
+ try:
799
+ self.user_sessions['processing_settings'] = settings
800
+ return '<div class="status-box success-box"> Processing settings saved</div>'
801
+ except Exception as e:
802
+ return f'<div class="status-box error-box"> Error saving settings: {str(e)}</div>'
803
+
804
+ def _export_analysis(self):
805
+ """Export current analysis"""
806
+ if not self.current_analysis:
807
+ return None
808
+
809
+ try:
810
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
811
+ json.dump(self.current_analysis, f, indent=2, default=str)
812
+ return f.name
813
+ except Exception as e:
814
+ self.logger.error(f"Export error: {e}")
815
+ return None
816
+
817
+ def _clear_data(self):
818
+ """Clear all data"""
819
+ self.current_analysis = None
820
+ self.spend_analyzer = SpendAnalyzer() # Reset analyzer
821
+
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"""
828
+ interface = RealSpendAnalyzerInterface()
829
+ app = interface.create_interface()
830
+
831
+ print(" Starting Spend Analyzer MCP - Real PDF Processing")
832
+ print(" Upload your bank statement PDFs for analysis")
833
+ print(" Opening in browser...")
834
+
835
+ app.launch(
836
+ server_name="0.0.0.0",
837
+ server_port=7862,
838
+ share=False,
839
+ debug=True,
840
+ show_error=True,
841
+ inbrowser=True
842
+ )
843
+
844
+ if __name__ == "__main__":
845
+ launch_interface()
mcp_server.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 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):
13
+ REQUEST = "request"
14
+ RESPONSE = "response"
15
+ NOTIFICATION = "notification"
16
+
17
+ @dataclass
18
+ class MCPMessage:
19
+ jsonrpc: str = "2.0"
20
+ id: Optional[str] = None
21
+ method: Optional[str] = None
22
+ params: Optional[Dict] = None
23
+ result: Optional[Any] = None
24
+ error: Optional[Dict] = None
25
+
26
+ class MCPServer:
27
+ def __init__(self):
28
+ self.tools = {}
29
+ self.resources = {}
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):
46
+ """Register a resource that provides data"""
47
+ self.resources[uri] = {
48
+ "name": name,
49
+ "description": description,
50
+ "handler": handler,
51
+ "mimeType": "application/json"
52
+ }
53
+
54
+ async def handle_message(self, message: Dict) -> Dict:
55
+ """Handle incoming MCP messages"""
56
+ try:
57
+ method = message.get("method")
58
+ params = message.get("params", {})
59
+ msg_id = message.get("id")
60
+
61
+ if method == "initialize":
62
+ return self._handle_initialize(msg_id)
63
+ elif method == "tools/list":
64
+ return self._handle_list_tools(msg_id)
65
+ elif method == "tools/call":
66
+ return await self._handle_call_tool(msg_id, params)
67
+ elif method == "resources/list":
68
+ return self._handle_list_resources(msg_id)
69
+ elif method == "resources/read":
70
+ return await self._handle_read_resource(msg_id, params)
71
+ else:
72
+ return self._error_response(msg_id, -32601, f"Method not found: {method}")
73
+
74
+ except Exception as e:
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",
82
+ "id": msg_id,
83
+ "result": {
84
+ "protocolVersion": "2024-11-05",
85
+ "capabilities": {
86
+ "tools": {},
87
+ "resources": {},
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():
101
+ tools_list.append({
102
+ "name": name,
103
+ "description": tool["description"],
104
+ "inputSchema": tool["input_schema"]
105
+ })
106
+
107
+ return {
108
+ "jsonrpc": "2.0",
109
+ "id": msg_id,
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", {})
117
+
118
+ if tool_name not in self.tools:
119
+ return self._error_response(msg_id, -32602, f"Tool not found: {tool_name}")
120
+
121
+ try:
122
+ handler = self.tools[tool_name]["handler"]
123
+ result = await handler(arguments)
124
+ return {
125
+ "jsonrpc": "2.0",
126
+ "id": msg_id,
127
+ "result": {
128
+ "content": [
129
+ {
130
+ "type": "text",
131
+ "text": json.dumps(result)
132
+ }
133
+ ]
134
+ }
135
+ }
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():
143
+ resources_list.append({
144
+ "uri": uri,
145
+ "name": resource["name"],
146
+ "description": resource["description"],
147
+ "mimeType": resource["mimeType"]
148
+ })
149
+
150
+ return {
151
+ "jsonrpc": "2.0",
152
+ "id": msg_id,
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
+
160
+ if uri not in self.resources:
161
+ return self._error_response(msg_id, -32602, f"Resource not found: {uri}")
162
+
163
+ try:
164
+ handler = self.resources[uri]["handler"]
165
+ content = await handler()
166
+ return {
167
+ "jsonrpc": "2.0",
168
+ "id": msg_id,
169
+ "result": {
170
+ "contents": [
171
+ {
172
+ "uri": uri,
173
+ "mimeType": "application/json",
174
+ "text": json.dumps(content, indent=2)
175
+ }
176
+ ]
177
+ }
178
+ }
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",
186
+ "id": msg_id,
187
+ "error": {
188
+ "code": code,
189
+ "message": message
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())
modal_deployment.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modal.com Deployment Configuration for Spend Analyzer MCP
3
+ """
4
+ import modal
5
+ import os
6
+ from typing import Dict, Any, Optional
7
+ import json
8
+ import asyncio
9
+ 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 = (
17
+ modal.Image.debian_slim(python_version="3.11")
18
+ .pip_install([
19
+ "fastapi",
20
+ "uvicorn",
21
+ "gradio",
22
+ "pandas",
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
+
45
+ # Shared volume for persistent storage
46
+ volume = modal.Volume.from_name("spend-analyzer-data", create_if_missing=True)
47
+
48
+ @app.function(
49
+ image=image,
50
+ secrets=secrets,
51
+ volumes={"/data": volume},
52
+ timeout=300,
53
+ memory=2048,
54
+ cpu=2.0
55
+ )
56
+ def process_bank_statements(email_config: Dict, days_back: int = 30, passwords: Optional[Dict] = None):
57
+ """
58
+ Modal function to process bank statements from email
59
+ """
60
+ import sys
61
+ sys.path.append("/data")
62
+
63
+ from email_processor import EmailProcessor, PDFProcessor
64
+ from spend_analyzer import SpendAnalyzer
65
+
66
+ try:
67
+ # Initialize processors
68
+ email_processor = EmailProcessor(email_config)
69
+ pdf_processor = PDFProcessor()
70
+ analyzer = SpendAnalyzer()
71
+
72
+ # Fetch emails
73
+ emails = asyncio.run(email_processor.fetch_bank_emails(days_back))
74
+
75
+ all_transactions = []
76
+ processed_statements = []
77
+
78
+ for email_msg in emails:
79
+ try:
80
+ # Extract attachments
81
+ attachments = asyncio.run(email_processor.extract_attachments(email_msg))
82
+
83
+ for filename, content, file_type in attachments:
84
+ if file_type == 'pdf':
85
+ # Try to process PDF
86
+ password = None
87
+ if passwords and filename in passwords:
88
+ password = passwords[filename]
89
+
90
+ try:
91
+ statement_info = asyncio.run(pdf_processor.process_pdf(content, password))
92
+ all_transactions.extend(statement_info.transactions)
93
+ processed_statements.append({
94
+ 'filename': filename,
95
+ 'bank': statement_info.bank_name,
96
+ 'account': statement_info.account_number,
97
+ 'period': statement_info.statement_period,
98
+ 'transaction_count': len(statement_info.transactions)
99
+ })
100
+
101
+ except ValueError as e:
102
+ if "password" in str(e).lower():
103
+ # PDF requires password
104
+ processed_statements.append({
105
+ 'filename': filename,
106
+ 'status': 'password_required',
107
+ 'error': str(e)
108
+ })
109
+ else:
110
+ processed_statements.append({
111
+ 'filename': filename,
112
+ 'status': 'error',
113
+ 'error': str(e)
114
+ })
115
+
116
+ except Exception as e:
117
+ logging.error(f"Error processing email: {e}")
118
+ continue
119
+
120
+ # Analyze transactions
121
+ if all_transactions:
122
+ analyzer.load_transactions(all_transactions)
123
+ analysis_data = analyzer.export_analysis_data()
124
+ else:
125
+ analysis_data = {'message': 'No transactions found'}
126
+
127
+ return {
128
+ 'processed_statements': processed_statements,
129
+ 'total_transactions': len(all_transactions),
130
+ 'analysis': analysis_data,
131
+ 'timestamp': datetime.now().isoformat()
132
+ }
133
+
134
+ except Exception as e:
135
+ logging.error(f"Error in process_bank_statements: {e}")
136
+ return {'error': str(e)}
137
+
138
+ @app.function(
139
+ image=image,
140
+ secrets=secrets,
141
+ timeout=60
142
+ )
143
+ def analyze_uploaded_statements(pdf_contents: Dict[str, bytes], passwords: Optional[Dict] = None):
144
+ """
145
+ Modal function to analyze directly uploaded PDF statements
146
+ """
147
+ from pdf_processor import PDFProcessor
148
+ from spend_analyzer import SpendAnalyzer
149
+
150
+ try:
151
+ pdf_processor = PDFProcessor()
152
+ analyzer = SpendAnalyzer()
153
+
154
+ all_transactions = []
155
+ processed_files = []
156
+
157
+ for filename, content in pdf_contents.items():
158
+ try:
159
+ password = passwords.get(filename) if passwords else None
160
+ statement_info = asyncio.run(pdf_processor.process_pdf(content, password))
161
+
162
+ all_transactions.extend(statement_info.transactions)
163
+ processed_files.append({
164
+ 'filename': filename,
165
+ 'bank': statement_info.bank_name,
166
+ 'account': statement_info.account_number,
167
+ 'transaction_count': len(statement_info.transactions),
168
+ 'status': 'success'
169
+ })
170
+
171
+ except Exception as e:
172
+ processed_files.append({
173
+ 'filename': filename,
174
+ 'status': 'error',
175
+ 'error': str(e)
176
+ })
177
+
178
+ # Analyze transactions
179
+ if all_transactions:
180
+ analyzer.load_transactions(all_transactions)
181
+ analysis_data = analyzer.export_analysis_data()
182
+ else:
183
+ analysis_data = {'message': 'No transactions found'}
184
+
185
+ return {
186
+ 'processed_files': processed_files,
187
+ 'total_transactions': len(all_transactions),
188
+ 'analysis': analysis_data
189
+ }
190
+
191
+ except Exception as e:
192
+ return {'error': str(e)}
193
+
194
+ @app.function(
195
+ image=image,
196
+ secrets=secrets,
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)}
213
+
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},
252
+ timeout=30
253
+ )
254
+ def save_user_data(user_id: str, data: Dict):
255
+ """
256
+ Save user analysis data to persistent storage
257
+ """
258
+ try:
259
+ import json
260
+ import os
261
+
262
+ user_dir = f"/data/users/{user_id}"
263
+ os.makedirs(user_dir, exist_ok=True)
264
+
265
+ # Save analysis data
266
+ with open(f"{user_dir}/analysis.json", "w") as f:
267
+ json.dump(data, f, indent=2, default=str)
268
+
269
+ # Save timestamp
270
+ with open(f"{user_dir}/last_updated.txt", "w") as f:
271
+ f.write(datetime.now().isoformat())
272
+
273
+ return {"status": "saved", "path": user_dir}
274
+
275
+ except Exception as e:
276
+ return {"error": str(e)}
277
+
278
+ @app.function(
279
+ image=image,
280
+ volumes={"/data": volume},
281
+ timeout=30
282
+ )
283
+ def load_user_data(user_id: str):
284
+ """
285
+ Load user analysis data from persistent storage
286
+ """
287
+ try:
288
+ import json
289
+
290
+ user_dir = f"/data/users/{user_id}"
291
+ analysis_file = f"{user_dir}/analysis.json"
292
+
293
+ if os.path.exists(analysis_file):
294
+ with open(analysis_file, "r") as f:
295
+ data = json.load(f)
296
+
297
+ # Get last updated time
298
+ last_updated = None
299
+ if os.path.exists(f"{user_dir}/last_updated.txt"):
300
+ with open(f"{user_dir}/last_updated.txt", "r") as f:
301
+ last_updated = f.read().strip()
302
+
303
+ return {
304
+ "data": data,
305
+ "last_updated": last_updated,
306
+ "status": "found"
307
+ }
308
+ else:
309
+ return {"status": "not_found"}
310
+
311
+ except Exception as e:
312
+ return {"error": str(e)}
313
+
314
+ # Webhook endpoint for MCP integration
315
+ @app.function(
316
+ image=image,
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
324
+ """
325
+ try:
326
+ from mcp_server import MCPServer
327
+
328
+ # Initialize MCP server
329
+ server = MCPServer()
330
+
331
+ # Register tools
332
+ async def process_statements_tool(args):
333
+ email_config = args.get('email_config', {})
334
+ days_back = args.get('days_back', 30)
335
+ passwords = args.get('passwords', {})
336
+
337
+ result = process_bank_statements.remote(email_config, days_back, passwords)
338
+ return result
339
+
340
+ async def analyze_pdf_tool(args):
341
+ pdf_contents = args.get('pdf_contents', {})
342
+ passwords = args.get('passwords', {})
343
+
344
+ result = analyze_uploaded_statements.remote(pdf_contents, passwords)
345
+ return result
346
+
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))
361
+ return response
362
+
363
+ except Exception as e:
364
+ return {
365
+ "jsonrpc": "2.0",
366
+ "id": request_data.get("id"),
367
+ "error": {
368
+ "code": -32603,
369
+ "message": str(e)
370
+ }
371
+ }
372
+
373
+ # CLI for local testing
374
+ @app.local_entrypoint()
375
+ def main():
376
+ """
377
+ Local entrypoint for testing Modal functions
378
+ """
379
+ print("Testing Modal deployment...")
380
+
381
+ # Test basic functionality
382
+ test_data = {
383
+ "spending_insights": [],
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)
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies for Spend Analyzer MCP
2
+ gradio>=4.0.0
3
+ pandas>=1.5.0
4
+ plotly>=5.0.0
5
+ numpy>=1.21.0
6
+
7
+ # PDF processing
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
setup_local.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Local setup script for Spend Analyzer MCP
4
+ """
5
+ import os
6
+ import sys
7
+ import subprocess
8
+ import logging
9
+
10
+ def check_python_version():
11
+ """Check if Python version is compatible"""
12
+ if sys.version_info < (3, 8):
13
+ print("❌ Python 3.8 or higher is required")
14
+ return False
15
+ print(f"✅ Python {sys.version_info.major}.{sys.version_info.minor} detected")
16
+ return True
17
+
18
+ def install_dependencies():
19
+ """Install required dependencies"""
20
+ print("📦 Installing dependencies...")
21
+ try:
22
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
23
+ print("✅ Dependencies installed successfully")
24
+ return True
25
+ except subprocess.CalledProcessError as e:
26
+ print(f"❌ Failed to install dependencies: {e}")
27
+ return False
28
+
29
+ def create_env_file():
30
+ """Create .env file template"""
31
+ env_content = """# Spend Analyzer MCP Environment Variables
32
+ # Copy this file to .env and fill in your actual values
33
+
34
+ # Claude API Key (optional for local demo)
35
+ ANTHROPIC_API_KEY=your_claude_api_key_here
36
+
37
+ # Email Configuration (optional for local demo)
38
39
+ EMAIL_PASS=your_app_password_here
40
+ IMAP_SERVER=imap.gmail.com
41
+
42
+ # Modal Configuration (optional)
43
+ MODAL_TOKEN_ID=your_modal_token_id
44
+ MODAL_TOKEN_SECRET=your_modal_token_secret
45
+ """
46
+
47
+ if not os.path.exists('.env.template'):
48
+ with open('.env.template', 'w') as f:
49
+ f.write(env_content)
50
+ print("✅ Created .env.template file")
51
+ print("📝 Please copy .env.template to .env and fill in your API keys")
52
+ else:
53
+ print("✅ .env file already exists")
54
+
55
+ def test_imports():
56
+ """Test if all required modules can be imported"""
57
+ print("🧪 Testing imports...")
58
+
59
+ required_modules = [
60
+ 'gradio',
61
+ 'pandas',
62
+ 'plotly',
63
+ 'numpy'
64
+ ]
65
+
66
+ failed_imports = []
67
+
68
+ for module in required_modules:
69
+ try:
70
+ __import__(module)
71
+ print(f" ✅ {module}")
72
+ except ImportError:
73
+ print(f" ❌ {module}")
74
+ failed_imports.append(module)
75
+
76
+ if failed_imports:
77
+ print(f"\n❌ Failed to import: {', '.join(failed_imports)}")
78
+ print("💡 Try running: pip install -r requirements.txt")
79
+ return False
80
+
81
+ print("✅ All required modules imported successfully")
82
+ return True
83
+
84
+ def create_demo_data():
85
+ """Create demo data for testing"""
86
+ demo_data = {
87
+ "transactions": [
88
+ {
89
+ "date": "2024-01-15",
90
+ "description": "STARBUCKS COFFEE",
91
+ "amount": -4.50,
92
+ "category": "Food & Dining"
93
+ },
94
+ {
95
+ "date": "2024-01-14",
96
+ "description": "AMAZON PURCHASE",
97
+ "amount": -29.99,
98
+ "category": "Shopping"
99
+ },
100
+ {
101
+ "date": "2024-01-13",
102
+ "description": "SALARY DEPOSIT",
103
+ "amount": 3000.00,
104
+ "category": "Income"
105
+ }
106
+ ]
107
+ }
108
+
109
+ import json
110
+ with open('demo_data.json', 'w') as f:
111
+ json.dump(demo_data, f, indent=2)
112
+
113
+ print("✅ Created demo_data.json for testing")
114
+
115
+ def main():
116
+ """Main setup function"""
117
+ print("🚀 Setting up Spend Analyzer MCP locally...\n")
118
+
119
+ # Check Python version
120
+ if not check_python_version():
121
+ return False
122
+
123
+ # Install dependencies
124
+ if not install_dependencies():
125
+ return False
126
+
127
+ # Test imports
128
+ if not test_imports():
129
+ return False
130
+
131
+ # Create environment file template
132
+ create_env_file()
133
+
134
+ # Create demo data
135
+ create_demo_data()
136
+
137
+ print("\n🎉 Local setup completed successfully!")
138
+ print("\n📋 Next steps:")
139
+ print("1. Copy .env.template to .env and add your API keys (optional for demo)")
140
+ print("2. Run: python gradio_interface.py")
141
+ print("3. Open http://localhost:7860 in your browser")
142
+ print("\n💡 The app will work in demo mode without API keys")
143
+
144
+ return True
145
+
146
+ if __name__ == "__main__":
147
+ success = main()
148
+ sys.exit(0 if success else 1)
spend_analyzer.py ADDED
@@ -0,0 +1,486 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Spend Analyzer - Financial Intelligence and Analysis Module
3
+ """
4
+ import pandas as pd
5
+ import numpy as np
6
+ from typing import Dict, List, Optional, Tuple
7
+ from datetime import datetime, timedelta
8
+ from dataclasses import dataclass, asdict
9
+ import json
10
+ from collections import defaultdict
11
+ import logging
12
+
13
+ @dataclass
14
+ class SpendingInsight:
15
+ category: str
16
+ total_amount: float
17
+ transaction_count: int
18
+ average_transaction: float
19
+ percentage_of_total: float
20
+ trend: str # 'increasing', 'decreasing', 'stable'
21
+ top_merchants: List[str]
22
+
23
+ @dataclass
24
+ class BudgetAlert:
25
+ category: str
26
+ budget_limit: float
27
+ current_spending: float
28
+ percentage_used: float
29
+ alert_level: str # 'warning', 'critical', 'info'
30
+ days_remaining: int
31
+
32
+ @dataclass
33
+ class FinancialSummary:
34
+ total_income: float
35
+ total_expenses: float
36
+ net_cash_flow: float
37
+ largest_expense: Dict
38
+ most_frequent_category: str
39
+ unusual_transactions: List[Dict]
40
+ monthly_trends: Dict[str, float]
41
+
42
+ class SpendAnalyzer:
43
+ def __init__(self):
44
+ self.logger = logging.getLogger(__name__)
45
+ self.transactions_df = pd.DataFrame()
46
+ self.budgets = {}
47
+
48
+ def load_transactions(self, transactions: List) -> None:
49
+ """Load transactions into pandas DataFrame for analysis"""
50
+ try:
51
+ # Convert transactions to DataFrame
52
+ data = []
53
+ for trans in transactions:
54
+ if hasattr(trans, '__dict__'):
55
+ data.append(asdict(trans))
56
+ else:
57
+ data.append(trans)
58
+
59
+ self.transactions_df = pd.DataFrame(data)
60
+
61
+ if not self.transactions_df.empty:
62
+ # Ensure date column is datetime
63
+ self.transactions_df['date'] = pd.to_datetime(self.transactions_df['date'])
64
+
65
+ # Sort by date
66
+ self.transactions_df = self.transactions_df.sort_values('date')
67
+
68
+ # Add derived columns
69
+ self.transactions_df['month'] = self.transactions_df['date'].dt.to_period('M')
70
+ self.transactions_df['week'] = self.transactions_df['date'].dt.to_period('W')
71
+ self.transactions_df['day_of_week'] = self.transactions_df['date'].dt.day_name()
72
+
73
+ self.logger.info(f"Loaded {len(self.transactions_df)} transactions")
74
+
75
+ except Exception as e:
76
+ self.logger.error(f"Error loading transactions: {e}")
77
+ raise
78
+
79
+ def set_budgets(self, budgets: Dict[str, float]) -> None:
80
+ """Set budget limits for categories"""
81
+ self.budgets = budgets
82
+
83
+ def analyze_spending_by_category(self, months_back: int = 6) -> List[SpendingInsight]:
84
+ """Analyze spending patterns by category"""
85
+ if self.transactions_df.empty:
86
+ return []
87
+
88
+ # Filter to recent months
89
+ cutoff_date = datetime.now() - timedelta(days=months_back * 30)
90
+ recent_df = self.transactions_df[self.transactions_df['date'] >= cutoff_date]
91
+
92
+ # Filter only expenses (negative amounts)
93
+ expenses_df = recent_df[recent_df['amount'] < 0].copy()
94
+ expenses_df['amount'] = expenses_df['amount'].abs() # Make positive for analysis
95
+
96
+ insights = []
97
+ total_spending = expenses_df['amount'].sum()
98
+
99
+ if total_spending == 0:
100
+ self.logger.warning("Total spending is zero; no insights can be generated.")
101
+ return insights
102
+
103
+ # Group by category
104
+ category_stats = expenses_df.groupby('category').agg({
105
+ 'amount': ['sum', 'count', 'mean'],
106
+ 'description': lambda x: list(x.value_counts().head(3).index)
107
+ }).round(2)
108
+
109
+ category_stats.columns = ['total', 'count', 'average', 'top_merchants']
110
+
111
+ for category, stats in category_stats.iterrows():
112
+ # Calculate trend
113
+ trend = self._calculate_trend(expenses_df, category)
114
+
115
+ insight = SpendingInsight(
116
+ category=category,
117
+ total_amount=stats['total'],
118
+ transaction_count=stats['count'],
119
+ average_transaction=stats['average'],
120
+ percentage_of_total=(stats['total'] / total_spending) * 100,
121
+ trend=trend,
122
+ top_merchants=stats['top_merchants'][:3]
123
+ )
124
+ insights.append(insight)
125
+
126
+ # Sort by total amount descending
127
+ insights.sort(key=lambda x: x.total_amount, reverse=True)
128
+ return insights
129
+
130
+ def _calculate_trend(self, df: pd.DataFrame, category: str) -> str:
131
+ """Calculate spending trend for a category"""
132
+ try:
133
+ category_df = df[df['category'] == category]
134
+ monthly_spending = category_df.groupby('month')['amount'].sum()
135
+
136
+ if len(monthly_spending) < 2:
137
+ return 'stable'
138
+
139
+ # Calculate trend using linear regression slope
140
+ x = np.arange(len(monthly_spending))
141
+ y = monthly_spending.values
142
+ slope = np.polyfit(x, y, 1)[0]
143
+
144
+ if slope > 0.1:
145
+ return 'increasing'
146
+ elif slope < -0.1:
147
+ return 'decreasing'
148
+ else:
149
+ return 'stable'
150
+
151
+ except Exception:
152
+ return 'stable'
153
+
154
+ def check_budget_alerts(self) -> List[BudgetAlert]:
155
+ """Check for budget alerts and overspending"""
156
+ if self.transactions_df.empty or not self.budgets:
157
+ return []
158
+
159
+ alerts = []
160
+ current_month = datetime.now().replace(day=1)
161
+ month_df = self.transactions_df[
162
+ (self.transactions_df['date'] >= current_month) &
163
+ (self.transactions_df['amount'] < 0) # Only expenses
164
+ ].copy()
165
+
166
+ month_df['amount'] = month_df['amount'].abs()
167
+
168
+ # Days remaining in month
169
+ import calendar
170
+ days_in_month = calendar.monthrange(current_month.year, current_month.month)[1]
171
+ days_remaining = days_in_month - datetime.now().day
172
+
173
+ # Check each budget category
174
+ for category, budget_limit in self.budgets.items():
175
+ current_spending = month_df[month_df['category'] == category]['amount'].sum()
176
+ percentage_used = (current_spending / budget_limit) * 100
177
+
178
+ # Determine alert level
179
+ if percentage_used >= 100:
180
+ alert_level = 'critical'
181
+ elif percentage_used >= 80:
182
+ alert_level = 'warning'
183
+ else:
184
+ alert_level = 'info'
185
+
186
+ alert = BudgetAlert(
187
+ category=category,
188
+ budget_limit=budget_limit,
189
+ current_spending=current_spending,
190
+ percentage_used=percentage_used,
191
+ alert_level=alert_level,
192
+ days_remaining=days_remaining
193
+ )
194
+ alerts.append(alert)
195
+
196
+ return alerts
197
+
198
+ def generate_financial_summary(self) -> FinancialSummary:
199
+ """Generate comprehensive financial summary"""
200
+ if self.transactions_df.empty:
201
+ return FinancialSummary(0, 0, 0, {}, "", [], {})
202
+
203
+ # Calculate basic metrics
204
+ income_df = self.transactions_df[self.transactions_df['amount'] > 0]
205
+ expense_df = self.transactions_df[self.transactions_df['amount'] < 0]
206
+
207
+ total_income = income_df['amount'].sum()
208
+ total_expenses = abs(expense_df['amount'].sum())
209
+ net_cash_flow = total_income - total_expenses
210
+
211
+ # Largest expense
212
+ if not expense_df.empty:
213
+ largest_expense_row = expense_df.loc[expense_df['amount'].idxmin()]
214
+ largest_expense = {
215
+ 'amount': abs(largest_expense_row['amount']),
216
+ 'description': largest_expense_row['description'],
217
+ 'date': largest_expense_row['date'].strftime('%Y-%m-%d'),
218
+ 'category': largest_expense_row['category']
219
+ }
220
+ else:
221
+ largest_expense = {}
222
+
223
+ # Most frequent category
224
+ most_frequent_category = expense_df['category'].mode().iloc[0] if not expense_df.empty else ""
225
+
226
+ # Unusual transactions (outliers)
227
+ unusual_transactions = self._detect_unusual_transactions()
228
+
229
+ # Monthly trends
230
+ monthly_trends = self._calculate_monthly_trends()
231
+
232
+ return FinancialSummary(
233
+ total_income=total_income,
234
+ total_expenses=total_expenses,
235
+ net_cash_flow=net_cash_flow,
236
+ largest_expense=largest_expense,
237
+ most_frequent_category=most_frequent_category,
238
+ unusual_transactions=unusual_transactions,
239
+ monthly_trends=monthly_trends
240
+ )
241
+
242
+ def _detect_unusual_transactions(self) -> List[Dict]:
243
+ """Detect unusual transactions using statistical methods"""
244
+ if self.transactions_df.empty:
245
+ return []
246
+
247
+ unusual = []
248
+
249
+ # Detect amount outliers by category
250
+ for category in self.transactions_df['category'].unique():
251
+ category_df = self.transactions_df[
252
+ (self.transactions_df['category'] == category) &
253
+ (self.transactions_df['amount'] < 0)
254
+ ].copy()
255
+
256
+ if len(category_df) < 5: # Need sufficient data
257
+ continue
258
+
259
+ amounts = category_df['amount'].abs()
260
+ Q1 = amounts.quantile(0.25)
261
+ Q3 = amounts.quantile(0.75)
262
+ IQR = Q3 - Q1
263
+
264
+ # Define outliers as values beyond 1.5 * IQR
265
+ lower_bound = Q1 - 1.5 * IQR
266
+ upper_bound = Q3 + 1.5 * IQR
267
+
268
+ outliers = category_df[(amounts < lower_bound) | (amounts > upper_bound)]
269
+
270
+ for _, row in outliers.iterrows():
271
+ unusual.append({
272
+ 'date': row['date'].strftime('%Y-%m-%d'),
273
+ 'description': row['description'],
274
+ 'amount': abs(row['amount']),
275
+ 'category': row['category'],
276
+ 'reason': 'Amount significantly higher than usual for this category'
277
+ })
278
+
279
+ # Detect frequency outliers (multiple transactions same day/merchant)
280
+ daily_merchant = self.transactions_df.groupby([
281
+ self.transactions_df['date'].dt.date, 'description'
282
+ ]).size()
283
+
284
+ frequent_same_day = daily_merchant[daily_merchant > 3]
285
+
286
+ for (date, merchant), count in frequent_same_day.items():
287
+ unusual.append({
288
+ 'date': str(date),
289
+ 'description': merchant,
290
+ 'count': count,
291
+ 'reason': f'{count} transactions with same merchant on same day'
292
+ })
293
+
294
+ return unusual[:10] # Return top 10 unusual transactions
295
+
296
+ def _calculate_monthly_trends(self) -> Dict[str, float]:
297
+ """Calculate monthly spending trends"""
298
+ if self.transactions_df.empty:
299
+ return {}
300
+
301
+ # Get last 12 months of expense data
302
+ expense_df = self.transactions_df[self.transactions_df['amount'] < 0].copy()
303
+ expense_df['amount'] = expense_df['amount'].abs()
304
+
305
+ monthly_spending = expense_df.groupby('month')['amount'].sum()
306
+
307
+ # Get last 6 months for trend calculation
308
+ recent_months = monthly_spending.tail(6)
309
+
310
+ trends = {}
311
+ if len(recent_months) >= 2:
312
+ # Overall trend
313
+ x = np.arange(len(recent_months))
314
+ y = recent_months.values
315
+ slope = np.polyfit(x, y, 1)[0]
316
+ trends['overall_trend'] = slope
317
+
318
+ # Month-over-month change
319
+ if len(recent_months) >= 2:
320
+ current_month = recent_months.iloc[-1]
321
+ previous_month = recent_months.iloc[-2]
322
+ mom_change = ((current_month - previous_month) / previous_month) * 100
323
+ trends['month_over_month_change'] = mom_change
324
+
325
+ # Average monthly spending
326
+ trends['average_monthly'] = recent_months.mean()
327
+ trends['highest_month'] = recent_months.max()
328
+ trends['lowest_month'] = recent_months.min()
329
+
330
+ return trends
331
+
332
+ def predict_future_spending(self, months_ahead: int = 3) -> Dict[str, float]:
333
+ """Predict future spending based on historical trends"""
334
+ if self.transactions_df.empty:
335
+ return {}
336
+
337
+ # Get historical monthly spending by category
338
+ expense_df = self.transactions_df[self.transactions_df['amount'] < 0].copy()
339
+ expense_df['amount'] = expense_df['amount'].abs()
340
+
341
+ monthly_category_spending = expense_df.groupby(['month', 'category'])['amount'].sum().unstack(fill_value=0)
342
+
343
+ predictions = {}
344
+
345
+ for category in monthly_category_spending.columns:
346
+ category_data = monthly_category_spending[category]
347
+
348
+ if len(category_data) >= 3: # Need at least 3 months of data
349
+ # Simple linear trend prediction
350
+ x = np.arange(len(category_data))
351
+ y = category_data.values
352
+
353
+ # Fit linear model
354
+ coeffs = np.polyfit(x, y, 1)
355
+ slope, intercept = coeffs
356
+
357
+ # Predict future months
358
+ future_months = []
359
+ for i in range(1, months_ahead + 1):
360
+ future_x = len(category_data) + i - 1
361
+ predicted_amount = slope * future_x + intercept
362
+ future_months.append(max(0, predicted_amount)) # Don't predict negative spending
363
+
364
+ predictions[category] = {
365
+ 'next_month': future_months[0] if future_months else 0,
366
+ 'total_predicted': sum(future_months),
367
+ 'average_predicted': np.mean(future_months) if future_months else 0
368
+ }
369
+
370
+ return predictions
371
+
372
+ def get_spending_recommendations(self) -> List[str]:
373
+ """Generate spending recommendations based on analysis"""
374
+ recommendations = []
375
+
376
+ if self.transactions_df.empty:
377
+ return ["No transaction data available for analysis"]
378
+
379
+ # Analyze spending patterns
380
+ insights = self.analyze_spending_by_category()
381
+ budget_alerts = self.check_budget_alerts()
382
+ summary = self.generate_financial_summary()
383
+
384
+ # Check for overspending categories
385
+ overspending_categories = [alert for alert in budget_alerts if alert.percentage_used > 100]
386
+ if overspending_categories:
387
+ for alert in overspending_categories:
388
+ recommendations.append(
389
+ f"You've exceeded your {alert.category} budget by "
390
+ f"${alert.current_spending - alert.budget_limit:.2f} this month. "
391
+ f"Consider reducing spending in this category."
392
+ )
393
+
394
+ # Check for high-spending categories
395
+ if insights:
396
+ top_category = insights[0]
397
+ if top_category.percentage_of_total > 40:
398
+ recommendations.append(
399
+ f"{top_category.category} accounts for {top_category.percentage_of_total:.1f}% "
400
+ f"of your spending. Consider if this allocation aligns with your priorities."
401
+ )
402
+
403
+ # Check cash flow
404
+ if summary.net_cash_flow < 0:
405
+ recommendations.append(
406
+ f"Your expenses (${summary.total_expenses:.2f}) exceed your income "
407
+ f"(${summary.total_income:.2f}) by ${abs(summary.net_cash_flow):.2f}. "
408
+ f"Focus on reducing expenses or increasing income."
409
+ )
410
+
411
+ # Check for increasing trends
412
+ increasing_categories = [i for i in insights if i.trend == 'increasing']
413
+ if increasing_categories:
414
+ top_increasing = increasing_categories[0]
415
+ recommendations.append(
416
+ f"Your {top_increasing.category} spending is trending upward. "
417
+ f"Monitor this category to avoid budget overruns."
418
+ )
419
+
420
+ # Unusual transaction patterns
421
+ if summary.unusual_transactions:
422
+ recommendations.append(
423
+ f"Found {len(summary.unusual_transactions)} unusual transactions. "
424
+ f"Review these for potential errors or unauthorized charges."
425
+ )
426
+
427
+ # Positive reinforcement
428
+ decreasing_categories = [i for i in insights if i.trend == 'decreasing']
429
+ if decreasing_categories:
430
+ recommendations.append(
431
+ f"Great job reducing {decreasing_categories[0].category} spending! "
432
+ f"This trend is helping improve your financial health."
433
+ )
434
+
435
+ if not recommendations:
436
+ recommendations.append("Your spending patterns look healthy. Keep up the good work!")
437
+
438
+ return recommendations
439
+
440
+ def export_analysis_data(self) -> Dict:
441
+ """Export all analysis data for Claude API integration"""
442
+ return {
443
+ 'spending_insights': [asdict(insight) for insight in self.analyze_spending_by_category()],
444
+ 'budget_alerts': [asdict(alert) for alert in self.check_budget_alerts()],
445
+ 'financial_summary': asdict(self.generate_financial_summary()),
446
+ 'predictions': self.predict_future_spending(),
447
+ 'recommendations': self.get_spending_recommendations(),
448
+ 'transaction_count': len(self.transactions_df),
449
+ 'analysis_date': datetime.now().isoformat()
450
+ }
451
+
452
+ # Example usage and testing
453
+ if __name__ == "__main__":
454
+ # Test the spend analyzer
455
+ analyzer = SpendAnalyzer()
456
+
457
+ # Sample transaction data for testing
458
+ sample_transactions = [
459
+ {
460
+ 'date': datetime.now() - timedelta(days=5),
461
+ 'description': 'Amazon Purchase',
462
+ 'amount': -45.67,
463
+ 'category': 'Shopping'
464
+ },
465
+ {
466
+ 'date': datetime.now() - timedelta(days=10),
467
+ 'description': 'Grocery Store',
468
+ 'amount': -120.50,
469
+ 'category': 'Food & Dining'
470
+ },
471
+ {
472
+ 'date': datetime.now() - timedelta(days=15),
473
+ 'description': 'Salary Deposit',
474
+ 'amount': 3000.00,
475
+ 'category': 'Income'
476
+ }
477
+ ]
478
+
479
+ analyzer.load_transactions(sample_transactions)
480
+ analyzer.set_budgets({'Shopping': 100, 'Food & Dining': 200})
481
+
482
+ insights = analyzer.analyze_spending_by_category()
483
+ print(f"Generated {len(insights)} spending insights")
484
+
485
+ recommendations = analyzer.get_spending_recommendations()
486
+ print(f"Generated {len(recommendations)} recommendations")