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
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