banko-ai-assistant 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. banko_ai/__init__.py +19 -0
  2. banko_ai/__main__.py +10 -0
  3. banko_ai/ai_providers/__init__.py +18 -0
  4. banko_ai/ai_providers/aws_provider.py +337 -0
  5. banko_ai/ai_providers/base.py +175 -0
  6. banko_ai/ai_providers/factory.py +84 -0
  7. banko_ai/ai_providers/gemini_provider.py +340 -0
  8. banko_ai/ai_providers/openai_provider.py +295 -0
  9. banko_ai/ai_providers/watsonx_provider.py +591 -0
  10. banko_ai/cli.py +374 -0
  11. banko_ai/config/__init__.py +5 -0
  12. banko_ai/config/settings.py +216 -0
  13. banko_ai/static/Anallytics.png +0 -0
  14. banko_ai/static/Graph.png +0 -0
  15. banko_ai/static/Graph2.png +0 -0
  16. banko_ai/static/ai-status.png +0 -0
  17. banko_ai/static/banko-ai-assistant-watsonx.gif +0 -0
  18. banko_ai/static/banko-db-ops.png +0 -0
  19. banko_ai/static/banko-response.png +0 -0
  20. banko_ai/static/cache-stats.png +0 -0
  21. banko_ai/static/creditcard.png +0 -0
  22. banko_ai/static/profilepic.jpeg +0 -0
  23. banko_ai/static/query_watcher.png +0 -0
  24. banko_ai/static/roach-logo.svg +54 -0
  25. banko_ai/static/watsonx-icon.svg +1 -0
  26. banko_ai/templates/base.html +59 -0
  27. banko_ai/templates/dashboard.html +569 -0
  28. banko_ai/templates/index.html +1499 -0
  29. banko_ai/templates/login.html +41 -0
  30. banko_ai/utils/__init__.py +8 -0
  31. banko_ai/utils/cache_manager.py +525 -0
  32. banko_ai/utils/database.py +202 -0
  33. banko_ai/utils/migration.py +123 -0
  34. banko_ai/vector_search/__init__.py +18 -0
  35. banko_ai/vector_search/enrichment.py +278 -0
  36. banko_ai/vector_search/generator.py +329 -0
  37. banko_ai/vector_search/search.py +463 -0
  38. banko_ai/web/__init__.py +13 -0
  39. banko_ai/web/app.py +668 -0
  40. banko_ai/web/auth.py +73 -0
  41. banko_ai_assistant-1.0.0.dist-info/METADATA +414 -0
  42. banko_ai_assistant-1.0.0.dist-info/RECORD +46 -0
  43. banko_ai_assistant-1.0.0.dist-info/WHEEL +5 -0
  44. banko_ai_assistant-1.0.0.dist-info/entry_points.txt +2 -0
  45. banko_ai_assistant-1.0.0.dist-info/licenses/LICENSE +21 -0
  46. banko_ai_assistant-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,591 @@
1
+ """
2
+ IBM Watsonx AI Provider for Banko Assistant
3
+
4
+ This module provides integration with IBM Watsonx AI services to power the
5
+ Banko Assistant's conversational capabilities. It includes functions for:
6
+ - Expense data search and retrieval
7
+ - RAG (Retrieval Augmented Generation) responses
8
+ - Financial data analysis
9
+
10
+ Dependencies:
11
+ - requests: For HTTP API calls to Watsonx
12
+ - numpy: For vector operations
13
+ - json: For data serialization
14
+ - sentence_transformers: For embedding generation
15
+ - sqlalchemy: For database operations
16
+
17
+ Author: Banko AI Team
18
+ Date: 2025
19
+ """
20
+
21
+ import os
22
+ import requests
23
+ import json
24
+ import numpy as np
25
+ from typing import List, Optional, Dict, Any
26
+ from ..ai_providers.base import AIProvider, RAGResponse, SearchResult
27
+
28
+
29
+ class WatsonxProvider(AIProvider):
30
+ """IBM Watsonx AI Provider implementation."""
31
+
32
+ def __init__(self, config: Dict[str, Any] = None, cache_manager=None):
33
+ """Initialize Watsonx provider with configuration."""
34
+ if config is None:
35
+ config = {}
36
+
37
+ # Store config for base class compatibility
38
+ self.config = config
39
+ self.cache_manager = cache_manager
40
+
41
+ self.api_key = config.get('api_key') or os.getenv('WATSONX_API_KEY')
42
+ self.project_id = config.get('project_id') or os.getenv('WATSONX_PROJECT_ID')
43
+ self.current_model = config.get('model', config.get('model_id')) or os.getenv('WATSONX_MODEL_ID', 'openai/gpt-oss-120b')
44
+ self.api_url = "https://us-south.ml.cloud.ibm.com/ml/v1/text/chat?version=2023-05-29"
45
+
46
+ # Make API key and project ID optional for demo purposes
47
+ if not self.api_key:
48
+ print("⚠️ WATSONX_API_KEY not found - running in demo mode")
49
+ if not self.project_id:
50
+ print("⚠️ WATSONX_PROJECT_ID not found - running in demo mode")
51
+
52
+ def _validate_config(self) -> None:
53
+ """Validate Watsonx configuration."""
54
+ # Configuration is optional for demo mode
55
+ pass
56
+
57
+ def get_default_model(self) -> str:
58
+ """Get the default model for Watsonx."""
59
+ return 'openai/gpt-oss-120b'
60
+
61
+ def generate_embedding(self, text: str) -> List[float]:
62
+ """Generate embedding vector for the given text."""
63
+ try:
64
+ from sentence_transformers import SentenceTransformer
65
+ model = SentenceTransformer('all-MiniLM-L6-v2')
66
+ embedding = model.encode([text])[0]
67
+ return embedding.tolist()
68
+ except Exception as e:
69
+ print(f"Error generating embedding: {e}")
70
+ return []
71
+
72
+ def search_expenses(
73
+ self,
74
+ query: str,
75
+ user_id: Optional[str] = None,
76
+ limit: int = 10,
77
+ threshold: float = 0.7
78
+ ) -> List[SearchResult]:
79
+ """Search for expenses using vector similarity."""
80
+ # This method should delegate to the vector search engine
81
+ # For now, return empty list as the search is handled by the web app
82
+ return []
83
+
84
+ def get_available_models(self) -> List[str]:
85
+ """Get list of available Watsonx models."""
86
+ return [
87
+ 'openai/gpt-oss-120b',
88
+ 'meta-llama/llama-2-70b-chat',
89
+ 'meta-llama/llama-2-13b-chat',
90
+ 'meta-llama/llama-2-7b-chat',
91
+ 'ibm/granite-13b-chat-v2',
92
+ 'ibm/granite-13b-instruct-v2'
93
+ ]
94
+
95
+ def set_model(self, model_id: str) -> bool:
96
+ """Set the current model."""
97
+ if model_id in self.get_available_models():
98
+ self.current_model = model_id
99
+ return True
100
+ return False
101
+
102
+ def test_connection(self) -> bool:
103
+ """Test connection to Watsonx API."""
104
+ if not self.api_key or not self.project_id:
105
+ return False
106
+
107
+ try:
108
+ test_messages = [{"role": "user", "content": "Hello, this is a test message."}]
109
+ response = self._call_watsonx_api(test_messages)
110
+ return True
111
+ except Exception as e:
112
+ return False
113
+
114
+ def _get_access_token(self):
115
+ """Get IBM Cloud access token from API key (copied from original)."""
116
+ token_url = "https://iam.cloud.ibm.com/identity/token"
117
+ headers = {
118
+ "Content-Type": "application/x-www-form-urlencoded",
119
+ "Accept": "application/json"
120
+ }
121
+ data = {
122
+ "grant_type": "urn:ibm:params:oauth:grant-type:apikey",
123
+ "apikey": self.api_key
124
+ }
125
+
126
+ try:
127
+ response = requests.post(token_url, headers=headers, data=data, timeout=30)
128
+ if response.status_code != 200:
129
+ raise Exception(f"Failed to get access token (status {response.status_code}): {response.text}")
130
+ token_data = response.json()
131
+ access_token = token_data.get('access_token')
132
+ if access_token:
133
+ return access_token
134
+ else:
135
+ raise Exception("No access token in response")
136
+ except Exception as e:
137
+ raise Exception(f"Token request failed: {str(e)}")
138
+
139
+ def _call_watsonx_api(self, messages):
140
+ """Make a direct API call to IBM Watsonx chat endpoint (copied from original)."""
141
+ access_token = self._get_access_token()
142
+ headers = {
143
+ "Accept": "application/json",
144
+ "Content-Type": "application/json",
145
+ "Authorization": f"Bearer {access_token}"
146
+ }
147
+ body = {
148
+ "project_id": self.project_id,
149
+ "model_id": self.current_model,
150
+ "messages": messages,
151
+ "frequency_penalty": 0,
152
+ "max_tokens": 2000,
153
+ "presence_penalty": 0,
154
+ "temperature": 0.7,
155
+ "top_p": 1
156
+ }
157
+
158
+ try:
159
+ response = requests.post(
160
+ self.api_url,
161
+ headers=headers,
162
+ json=body,
163
+ timeout=30
164
+ )
165
+ if response.status_code != 200:
166
+ raise Exception(f"Watsonx API error (status {response.status_code}): {response.text}")
167
+ data = response.json()
168
+ if 'choices' in data and len(data['choices']) > 0:
169
+ return data['choices'][0]['message']['content']
170
+ elif 'generated_text' in data:
171
+ return data['generated_text']
172
+ else:
173
+ print(f"Unexpected Watsonx response format: {data}")
174
+ return "I apologize, but I'm having trouble generating a response right now."
175
+ except Exception as e:
176
+ raise Exception(f"Watsonx API call failed: {str(e)}")
177
+
178
+ def _get_financial_insights(self, search_results: List[SearchResult]) -> dict:
179
+ """Generate comprehensive financial insights from expense data (copied from original)."""
180
+ if not search_results:
181
+ return {}
182
+
183
+ total_amount = sum(float(result.amount) for result in search_results)
184
+ categories = {}
185
+ merchants = {}
186
+ payment_methods = {}
187
+
188
+ for result in search_results:
189
+ # Category analysis
190
+ category = result.metadata.get('shopping_type', 'Unknown')
191
+ categories[category] = categories.get(category, 0) + float(result.amount)
192
+
193
+ # Merchant analysis
194
+ merchant = result.merchant
195
+ merchants[merchant] = merchants.get(merchant, 0) + float(result.amount)
196
+
197
+ # Payment method analysis
198
+ payment = result.metadata.get('payment_method', 'Unknown')
199
+ payment_methods[payment] = payment_methods.get(payment, 0) + float(result.amount)
200
+
201
+ # Find top categories and merchants
202
+ top_category = max(categories.items(), key=lambda x: x[1]) if categories else None
203
+ top_merchant = max(merchants.items(), key=lambda x: x[1]) if merchants else None
204
+
205
+ return {
206
+ 'total_amount': total_amount,
207
+ 'num_transactions': len(search_results),
208
+ 'avg_transaction': total_amount / len(search_results) if search_results else 0,
209
+ 'categories': categories,
210
+ 'top_category': top_category,
211
+ 'top_merchant': top_merchant,
212
+ 'payment_methods': payment_methods
213
+ }
214
+
215
+ def _generate_budget_recommendations(self, insights: dict, prompt: str) -> str:
216
+ """Generate personalized budget recommendations based on spending patterns (copied from original)."""
217
+ if not insights:
218
+ return ""
219
+
220
+ recommendations = []
221
+
222
+ # High spending category recommendations
223
+ if insights.get('top_category'):
224
+ category, amount = insights['top_category']
225
+ recommendations.append(f"Your highest spending category is **{category}** at **${amount:.2f}**. Consider setting a monthly budget limit for this category.")
226
+
227
+ # Average transaction analysis
228
+ avg_amount = insights.get('avg_transaction', 0)
229
+ if avg_amount > 100:
230
+ recommendations.append(f"Your average transaction is **${avg_amount:.2f}**. Consider reviewing larger purchases to identify potential savings.")
231
+
232
+ # Merchant frequency analysis
233
+ if insights.get('top_merchant'):
234
+ merchant, amount = insights['top_merchant']
235
+ recommendations.append(f"You frequently shop at **{merchant}** (${amount:.2f} total). Look for loyalty programs or discounts at this merchant.")
236
+
237
+ # General budgeting tips
238
+ if insights.get('total_amount', 0) > 500:
239
+ recommendations.append("💡 **Budget Tip**: Consider the 50/30/20 rule: 50% for needs, 30% for wants, 20% for savings and debt repayment.")
240
+
241
+ return "\n".join(recommendations) if recommendations else ""
242
+
243
+ def rag_response(
244
+ self,
245
+ query: str,
246
+ context: List[SearchResult],
247
+ language: str = "en"
248
+ ) -> str:
249
+ """Generate a RAG response using Watsonx API (copied from original working implementation)."""
250
+ try:
251
+ if not self.api_key or not self.project_id:
252
+ # Return structured demo response if no API credentials
253
+ if not context:
254
+ return f"""## Financial Analysis for: "{query}"
255
+
256
+ ### 📋 Transaction Details
257
+ No expense records found for this query.
258
+
259
+ ### 📊 Financial Summary
260
+ No data available for analysis.
261
+
262
+ ### 🤖 AI-Powered Insights
263
+ I couldn't find any relevant expense records for your query. Please try:
264
+ - Different keywords (e.g., "groceries", "restaurants", "transportation")
265
+ - Broader categories (e.g., "food", "shopping", "bills")
266
+ - Time periods (e.g., "last month", "this week")
267
+
268
+ **Note**: I need API credentials to generate more detailed AI-powered insights."""
269
+
270
+ # Generate financial insights from search results
271
+ insights = self._get_financial_insights(context)
272
+ budget_recommendations = self._generate_budget_recommendations(insights, query)
273
+
274
+ # Create table text from search results
275
+ table_text = ""
276
+ if context:
277
+ table_text = "\n".join([
278
+ f"• **{result.metadata.get('shopping_type', 'Unknown')}** at {result.merchant}: ${result.amount} ({result.metadata.get('payment_method', 'Unknown')}) - {result.description}"
279
+ for result in context
280
+ ])
281
+
282
+ # Create context text with financial summary
283
+ context_text = f"""**📊 Financial Summary:**
284
+ • Total Amount: ${insights.get('total_amount', 0):.2f}
285
+ • Number of Transactions: {insights.get('num_transactions', 0)}
286
+ • Average Transaction: ${insights.get('avg_transaction', 0):.2f}
287
+ • Top Category: {insights.get('top_category', ('Unknown', 0))[0] if insights.get('top_category') else 'Unknown'}
288
+ • Most frequent category: {insights.get('top_category', ('Unknown', 0))[0] if insights.get('top_category') else 'Unknown'}
289
+
290
+ **Recommendations:**
291
+ {budget_recommendations if budget_recommendations else '• Consider reviewing your spending patterns regularly' + chr(10) + '• Set up budget alerts for high-value categories'}
292
+
293
+ **Note**: I can see {len(context)} relevant expense records, but I need API credentials to generate more detailed AI-powered insights."""
294
+ else:
295
+ # Make actual Watsonx API call with enhanced prompt (copied from original)
296
+ try:
297
+ # Generate financial insights from search results
298
+ insights = self._get_financial_insights(context)
299
+ budget_recommendations = self._generate_budget_recommendations(insights, query)
300
+
301
+ # Prepare the search results context with enhanced analysis (copied from original)
302
+ search_results_text = ""
303
+ if context:
304
+ search_results_text = "\n".join(
305
+ f"• **{result.metadata.get('shopping_type', 'Unknown')}** at {result.merchant}: ${result.amount} ({result.metadata.get('payment_method', 'Unknown')}) - {result.description}"
306
+ for result in context
307
+ )
308
+
309
+ # Add financial summary (copied from original)
310
+ if insights:
311
+ search_results_text += f"\n\n**📊 Financial Summary:**\n"
312
+ search_results_text += f"• Total Amount: **${insights['total_amount']:.2f}**\n"
313
+ search_results_text += f"• Number of Transactions: **{insights['num_transactions']}**\n"
314
+ search_results_text += f"• Average Transaction: **${insights['avg_transaction']:.2f}**\n"
315
+ if insights.get('top_category'):
316
+ cat, amt = insights['top_category']
317
+ search_results_text += f"• Top Category: **{cat}** (${amt:.2f})\n"
318
+ else:
319
+ search_results_text = "No specific expense records found for this query."
320
+
321
+ # Create optimized prompt (copied from original)
322
+ enhanced_prompt = f"""You are Banko, a financial assistant. Answer based on this expense data:
323
+
324
+ Q: {query}
325
+
326
+ Data:
327
+ {search_results_text}
328
+
329
+ {budget_recommendations if budget_recommendations else ''}
330
+
331
+ Provide helpful insights with numbers, markdown formatting, and actionable advice."""
332
+
333
+ # Prepare messages for chat format (copied from original)
334
+ messages = [
335
+ {
336
+ "role": "user",
337
+ "content": enhanced_prompt
338
+ }
339
+ ]
340
+
341
+ # Call Watsonx API (copied from original implementation)
342
+ response_text = self._call_watsonx_api(messages)
343
+
344
+ except Exception as e:
345
+ # Fallback to structured response if API call fails
346
+ error_msg = str(e)
347
+ response_text = "## Financial Analysis for: \"" + query + "\"\n\n"
348
+ response_text += "### 📋 Transaction Details\n"
349
+ response_text += search_results_text if 'search_results_text' in locals() else 'No data available'
350
+ response_text += "\n\n### 📊 Financial Summary\n"
351
+ response_text += f"{insights.get('total_amount', 0):.2f} total across {insights.get('num_transactions', 0)} transactions"
352
+ response_text += "\n\n### 🤖 AI-Powered Insights\n"
353
+ response_text += f"Based on your expense data, I found {len(context)} relevant records. Here's a comprehensive analysis:\n\n"
354
+ response_text += "**Spending Analysis:**\n"
355
+ response_text += f"- Total Amount: ${insights.get('total_amount', 0):.2f}\n"
356
+ response_text += f"- Transaction Count: {insights.get('num_transactions', 0)}\n"
357
+ response_text += f"- Average Transaction: ${insights.get('avg_transaction', 0):.2f}\n"
358
+ top_category = insights.get('top_category', ('Unknown', 0))
359
+ response_text += "- Top Category: " + (top_category[0] if top_category else 'Unknown') + " ($" + f"{top_category[1]:.2f}" + " if top_category else 0)\n\n"
360
+ response_text += "**Smart Recommendations:**\n"
361
+ response_text += budget_recommendations if budget_recommendations else '• Monitor your spending patterns regularly\n• Consider setting up budget alerts\n• Review high-value transactions for optimization opportunities'
362
+ response_text += "\n\n**Next Steps:**\n"
363
+ response_text += "• Track your spending trends over time\n"
364
+ response_text += "• Set realistic budget goals for each category\n"
365
+ response_text += "• Review and optimize your payment methods\n\n"
366
+ response_text += "**Note**: API call failed, showing structured analysis above."
367
+
368
+ return response_text
369
+
370
+ except Exception as e:
371
+ return f"Sorry, I'm experiencing technical difficulties. Error: {str(e)}"
372
+
373
+ def generate_rag_response(
374
+ self,
375
+ query: str,
376
+ context: List[SearchResult],
377
+ user_id: Optional[str] = None,
378
+ language: str = "en"
379
+ ) -> RAGResponse:
380
+ """Generate a RAG response using Watsonx API (copied from original working implementation)."""
381
+ try:
382
+ print(f"\n🤖 WATSONX RAG (with caching):")
383
+ print(f"1. Query: '{query[:60]}...'")
384
+
385
+ # Check for cached response first
386
+ if self.cache_manager:
387
+ # Convert SearchResult objects to dict format for cache lookup
388
+ search_results_dict = []
389
+ for result in context:
390
+ search_results_dict.append({
391
+ 'expense_id': result.expense_id,
392
+ 'user_id': result.user_id,
393
+ 'description': result.description,
394
+ 'merchant': result.merchant,
395
+ 'expense_amount': result.amount,
396
+ 'expense_date': result.date,
397
+ 'similarity_score': result.similarity_score,
398
+ 'shopping_type': result.metadata.get('shopping_type'),
399
+ 'payment_method': result.metadata.get('payment_method'),
400
+ 'recurring': result.metadata.get('recurring'),
401
+ 'tags': result.metadata.get('tags')
402
+ })
403
+
404
+ cached_response = self.cache_manager.get_cached_response(
405
+ query, search_results_dict, "watsonx"
406
+ )
407
+ if cached_response:
408
+ print(f"2. ✅ Response cache HIT! Returning cached response")
409
+ return RAGResponse(
410
+ response=cached_response,
411
+ sources=context,
412
+ metadata={
413
+ 'provider': 'watsonx',
414
+ 'model': self.current_model,
415
+ 'user_id': user_id,
416
+ 'language': language,
417
+ 'cached': True
418
+ }
419
+ )
420
+ print(f"2. ❌ Response cache MISS, generating fresh response")
421
+ else:
422
+ print(f"2. No cache manager available, generating fresh response")
423
+ if not self.api_key or not self.project_id:
424
+ # Return structured demo response if no API credentials
425
+ if not context:
426
+ ai_response = f"""## Financial Analysis for: "{query}"
427
+
428
+ ### 📋 Transaction Details
429
+ No expense records found for this query.
430
+
431
+ ### 📊 Financial Summary
432
+ No data available for analysis.
433
+
434
+ ### 🤖 AI-Powered Insights
435
+ I couldn't find any relevant expense records for your query. Please try:
436
+ - Different keywords (e.g., "groceries", "restaurants", "transportation")
437
+ - Broader categories (e.g., "food", "shopping", "bills")
438
+ - Time periods (e.g., "last month", "this week")
439
+
440
+ **Note**: I need API credentials to generate more detailed AI-powered insights."""
441
+
442
+ # Generate financial insights from search results
443
+ insights = self._get_financial_insights(context)
444
+ budget_recommendations = self._generate_budget_recommendations(insights, query)
445
+
446
+ # Create table text from search results
447
+ table_text = ""
448
+ if context:
449
+ table_text = "\n".join([
450
+ f"• **{result.metadata.get('shopping_type', 'Unknown')}** at {result.merchant}: ${result.amount} ({result.metadata.get('payment_method', 'Unknown')}) - {result.description}"
451
+ for result in context
452
+ ])
453
+
454
+ # Create context text with financial summary
455
+ context_text = f"""**📊 Financial Summary:**
456
+ • Total Amount: ${insights.get('total_amount', 0):.2f}
457
+ • Number of Transactions: {insights.get('num_transactions', 0)}
458
+ • Average Transaction: ${insights.get('avg_transaction', 0):.2f}
459
+ • Top Category: {insights.get('top_category', ('Unknown', 0))[0] if insights.get('top_category') else 'Unknown'}
460
+ • Most frequent category: {insights.get('top_category', ('Unknown', 0))[0] if insights.get('top_category') else 'Unknown'}
461
+
462
+ **Recommendations:**
463
+ {budget_recommendations if budget_recommendations else '• Consider reviewing your spending patterns regularly' + chr(10) + '• Set up budget alerts for high-value categories'}
464
+
465
+ **Note**: I can see {len(context)} relevant expense records, but I need API credentials to generate more detailed AI-powered insights."""
466
+ else:
467
+ # Make actual Watsonx API call with enhanced prompt (copied from original)
468
+ try:
469
+ # Generate financial insights from search results
470
+ insights = self._get_financial_insights(context)
471
+ budget_recommendations = self._generate_budget_recommendations(insights, query)
472
+
473
+ # Prepare the search results context with enhanced analysis (copied from original)
474
+ search_results_text = ""
475
+ if context:
476
+ search_results_text = "\n".join(
477
+ f"• **{result.metadata.get('shopping_type', 'Unknown')}** at {result.merchant}: ${result.amount} ({result.metadata.get('payment_method', 'Unknown')}) - {result.description}"
478
+ for result in context
479
+ )
480
+
481
+ # Add financial summary (copied from original)
482
+ if insights:
483
+ search_results_text += f"\n\n**📊 Financial Summary:**\n"
484
+ search_results_text += f"• Total Amount: **${insights['total_amount']:.2f}**\n"
485
+ search_results_text += f"• Number of Transactions: **{insights['num_transactions']}**\n"
486
+ search_results_text += f"• Average Transaction: **${insights['avg_transaction']:.2f}**\n"
487
+ if insights.get('top_category'):
488
+ cat, amt = insights['top_category']
489
+ search_results_text += f"• Top Category: **{cat}** (${amt:.2f})\n"
490
+ else:
491
+ search_results_text = "No specific expense records found for this query."
492
+
493
+ # Create optimized prompt (copied from original)
494
+ enhanced_prompt = f"""You are Banko, a financial assistant. Answer based on this expense data:
495
+
496
+ Q: {query}
497
+
498
+ Data:
499
+ {search_results_text}
500
+
501
+ {budget_recommendations if budget_recommendations else ''}
502
+
503
+ Provide helpful insights with numbers, markdown formatting, and actionable advice."""
504
+
505
+ # Prepare messages for chat format (copied from original)
506
+ messages = [
507
+ {
508
+ "role": "user",
509
+ "content": enhanced_prompt
510
+ }
511
+ ]
512
+
513
+ # Call Watsonx API (copied from original implementation)
514
+ ai_response = self._call_watsonx_api(messages)
515
+
516
+ except Exception as e:
517
+ # Fallback to structured response if API call fails
518
+ error_msg = str(e)
519
+ ai_response = "## Financial Analysis for: \"" + query + "\"\n\n"
520
+ ai_response += "### 📋 Transaction Details\n"
521
+ ai_response += search_results_text if 'search_results_text' in locals() else 'No data available'
522
+ ai_response += "\n\n### 📊 Financial Summary\n"
523
+ ai_response += f"{insights.get('total_amount', 0):.2f} total across {insights.get('num_transactions', 0)} transactions"
524
+ ai_response += "\n\n### 🤖 AI-Powered Insights\n"
525
+ ai_response += f"Based on your expense data, I found {len(context)} relevant records. Here's a comprehensive analysis:\n\n"
526
+ ai_response += "**Spending Analysis:**\n"
527
+ ai_response += f"- Total Amount: ${insights.get('total_amount', 0):.2f}\n"
528
+ ai_response += f"- Transaction Count: {insights.get('num_transactions', 0)}\n"
529
+ ai_response += f"- Average Transaction: ${insights.get('avg_transaction', 0):.2f}\n"
530
+ top_category = insights.get('top_category', ('Unknown', 0))
531
+ ai_response += "- Top Category: " + (top_category[0] if top_category else 'Unknown') + " ($" + f"{top_category[1]:.2f}" + " if top_category else 0)\n\n"
532
+ ai_response += "**Smart Recommendations:**\n"
533
+ ai_response += budget_recommendations if budget_recommendations else '• Monitor your spending patterns regularly\n• Consider setting up budget alerts\n• Review high-value transactions for optimization opportunities'
534
+ ai_response += "\n\n**Next Steps:**\n"
535
+ ai_response += "• Track your spending trends over time\n"
536
+ ai_response += "• Set realistic budget goals for each category\n"
537
+ ai_response += "• Review and optimize your payment methods\n\n"
538
+ ai_response += "**Note**: API call failed, showing structured analysis above."
539
+
540
+ # Cache the response for future similar queries
541
+ if self.cache_manager and ai_response:
542
+ # Convert SearchResult objects to dict format for caching
543
+ search_results_dict = []
544
+ for result in context:
545
+ search_results_dict.append({
546
+ 'expense_id': result.expense_id,
547
+ 'user_id': result.user_id,
548
+ 'description': result.description,
549
+ 'merchant': result.merchant,
550
+ 'expense_amount': result.amount,
551
+ 'expense_date': result.date,
552
+ 'similarity_score': result.similarity_score,
553
+ 'shopping_type': result.metadata.get('shopping_type'),
554
+ 'payment_method': result.metadata.get('payment_method'),
555
+ 'recurring': result.metadata.get('recurring'),
556
+ 'tags': result.metadata.get('tags')
557
+ })
558
+
559
+ # Estimate token usage (rough approximation)
560
+ prompt_tokens = len(query.split()) * 1.3 # ~1.3 tokens per word
561
+ response_tokens = len(ai_response.split()) * 1.3
562
+
563
+ self.cache_manager.cache_response(
564
+ query, ai_response, search_results_dict, "watsonx",
565
+ int(prompt_tokens), int(response_tokens)
566
+ )
567
+ print(f"3. ✅ Cached response (est. {int(prompt_tokens + response_tokens)} tokens)")
568
+
569
+ return RAGResponse(
570
+ response=ai_response,
571
+ sources=context,
572
+ metadata={
573
+ 'provider': 'watsonx',
574
+ 'model': self.current_model,
575
+ 'user_id': user_id,
576
+ 'language': language
577
+ }
578
+ )
579
+
580
+ except Exception as e:
581
+ return RAGResponse(
582
+ response=f"Sorry, I'm experiencing technical difficulties. Error: {str(e)}",
583
+ sources=context,
584
+ metadata={
585
+ 'provider': 'watsonx',
586
+ 'model': self.current_model,
587
+ 'user_id': user_id,
588
+ 'language': language,
589
+ 'error': str(e)
590
+ }
591
+ )