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.
- banko_ai/__init__.py +19 -0
- banko_ai/__main__.py +10 -0
- banko_ai/ai_providers/__init__.py +18 -0
- banko_ai/ai_providers/aws_provider.py +337 -0
- banko_ai/ai_providers/base.py +175 -0
- banko_ai/ai_providers/factory.py +84 -0
- banko_ai/ai_providers/gemini_provider.py +340 -0
- banko_ai/ai_providers/openai_provider.py +295 -0
- banko_ai/ai_providers/watsonx_provider.py +591 -0
- banko_ai/cli.py +374 -0
- banko_ai/config/__init__.py +5 -0
- banko_ai/config/settings.py +216 -0
- banko_ai/static/Anallytics.png +0 -0
- banko_ai/static/Graph.png +0 -0
- banko_ai/static/Graph2.png +0 -0
- banko_ai/static/ai-status.png +0 -0
- banko_ai/static/banko-ai-assistant-watsonx.gif +0 -0
- banko_ai/static/banko-db-ops.png +0 -0
- banko_ai/static/banko-response.png +0 -0
- banko_ai/static/cache-stats.png +0 -0
- banko_ai/static/creditcard.png +0 -0
- banko_ai/static/profilepic.jpeg +0 -0
- banko_ai/static/query_watcher.png +0 -0
- banko_ai/static/roach-logo.svg +54 -0
- banko_ai/static/watsonx-icon.svg +1 -0
- banko_ai/templates/base.html +59 -0
- banko_ai/templates/dashboard.html +569 -0
- banko_ai/templates/index.html +1499 -0
- banko_ai/templates/login.html +41 -0
- banko_ai/utils/__init__.py +8 -0
- banko_ai/utils/cache_manager.py +525 -0
- banko_ai/utils/database.py +202 -0
- banko_ai/utils/migration.py +123 -0
- banko_ai/vector_search/__init__.py +18 -0
- banko_ai/vector_search/enrichment.py +278 -0
- banko_ai/vector_search/generator.py +329 -0
- banko_ai/vector_search/search.py +463 -0
- banko_ai/web/__init__.py +13 -0
- banko_ai/web/app.py +668 -0
- banko_ai/web/auth.py +73 -0
- banko_ai_assistant-1.0.0.dist-info/METADATA +414 -0
- banko_ai_assistant-1.0.0.dist-info/RECORD +46 -0
- banko_ai_assistant-1.0.0.dist-info/WHEEL +5 -0
- banko_ai_assistant-1.0.0.dist-info/entry_points.txt +2 -0
- banko_ai_assistant-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|