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
banko_ai/web/app.py
ADDED
@@ -0,0 +1,668 @@
|
|
1
|
+
"""
|
2
|
+
Main Flask application for Banko AI Assistant.
|
3
|
+
|
4
|
+
This module creates and configures the Flask application with all routes and functionality.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
from flask import Flask, render_template, request, jsonify, session, redirect, url_for
|
9
|
+
from sqlalchemy import create_engine, text
|
10
|
+
|
11
|
+
# Apply CockroachDB version parsing workaround before any database imports
|
12
|
+
from sqlalchemy.dialects.postgresql.base import PGDialect
|
13
|
+
original_get_server_version_info = PGDialect._get_server_version_info
|
14
|
+
|
15
|
+
def patched_get_server_version_info(self, connection):
|
16
|
+
try:
|
17
|
+
return original_get_server_version_info(self, connection)
|
18
|
+
except Exception:
|
19
|
+
return (25, 3, 0)
|
20
|
+
|
21
|
+
PGDialect._get_server_version_info = patched_get_server_version_info
|
22
|
+
|
23
|
+
from ..config.settings import get_config
|
24
|
+
from ..ai_providers.factory import AIProviderFactory
|
25
|
+
from ..vector_search.search import VectorSearchEngine
|
26
|
+
from ..vector_search.generator import EnhancedExpenseGenerator
|
27
|
+
from ..utils.cache_manager import BankoCacheManager
|
28
|
+
from .auth import UserManager
|
29
|
+
|
30
|
+
|
31
|
+
def create_app() -> Flask:
|
32
|
+
"""Create and configure the Flask application."""
|
33
|
+
# Get the directory containing this file
|
34
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
35
|
+
template_dir = os.path.join(current_dir, '..', 'templates')
|
36
|
+
static_dir = os.path.join(current_dir, '..', 'static')
|
37
|
+
|
38
|
+
app = Flask(__name__,
|
39
|
+
template_folder=template_dir,
|
40
|
+
static_folder=static_dir)
|
41
|
+
|
42
|
+
# Load configuration
|
43
|
+
config = get_config()
|
44
|
+
app.config['SECRET_KEY'] = config.secret_key
|
45
|
+
app.config['DEBUG'] = config.debug
|
46
|
+
|
47
|
+
# Initialize components
|
48
|
+
user_manager = UserManager()
|
49
|
+
cache_manager = BankoCacheManager()
|
50
|
+
search_engine = VectorSearchEngine(config.database_url, cache_manager)
|
51
|
+
expense_generator = EnhancedExpenseGenerator(config.database_url)
|
52
|
+
|
53
|
+
# Initialize AI provider
|
54
|
+
try:
|
55
|
+
ai_config = config.get_ai_config()
|
56
|
+
ai_provider = AIProviderFactory.create_provider(
|
57
|
+
config.ai_service,
|
58
|
+
ai_config[config.ai_service],
|
59
|
+
cache_manager
|
60
|
+
)
|
61
|
+
except Exception as e:
|
62
|
+
print(f"Warning: Could not initialize AI provider: {e}")
|
63
|
+
ai_provider = None
|
64
|
+
|
65
|
+
@app.route('/')
|
66
|
+
def index():
|
67
|
+
"""Main application page."""
|
68
|
+
# Ensure user is logged in
|
69
|
+
user_id = user_manager.get_or_create_current_user()
|
70
|
+
current_user = user_manager.get_current_user()
|
71
|
+
|
72
|
+
return render_template('index.html',
|
73
|
+
user=current_user,
|
74
|
+
ai_provider=ai_provider)
|
75
|
+
|
76
|
+
@app.route('/login', methods=['GET', 'POST'])
|
77
|
+
def login():
|
78
|
+
"""User login page."""
|
79
|
+
if request.method == 'POST':
|
80
|
+
username = request.form.get('username')
|
81
|
+
if username:
|
82
|
+
user_id = user_manager.create_user(username)
|
83
|
+
user_manager.login_user(user_id)
|
84
|
+
return redirect(url_for('index'))
|
85
|
+
|
86
|
+
return render_template('login.html')
|
87
|
+
|
88
|
+
@app.route('/logout')
|
89
|
+
def logout():
|
90
|
+
"""User logout."""
|
91
|
+
user_manager.logout_user()
|
92
|
+
return redirect(url_for('index'))
|
93
|
+
|
94
|
+
@app.route('/api/search', methods=['POST'])
|
95
|
+
def api_search():
|
96
|
+
"""API endpoint for expense search."""
|
97
|
+
try:
|
98
|
+
data = request.get_json()
|
99
|
+
query = data.get('query', '')
|
100
|
+
limit = data.get('limit', 10)
|
101
|
+
threshold = data.get('threshold', 0.7)
|
102
|
+
user_id = user_manager.get_current_user()['id'] if user_manager.is_logged_in() else None
|
103
|
+
|
104
|
+
# Perform vector search
|
105
|
+
results = search_engine.search_expenses(
|
106
|
+
query=query,
|
107
|
+
user_id=user_id,
|
108
|
+
limit=limit,
|
109
|
+
threshold=threshold
|
110
|
+
)
|
111
|
+
|
112
|
+
# Convert to serializable format
|
113
|
+
search_results = []
|
114
|
+
for result in results:
|
115
|
+
search_results.append({
|
116
|
+
'expense_id': result.expense_id,
|
117
|
+
'user_id': result.user_id,
|
118
|
+
'description': result.description,
|
119
|
+
'merchant': result.merchant,
|
120
|
+
'amount': result.amount,
|
121
|
+
'date': result.date,
|
122
|
+
'similarity_score': result.similarity_score,
|
123
|
+
'metadata': result.metadata
|
124
|
+
})
|
125
|
+
|
126
|
+
return jsonify({
|
127
|
+
'success': True,
|
128
|
+
'results': search_results,
|
129
|
+
'query': query,
|
130
|
+
'user_id': user_id
|
131
|
+
})
|
132
|
+
|
133
|
+
except Exception as e:
|
134
|
+
return jsonify({
|
135
|
+
'success': False,
|
136
|
+
'error': str(e)
|
137
|
+
}), 500
|
138
|
+
|
139
|
+
@app.route('/api/rag', methods=['POST'])
|
140
|
+
def api_rag():
|
141
|
+
"""API endpoint for RAG responses."""
|
142
|
+
try:
|
143
|
+
if not ai_provider:
|
144
|
+
return jsonify({
|
145
|
+
'success': False,
|
146
|
+
'error': 'AI provider not available'
|
147
|
+
}), 500
|
148
|
+
|
149
|
+
data = request.get_json()
|
150
|
+
query = data.get('query', '')
|
151
|
+
language = data.get('language', 'en')
|
152
|
+
user_id = user_manager.get_current_user()['id'] if user_manager.is_logged_in() else None
|
153
|
+
|
154
|
+
# Search for relevant expenses
|
155
|
+
search_results = search_engine.search_expenses(
|
156
|
+
query=query,
|
157
|
+
user_id=user_id,
|
158
|
+
limit=5,
|
159
|
+
threshold=0.7
|
160
|
+
)
|
161
|
+
|
162
|
+
# Generate RAG response
|
163
|
+
rag_response = ai_provider.generate_rag_response(
|
164
|
+
query=query,
|
165
|
+
context=search_results,
|
166
|
+
user_id=user_id,
|
167
|
+
language=language
|
168
|
+
)
|
169
|
+
|
170
|
+
return jsonify({
|
171
|
+
'success': True,
|
172
|
+
'response': rag_response.response,
|
173
|
+
'sources': [
|
174
|
+
{
|
175
|
+
'expense_id': result.expense_id,
|
176
|
+
'description': result.description,
|
177
|
+
'merchant': result.merchant,
|
178
|
+
'amount': result.amount,
|
179
|
+
'similarity_score': result.similarity_score
|
180
|
+
}
|
181
|
+
for result in rag_response.sources
|
182
|
+
],
|
183
|
+
'metadata': rag_response.metadata
|
184
|
+
})
|
185
|
+
|
186
|
+
except Exception as e:
|
187
|
+
return jsonify({
|
188
|
+
'success': False,
|
189
|
+
'error': str(e)
|
190
|
+
}), 500
|
191
|
+
|
192
|
+
@app.route('/api/generate-data', methods=['POST'])
|
193
|
+
def api_generate_data():
|
194
|
+
"""API endpoint for generating sample data."""
|
195
|
+
try:
|
196
|
+
data = request.get_json()
|
197
|
+
count = data.get('count', 1000)
|
198
|
+
user_id = user_manager.get_current_user()['id'] if user_manager.is_logged_in() else None
|
199
|
+
clear_existing = data.get('clear_existing', False)
|
200
|
+
|
201
|
+
# Generate expenses
|
202
|
+
generated_count = expense_generator.generate_and_save(
|
203
|
+
count=count,
|
204
|
+
user_id=user_id,
|
205
|
+
clear_existing=clear_existing
|
206
|
+
)
|
207
|
+
|
208
|
+
return jsonify({
|
209
|
+
'success': True,
|
210
|
+
'generated_count': generated_count,
|
211
|
+
'message': f'Generated {generated_count} expense records'
|
212
|
+
})
|
213
|
+
|
214
|
+
except Exception as e:
|
215
|
+
return jsonify({
|
216
|
+
'success': False,
|
217
|
+
'error': str(e)
|
218
|
+
}), 500
|
219
|
+
|
220
|
+
@app.route('/api/user-summary')
|
221
|
+
def api_user_summary():
|
222
|
+
"""API endpoint for user spending summary."""
|
223
|
+
try:
|
224
|
+
if not user_manager.is_logged_in():
|
225
|
+
return jsonify({
|
226
|
+
'success': False,
|
227
|
+
'error': 'User not logged in'
|
228
|
+
}), 401
|
229
|
+
|
230
|
+
user_id = user_manager.get_current_user()['id']
|
231
|
+
summary = search_engine.get_user_spending_summary(user_id)
|
232
|
+
|
233
|
+
return jsonify({
|
234
|
+
'success': True,
|
235
|
+
'summary': summary
|
236
|
+
})
|
237
|
+
|
238
|
+
except Exception as e:
|
239
|
+
return jsonify({
|
240
|
+
'success': False,
|
241
|
+
'error': str(e)
|
242
|
+
}), 500
|
243
|
+
|
244
|
+
@app.route('/api/ai-providers')
|
245
|
+
def api_ai_providers():
|
246
|
+
"""API endpoint for available AI providers."""
|
247
|
+
try:
|
248
|
+
providers = AIProviderFactory.get_available_providers()
|
249
|
+
current_provider = config.ai_service
|
250
|
+
|
251
|
+
return jsonify({
|
252
|
+
'success': True,
|
253
|
+
'providers': providers,
|
254
|
+
'current': current_provider
|
255
|
+
})
|
256
|
+
|
257
|
+
except Exception as e:
|
258
|
+
return jsonify({
|
259
|
+
'success': False,
|
260
|
+
'error': str(e)
|
261
|
+
}), 500
|
262
|
+
|
263
|
+
@app.route('/api/models')
|
264
|
+
def api_models():
|
265
|
+
"""API endpoint for available models for current provider."""
|
266
|
+
try:
|
267
|
+
if not ai_provider:
|
268
|
+
return jsonify({
|
269
|
+
'success': False,
|
270
|
+
'error': 'AI provider not available'
|
271
|
+
}), 500
|
272
|
+
|
273
|
+
available_models = ai_provider.get_available_models()
|
274
|
+
current_model = ai_provider.get_current_model()
|
275
|
+
|
276
|
+
return jsonify({
|
277
|
+
'success': True,
|
278
|
+
'models': available_models,
|
279
|
+
'current': current_model,
|
280
|
+
'provider': config.ai_service
|
281
|
+
})
|
282
|
+
|
283
|
+
except Exception as e:
|
284
|
+
return jsonify({
|
285
|
+
'success': False,
|
286
|
+
'error': str(e)
|
287
|
+
}), 500
|
288
|
+
|
289
|
+
@app.route('/api/models', methods=['POST'])
|
290
|
+
def api_set_model():
|
291
|
+
"""API endpoint for switching models."""
|
292
|
+
try:
|
293
|
+
if not ai_provider:
|
294
|
+
return jsonify({
|
295
|
+
'success': False,
|
296
|
+
'error': 'AI provider not available'
|
297
|
+
}), 500
|
298
|
+
|
299
|
+
data = request.get_json()
|
300
|
+
model = data.get('model')
|
301
|
+
|
302
|
+
if not model:
|
303
|
+
return jsonify({
|
304
|
+
'success': False,
|
305
|
+
'error': 'Model name is required'
|
306
|
+
}), 400
|
307
|
+
|
308
|
+
success = ai_provider.set_model(model)
|
309
|
+
|
310
|
+
if success:
|
311
|
+
return jsonify({
|
312
|
+
'success': True,
|
313
|
+
'message': f'Switched to {model}',
|
314
|
+
'current_model': ai_provider.get_current_model()
|
315
|
+
})
|
316
|
+
else:
|
317
|
+
return jsonify({
|
318
|
+
'success': False,
|
319
|
+
'error': f'Model {model} is not available'
|
320
|
+
}), 400
|
321
|
+
|
322
|
+
except Exception as e:
|
323
|
+
return jsonify({
|
324
|
+
'success': False,
|
325
|
+
'error': str(e)
|
326
|
+
}), 500
|
327
|
+
|
328
|
+
@app.route('/api/health')
|
329
|
+
def api_health():
|
330
|
+
"""Health check endpoint."""
|
331
|
+
try:
|
332
|
+
# Check database connection
|
333
|
+
engine = create_engine(config.database_url)
|
334
|
+
with engine.connect() as conn:
|
335
|
+
conn.execute(text("SELECT 1"))
|
336
|
+
|
337
|
+
# Check AI provider
|
338
|
+
ai_status = "unknown"
|
339
|
+
current_model = "unknown"
|
340
|
+
if ai_provider:
|
341
|
+
ai_status = "connected" if ai_provider.test_connection() else "disconnected"
|
342
|
+
current_model = ai_provider.get_current_model()
|
343
|
+
|
344
|
+
return jsonify({
|
345
|
+
'success': True,
|
346
|
+
'database': 'connected',
|
347
|
+
'ai_provider': ai_status,
|
348
|
+
'ai_service': config.ai_service,
|
349
|
+
'current_model': current_model
|
350
|
+
})
|
351
|
+
|
352
|
+
except Exception as e:
|
353
|
+
return jsonify({
|
354
|
+
'success': False,
|
355
|
+
'error': str(e)
|
356
|
+
}), 500
|
357
|
+
|
358
|
+
@app.route('/banko', methods=['GET', 'POST'])
|
359
|
+
def chat():
|
360
|
+
"""Main chat interface."""
|
361
|
+
if 'chat' not in session:
|
362
|
+
session['chat'] = []
|
363
|
+
|
364
|
+
# Get AI provider info for display
|
365
|
+
provider_info = ai_provider.get_provider_info() if ai_provider else {'name': 'Unknown', 'current_model': 'Unknown'}
|
366
|
+
ai_provider_display = {
|
367
|
+
'name': provider_info['name'],
|
368
|
+
'current_service': config.ai_service.upper(),
|
369
|
+
'icon': '🧠' if config.ai_service.lower() == 'watsonx' else '☁️'
|
370
|
+
}
|
371
|
+
|
372
|
+
if request.method == 'POST':
|
373
|
+
# Handle both 'message' and 'user_input' field names for compatibility
|
374
|
+
user_message = request.form.get('user_input') or request.form.get('message')
|
375
|
+
response_language = request.form.get('response_language', 'en-US')
|
376
|
+
|
377
|
+
if user_message:
|
378
|
+
session['chat'].append({'text': user_message, 'class': 'User'})
|
379
|
+
prompt = user_message
|
380
|
+
|
381
|
+
# Map language codes to language names for AI prompt
|
382
|
+
language_map = {
|
383
|
+
'en-US': 'English',
|
384
|
+
'es-ES': 'Spanish',
|
385
|
+
'fr-FR': 'French',
|
386
|
+
'de-DE': 'German',
|
387
|
+
'it-IT': 'Italian',
|
388
|
+
'pt-PT': 'Portuguese',
|
389
|
+
'ja-JP': 'Japanese',
|
390
|
+
'ko-KR': 'Korean',
|
391
|
+
'zh-CN': 'Chinese',
|
392
|
+
'hi-IN': 'Hindi'
|
393
|
+
}
|
394
|
+
|
395
|
+
target_language = language_map.get(response_language, 'English')
|
396
|
+
|
397
|
+
try:
|
398
|
+
# Search for relevant expenses using the configured AI service
|
399
|
+
user_id = user_manager.get_current_user()['id'] if user_manager.is_logged_in() else None
|
400
|
+
result = search_engine.search_expenses(
|
401
|
+
query=prompt,
|
402
|
+
user_id=user_id,
|
403
|
+
limit=10,
|
404
|
+
threshold=0.7
|
405
|
+
)
|
406
|
+
print(f"Using {provider_info['name']} for response generation in {target_language}")
|
407
|
+
|
408
|
+
# Generate RAG response with language preference
|
409
|
+
if target_language != 'English':
|
410
|
+
enhanced_prompt = f"{user_message}\n\nPlease respond in {target_language}."
|
411
|
+
rag_response = ai_provider.generate_rag_response(enhanced_prompt, result, user_id, response_language)
|
412
|
+
else:
|
413
|
+
rag_response = ai_provider.generate_rag_response(user_message, result, user_id, response_language)
|
414
|
+
|
415
|
+
print(f"Response from {provider_info['name']}: {rag_response.response}")
|
416
|
+
|
417
|
+
session['chat'].append({'text': rag_response.response, 'class': 'Assistant'})
|
418
|
+
|
419
|
+
except Exception as e:
|
420
|
+
error_message = f"Sorry, I'm experiencing technical difficulties with {provider_info['name']}. Please try again later."
|
421
|
+
print(f"Error with {provider_info['name']}: {str(e)}")
|
422
|
+
session['chat'].append({'text': error_message, 'class': 'Assistant'})
|
423
|
+
|
424
|
+
return render_template('index.html',
|
425
|
+
chat=session['chat'],
|
426
|
+
ai_provider=ai_provider_display,
|
427
|
+
current_page='banko')
|
428
|
+
|
429
|
+
@app.route('/home')
|
430
|
+
def dashboard():
|
431
|
+
return render_template('dashboard.html', current_page='home')
|
432
|
+
|
433
|
+
@app.route('/savings')
|
434
|
+
def savings():
|
435
|
+
return render_template('dashboard.html', current_page='savings')
|
436
|
+
|
437
|
+
@app.route('/wallet')
|
438
|
+
def wallet():
|
439
|
+
return render_template('dashboard.html', current_page='wallet')
|
440
|
+
|
441
|
+
@app.route('/credit-card')
|
442
|
+
def credit_card():
|
443
|
+
return render_template('dashboard.html', current_page='credit-card')
|
444
|
+
|
445
|
+
@app.route('/statements')
|
446
|
+
def statements():
|
447
|
+
return render_template('dashboard.html', current_page='statements')
|
448
|
+
|
449
|
+
@app.route('/benefits')
|
450
|
+
def benefits():
|
451
|
+
return render_template('dashboard.html', current_page='benefits')
|
452
|
+
|
453
|
+
@app.route('/settings')
|
454
|
+
def settings():
|
455
|
+
# Get AI provider info for display (without making LLM calls)
|
456
|
+
if ai_provider:
|
457
|
+
# Use cached provider info to avoid LLM calls
|
458
|
+
provider_name = ai_provider.get_provider_name()
|
459
|
+
current_model = getattr(ai_provider, 'current_model', 'Unknown')
|
460
|
+
# Check if we have API credentials without making a call
|
461
|
+
has_credentials = bool(
|
462
|
+
getattr(ai_provider, 'api_key', None) or
|
463
|
+
getattr(ai_provider, 'access_key_id', None) or
|
464
|
+
getattr(ai_provider, 'project_id', None)
|
465
|
+
)
|
466
|
+
connection_status = 'connected' if has_credentials else 'demo'
|
467
|
+
else:
|
468
|
+
provider_name = 'Unknown'
|
469
|
+
current_model = 'Unknown'
|
470
|
+
connection_status = 'disconnected'
|
471
|
+
|
472
|
+
ai_provider_display = {
|
473
|
+
'name': provider_name,
|
474
|
+
'current_model': current_model,
|
475
|
+
'current_service': config.ai_service.upper(),
|
476
|
+
'status': connection_status,
|
477
|
+
'icon': '🧠' if config.ai_service.lower() == 'watsonx' else '☁️'
|
478
|
+
}
|
479
|
+
return render_template('dashboard.html',
|
480
|
+
current_page='settings',
|
481
|
+
ai_provider=ai_provider_display)
|
482
|
+
|
483
|
+
@app.route('/ai-status')
|
484
|
+
def ai_status():
|
485
|
+
"""Endpoint to check the status of AI services and database."""
|
486
|
+
# Check database status
|
487
|
+
db_connected, db_message, table_exists, record_count = db_manager.test_connection()
|
488
|
+
|
489
|
+
status = {
|
490
|
+
'current_service': config.ai_service,
|
491
|
+
'watsonx_available': config.ai_service.lower() == 'watsonx',
|
492
|
+
'aws_bedrock_available': config.ai_service.lower() == 'aws',
|
493
|
+
'database': {
|
494
|
+
'connected': db_connected,
|
495
|
+
'status': db_message,
|
496
|
+
'expenses_table_exists': table_exists,
|
497
|
+
'record_count': record_count,
|
498
|
+
'connection_string': config.database_url.replace("@", "@***") if db_connected else "Not connected"
|
499
|
+
}
|
500
|
+
}
|
501
|
+
|
502
|
+
# Check AI provider status (without making LLM calls)
|
503
|
+
if ai_provider:
|
504
|
+
# Check credentials without making API calls
|
505
|
+
has_credentials = bool(
|
506
|
+
getattr(ai_provider, 'api_key', None) or
|
507
|
+
getattr(ai_provider, 'access_key_id', None) or
|
508
|
+
getattr(ai_provider, 'project_id', None)
|
509
|
+
)
|
510
|
+
provider_name = ai_provider.get_provider_name()
|
511
|
+
current_model = getattr(ai_provider, 'current_model', 'Unknown')
|
512
|
+
|
513
|
+
status['ai_status'] = {
|
514
|
+
'connected': has_credentials,
|
515
|
+
'message': 'API credentials configured' if has_credentials else 'Running in demo mode'
|
516
|
+
}
|
517
|
+
status['active_service_name'] = provider_name
|
518
|
+
status['current_model'] = current_model
|
519
|
+
else:
|
520
|
+
status['ai_status'] = {
|
521
|
+
'connected': False,
|
522
|
+
'message': 'No AI provider configured'
|
523
|
+
}
|
524
|
+
status['active_service_name'] = 'Unknown'
|
525
|
+
status['current_model'] = 'Unknown'
|
526
|
+
|
527
|
+
return status
|
528
|
+
|
529
|
+
@app.route('/test-ai-connection')
|
530
|
+
def test_ai_connection():
|
531
|
+
"""Endpoint to actually test AI provider connection (makes LLM call)."""
|
532
|
+
if not ai_provider:
|
533
|
+
return {'error': 'No AI provider configured'}, 400
|
534
|
+
|
535
|
+
try:
|
536
|
+
# This will make an actual LLM call to test connection
|
537
|
+
is_connected = ai_provider.test_connection()
|
538
|
+
return {
|
539
|
+
'connected': is_connected,
|
540
|
+
'message': 'Connection test successful' if is_connected else 'Connection test failed',
|
541
|
+
'provider': ai_provider.get_provider_name(),
|
542
|
+
'model': getattr(ai_provider, 'current_model', 'Unknown')
|
543
|
+
}
|
544
|
+
except Exception as e:
|
545
|
+
return {
|
546
|
+
'connected': False,
|
547
|
+
'message': f'Connection test failed: {str(e)}',
|
548
|
+
'provider': ai_provider.get_provider_name(),
|
549
|
+
'model': getattr(ai_provider, 'current_model', 'Unknown')
|
550
|
+
}, 500
|
551
|
+
|
552
|
+
@app.route('/cache-stats')
|
553
|
+
def cache_stats():
|
554
|
+
"""Endpoint to view cache performance statistics"""
|
555
|
+
if not cache_manager:
|
556
|
+
return {'error': 'Cache manager not available'}, 503
|
557
|
+
|
558
|
+
try:
|
559
|
+
stats = cache_manager.get_cache_stats(hours=24)
|
560
|
+
|
561
|
+
# Calculate overall hit rate
|
562
|
+
total_requests = 0
|
563
|
+
total_hits = 0
|
564
|
+
|
565
|
+
for cache_type, cache_stats in stats.items():
|
566
|
+
if cache_type != 'total_tokens_saved':
|
567
|
+
total_requests += cache_stats.get('hits', 0) + cache_stats.get('misses', 0)
|
568
|
+
total_hits += cache_stats.get('hits', 0)
|
569
|
+
|
570
|
+
overall_hit_rate = (total_hits / total_requests) if total_requests > 0 else 0
|
571
|
+
|
572
|
+
return {
|
573
|
+
'success': True,
|
574
|
+
'cache_enabled': True,
|
575
|
+
'overall_hit_rate': overall_hit_rate,
|
576
|
+
'total_requests': total_requests,
|
577
|
+
'total_hits': total_hits,
|
578
|
+
'total_tokens_saved': stats.get('total_tokens_saved', 0),
|
579
|
+
'cache_details': stats
|
580
|
+
}
|
581
|
+
except Exception as e:
|
582
|
+
return {'error': f'Failed to get cache stats: {str(e)}'}, 500
|
583
|
+
|
584
|
+
@app.route('/cache-cleanup', methods=['POST'])
|
585
|
+
def cache_cleanup():
|
586
|
+
"""Endpoint to manually trigger cache cleanup"""
|
587
|
+
if not cache_manager:
|
588
|
+
return {'error': 'Cache manager not available'}, 503
|
589
|
+
|
590
|
+
try:
|
591
|
+
cache_manager.cleanup_expired_cache()
|
592
|
+
return {'message': 'Cache cleanup completed successfully'}
|
593
|
+
except Exception as e:
|
594
|
+
return {'error': f'Cache cleanup failed: {str(e)}'}, 500
|
595
|
+
|
596
|
+
@app.route('/diagnostics/watsonx')
|
597
|
+
def watsonx_diagnostics():
|
598
|
+
"""Watsonx connection diagnostics endpoint"""
|
599
|
+
import socket
|
600
|
+
import requests
|
601
|
+
|
602
|
+
results = {
|
603
|
+
'dns_test': {'status': 'unknown', 'message': ''},
|
604
|
+
'http_test': {'status': 'unknown', 'message': ''},
|
605
|
+
'config_test': {'status': 'unknown', 'message': ''},
|
606
|
+
'overall_status': 'unknown',
|
607
|
+
'suggestions': []
|
608
|
+
}
|
609
|
+
|
610
|
+
try:
|
611
|
+
# Test DNS resolution
|
612
|
+
socket.gethostbyname("iam.cloud.ibm.com")
|
613
|
+
results['dns_test'] = {'status': 'success', 'message': 'DNS resolution successful'}
|
614
|
+
|
615
|
+
# Test HTTP connectivity
|
616
|
+
response = requests.get("https://iam.cloud.ibm.com", timeout=10)
|
617
|
+
results['http_test'] = {'status': 'success', 'message': f'HTTP connectivity successful (status: {response.status_code})'}
|
618
|
+
|
619
|
+
# Test configuration
|
620
|
+
if config.ai_service.lower() == 'watsonx':
|
621
|
+
results['config_test'] = {'status': 'success', 'message': 'Watsonx configuration available'}
|
622
|
+
results['overall_status'] = 'healthy'
|
623
|
+
else:
|
624
|
+
results['config_test'] = {'status': 'warning', 'message': 'Watsonx not configured or unavailable'}
|
625
|
+
results['overall_status'] = 'degraded'
|
626
|
+
results['suggestions'].append('Configure WATSONX_API_KEY environment variable')
|
627
|
+
|
628
|
+
except socket.gaierror as e:
|
629
|
+
results['dns_test'] = {'status': 'error', 'message': f'DNS resolution failed: {str(e)}'}
|
630
|
+
results['overall_status'] = 'unhealthy'
|
631
|
+
results['suggestions'].extend([
|
632
|
+
'Check your internet connection',
|
633
|
+
'Verify DNS settings',
|
634
|
+
'Try: nslookup iam.cloud.ibm.com'
|
635
|
+
])
|
636
|
+
except requests.exceptions.ConnectionError as e:
|
637
|
+
results['http_test'] = {'status': 'error', 'message': f'Connection failed: {str(e)}'}
|
638
|
+
results['overall_status'] = 'unhealthy'
|
639
|
+
results['suggestions'].extend([
|
640
|
+
'Check firewall settings',
|
641
|
+
'Verify network connectivity',
|
642
|
+
'Switch to AWS Bedrock: export AI_SERVICE=aws'
|
643
|
+
])
|
644
|
+
except requests.exceptions.Timeout as e:
|
645
|
+
results['http_test'] = {'status': 'error', 'message': f'Connection timeout: {str(e)}'}
|
646
|
+
results['overall_status'] = 'unhealthy'
|
647
|
+
results['suggestions'].extend([
|
648
|
+
'Check network latency',
|
649
|
+
'Try again later',
|
650
|
+
'Switch to AWS Bedrock: export AI_SERVICE=aws'
|
651
|
+
])
|
652
|
+
except Exception as e:
|
653
|
+
results['config_test'] = {'status': 'error', 'message': f'Unexpected error: {str(e)}'}
|
654
|
+
results['overall_status'] = 'unhealthy'
|
655
|
+
results['suggestions'].append('Check application logs for more details')
|
656
|
+
|
657
|
+
return results
|
658
|
+
|
659
|
+
# Error handlers
|
660
|
+
@app.errorhandler(404)
|
661
|
+
def not_found(error):
|
662
|
+
return jsonify({'error': 'Not found'}), 404
|
663
|
+
|
664
|
+
@app.errorhandler(500)
|
665
|
+
def internal_error(error):
|
666
|
+
return jsonify({'error': 'Internal server error'}), 500
|
667
|
+
|
668
|
+
return app
|