chrisjcc commited on
Commit
1ecc53e
Β·
1 Parent(s): 2735ddb

Refactoring App Structure (#1)

Browse files
Files changed (4) hide show
  1. README.md +1 -1
  2. app.py +815 -721
  3. app_with_confluence.py +0 -925
  4. utils.py +685 -0
README.md CHANGED
@@ -7,7 +7,7 @@ sdk: docker
7
  sdk_version: 7.1.0
8
  secrets:
9
  - GITHUB_TOKEN
10
- app_file: app_with_confluence.py
11
  pinned: false
12
  license: apache-2.0
13
  short_description: Fraud Model Explainability Assistant using Strands Agents
 
7
  sdk_version: 7.1.0
8
  secrets:
9
  - GITHUB_TOKEN
10
+ app_file: app.py
11
  pinned: false
12
  license: apache-2.0
13
  short_description: Fraud Model Explainability Assistant using Strands Agents
app.py CHANGED
@@ -1,672 +1,285 @@
 
1
  """
2
  Fraud Model Explainability Assistant - Strands Agents
3
-
4
  An AI-powered assistant that helps fraud analysts and executives understand
5
  why specific applications were flagged as fraudulent, translating complex
6
  model outputs into actionable insights.
7
 
 
 
8
  Use Cases:
9
  - Executive briefings on fraud decisions
10
  - Fair lending compliance documentation
11
  - Analyst investigation support
12
  - Model decision audit trails
13
 
14
- Author: Fraud Model Data Science Team
 
 
 
 
 
 
 
 
 
 
15
  """
16
 
17
  import os
18
- import random
 
19
  import warnings
20
- from datetime import datetime, timedelta
 
 
21
  from typing import Optional
 
22
 
23
- # Suppress asyncio "Invalid file descriptor" warnings in containerized environments
24
- # These are harmless cleanup warnings during garbage collection
25
  warnings.filterwarnings("ignore", category=ResourceWarning)
26
  os.environ["PYTHONWARNINGS"] = "ignore::ResourceWarning"
27
 
28
- import gradio as gr
29
- from strands import Agent, tool
 
 
 
 
 
 
 
 
 
 
 
 
30
  from strands.models.openai import OpenAIModel
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  # =============================================================================
34
- # MOCK DATA GENERATORS
35
  # =============================================================================
36
- # In production, these would connect to your actual data systems
37
- # (e.g., Snowflake, feature store, model serving infrastructure)
38
 
39
- def generate_mock_application(app_id: str) -> dict:
40
- """Generate realistic mock application data for demo purposes."""
41
- random.seed(hash(app_id) % 2**32)
42
-
43
- risk_level = random.choice(["low", "medium", "high", "very_high"])
44
-
45
- base_data = {
46
- "application_id": app_id,
47
- "timestamp": (datetime.now() - timedelta(days=random.randint(0, 30))).isoformat(),
48
- "portfolio": random.choice(["Retail Card", "Payment Solutions", "CareCredit"]),
49
- "requested_credit_line": random.randint(500, 25000),
50
- "fraud_score": {
51
- "low": random.randint(150, 350),
52
- "medium": random.randint(400, 550),
53
- "high": random.randint(600, 750),
54
- "very_high": random.randint(800, 950)
55
- }[risk_level],
56
- "fraud_score_percentile": {
57
- "low": random.randint(5, 30),
58
- "medium": random.randint(40, 60),
59
- "high": random.randint(75, 90),
60
- "very_high": random.randint(92, 99)
61
- }[risk_level],
62
- "decision": "FLAGGED" if risk_level in ["high", "very_high"] else "APPROVED",
63
- "risk_level": risk_level,
64
- }
65
-
66
- # Features that contribute to fraud scoring
67
- if risk_level in ["high", "very_high"]:
68
- base_data["features"] = {
69
- "ssn_issue_date_vs_credit_age_mismatch": random.uniform(0.7, 0.95),
70
- "device_velocity_30d": random.randint(5, 15),
71
- "address_type": random.choice(["CMRA", "PO_BOX", "VACANT"]),
72
- "phone_type": random.choice(["VOIP", "PREPAID"]),
73
- "email_domain_age_days": random.randint(1, 30),
74
- "application_velocity_14d": random.randint(3, 8),
75
- "identity_linkage_count": random.randint(4, 12),
76
- "credit_inquiry_spike": True,
77
- "synthetic_id_score": random.uniform(0.75, 0.98),
78
- }
79
- else:
80
- base_data["features"] = {
81
- "ssn_issue_date_vs_credit_age_mismatch": random.uniform(0.0, 0.2),
82
- "device_velocity_30d": random.randint(1, 2),
83
- "address_type": "RESIDENTIAL",
84
- "phone_type": "POSTPAID",
85
- "email_domain_age_days": random.randint(365, 3650),
86
- "application_velocity_14d": random.randint(0, 1),
87
- "identity_linkage_count": random.randint(0, 2),
88
- "credit_inquiry_spike": False,
89
- "synthetic_id_score": random.uniform(0.05, 0.25),
 
 
 
 
 
 
 
 
 
90
  }
91
-
92
- return base_data
 
 
93
 
94
 
95
  # =============================================================================
96
- # FRAUD EXPLAINABILITY TOOLS
97
  # =============================================================================
98
 
99
- @tool
100
- def get_application_summary(application_id: str) -> str:
101
- """
102
- Retrieve basic information about a credit application including
103
- fraud score, decision, portfolio, and timestamp.
104
-
105
- Args:
106
- application_id: The unique identifier for the application (e.g., "APP-12345")
107
-
108
- Returns:
109
- A summary of the application details and fraud assessment
110
- """
111
- app = generate_mock_application(application_id)
112
-
113
- return f"""
114
- APPLICATION SUMMARY
115
- ==================
116
- Application ID: {app['application_id']}
117
- Submission Date: {app['timestamp'][:10]}
118
- Portfolio: {app['portfolio']}
119
- Requested Credit Line: ${app['requested_credit_line']:,}
120
-
121
- FRAUD ASSESSMENT
122
- ----------------
123
- Fraud Score: {app['fraud_score']} / 1000
124
- Risk Percentile: {app['fraud_score_percentile']}th percentile
125
- Risk Level: {app['risk_level'].upper()}
126
- Decision: {app['decision']}
127
- """
128
 
129
 
130
- @tool
131
- def explain_fraud_score(application_id: str) -> str:
132
- """
133
- Get detailed SHAP-style feature attribution explanation for why an
134
- application received its fraud score. Shows which factors contributed
135
- most to the risk assessment.
136
-
137
- Args:
138
- application_id: The unique identifier for the application
139
-
140
- Returns:
141
- Detailed breakdown of contributing factors with impact scores
142
- """
143
- app = generate_mock_application(application_id)
144
- features = app["features"]
145
-
146
- # Simulate SHAP values (in production, these come from your model)
147
- explanations = []
148
-
149
- if features["ssn_issue_date_vs_credit_age_mismatch"] > 0.5:
150
- explanations.append({
151
- "feature": "SSN Issue Date vs Credit Age Mismatch",
152
- "value": f"{features['ssn_issue_date_vs_credit_age_mismatch']:.0%}",
153
- "impact": "+187 points",
154
- "direction": "INCREASES RISK",
155
- "explanation": "SSN was issued recently but credit file shows longer history, a key synthetic ID indicator"
156
- })
157
-
158
- if features["device_velocity_30d"] > 3:
159
- explanations.append({
160
- "feature": "Device Velocity (30 days)",
161
- "value": f"{features['device_velocity_30d']} applications",
162
- "impact": "+142 points",
163
- "direction": "INCREASES RISK",
164
- "explanation": "Same device fingerprint linked to multiple applications in short period"
165
- })
166
-
167
- if features["address_type"] in ["CMRA", "PO_BOX", "VACANT"]:
168
- explanations.append({
169
- "feature": "Address Type",
170
- "value": features["address_type"],
171
- "impact": "+98 points",
172
- "direction": "INCREASES RISK",
173
- "explanation": f"Address classified as {features['address_type']} (Commercial Mail Receiving Agency or high-risk type)"
174
- })
175
-
176
- if features["synthetic_id_score"] > 0.6:
177
- explanations.append({
178
- "feature": "Synthetic Identity Score",
179
- "value": f"{features['synthetic_id_score']:.0%}",
180
- "impact": "+156 points",
181
- "direction": "INCREASES RISK",
182
- "explanation": "Composite score from ensemble model indicates high probability of synthetic identity"
183
- })
184
-
185
- if features["application_velocity_14d"] > 2:
186
- explanations.append({
187
- "feature": "Application Velocity (14 days)",
188
- "value": f"{features['application_velocity_14d']} applications",
189
- "impact": "+78 points",
190
- "direction": "INCREASES RISK",
191
- "explanation": "Multiple credit applications submitted in short timeframe"
192
- })
193
-
194
- if features["email_domain_age_days"] < 60:
195
- explanations.append({
196
- "feature": "Email Domain Age",
197
- "value": f"{features['email_domain_age_days']} days",
198
- "impact": "+45 points",
199
- "direction": "INCREASES RISK",
200
- "explanation": "Email address created very recently"
201
- })
202
-
203
- if features["phone_type"] in ["VOIP", "PREPAID"]:
204
- explanations.append({
205
- "feature": "Phone Type",
206
- "value": features["phone_type"],
207
- "impact": "+62 points",
208
- "direction": "INCREASES RISK",
209
- "explanation": "Non-traditional phone type associated with higher fraud rates"
210
- })
211
-
212
- # If low risk, show protective factors
213
- if app["risk_level"] == "low":
214
- explanations = [
215
- {
216
- "feature": "Established Credit History",
217
- "value": "12+ years",
218
- "impact": "-120 points",
219
- "direction": "DECREASES RISK",
220
- "explanation": "Long credit history consistent with SSN issue date"
221
- },
222
- {
223
- "feature": "Stable Contact Information",
224
- "value": "Verified",
225
- "impact": "-85 points",
226
- "direction": "DECREASES RISK",
227
- "explanation": "Phone and address verified with multiple data sources"
228
- },
229
- {
230
- "feature": "Low Application Velocity",
231
- "value": "1 in 90 days",
232
- "impact": "-45 points",
233
- "direction": "DECREASES RISK",
234
- "explanation": "Normal application pattern"
235
  }
236
- ]
237
-
238
- # Format output
239
- output = f"""
240
- FRAUD SCORE EXPLANATION
241
- =======================
242
- Application ID: {application_id}
243
- Final Fraud Score: {app['fraud_score']} / 1000
244
- Model: XGBoost Fraud Ensemble v3.2
245
-
246
- TOP CONTRIBUTING FACTORS (ranked by impact):
247
- --------------------------------------------
248
- """
249
-
250
- for i, exp in enumerate(sorted(explanations, key=lambda x: abs(int(x["impact"].split()[0])), reverse=True), 1):
251
- output += f"""
252
- {i}. {exp['feature']}
253
- Value: {exp['value']}
254
- Impact: {exp['impact']} ({exp['direction']})
255
- β†’ {exp['explanation']}
256
- """
257
-
258
- return output
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
- @tool
262
- def compare_to_population(application_id: str, comparison_group: str = "approved") -> str:
263
- """
264
- Compare an application's features to the approved or denied population
265
- to show how unusual the applicant's characteristics are.
266
-
267
- Args:
268
- application_id: The unique identifier for the application
269
- comparison_group: Either "approved" or "denied" population to compare against
270
-
271
- Returns:
272
- Statistical comparison showing how the application differs from typical cases
273
- """
274
- app = generate_mock_application(application_id)
275
- features = app["features"]
276
-
277
- # Mock population statistics
278
- population_stats = {
279
- "approved": {
280
- "ssn_credit_mismatch_mean": 0.08,
281
- "ssn_credit_mismatch_std": 0.12,
282
- "device_velocity_mean": 1.2,
283
- "device_velocity_std": 0.8,
284
- "synthetic_score_mean": 0.15,
285
- "synthetic_score_std": 0.10,
286
- "app_velocity_mean": 0.5,
287
- "app_velocity_std": 0.7,
288
- },
289
- "denied": {
290
- "ssn_credit_mismatch_mean": 0.72,
291
- "ssn_credit_mismatch_std": 0.18,
292
- "device_velocity_mean": 6.5,
293
- "device_velocity_std": 3.2,
294
- "synthetic_score_mean": 0.78,
295
- "synthetic_score_std": 0.15,
296
- "app_velocity_mean": 4.2,
297
- "app_velocity_std": 2.1,
298
- }
299
- }
300
-
301
- stats = population_stats.get(comparison_group, population_stats["approved"])
302
-
303
- def calc_z_score(value, mean, std):
304
- if std == 0:
305
- return 0
306
- return (value - mean) / std
307
-
308
- comparisons = [
309
- {
310
- "feature": "SSN/Credit Age Mismatch",
311
- "applicant_value": f"{features['ssn_issue_date_vs_credit_age_mismatch']:.0%}",
312
- "population_mean": f"{stats['ssn_credit_mismatch_mean']:.0%}",
313
- "z_score": calc_z_score(features['ssn_issue_date_vs_credit_age_mismatch'],
314
- stats['ssn_credit_mismatch_mean'],
315
- stats['ssn_credit_mismatch_std'])
316
- },
317
- {
318
- "feature": "Device Velocity (30d)",
319
- "applicant_value": str(features['device_velocity_30d']),
320
- "population_mean": f"{stats['device_velocity_mean']:.1f}",
321
- "z_score": calc_z_score(features['device_velocity_30d'],
322
- stats['device_velocity_mean'],
323
- stats['device_velocity_std'])
324
- },
325
- {
326
- "feature": "Synthetic ID Score",
327
- "applicant_value": f"{features['synthetic_id_score']:.0%}",
328
- "population_mean": f"{stats['synthetic_score_mean']:.0%}",
329
- "z_score": calc_z_score(features['synthetic_id_score'],
330
- stats['synthetic_score_mean'],
331
- stats['synthetic_score_std'])
332
- },
333
- {
334
- "feature": "Application Velocity (14d)",
335
- "applicant_value": str(features['application_velocity_14d']),
336
- "population_mean": f"{stats['app_velocity_mean']:.1f}",
337
- "z_score": calc_z_score(features['application_velocity_14d'],
338
- stats['app_velocity_mean'],
339
- stats['app_velocity_std'])
340
- },
341
- ]
342
-
343
- output = f"""
344
- POPULATION COMPARISON ANALYSIS
345
- ==============================
346
- Application ID: {application_id}
347
- Comparison Group: {comparison_group.upper()} applications (last 12 months)
348
- Sample Size: {'847,293' if comparison_group == 'approved' else '23,847'} applications
349
-
350
- FEATURE COMPARISON:
351
- -------------------
352
- {"Feature":<30} {"Applicant":<15} {"Population Mean":<18} {"Z-Score":<10} {"Assessment"}
353
- {"-"*95}
354
- """
355
-
356
- for comp in comparisons:
357
- z = comp["z_score"]
358
- if abs(z) > 3:
359
- assessment = "⚠️ EXTREME OUTLIER"
360
- elif abs(z) > 2:
361
- assessment = "πŸ”Ά SIGNIFICANT DEVIATION"
362
- elif abs(z) > 1:
363
- assessment = "πŸ”· MILD DEVIATION"
364
- else:
365
- assessment = "βœ… WITHIN NORMAL"
366
-
367
- output += f"{comp['feature']:<30} {comp['applicant_value']:<15} {comp['population_mean']:<18} {z:>+.2f}Οƒ {assessment}\n"
368
-
369
- # Summary
370
- extreme_count = sum(1 for c in comparisons if abs(c["z_score"]) > 2)
371
-
372
- output += f"""
373
- SUMMARY:
374
- --------
375
- {extreme_count} of {len(comparisons)} features show significant deviation (|z| > 2Οƒ) from {comparison_group} population.
376
- """
377
-
378
- if extreme_count >= 2:
379
- output += f"This application's profile is statistically unusual compared to typically {comparison_group} applications."
380
-
381
- return output
382
 
 
 
 
383
 
384
- @tool
385
- def check_fair_lending_flags(application_id: str) -> str:
386
- """
387
- Check for potential fair lending concerns in the fraud decision.
388
- Reviews whether protected class proxies may have influenced the score
389
- and provides compliance documentation.
390
-
391
- Args:
392
- application_id: The unique identifier for the application
393
-
394
- Returns:
395
- Fair lending compliance assessment and documentation
396
- """
397
- app = generate_mock_application(application_id)
398
-
399
- # Mock fair lending analysis
400
- output = f"""
401
- FAIR LENDING COMPLIANCE REVIEW
402
- ==============================
403
- Application ID: {application_id}
404
- Review Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
405
- Model: XGBoost Fraud Ensemble v3.2
406
-
407
- PROTECTED CLASS PROXY ANALYSIS:
408
- -------------------------------
409
- The following features were analyzed for potential correlation with protected characteristics:
410
-
411
- βœ… Geography-Based Features:
412
- - ZIP code used only for velocity calculations, not scoring
413
- - No direct geographic risk scoring applied
414
- - Compliant with ECOA geographic restrictions
415
-
416
- βœ… Name-Based Features:
417
- - No name-based features used in fraud model
418
- - Identity verification uses SSN/DOB only
419
-
420
- βœ… Age-Related Features:
421
- - Credit age features measure account history, not applicant age
422
- - SSN issuance analysis targets synthetic ID patterns, not age discrimination
423
- - Model tested for age disparate impact: PASSED (adverse impact ratio: 0.94)
424
-
425
- ⚠️ REVIEW ITEMS:
426
- -----------------
427
- """
428
-
429
- if app["features"].get("phone_type") in ["VOIP", "PREPAID"]:
430
- output += """
431
- β€’ Phone Type Feature:
432
- - VOIP/Prepaid flagged as risk factor
433
- - Documented business justification: 73% of confirmed synthetic fraud uses VOIP
434
- - Disparate impact testing: PASSED (ratio: 0.89)
435
- - Alternative considered: None available with equivalent predictive power
436
- """
437
-
438
- if app["features"].get("address_type") in ["CMRA", "PO_BOX"]:
439
- output += """
440
- β€’ Address Type Feature:
441
- - CMRA/PO Box flagged as risk factor
442
- - Documented business justification: Required for synthetic ID detection
443
- - Disparate impact testing: PASSED (ratio: 0.91)
444
- - Accommodations: Manual review pathway available for legitimate CMRA users
445
- """
446
-
447
- output += f"""
448
- MODEL VALIDATION STATUS:
449
- ------------------------
450
- Last Disparate Impact Test: 2024-11-15
451
- Last Adverse Action Review: 2024-12-01
452
- Model Risk Rating: LOW
453
- SR 11-7 Compliance: COMPLIANT
454
-
455
- ADVERSE ACTION REASON CODES:
456
- ----------------------------
457
- If this application is denied, the following reason codes apply:
458
- """
459
-
460
- if app["decision"] == "FLAGGED":
461
- reasons = [
462
- "FA01 - Unable to verify identity information",
463
- "FA03 - Inconsistent application information",
464
- "FA07 - High-risk contact information patterns",
465
- ]
466
- for i, reason in enumerate(reasons, 1):
467
- output += f" {i}. {reason}\n"
468
- else:
469
- output += " N/A - Application approved\n"
470
-
471
- output += """
472
- DOCUMENTATION:
473
- --------------
474
- This analysis is auto-generated for compliance documentation.
475
- Full model documentation available in Model Risk Management system.
476
- Contact: model-governance@company.com
477
- """
478
-
479
- return output
480
 
 
 
 
481
 
482
- @tool
483
- def get_identity_network(application_id: str) -> str:
484
- """
485
- Analyze the identity linkage network for an application, showing
486
- connections to other applications via shared attributes (device,
487
- phone, email, address, SSN patterns).
488
-
489
- Args:
490
- application_id: The unique identifier for the application
491
-
492
- Returns:
493
- Network analysis showing linked applications and risk patterns
494
- """
495
- app = generate_mock_application(application_id)
496
- features = app["features"]
497
-
498
- linkage_count = features.get("identity_linkage_count", 0)
499
-
500
- output = f"""
501
- IDENTITY NETWORK ANALYSIS
502
- =========================
503
- Application ID: {application_id}
504
- Analysis Date: {datetime.now().strftime('%Y-%m-%d')}
505
-
506
- LINKAGE SUMMARY:
507
- ----------------
508
- Total Linked Applications: {linkage_count}
509
- """
510
-
511
- if linkage_count > 3:
512
- # Generate mock linked applications for high-risk cases
513
- random.seed(hash(application_id) % 2**32)
514
-
515
- link_types = {
516
- "device_fingerprint": random.randint(2, min(linkage_count, 8)),
517
- "phone_number": random.randint(1, min(linkage_count, 4)),
518
- "email_pattern": random.randint(1, min(linkage_count, 3)),
519
- "address": random.randint(1, min(linkage_count, 5)),
520
- }
521
-
522
- output += f"""
523
- LINKAGE BREAKDOWN:
524
- ------------------
525
- β€’ Device Fingerprint Links: {link_types['device_fingerprint']} applications
526
- β€’ Phone Number Links: {link_types['phone_number']} applications
527
- β€’ Email Pattern Links: {link_types['email_pattern']} applications
528
- β€’ Address Links: {link_types['address']} applications
529
-
530
- LINKED APPLICATION DETAILS:
531
- ---------------------------
532
- """
533
-
534
- statuses = ["CONFIRMED_FRAUD", "FLAGGED", "DENIED", "CHARGED_OFF", "APPROVED"]
535
- weights = [0.3, 0.25, 0.2, 0.15, 0.1] if app["risk_level"] in ["high", "very_high"] else [0.05, 0.1, 0.15, 0.1, 0.6]
536
-
537
- for i in range(min(linkage_count, 6)):
538
- linked_id = f"APP-{random.randint(10000, 99999)}"
539
- link_type = random.choice(list(link_types.keys()))
540
- status = random.choices(statuses, weights=weights)[0]
541
- days_ago = random.randint(1, 180)
542
-
543
- status_emoji = {
544
- "CONFIRMED_FRAUD": "πŸ”΄",
545
- "FLAGGED": "🟠",
546
- "DENIED": "🟑",
547
- "CHARGED_OFF": "πŸ”΄",
548
- "APPROVED": "🟒"
549
- }
550
-
551
- output += f" {status_emoji.get(status, 'βšͺ')} {linked_id} | {link_type.replace('_', ' ').title()} | {status} | {days_ago}d ago\n"
552
-
553
- # Risk assessment
554
- fraud_links = sum(1 for _ in range(linkage_count) if random.random() < 0.4)
555
-
556
- output += f"""
557
- NETWORK RISK ASSESSMENT:
558
- ------------------------
559
- β€’ Confirmed Fraud in Network: {fraud_links} application(s)
560
- β€’ Network Risk Score: {min(100, linkage_count * 12 + fraud_links * 25)}/100
561
- β€’ Ring Pattern Detected: {"YES ⚠️" if linkage_count > 5 else "NO"}
562
- β€’ Velocity Anomaly: {"YES ⚠️" if features.get('device_velocity_30d', 0) > 5 else "NO"}
563
-
564
- RECOMMENDATION:
565
- ---------------
566
- {"⚠️ HIGH-RISK NETWORK - Manual review recommended" if linkage_count > 5 else "πŸ”Ά ELEVATED RISK - Monitor for additional activity"}
567
- """
568
-
569
- else:
570
- output += """
571
- LINKAGE BREAKDOWN:
572
- ------------------
573
- β€’ Device Fingerprint Links: 0-1 applications
574
- β€’ Phone Number Links: 0 applications
575
- β€’ Email Pattern Links: 0 applications
576
- β€’ Address Links: 1 application (same household likely)
577
-
578
- NETWORK RISK ASSESSMENT:
579
- ------------------------
580
- β€’ Network Risk Score: LOW
581
- β€’ No suspicious patterns detected
582
- β€’ Normal application profile
583
-
584
- βœ… No concerning identity network patterns identified.
585
- """
586
-
587
- return output
588
 
 
 
 
 
589
 
590
- @tool
591
- def get_model_performance(model_name: str = "xgboost_fraud_v3.2", portfolio: str = "all") -> str:
592
- """
593
- Retrieve current performance metrics for a fraud detection model,
594
- including precision, recall, KS statistic, and financial impact.
595
-
596
- Args:
597
- model_name: Name of the fraud model (default: xgboost_fraud_v3.2)
598
- portfolio: Portfolio to filter by ("Retail Card", "Payment Solutions", "CareCredit", or "all")
599
-
600
- Returns:
601
- Model performance metrics and trends
602
- """
603
- output = f"""
604
- MODEL PERFORMANCE DASHBOARD
605
- ===========================
606
- Model: {model_name}
607
- Portfolio: {portfolio.upper()}
608
- Reporting Period: Last 30 Days
609
- Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
610
-
611
- DETECTION METRICS:
612
- ------------------
613
- Current Prior Month Ξ” Change
614
- Fraud Detection Rate: 87.3% 84.1% +3.2% βœ…
615
- Precision (PPV): 34.2% 31.8% +2.4% βœ…
616
- False Positive Rate: 2.1% 2.4% -0.3% βœ…
617
- KS Statistic: 0.72 0.69 +0.03 βœ…
618
- Gini Coefficient: 0.81 0.78 +0.03 βœ…
619
- AUC-ROC: 0.91 0.89 +0.02 βœ…
620
-
621
- FINANCIAL IMPACT:
622
- -----------------
623
- Current Prior Month Ξ” Change
624
- Fraud Losses Prevented: $4.2M $3.8M +$400K βœ…
625
- False Positive Cost: $890K $920K -$30K βœ…
626
- Net Benefit: $3.31M $2.88M +$430K βœ…
627
- ROI: 372% 317% +55% βœ…
628
-
629
- VOLUME METRICS:
630
- ---------------
631
- Applications Scored: 1,247,832
632
- High-Risk Flags: 26,847 (2.15%)
633
- Manual Reviews: 8,421
634
- Confirmed Fraud: 9,182
635
- """
636
-
637
- if portfolio != "all":
638
- output += f"""
639
- PORTFOLIO BREAKDOWN ({portfolio}):
640
- {'='*40}
641
- Applications: {random.randint(200000, 500000):,}
642
- Fraud Rate: {random.uniform(0.5, 1.2):.2f}%
643
- Detection Rate: {random.uniform(82, 92):.1f}%
644
- """
645
-
646
- output += """
647
- MODEL HEALTH:
648
- -------------
649
- βœ… Feature Drift (PSI): 0.08 (threshold: 0.25)
650
- βœ… Score Distribution: Stable
651
- βœ… Latency P99: 45ms (SLA: 100ms)
652
- ⚠️ Challenger Model: +2.1% lift in shadow mode - review scheduled
653
-
654
- TREND ALERT:
655
- ------------
656
- πŸ“ˆ Synthetic ID fraud attempts up 23% MoM - model adapting well
657
- πŸ“‰ First-party fraud stable at historical levels
658
- """
659
-
660
- return output
661
 
662
 
663
  # =============================================================================
664
- # SYSTEM PROMPT
665
  # =============================================================================
666
 
667
- SYSTEM_PROMPT = """
668
  You are a Fraud Model Explainability Assistant for a major financial services company.
669
- Your role is to help fraud analysts, data scientists, and executives understand
670
  fraud model decisions and their implications.
671
 
672
  You have access to tools that can:
@@ -676,6 +289,8 @@ You have access to tools that can:
676
  4. Check for fair lending compliance concerns
677
  5. Analyze identity networks and linkages
678
  6. Show model performance metrics
 
 
679
 
680
  When answering questions:
681
  - Be precise and data-driven
@@ -684,12 +299,19 @@ When answering questions:
684
  - Always mention fair lending implications when relevant
685
  - Provide actionable insights, not just data
686
 
 
 
 
 
 
 
 
687
  For flagged applications, structure your response as:
688
  1. Quick summary (score, decision, risk level)
689
  2. Top contributing factors
690
  3. How unusual this is compared to the population
691
- 4. Any compliance considerations
692
- 5. Recommended next steps
693
 
694
  Remember: Your explanations may be used in regulatory examinations and audits,
695
  so be accurate and thorough.
@@ -697,135 +319,607 @@ so be accurate and thorough.
697
 
698
 
699
  # =============================================================================
700
- # AGENT SETUP
701
  # =============================================================================
702
 
703
- def create_agent():
704
- """Create and configure the Strands fraud explainability agent."""
705
- openai_api_key = os.environ.get('OPENAI_API_KEY')
 
 
 
706
 
707
- tools = [
708
- get_application_summary,
709
- explain_fraud_score,
710
- compare_to_population,
711
- check_fair_lending_flags,
712
- get_identity_network,
713
- get_model_performance,
714
- ]
715
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
  if openai_api_key:
717
  model = OpenAIModel(
718
  client_args={"api_key": openai_api_key},
719
  model_id="gpt-4o",
720
- params={"temperature": 0.1, "max_tokens": 2048}
721
  )
722
- return Agent(model=model, system_prompt=SYSTEM_PROMPT, tools=tools)
723
  else:
724
- # Default to Bedrock
725
- return Agent(system_prompt=SYSTEM_PROMPT, tools=tools)
 
726
 
727
 
728
- def query(question: str) -> str:
729
- """Process a question using the fraud explainability agent."""
730
  try:
731
- agent = create_agent()
 
732
  result = agent(question)
 
733
  return str(result)
734
  except Exception as e:
735
- return f"Error processing question: {str(e)}"
 
 
736
 
737
 
738
  # =============================================================================
739
- # GRADIO INTERFACE
740
  # =============================================================================
741
 
742
- def process_question(question: str) -> str:
743
- """Wrapper for Gradio interface."""
744
- return query(question)
745
 
 
 
 
 
 
 
 
746
 
747
- # Create the interface (Gradio 6.0 compatible)
748
- with gr.Blocks(title="Fraud Model Explainability Assistant") as iface:
749
- gr.Markdown("""
750
- # πŸ” Fraud Model Explainability Assistant
751
-
752
- An AI-powered assistant that helps you understand fraud model decisions,
753
- investigate flagged applications, and ensure fair lending compliance.
754
-
755
- **Capabilities:**
756
- - Explain why applications were flagged (SHAP-style feature attribution)
757
- - Compare applications to approved/denied populations
758
- - Analyze identity networks and linkages
759
- - Check fair lending compliance
760
- - Review model performance metrics
761
- """)
762
-
763
- with gr.Row():
764
- with gr.Column(scale=2):
765
- question_input = gr.Textbox(
766
- label="Ask a Question",
767
- placeholder="e.g., Why was application APP-12345 flagged as high risk?",
768
- lines=3
769
- )
770
- submit_btn = gr.Button("πŸ” Analyze", variant="primary")
771
-
772
- with gr.Row():
773
- output = gr.Textbox(
774
- label="Analysis Results",
775
- lines=25
776
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
777
 
778
- gr.Markdown("### πŸ’‘ Example Questions")
779
-
780
- examples = gr.Examples(
781
- examples=[
782
- ["Why was application APP-78432 flagged as high risk?"],
783
- ["Explain the fraud score for APP-12345 and compare it to approved applications"],
784
- ["Check fair lending compliance for application APP-55555"],
785
- ["Show me the identity network analysis for APP-78432"],
786
- ["What's the current model performance for the Retail Card portfolio?"],
787
- ["I need to present APP-99999 to the CCO. Give me a complete risk summary with compliance review."],
788
- ["Compare APP-12345 to the denied population and explain if this looks like synthetic ID fraud"],
789
- ],
790
- inputs=question_input
791
- )
792
-
793
- submit_btn.click(fn=process_question, inputs=question_input, outputs=output)
794
- question_input.submit(fn=process_question, inputs=question_input, outputs=output)
795
-
796
- gr.Markdown("""
797
- ---
798
- *Powered by Amazon Strands Agents SDK | Demo with synthetic data*
799
 
800
- **Note:** This demo uses mock data. In production, tools would connect to:
801
- - Feature Store / Data Warehouse
802
- - Model Serving Infrastructure
803
- - SHAP/Model Interpretation Services
804
- - Compliance Documentation Systems
805
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806
 
807
 
808
  # =============================================================================
809
- # MAIN
810
  # =============================================================================
811
 
812
  if __name__ == "__main__":
813
- import sys
814
-
815
- if "--demo" in sys.argv:
816
- print("\n" + "="*70)
817
- print("FRAUD MODEL EXPLAINABILITY ASSISTANT - DEMO")
818
- print("="*70 + "\n")
819
-
820
- test_questions = [
821
- "Why was application APP-78432 flagged as high risk?",
822
- "What's the model performance for Retail Card?",
823
- ]
824
-
825
- for q in test_questions:
826
- print(f"Question: {q}")
827
- print("-" * 50)
828
- print(query(q))
829
- print("\n")
830
- else:
831
- iface.launch(ssr_mode=False)
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
  """
3
  Fraud Model Explainability Assistant - Strands Agents
 
4
  An AI-powered assistant that helps fraud analysts and executives understand
5
  why specific applications were flagged as fraudulent, translating complex
6
  model outputs into actionable insights.
7
 
8
+ Author: Fraud Model Data Science Team
9
+
10
  Use Cases:
11
  - Executive briefings on fraud decisions
12
  - Fair lending compliance documentation
13
  - Analyst investigation support
14
  - Model decision audit trails
15
 
16
+ Production-Ready Confluence Integration (FastAPI Version)
17
+
18
+ Features:
19
+ - Comprehensive logging and monitoring
20
+ - Error handling and recovery
21
+ - Scheduled re-ingestion for keeping data fresh
22
+ - Performance metrics tracking
23
+ - FastAPI + uvicorn for Docker deployment
24
+
25
+ Prerequisites:
26
+ - Configure .env with Confluence credentials
27
  """
28
 
29
  import os
30
+ import sys
31
+ import json
32
  import warnings
33
+ import logging
34
+ import time
35
+ from functools import lru_cache
36
  from typing import Optional
37
+ from datetime import datetime
38
 
39
+ # Suppress ResourceWarning for cleaner output
 
40
  warnings.filterwarnings("ignore", category=ResourceWarning)
41
  os.environ["PYTHONWARNINGS"] = "ignore::ResourceWarning"
42
 
43
+ # Load environment variables from .env file
44
+ try:
45
+ from dotenv import load_dotenv
46
+ load_dotenv()
47
+ except ImportError:
48
+ print("⚠ Warning: python-dotenv not installed. Install with: pip install python-dotenv")
49
+ print(" Environment variables must be set manually.")
50
+
51
+ from fastapi import FastAPI, HTTPException
52
+ from fastapi.responses import HTMLResponse
53
+ from fastapi.middleware.cors import CORSMiddleware
54
+ from pydantic import BaseModel
55
+
56
+ from strands import Agent
57
  from strands.models.openai import OpenAIModel
58
 
59
+ # Import confluence-ingestor
60
+ from confluence_ingestor import ConfluenceRAG
61
+ from confluence_ingestor.adapters.strands import (
62
+ create_confluence_search_tool,
63
+ create_confluence_loader_tool,
64
+ )
65
+
66
+ # Import your existing fraud tools
67
+ from utils import (
68
+ get_application_summary,
69
+ explain_fraud_score,
70
+ compare_to_population,
71
+ check_fair_lending_flags,
72
+ get_identity_network,
73
+ get_model_performance,
74
+ SYSTEM_PROMPT as ORIGINAL_PROMPT,
75
+ )
76
+
77
 
78
  # =============================================================================
79
+ # LOGGING CONFIGURATION
80
  # =============================================================================
 
 
81
 
82
+ logging.basicConfig(
83
+ level=logging.INFO,
84
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
85
+ handlers=[
86
+ logging.FileHandler('fraud_assistant_confluence.log'),
87
+ logging.StreamHandler()
88
+ ]
89
+ )
90
+
91
+ logger = logging.getLogger(__name__)
92
+
93
+
94
+ # =============================================================================
95
+ # METRICS TRACKING
96
+ # =============================================================================
97
+
98
+ class ConfluenceMetrics:
99
+ """Track Confluence integration performance metrics."""
100
+
101
+ def __init__(self):
102
+ self.search_count = 0
103
+ self.cache_hits = 0
104
+ self.cache_misses = 0
105
+ self.errors = 0
106
+ self.last_ingestion = None
107
+ self.query_times = []
108
+
109
+ def record_search(self, cached: bool = False, duration: float = 0.0):
110
+ """Record a search query."""
111
+ self.search_count += 1
112
+ if cached:
113
+ self.cache_hits += 1
114
+ else:
115
+ self.cache_misses += 1
116
+ self.query_times.append(duration)
117
+
118
+ def record_error(self):
119
+ """Record an error."""
120
+ self.errors += 1
121
+
122
+ def record_ingestion(self):
123
+ """Record a data ingestion."""
124
+ self.last_ingestion = datetime.now()
125
+
126
+ def get_stats(self) -> dict:
127
+ """Get current metrics."""
128
+ return {
129
+ "total_searches": self.search_count,
130
+ "cache_hit_rate": (
131
+ self.cache_hits / self.search_count
132
+ if self.search_count > 0
133
+ else 0.0
134
+ ),
135
+ "avg_query_time": (
136
+ sum(self.query_times) / len(self.query_times)
137
+ if self.query_times
138
+ else 0.0
139
+ ),
140
+ "errors": self.errors,
141
+ "last_ingestion": str(self.last_ingestion) if self.last_ingestion else None,
142
  }
143
+
144
+
145
+ # Global metrics instance
146
+ _metrics = ConfluenceMetrics()
147
 
148
 
149
  # =============================================================================
150
+ # CONFLUENCE INITIALIZATION
151
  # =============================================================================
152
 
153
+ _confluence_rag: Optional[ConfluenceRAG] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
 
156
+ def init_confluence():
157
+ """Initialize Confluence RAG with logging and metrics."""
158
+ global _confluence_rag
159
+
160
+ if _confluence_rag is None:
161
+ logger.info("Initializing Confluence integration...")
162
+
163
+ required_vars = ["CONFLUENCE_URL", "CONFLUENCE_EMAIL", "CONFLUENCE_API_TOKEN"]
164
+ missing_vars = [var for var in required_vars if not os.getenv(var)]
165
+
166
+ if missing_vars:
167
+ error_msg = f"Missing required environment variables: {', '.join(missing_vars)}"
168
+ logger.error(error_msg)
169
+ print(f"\n❌ ERROR: {error_msg}")
170
+ print("\nπŸ“ Setup Instructions:")
171
+ print(" 1. Copy .env.example to .env:")
172
+ print(" cp .env.example .env")
173
+ print(" 2. Edit .env with your Confluence credentials")
174
+ print(" 3. See CONFLUENCE_SETUP_GUIDE.md for detailed setup instructions")
175
+ print("\n⚠ App will run WITHOUT Confluence integration.\n")
176
+ raise ValueError(f"Missing Confluence credentials: {', '.join(missing_vars)}")
177
+
178
+ try:
179
+ _confluence_rag = ConfluenceRAG.from_env(
180
+ embedding_provider="huggingface",
181
+ vector_store_type="chroma",
182
+ )
183
+
184
+ spaces = {
185
+ "Acquisitio": 10,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
+ for space_key, max_pages in spaces.items():
189
+ try:
190
+ logger.info(f"Ingesting Confluence space: {space_key}")
191
+ stats = _confluence_rag.ingest_space(
192
+ space_key, max_pages=max_pages, force=False
193
+ )
194
+
195
+ if stats.get("skipped"):
196
+ logger.info(f"{space_key}: {stats['reason']}")
197
+ print(f" βœ“ {space_key}: {stats['reason']}")
198
+ else:
199
+ logger.info(f"{space_key}: Ingested {stats['pages']} pages")
200
+ print(f" βœ“ {space_key}: {stats['pages']} pages indexed")
201
+
202
+ try:
203
+ from confluence_ingestor import ConfluenceClient
204
+ client = ConfluenceClient.from_env()
205
+ pages = client.load_space(space_key, max_pages=max_pages)
206
+
207
+ if pages:
208
+ logger.info(f"Pages in {space_key} ({len(pages)} total):")
209
+ print(f" πŸ“„ Pages in {space_key}:")
210
+ for i, page in enumerate(pages, 1):
211
+ logger.info(f" {i}. {page.title} (ID: {page.page_id})")
212
+ print(f" {i}. {page.title}")
213
+ else:
214
+ logger.warning(f"No pages found in space: {space_key}")
215
+ except Exception as page_list_error:
216
+ logger.warning(f"Could not retrieve page titles for {space_key}: {page_list_error}")
217
+
218
+ except Exception as e:
219
+ logger.error(f"Failed to ingest {space_key}: {e}")
220
+ print(f" ⚠ {space_key}: Failed - {e}")
221
+ _metrics.record_error()
222
+
223
+ _metrics.record_ingestion()
224
+ logger.info("Confluence integration ready!")
225
+ print("βœ… Confluence integration ready!")
226
+
227
+ except Exception as e:
228
+ logger.error(f"Confluence initialization failed: {e}")
229
+ _metrics.record_error()
230
+ raise
231
+
232
+ return _confluence_rag
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
+ # =============================================================================
236
+ # SCHEDULED RE-INGESTION
237
+ # =============================================================================
238
 
239
+ def setup_scheduled_ingestion():
240
+ """Set up scheduled Confluence re-ingestion for keeping data fresh."""
241
+ try:
242
+ from apscheduler.schedulers.background import BackgroundScheduler
243
+ except ImportError:
244
+ logger.warning("apscheduler not installed. Scheduled ingestion disabled.")
245
+ logger.warning("Install with: pip install apscheduler")
246
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
+ def refresh_confluence():
249
+ """Re-ingest Confluence spaces to pick up new content."""
250
+ logger.info("Starting scheduled Confluence re-ingestion...")
251
 
252
+ try:
253
+ rag = init_confluence()
254
+ spaces = ["Acquisitio"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
+ for space in spaces:
257
+ logger.info(f"Re-ingesting {space}...")
258
+ stats = rag.ingest_space(space, max_pages=100, force=True)
259
+ logger.info(f"{space}: Updated {stats['pages']} pages")
260
 
261
+ _metrics.record_ingestion()
262
+ logger.info("Scheduled re-ingestion completed successfully")
263
+
264
+ except Exception as e:
265
+ logger.error(f"Scheduled re-ingestion failed: {e}")
266
+ _metrics.record_error()
267
+
268
+ scheduler = BackgroundScheduler()
269
+ scheduler.add_job(refresh_confluence, 'cron', hour=2)
270
+ scheduler.start()
271
+
272
+ logger.info("Scheduled re-ingestion enabled (runs daily at 2 AM)")
273
+ return scheduler
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
 
276
  # =============================================================================
277
+ # ENHANCED SYSTEM PROMPT
278
  # =============================================================================
279
 
280
+ ENHANCED_PROMPT = """
281
  You are a Fraud Model Explainability Assistant for a major financial services company.
282
+ Your role is to help fraud analysts, data scientists, and executives understand
283
  fraud model decisions and their implications.
284
 
285
  You have access to tools that can:
 
289
  4. Check for fair lending compliance concerns
290
  5. Analyze identity networks and linkages
291
  6. Show model performance metrics
292
+ 7. **Search company Confluence documentation** for policies, procedures, and guidelines
293
+ 8. **Load full Confluence pages** to extract specific information
294
 
295
  When answering questions:
296
  - Be precise and data-driven
 
299
  - Always mention fair lending implications when relevant
300
  - Provide actionable insights, not just data
301
 
302
+ **CRITICAL: How to Handle Confluence Information Requests**
303
+ When users ask you to "report", "list", "find", "show", or "provide" specific information from documents:
304
+ 1. **FIRST**: Use the confluence_search tool to find the relevant document and identify its title
305
+ 2. **SECOND**: Use the confluence_loader tool with BOTH space_key AND page_title parameters to load that specific page
306
+ 3. **THIRD**: Extract and present the requested information directly in your response from the loaded content
307
+ 4. **FOURTH**: Provide the Confluence page citation/link as a reference for verification
308
+
309
  For flagged applications, structure your response as:
310
  1. Quick summary (score, decision, risk level)
311
  2. Top contributing factors
312
  3. How unusual this is compared to the population
313
+ 4. Any compliance considerations (extract relevant policies from Confluence, then cite sources)
314
+ 5. Recommended next steps (reference procedures from playbooks if available)
315
 
316
  Remember: Your explanations may be used in regulatory examinations and audits,
317
  so be accurate and thorough.
 
319
 
320
 
321
  # =============================================================================
322
+ # AGENT CREATION
323
  # =============================================================================
324
 
325
+ _cached_agent = None
326
+
327
+
328
+ def create_enhanced_agent():
329
+ """Create fraud agent with Confluence integration."""
330
+ global _cached_agent
331
 
332
+ if _cached_agent is not None:
333
+ return _cached_agent
 
 
 
 
 
 
334
 
335
+ openai_api_key = os.environ.get("OPENAI_API_KEY")
336
+
337
+ try:
338
+ rag = init_confluence()
339
+
340
+ search_confluence = create_confluence_search_tool(rag=rag, k=5)
341
+ load_confluence_page = create_confluence_loader_tool(max_pages=10)
342
+
343
+ tools = [
344
+ get_application_summary,
345
+ explain_fraud_score,
346
+ compare_to_population,
347
+ check_fair_lending_flags,
348
+ get_identity_network,
349
+ get_model_performance,
350
+ search_confluence,
351
+ load_confluence_page,
352
+ ]
353
+
354
+ system_prompt = ENHANCED_PROMPT
355
+
356
+ except Exception as e:
357
+ logger.error(f"Confluence initialization failed: {e}")
358
+ print(f"⚠ Confluence disabled: {e}")
359
+ _metrics.record_error()
360
+
361
+ tools = [
362
+ get_application_summary,
363
+ explain_fraud_score,
364
+ compare_to_population,
365
+ check_fair_lending_flags,
366
+ get_identity_network,
367
+ get_model_performance,
368
+ ]
369
+
370
+ system_prompt = ORIGINAL_PROMPT
371
+
372
  if openai_api_key:
373
  model = OpenAIModel(
374
  client_args={"api_key": openai_api_key},
375
  model_id="gpt-4o",
376
+ params={"temperature": 0.1, "max_tokens": 2048},
377
  )
378
+ _cached_agent = Agent(model=model, system_prompt=system_prompt, tools=tools)
379
  else:
380
+ _cached_agent = Agent(system_prompt=system_prompt, tools=tools)
381
+
382
+ return _cached_agent
383
 
384
 
385
+ def query_agent(question: str) -> str:
386
+ """Process question with the enhanced agent."""
387
  try:
388
+ logger.info(f"Processing query: {question}")
389
+ agent = create_enhanced_agent()
390
  result = agent(question)
391
+ logger.info("Query completed successfully")
392
  return str(result)
393
  except Exception as e:
394
+ logger.error(f"Query failed: {e}")
395
+ _metrics.record_error()
396
+ return f"Error: {str(e)}"
397
 
398
 
399
  # =============================================================================
400
+ # FASTAPI APPLICATION
401
  # =============================================================================
402
 
403
+ app = FastAPI(title="Fraud Model Explainability Assistant")
 
 
404
 
405
+ app.add_middleware(
406
+ CORSMiddleware,
407
+ allow_origins=["*"],
408
+ allow_methods=["*"],
409
+ allow_headers=["*"],
410
+ allow_credentials=True,
411
+ )
412
 
413
+
414
+ class QuestionRequest(BaseModel):
415
+ question: str
416
+
417
+
418
+ class AnswerResponse(BaseModel):
419
+ answer: str
420
+ metrics: dict
421
+
422
+
423
+ @app.get("/")
424
+ async def index():
425
+ """Serve the main UI."""
426
+ return HTMLResponse(content=get_ui_html())
427
+
428
+
429
+ @app.post("/api/ask", response_model=AnswerResponse)
430
+ async def ask_question(request: QuestionRequest):
431
+ """Process a question and return the answer."""
432
+ try:
433
+ answer = query_agent(request.question)
434
+ return AnswerResponse(
435
+ answer=answer,
436
+ metrics=_metrics.get_stats()
 
 
 
 
 
437
  )
438
+ except Exception as e:
439
+ logger.error(f"API error: {e}")
440
+ raise HTTPException(status_code=500, detail=str(e))
441
+
442
+
443
+ @app.get("/api/metrics")
444
+ async def get_metrics():
445
+ """Get current performance metrics."""
446
+ return _metrics.get_stats()
447
+
448
+
449
+ @app.get("/api/health")
450
+ async def health_check():
451
+ """Health check endpoint."""
452
+ return {
453
+ "status": "healthy",
454
+ "confluence_initialized": _confluence_rag is not None,
455
+ "metrics": _metrics.get_stats()
456
+ }
457
+
458
+
459
+ # =============================================================================
460
+ # HTML UI
461
+ # =============================================================================
462
+
463
+ def get_ui_html() -> str:
464
+ """Generate the chat UI HTML."""
465
+
466
+ example_questions = [
467
+ "Why was application APP-78432 flagged as high risk?",
468
+ "Explain the fraud score for APP-12345 and compare it to approved applications",
469
+ "Check fair lending compliance for APP-55555 and cite relevant policies",
470
+ "Show me the identity network analysis for APP-78432",
471
+ "What's the current model performance for the Retail Card portfolio?",
472
+ "What does our fair lending policy say about synthetic ID detection?",
473
+ "Find the model validation report for XGBoost v3.2",
474
+ "What are the procedures for escalating high-risk applications?",
475
+ ]
476
 
477
+ examples_json = json.dumps(example_questions)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
479
+ return f"""
480
+ <!DOCTYPE html>
481
+ <html lang="en">
482
+ <head>
483
+ <meta charset="UTF-8">
484
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
485
+ <title>Fraud Model Explainability Assistant</title>
486
+ <style>
487
+ * {{
488
+ box-sizing: border-box;
489
+ margin: 0;
490
+ padding: 0;
491
+ }}
492
+
493
+ body {{
494
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
495
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
496
+ min-height: 100vh;
497
+ color: #e0e0e0;
498
+ }}
499
+
500
+ .container {{
501
+ max-width: 1200px;
502
+ margin: 0 auto;
503
+ padding: 20px;
504
+ }}
505
+
506
+ header {{
507
+ text-align: center;
508
+ padding: 30px 0;
509
+ border-bottom: 1px solid rgba(255,255,255,0.1);
510
+ margin-bottom: 30px;
511
+ }}
512
+
513
+ h1 {{
514
+ font-size: 2.5rem;
515
+ background: linear-gradient(90deg, #00d4ff, #7b2cbf);
516
+ -webkit-background-clip: text;
517
+ -webkit-text-fill-color: transparent;
518
+ background-clip: text;
519
+ margin-bottom: 10px;
520
+ }}
521
+
522
+ .subtitle {{
523
+ color: #888;
524
+ font-size: 1.1rem;
525
+ }}
526
+
527
+ .main-content {{
528
+ display: grid;
529
+ grid-template-columns: 1fr 300px;
530
+ gap: 30px;
531
+ }}
532
+
533
+ @media (max-width: 900px) {{
534
+ .main-content {{
535
+ grid-template-columns: 1fr;
536
+ }}
537
+ }}
538
+
539
+ .chat-section {{
540
+ background: rgba(255,255,255,0.05);
541
+ border-radius: 16px;
542
+ padding: 20px;
543
+ backdrop-filter: blur(10px);
544
+ border: 1px solid rgba(255,255,255,0.1);
545
+ }}
546
+
547
+ .input-area {{
548
+ display: flex;
549
+ gap: 10px;
550
+ margin-bottom: 20px;
551
+ }}
552
+
553
+ textarea {{
554
+ flex: 1;
555
+ padding: 15px;
556
+ border: 2px solid rgba(255,255,255,0.1);
557
+ border-radius: 12px;
558
+ background: rgba(0,0,0,0.3);
559
+ color: #fff;
560
+ font-size: 1rem;
561
+ resize: vertical;
562
+ min-height: 80px;
563
+ transition: border-color 0.3s;
564
+ }}
565
+
566
+ textarea:focus {{
567
+ outline: none;
568
+ border-color: #00d4ff;
569
+ }}
570
+
571
+ button {{
572
+ padding: 15px 30px;
573
+ background: linear-gradient(135deg, #00d4ff, #7b2cbf);
574
+ color: white;
575
+ border: none;
576
+ border-radius: 12px;
577
+ font-size: 1rem;
578
+ font-weight: 600;
579
+ cursor: pointer;
580
+ transition: transform 0.2s, box-shadow 0.2s;
581
+ }}
582
+
583
+ button:hover:not(:disabled) {{
584
+ transform: translateY(-2px);
585
+ box-shadow: 0 10px 30px rgba(0,212,255,0.3);
586
+ }}
587
+
588
+ button:disabled {{
589
+ opacity: 0.5;
590
+ cursor: not-allowed;
591
+ }}
592
+
593
+ .response-area {{
594
+ background: rgba(0,0,0,0.3);
595
+ border-radius: 12px;
596
+ padding: 20px;
597
+ min-height: 400px;
598
+ max-height: 600px;
599
+ overflow-y: auto;
600
+ }}
601
+
602
+ .response-area pre {{
603
+ white-space: pre-wrap;
604
+ word-wrap: break-word;
605
+ font-family: 'Fira Code', 'Consolas', monospace;
606
+ font-size: 0.9rem;
607
+ line-height: 1.6;
608
+ }}
609
+
610
+ .loading {{
611
+ display: flex;
612
+ align-items: center;
613
+ justify-content: center;
614
+ gap: 10px;
615
+ padding: 40px;
616
+ color: #00d4ff;
617
+ }}
618
+
619
+ .spinner {{
620
+ width: 24px;
621
+ height: 24px;
622
+ border: 3px solid rgba(0,212,255,0.2);
623
+ border-top-color: #00d4ff;
624
+ border-radius: 50%;
625
+ animation: spin 1s linear infinite;
626
+ }}
627
+
628
+ @keyframes spin {{
629
+ to {{ transform: rotate(360deg); }}
630
+ }}
631
+
632
+ .sidebar {{
633
+ display: flex;
634
+ flex-direction: column;
635
+ gap: 20px;
636
+ }}
637
+
638
+ .card {{
639
+ background: rgba(255,255,255,0.05);
640
+ border-radius: 12px;
641
+ padding: 20px;
642
+ border: 1px solid rgba(255,255,255,0.1);
643
+ }}
644
+
645
+ .card h3 {{
646
+ color: #00d4ff;
647
+ margin-bottom: 15px;
648
+ font-size: 1rem;
649
+ }}
650
+
651
+ .example-btn {{
652
+ display: block;
653
+ width: 100%;
654
+ padding: 10px;
655
+ margin-bottom: 8px;
656
+ background: rgba(0,0,0,0.3);
657
+ color: #ccc;
658
+ border: 1px solid rgba(255,255,255,0.1);
659
+ border-radius: 8px;
660
+ font-size: 0.85rem;
661
+ text-align: left;
662
+ cursor: pointer;
663
+ transition: all 0.2s;
664
+ }}
665
+
666
+ .example-btn:hover {{
667
+ background: rgba(0,212,255,0.1);
668
+ border-color: #00d4ff;
669
+ color: #fff;
670
+ }}
671
+
672
+ .metrics {{
673
+ display: grid;
674
+ grid-template-columns: 1fr 1fr;
675
+ gap: 10px;
676
+ }}
677
+
678
+ .metric {{
679
+ background: rgba(0,0,0,0.3);
680
+ padding: 12px;
681
+ border-radius: 8px;
682
+ text-align: center;
683
+ }}
684
+
685
+ .metric-value {{
686
+ font-size: 1.5rem;
687
+ font-weight: bold;
688
+ color: #00d4ff;
689
+ }}
690
+
691
+ .metric-label {{
692
+ font-size: 0.75rem;
693
+ color: #888;
694
+ margin-top: 4px;
695
+ }}
696
+
697
+ .features {{
698
+ list-style: none;
699
+ }}
700
+
701
+ .features li {{
702
+ padding: 8px 0;
703
+ border-bottom: 1px solid rgba(255,255,255,0.05);
704
+ font-size: 0.9rem;
705
+ }}
706
+
707
+ .features li:last-child {{
708
+ border-bottom: none;
709
+ }}
710
+
711
+ .features li::before {{
712
+ content: "βœ“ ";
713
+ color: #00d4ff;
714
+ }}
715
+
716
+ footer {{
717
+ text-align: center;
718
+ padding: 30px;
719
+ color: #666;
720
+ font-size: 0.9rem;
721
+ margin-top: 40px;
722
+ }}
723
+ </style>
724
+ </head>
725
+ <body>
726
+ <div class="container">
727
+ <header>
728
+ <h1>πŸ” Fraud Model Explainability Assistant</h1>
729
+ <p class="subtitle">Production-Ready with Confluence Integration</p>
730
+ </header>
731
+
732
+ <div class="main-content">
733
+ <div class="chat-section">
734
+ <div class="input-area">
735
+ <textarea
736
+ id="questionInput"
737
+ placeholder="Ask a question about fraud models, applications, or policies..."
738
+ onkeydown="if(event.key === 'Enter' && !event.shiftKey) {{ event.preventDefault(); askQuestion(); }}"
739
+ ></textarea>
740
+ <button id="askBtn" onclick="askQuestion()">πŸ” Analyze</button>
741
+ </div>
742
+
743
+ <div class="response-area" id="responseArea">
744
+ <pre id="responseText">Welcome! Ask me about:
745
+
746
+ β€’ Application fraud scores and explanations
747
+ β€’ Fair lending compliance checks
748
+ β€’ Identity network analysis
749
+ β€’ Model performance metrics
750
+ β€’ Confluence documentation and policies
751
+
752
+ Enter your question above and click "Analyze" to get started.</pre>
753
+ </div>
754
+ </div>
755
+
756
+ <div class="sidebar">
757
+ <div class="card">
758
+ <h3>πŸ“Š Performance Metrics</h3>
759
+ <div class="metrics">
760
+ <div class="metric">
761
+ <div class="metric-value" id="totalSearches">0</div>
762
+ <div class="metric-label">Searches</div>
763
+ </div>
764
+ <div class="metric">
765
+ <div class="metric-value" id="cacheRate">0%</div>
766
+ <div class="metric-label">Cache Rate</div>
767
+ </div>
768
+ <div class="metric">
769
+ <div class="metric-value" id="avgTime">0s</div>
770
+ <div class="metric-label">Avg Time</div>
771
+ </div>
772
+ <div class="metric">
773
+ <div class="metric-value" id="errors">0</div>
774
+ <div class="metric-label">Errors</div>
775
+ </div>
776
+ </div>
777
+ </div>
778
+
779
+ <div class="card">
780
+ <h3>πŸ’‘ Example Questions</h3>
781
+ <div id="examplesContainer"></div>
782
+ </div>
783
+
784
+ <div class="card">
785
+ <h3>✨ Production Features</h3>
786
+ <ul class="features">
787
+ <li>Structured logging</li>
788
+ <li>Performance tracking</li>
789
+ <li>Error monitoring</li>
790
+ <li>Daily auto-refresh (2 AM)</li>
791
+ <li>Confluence integration</li>
792
+ </ul>
793
+ </div>
794
+ </div>
795
+ </div>
796
+
797
+ <footer>
798
+ Powered by Strands Agents + Confluence Integration β€’ FastAPI Backend
799
+ </footer>
800
+ </div>
801
+
802
+ <script>
803
+ const examples = {examples_json};
804
+
805
+ // Populate examples
806
+ const examplesContainer = document.getElementById('examplesContainer');
807
+ examples.slice(0, 5).forEach(q => {{
808
+ const btn = document.createElement('button');
809
+ btn.className = 'example-btn';
810
+ btn.textContent = q.length > 50 ? q.substring(0, 50) + '...' : q;
811
+ btn.title = q;
812
+ btn.onclick = () => {{
813
+ document.getElementById('questionInput').value = q;
814
+ askQuestion();
815
+ }};
816
+ examplesContainer.appendChild(btn);
817
+ }});
818
+
819
+ async function askQuestion() {{
820
+ const input = document.getElementById('questionInput');
821
+ const btn = document.getElementById('askBtn');
822
+ const responseArea = document.getElementById('responseArea');
823
+ const responseText = document.getElementById('responseText');
824
+
825
+ const question = input.value.trim();
826
+ if (!question) return;
827
+
828
+ btn.disabled = true;
829
+ btn.textContent = '⏳ Processing...';
830
+ responseArea.innerHTML = '<div class="loading"><div class="spinner"></div>Analyzing your question...</div>';
831
+
832
+ try {{
833
+ const response = await fetch('/api/ask', {{
834
+ method: 'POST',
835
+ headers: {{
836
+ 'Content-Type': 'application/json',
837
+ }},
838
+ body: JSON.stringify({{ question }}),
839
+ }});
840
+
841
+ if (!response.ok) {{
842
+ throw new Error(`HTTP error! status: ${{response.status}}`);
843
+ }}
844
+
845
+ const data = await response.json();
846
+
847
+ responseArea.innerHTML = '<pre id="responseText"></pre>';
848
+ document.getElementById('responseText').textContent = data.answer;
849
+
850
+ // Update metrics
851
+ if (data.metrics) {{
852
+ document.getElementById('totalSearches').textContent = data.metrics.total_searches || 0;
853
+ document.getElementById('cacheRate').textContent =
854
+ ((data.metrics.cache_hit_rate || 0) * 100).toFixed(0) + '%';
855
+ document.getElementById('avgTime').textContent =
856
+ (data.metrics.avg_query_time || 0).toFixed(2) + 's';
857
+ document.getElementById('errors').textContent = data.metrics.errors || 0;
858
+ }}
859
+
860
+ }} catch (error) {{
861
+ responseArea.innerHTML = '<pre id="responseText" style="color: #ff6b6b;"></pre>';
862
+ document.getElementById('responseText').textContent = 'Error: ' + error.message;
863
+ }} finally {{
864
+ btn.disabled = false;
865
+ btn.textContent = 'πŸ” Analyze';
866
+ }}
867
+ }}
868
+
869
+ // Fetch initial metrics
870
+ async function fetchMetrics() {{
871
+ try {{
872
+ const response = await fetch('/api/metrics');
873
+ const metrics = await response.json();
874
+ document.getElementById('totalSearches').textContent = metrics.total_searches || 0;
875
+ document.getElementById('cacheRate').textContent =
876
+ ((metrics.cache_hit_rate || 0) * 100).toFixed(0) + '%';
877
+ document.getElementById('avgTime').textContent =
878
+ (metrics.avg_query_time || 0).toFixed(2) + 's';
879
+ document.getElementById('errors').textContent = metrics.errors || 0;
880
+ }} catch (e) {{
881
+ console.log('Could not fetch metrics:', e);
882
+ }}
883
+ }}
884
+
885
+ fetchMetrics();
886
+ setInterval(fetchMetrics, 30000);
887
+ </script>
888
+ </body>
889
+ </html>
890
+ """
891
 
892
 
893
  # =============================================================================
894
+ # MAIN ENTRYPOINT
895
  # =============================================================================
896
 
897
  if __name__ == "__main__":
898
+ import uvicorn
899
+
900
+ # Pre-initialize Confluence
901
+ try:
902
+ init_confluence()
903
+ except Exception as e:
904
+ logger.error(f"Confluence initialization failed: {e}")
905
+ print(f"Warning: Confluence initialization failed: {e}")
906
+ print("App will run without Confluence integration.")
907
+
908
+ # Set up scheduled re-ingestion
909
+ scheduler = setup_scheduled_ingestion()
910
+
911
+ # Launch FastAPI with uvicorn
912
+ logger.info("Launching FastAPI server...")
913
+ print("\n" + "=" * 60)
914
+ print("Fraud Model Explainability Assistant")
915
+ print(" - FastAPI backend on port 7860")
916
+ print(" - Confluence integration enabled")
917
+ print(" - Scheduled refresh at 2 AM daily")
918
+ print("=" * 60 + "\n")
919
+
920
+ uvicorn.run(
921
+ app,
922
+ host="0.0.0.0",
923
+ port=7860,
924
+ reload=False
925
+ )
app_with_confluence.py DELETED
@@ -1,925 +0,0 @@
1
- #!/usr/bin/env python
2
- """
3
- Fraud Model Explainability Assistant - Strands Agents
4
- An AI-powered assistant that helps fraud analysts and executives understand
5
- why specific applications were flagged as fraudulent, translating complex
6
- model outputs into actionable insights.
7
-
8
- Author: Fraud Model Data Science Team
9
-
10
- Use Cases:
11
- - Executive briefings on fraud decisions
12
- - Fair lending compliance documentation
13
- - Analyst investigation support
14
- - Model decision audit trails
15
-
16
- Production-Ready Confluence Integration (FastAPI Version)
17
-
18
- Features:
19
- - Comprehensive logging and monitoring
20
- - Error handling and recovery
21
- - Scheduled re-ingestion for keeping data fresh
22
- - Performance metrics tracking
23
- - FastAPI + uvicorn for Docker deployment
24
-
25
- Prerequisites:
26
- - Configure .env with Confluence credentials
27
- """
28
-
29
- import os
30
- import sys
31
- import json
32
- import warnings
33
- import logging
34
- import time
35
- from functools import lru_cache
36
- from typing import Optional
37
- from datetime import datetime
38
-
39
- # Suppress ResourceWarning for cleaner output
40
- warnings.filterwarnings("ignore", category=ResourceWarning)
41
- os.environ["PYTHONWARNINGS"] = "ignore::ResourceWarning"
42
-
43
- # Load environment variables from .env file
44
- try:
45
- from dotenv import load_dotenv
46
- load_dotenv()
47
- except ImportError:
48
- print("⚠ Warning: python-dotenv not installed. Install with: pip install python-dotenv")
49
- print(" Environment variables must be set manually.")
50
-
51
- from fastapi import FastAPI, HTTPException
52
- from fastapi.responses import HTMLResponse
53
- from fastapi.middleware.cors import CORSMiddleware
54
- from pydantic import BaseModel
55
-
56
- from strands import Agent
57
- from strands.models.openai import OpenAIModel
58
-
59
- # Import confluence-ingestor
60
- from confluence_ingestor import ConfluenceRAG
61
- from confluence_ingestor.adapters.strands import (
62
- create_confluence_search_tool,
63
- create_confluence_loader_tool,
64
- )
65
-
66
- # Import your existing fraud tools
67
- from app import (
68
- get_application_summary,
69
- explain_fraud_score,
70
- compare_to_population,
71
- check_fair_lending_flags,
72
- get_identity_network,
73
- get_model_performance,
74
- SYSTEM_PROMPT as ORIGINAL_PROMPT,
75
- )
76
-
77
-
78
- # =============================================================================
79
- # LOGGING CONFIGURATION
80
- # =============================================================================
81
-
82
- logging.basicConfig(
83
- level=logging.INFO,
84
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
85
- handlers=[
86
- logging.FileHandler('fraud_assistant_confluence.log'),
87
- logging.StreamHandler()
88
- ]
89
- )
90
-
91
- logger = logging.getLogger(__name__)
92
-
93
-
94
- # =============================================================================
95
- # METRICS TRACKING
96
- # =============================================================================
97
-
98
- class ConfluenceMetrics:
99
- """Track Confluence integration performance metrics."""
100
-
101
- def __init__(self):
102
- self.search_count = 0
103
- self.cache_hits = 0
104
- self.cache_misses = 0
105
- self.errors = 0
106
- self.last_ingestion = None
107
- self.query_times = []
108
-
109
- def record_search(self, cached: bool = False, duration: float = 0.0):
110
- """Record a search query."""
111
- self.search_count += 1
112
- if cached:
113
- self.cache_hits += 1
114
- else:
115
- self.cache_misses += 1
116
- self.query_times.append(duration)
117
-
118
- def record_error(self):
119
- """Record an error."""
120
- self.errors += 1
121
-
122
- def record_ingestion(self):
123
- """Record a data ingestion."""
124
- self.last_ingestion = datetime.now()
125
-
126
- def get_stats(self) -> dict:
127
- """Get current metrics."""
128
- return {
129
- "total_searches": self.search_count,
130
- "cache_hit_rate": (
131
- self.cache_hits / self.search_count
132
- if self.search_count > 0
133
- else 0.0
134
- ),
135
- "avg_query_time": (
136
- sum(self.query_times) / len(self.query_times)
137
- if self.query_times
138
- else 0.0
139
- ),
140
- "errors": self.errors,
141
- "last_ingestion": str(self.last_ingestion) if self.last_ingestion else None,
142
- }
143
-
144
-
145
- # Global metrics instance
146
- _metrics = ConfluenceMetrics()
147
-
148
-
149
- # =============================================================================
150
- # CONFLUENCE INITIALIZATION
151
- # =============================================================================
152
-
153
- _confluence_rag: Optional[ConfluenceRAG] = None
154
-
155
-
156
- def init_confluence():
157
- """Initialize Confluence RAG with logging and metrics."""
158
- global _confluence_rag
159
-
160
- if _confluence_rag is None:
161
- logger.info("Initializing Confluence integration...")
162
-
163
- required_vars = ["CONFLUENCE_URL", "CONFLUENCE_EMAIL", "CONFLUENCE_API_TOKEN"]
164
- missing_vars = [var for var in required_vars if not os.getenv(var)]
165
-
166
- if missing_vars:
167
- error_msg = f"Missing required environment variables: {', '.join(missing_vars)}"
168
- logger.error(error_msg)
169
- print(f"\n❌ ERROR: {error_msg}")
170
- print("\nπŸ“ Setup Instructions:")
171
- print(" 1. Copy .env.example to .env:")
172
- print(" cp .env.example .env")
173
- print(" 2. Edit .env with your Confluence credentials")
174
- print(" 3. See CONFLUENCE_SETUP_GUIDE.md for detailed setup instructions")
175
- print("\n⚠ App will run WITHOUT Confluence integration.\n")
176
- raise ValueError(f"Missing Confluence credentials: {', '.join(missing_vars)}")
177
-
178
- try:
179
- _confluence_rag = ConfluenceRAG.from_env(
180
- embedding_provider="huggingface",
181
- vector_store_type="chroma",
182
- )
183
-
184
- spaces = {
185
- "Acquisitio": 10,
186
- }
187
-
188
- for space_key, max_pages in spaces.items():
189
- try:
190
- logger.info(f"Ingesting Confluence space: {space_key}")
191
- stats = _confluence_rag.ingest_space(
192
- space_key, max_pages=max_pages, force=False
193
- )
194
-
195
- if stats.get("skipped"):
196
- logger.info(f"{space_key}: {stats['reason']}")
197
- print(f" βœ“ {space_key}: {stats['reason']}")
198
- else:
199
- logger.info(f"{space_key}: Ingested {stats['pages']} pages")
200
- print(f" βœ“ {space_key}: {stats['pages']} pages indexed")
201
-
202
- try:
203
- from confluence_ingestor import ConfluenceClient
204
- client = ConfluenceClient.from_env()
205
- pages = client.load_space(space_key, max_pages=max_pages)
206
-
207
- if pages:
208
- logger.info(f"Pages in {space_key} ({len(pages)} total):")
209
- print(f" πŸ“„ Pages in {space_key}:")
210
- for i, page in enumerate(pages, 1):
211
- logger.info(f" {i}. {page.title} (ID: {page.page_id})")
212
- print(f" {i}. {page.title}")
213
- else:
214
- logger.warning(f"No pages found in space: {space_key}")
215
- except Exception as page_list_error:
216
- logger.warning(f"Could not retrieve page titles for {space_key}: {page_list_error}")
217
-
218
- except Exception as e:
219
- logger.error(f"Failed to ingest {space_key}: {e}")
220
- print(f" ⚠ {space_key}: Failed - {e}")
221
- _metrics.record_error()
222
-
223
- _metrics.record_ingestion()
224
- logger.info("Confluence integration ready!")
225
- print("βœ… Confluence integration ready!")
226
-
227
- except Exception as e:
228
- logger.error(f"Confluence initialization failed: {e}")
229
- _metrics.record_error()
230
- raise
231
-
232
- return _confluence_rag
233
-
234
-
235
- # =============================================================================
236
- # SCHEDULED RE-INGESTION
237
- # =============================================================================
238
-
239
- def setup_scheduled_ingestion():
240
- """Set up scheduled Confluence re-ingestion for keeping data fresh."""
241
- try:
242
- from apscheduler.schedulers.background import BackgroundScheduler
243
- except ImportError:
244
- logger.warning("apscheduler not installed. Scheduled ingestion disabled.")
245
- logger.warning("Install with: pip install apscheduler")
246
- return None
247
-
248
- def refresh_confluence():
249
- """Re-ingest Confluence spaces to pick up new content."""
250
- logger.info("Starting scheduled Confluence re-ingestion...")
251
-
252
- try:
253
- rag = init_confluence()
254
- spaces = ["Acquisitio"]
255
-
256
- for space in spaces:
257
- logger.info(f"Re-ingesting {space}...")
258
- stats = rag.ingest_space(space, max_pages=100, force=True)
259
- logger.info(f"{space}: Updated {stats['pages']} pages")
260
-
261
- _metrics.record_ingestion()
262
- logger.info("Scheduled re-ingestion completed successfully")
263
-
264
- except Exception as e:
265
- logger.error(f"Scheduled re-ingestion failed: {e}")
266
- _metrics.record_error()
267
-
268
- scheduler = BackgroundScheduler()
269
- scheduler.add_job(refresh_confluence, 'cron', hour=2)
270
- scheduler.start()
271
-
272
- logger.info("Scheduled re-ingestion enabled (runs daily at 2 AM)")
273
- return scheduler
274
-
275
-
276
- # =============================================================================
277
- # ENHANCED SYSTEM PROMPT
278
- # =============================================================================
279
-
280
- ENHANCED_PROMPT = """
281
- You are a Fraud Model Explainability Assistant for a major financial services company.
282
- Your role is to help fraud analysts, data scientists, and executives understand
283
- fraud model decisions and their implications.
284
-
285
- You have access to tools that can:
286
- 1. Retrieve application summaries and fraud scores
287
- 2. Explain why applications received specific fraud scores (SHAP-style explanations)
288
- 3. Compare applications to approved/denied populations statistically
289
- 4. Check for fair lending compliance concerns
290
- 5. Analyze identity networks and linkages
291
- 6. Show model performance metrics
292
- 7. **Search company Confluence documentation** for policies, procedures, and guidelines
293
- 8. **Load full Confluence pages** to extract specific information
294
-
295
- When answering questions:
296
- - Be precise and data-driven
297
- - Highlight the most important risk factors first
298
- - Explain technical concepts in business terms when speaking to executives
299
- - Always mention fair lending implications when relevant
300
- - Provide actionable insights, not just data
301
-
302
- **CRITICAL: How to Handle Confluence Information Requests**
303
- When users ask you to "report", "list", "find", "show", or "provide" specific information from documents:
304
- 1. **FIRST**: Use the confluence_search tool to find the relevant document and identify its title
305
- 2. **SECOND**: Use the confluence_loader tool with BOTH space_key AND page_title parameters to load that specific page
306
- 3. **THIRD**: Extract and present the requested information directly in your response from the loaded content
307
- 4. **FOURTH**: Provide the Confluence page citation/link as a reference for verification
308
-
309
- For flagged applications, structure your response as:
310
- 1. Quick summary (score, decision, risk level)
311
- 2. Top contributing factors
312
- 3. How unusual this is compared to the population
313
- 4. Any compliance considerations (extract relevant policies from Confluence, then cite sources)
314
- 5. Recommended next steps (reference procedures from playbooks if available)
315
-
316
- Remember: Your explanations may be used in regulatory examinations and audits,
317
- so be accurate and thorough.
318
- """.strip()
319
-
320
-
321
- # =============================================================================
322
- # AGENT CREATION
323
- # =============================================================================
324
-
325
- _cached_agent = None
326
-
327
-
328
- def create_enhanced_agent():
329
- """Create fraud agent with Confluence integration."""
330
- global _cached_agent
331
-
332
- if _cached_agent is not None:
333
- return _cached_agent
334
-
335
- openai_api_key = os.environ.get("OPENAI_API_KEY")
336
-
337
- try:
338
- rag = init_confluence()
339
-
340
- search_confluence = create_confluence_search_tool(rag=rag, k=5)
341
- load_confluence_page = create_confluence_loader_tool(max_pages=10)
342
-
343
- tools = [
344
- get_application_summary,
345
- explain_fraud_score,
346
- compare_to_population,
347
- check_fair_lending_flags,
348
- get_identity_network,
349
- get_model_performance,
350
- search_confluence,
351
- load_confluence_page,
352
- ]
353
-
354
- system_prompt = ENHANCED_PROMPT
355
-
356
- except Exception as e:
357
- logger.error(f"Confluence initialization failed: {e}")
358
- print(f"⚠ Confluence disabled: {e}")
359
- _metrics.record_error()
360
-
361
- tools = [
362
- get_application_summary,
363
- explain_fraud_score,
364
- compare_to_population,
365
- check_fair_lending_flags,
366
- get_identity_network,
367
- get_model_performance,
368
- ]
369
-
370
- system_prompt = ORIGINAL_PROMPT
371
-
372
- if openai_api_key:
373
- model = OpenAIModel(
374
- client_args={"api_key": openai_api_key},
375
- model_id="gpt-4o",
376
- params={"temperature": 0.1, "max_tokens": 2048},
377
- )
378
- _cached_agent = Agent(model=model, system_prompt=system_prompt, tools=tools)
379
- else:
380
- _cached_agent = Agent(system_prompt=system_prompt, tools=tools)
381
-
382
- return _cached_agent
383
-
384
-
385
- def query_agent(question: str) -> str:
386
- """Process question with the enhanced agent."""
387
- try:
388
- logger.info(f"Processing query: {question}")
389
- agent = create_enhanced_agent()
390
- result = agent(question)
391
- logger.info("Query completed successfully")
392
- return str(result)
393
- except Exception as e:
394
- logger.error(f"Query failed: {e}")
395
- _metrics.record_error()
396
- return f"Error: {str(e)}"
397
-
398
-
399
- # =============================================================================
400
- # FASTAPI APPLICATION
401
- # =============================================================================
402
-
403
- app = FastAPI(title="Fraud Model Explainability Assistant")
404
-
405
- app.add_middleware(
406
- CORSMiddleware,
407
- allow_origins=["*"],
408
- allow_methods=["*"],
409
- allow_headers=["*"],
410
- allow_credentials=True,
411
- )
412
-
413
-
414
- class QuestionRequest(BaseModel):
415
- question: str
416
-
417
-
418
- class AnswerResponse(BaseModel):
419
- answer: str
420
- metrics: dict
421
-
422
-
423
- @app.get("/")
424
- async def index():
425
- """Serve the main UI."""
426
- return HTMLResponse(content=get_ui_html())
427
-
428
-
429
- @app.post("/api/ask", response_model=AnswerResponse)
430
- async def ask_question(request: QuestionRequest):
431
- """Process a question and return the answer."""
432
- try:
433
- answer = query_agent(request.question)
434
- return AnswerResponse(
435
- answer=answer,
436
- metrics=_metrics.get_stats()
437
- )
438
- except Exception as e:
439
- logger.error(f"API error: {e}")
440
- raise HTTPException(status_code=500, detail=str(e))
441
-
442
-
443
- @app.get("/api/metrics")
444
- async def get_metrics():
445
- """Get current performance metrics."""
446
- return _metrics.get_stats()
447
-
448
-
449
- @app.get("/api/health")
450
- async def health_check():
451
- """Health check endpoint."""
452
- return {
453
- "status": "healthy",
454
- "confluence_initialized": _confluence_rag is not None,
455
- "metrics": _metrics.get_stats()
456
- }
457
-
458
-
459
- # =============================================================================
460
- # HTML UI
461
- # =============================================================================
462
-
463
- def get_ui_html() -> str:
464
- """Generate the chat UI HTML."""
465
-
466
- example_questions = [
467
- "Why was application APP-78432 flagged as high risk?",
468
- "Explain the fraud score for APP-12345 and compare it to approved applications",
469
- "Check fair lending compliance for APP-55555 and cite relevant policies",
470
- "Show me the identity network analysis for APP-78432",
471
- "What's the current model performance for the Retail Card portfolio?",
472
- "What does our fair lending policy say about synthetic ID detection?",
473
- "Find the model validation report for XGBoost v3.2",
474
- "What are the procedures for escalating high-risk applications?",
475
- ]
476
-
477
- examples_json = json.dumps(example_questions)
478
-
479
- return f"""
480
- <!DOCTYPE html>
481
- <html lang="en">
482
- <head>
483
- <meta charset="UTF-8">
484
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
485
- <title>Fraud Model Explainability Assistant</title>
486
- <style>
487
- * {{
488
- box-sizing: border-box;
489
- margin: 0;
490
- padding: 0;
491
- }}
492
-
493
- body {{
494
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
495
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
496
- min-height: 100vh;
497
- color: #e0e0e0;
498
- }}
499
-
500
- .container {{
501
- max-width: 1200px;
502
- margin: 0 auto;
503
- padding: 20px;
504
- }}
505
-
506
- header {{
507
- text-align: center;
508
- padding: 30px 0;
509
- border-bottom: 1px solid rgba(255,255,255,0.1);
510
- margin-bottom: 30px;
511
- }}
512
-
513
- h1 {{
514
- font-size: 2.5rem;
515
- background: linear-gradient(90deg, #00d4ff, #7b2cbf);
516
- -webkit-background-clip: text;
517
- -webkit-text-fill-color: transparent;
518
- background-clip: text;
519
- margin-bottom: 10px;
520
- }}
521
-
522
- .subtitle {{
523
- color: #888;
524
- font-size: 1.1rem;
525
- }}
526
-
527
- .main-content {{
528
- display: grid;
529
- grid-template-columns: 1fr 300px;
530
- gap: 30px;
531
- }}
532
-
533
- @media (max-width: 900px) {{
534
- .main-content {{
535
- grid-template-columns: 1fr;
536
- }}
537
- }}
538
-
539
- .chat-section {{
540
- background: rgba(255,255,255,0.05);
541
- border-radius: 16px;
542
- padding: 20px;
543
- backdrop-filter: blur(10px);
544
- border: 1px solid rgba(255,255,255,0.1);
545
- }}
546
-
547
- .input-area {{
548
- display: flex;
549
- gap: 10px;
550
- margin-bottom: 20px;
551
- }}
552
-
553
- textarea {{
554
- flex: 1;
555
- padding: 15px;
556
- border: 2px solid rgba(255,255,255,0.1);
557
- border-radius: 12px;
558
- background: rgba(0,0,0,0.3);
559
- color: #fff;
560
- font-size: 1rem;
561
- resize: vertical;
562
- min-height: 80px;
563
- transition: border-color 0.3s;
564
- }}
565
-
566
- textarea:focus {{
567
- outline: none;
568
- border-color: #00d4ff;
569
- }}
570
-
571
- button {{
572
- padding: 15px 30px;
573
- background: linear-gradient(135deg, #00d4ff, #7b2cbf);
574
- color: white;
575
- border: none;
576
- border-radius: 12px;
577
- font-size: 1rem;
578
- font-weight: 600;
579
- cursor: pointer;
580
- transition: transform 0.2s, box-shadow 0.2s;
581
- }}
582
-
583
- button:hover:not(:disabled) {{
584
- transform: translateY(-2px);
585
- box-shadow: 0 10px 30px rgba(0,212,255,0.3);
586
- }}
587
-
588
- button:disabled {{
589
- opacity: 0.5;
590
- cursor: not-allowed;
591
- }}
592
-
593
- .response-area {{
594
- background: rgba(0,0,0,0.3);
595
- border-radius: 12px;
596
- padding: 20px;
597
- min-height: 400px;
598
- max-height: 600px;
599
- overflow-y: auto;
600
- }}
601
-
602
- .response-area pre {{
603
- white-space: pre-wrap;
604
- word-wrap: break-word;
605
- font-family: 'Fira Code', 'Consolas', monospace;
606
- font-size: 0.9rem;
607
- line-height: 1.6;
608
- }}
609
-
610
- .loading {{
611
- display: flex;
612
- align-items: center;
613
- justify-content: center;
614
- gap: 10px;
615
- padding: 40px;
616
- color: #00d4ff;
617
- }}
618
-
619
- .spinner {{
620
- width: 24px;
621
- height: 24px;
622
- border: 3px solid rgba(0,212,255,0.2);
623
- border-top-color: #00d4ff;
624
- border-radius: 50%;
625
- animation: spin 1s linear infinite;
626
- }}
627
-
628
- @keyframes spin {{
629
- to {{ transform: rotate(360deg); }}
630
- }}
631
-
632
- .sidebar {{
633
- display: flex;
634
- flex-direction: column;
635
- gap: 20px;
636
- }}
637
-
638
- .card {{
639
- background: rgba(255,255,255,0.05);
640
- border-radius: 12px;
641
- padding: 20px;
642
- border: 1px solid rgba(255,255,255,0.1);
643
- }}
644
-
645
- .card h3 {{
646
- color: #00d4ff;
647
- margin-bottom: 15px;
648
- font-size: 1rem;
649
- }}
650
-
651
- .example-btn {{
652
- display: block;
653
- width: 100%;
654
- padding: 10px;
655
- margin-bottom: 8px;
656
- background: rgba(0,0,0,0.3);
657
- color: #ccc;
658
- border: 1px solid rgba(255,255,255,0.1);
659
- border-radius: 8px;
660
- font-size: 0.85rem;
661
- text-align: left;
662
- cursor: pointer;
663
- transition: all 0.2s;
664
- }}
665
-
666
- .example-btn:hover {{
667
- background: rgba(0,212,255,0.1);
668
- border-color: #00d4ff;
669
- color: #fff;
670
- }}
671
-
672
- .metrics {{
673
- display: grid;
674
- grid-template-columns: 1fr 1fr;
675
- gap: 10px;
676
- }}
677
-
678
- .metric {{
679
- background: rgba(0,0,0,0.3);
680
- padding: 12px;
681
- border-radius: 8px;
682
- text-align: center;
683
- }}
684
-
685
- .metric-value {{
686
- font-size: 1.5rem;
687
- font-weight: bold;
688
- color: #00d4ff;
689
- }}
690
-
691
- .metric-label {{
692
- font-size: 0.75rem;
693
- color: #888;
694
- margin-top: 4px;
695
- }}
696
-
697
- .features {{
698
- list-style: none;
699
- }}
700
-
701
- .features li {{
702
- padding: 8px 0;
703
- border-bottom: 1px solid rgba(255,255,255,0.05);
704
- font-size: 0.9rem;
705
- }}
706
-
707
- .features li:last-child {{
708
- border-bottom: none;
709
- }}
710
-
711
- .features li::before {{
712
- content: "βœ“ ";
713
- color: #00d4ff;
714
- }}
715
-
716
- footer {{
717
- text-align: center;
718
- padding: 30px;
719
- color: #666;
720
- font-size: 0.9rem;
721
- margin-top: 40px;
722
- }}
723
- </style>
724
- </head>
725
- <body>
726
- <div class="container">
727
- <header>
728
- <h1>πŸ” Fraud Model Explainability Assistant</h1>
729
- <p class="subtitle">Production-Ready with Confluence Integration</p>
730
- </header>
731
-
732
- <div class="main-content">
733
- <div class="chat-section">
734
- <div class="input-area">
735
- <textarea
736
- id="questionInput"
737
- placeholder="Ask a question about fraud models, applications, or policies..."
738
- onkeydown="if(event.key === 'Enter' && !event.shiftKey) {{ event.preventDefault(); askQuestion(); }}"
739
- ></textarea>
740
- <button id="askBtn" onclick="askQuestion()">πŸ” Analyze</button>
741
- </div>
742
-
743
- <div class="response-area" id="responseArea">
744
- <pre id="responseText">Welcome! Ask me about:
745
-
746
- β€’ Application fraud scores and explanations
747
- β€’ Fair lending compliance checks
748
- β€’ Identity network analysis
749
- β€’ Model performance metrics
750
- β€’ Confluence documentation and policies
751
-
752
- Enter your question above and click "Analyze" to get started.</pre>
753
- </div>
754
- </div>
755
-
756
- <div class="sidebar">
757
- <div class="card">
758
- <h3>πŸ“Š Performance Metrics</h3>
759
- <div class="metrics">
760
- <div class="metric">
761
- <div class="metric-value" id="totalSearches">0</div>
762
- <div class="metric-label">Searches</div>
763
- </div>
764
- <div class="metric">
765
- <div class="metric-value" id="cacheRate">0%</div>
766
- <div class="metric-label">Cache Rate</div>
767
- </div>
768
- <div class="metric">
769
- <div class="metric-value" id="avgTime">0s</div>
770
- <div class="metric-label">Avg Time</div>
771
- </div>
772
- <div class="metric">
773
- <div class="metric-value" id="errors">0</div>
774
- <div class="metric-label">Errors</div>
775
- </div>
776
- </div>
777
- </div>
778
-
779
- <div class="card">
780
- <h3>πŸ’‘ Example Questions</h3>
781
- <div id="examplesContainer"></div>
782
- </div>
783
-
784
- <div class="card">
785
- <h3>✨ Production Features</h3>
786
- <ul class="features">
787
- <li>Structured logging</li>
788
- <li>Performance tracking</li>
789
- <li>Error monitoring</li>
790
- <li>Daily auto-refresh (2 AM)</li>
791
- <li>Confluence integration</li>
792
- </ul>
793
- </div>
794
- </div>
795
- </div>
796
-
797
- <footer>
798
- Powered by Strands Agents + Confluence Integration β€’ FastAPI Backend
799
- </footer>
800
- </div>
801
-
802
- <script>
803
- const examples = {examples_json};
804
-
805
- // Populate examples
806
- const examplesContainer = document.getElementById('examplesContainer');
807
- examples.slice(0, 5).forEach(q => {{
808
- const btn = document.createElement('button');
809
- btn.className = 'example-btn';
810
- btn.textContent = q.length > 50 ? q.substring(0, 50) + '...' : q;
811
- btn.title = q;
812
- btn.onclick = () => {{
813
- document.getElementById('questionInput').value = q;
814
- askQuestion();
815
- }};
816
- examplesContainer.appendChild(btn);
817
- }});
818
-
819
- async function askQuestion() {{
820
- const input = document.getElementById('questionInput');
821
- const btn = document.getElementById('askBtn');
822
- const responseArea = document.getElementById('responseArea');
823
- const responseText = document.getElementById('responseText');
824
-
825
- const question = input.value.trim();
826
- if (!question) return;
827
-
828
- btn.disabled = true;
829
- btn.textContent = '⏳ Processing...';
830
- responseArea.innerHTML = '<div class="loading"><div class="spinner"></div>Analyzing your question...</div>';
831
-
832
- try {{
833
- const response = await fetch('/api/ask', {{
834
- method: 'POST',
835
- headers: {{
836
- 'Content-Type': 'application/json',
837
- }},
838
- body: JSON.stringify({{ question }}),
839
- }});
840
-
841
- if (!response.ok) {{
842
- throw new Error(`HTTP error! status: ${{response.status}}`);
843
- }}
844
-
845
- const data = await response.json();
846
-
847
- responseArea.innerHTML = '<pre id="responseText"></pre>';
848
- document.getElementById('responseText').textContent = data.answer;
849
-
850
- // Update metrics
851
- if (data.metrics) {{
852
- document.getElementById('totalSearches').textContent = data.metrics.total_searches || 0;
853
- document.getElementById('cacheRate').textContent =
854
- ((data.metrics.cache_hit_rate || 0) * 100).toFixed(0) + '%';
855
- document.getElementById('avgTime').textContent =
856
- (data.metrics.avg_query_time || 0).toFixed(2) + 's';
857
- document.getElementById('errors').textContent = data.metrics.errors || 0;
858
- }}
859
-
860
- }} catch (error) {{
861
- responseArea.innerHTML = '<pre id="responseText" style="color: #ff6b6b;"></pre>';
862
- document.getElementById('responseText').textContent = 'Error: ' + error.message;
863
- }} finally {{
864
- btn.disabled = false;
865
- btn.textContent = 'πŸ” Analyze';
866
- }}
867
- }}
868
-
869
- // Fetch initial metrics
870
- async function fetchMetrics() {{
871
- try {{
872
- const response = await fetch('/api/metrics');
873
- const metrics = await response.json();
874
- document.getElementById('totalSearches').textContent = metrics.total_searches || 0;
875
- document.getElementById('cacheRate').textContent =
876
- ((metrics.cache_hit_rate || 0) * 100).toFixed(0) + '%';
877
- document.getElementById('avgTime').textContent =
878
- (metrics.avg_query_time || 0).toFixed(2) + 's';
879
- document.getElementById('errors').textContent = metrics.errors || 0;
880
- }} catch (e) {{
881
- console.log('Could not fetch metrics:', e);
882
- }}
883
- }}
884
-
885
- fetchMetrics();
886
- setInterval(fetchMetrics, 30000);
887
- </script>
888
- </body>
889
- </html>
890
- """
891
-
892
-
893
- # =============================================================================
894
- # MAIN ENTRYPOINT
895
- # =============================================================================
896
-
897
- if __name__ == "__main__":
898
- import uvicorn
899
-
900
- # Pre-initialize Confluence
901
- try:
902
- init_confluence()
903
- except Exception as e:
904
- logger.error(f"Confluence initialization failed: {e}")
905
- print(f"Warning: Confluence initialization failed: {e}")
906
- print("App will run without Confluence integration.")
907
-
908
- # Set up scheduled re-ingestion
909
- scheduler = setup_scheduled_ingestion()
910
-
911
- # Launch FastAPI with uvicorn
912
- logger.info("Launching FastAPI server...")
913
- print("\n" + "=" * 60)
914
- print("Fraud Model Explainability Assistant")
915
- print(" - FastAPI backend on port 7860")
916
- print(" - Confluence integration enabled")
917
- print(" - Scheduled refresh at 2 AM daily")
918
- print("=" * 60 + "\n")
919
-
920
- uvicorn.run(
921
- app,
922
- host="0.0.0.0",
923
- port=7860,
924
- reload=False
925
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils.py ADDED
@@ -0,0 +1,685 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fraud Model Explainability Assistant - Shared Utilities
3
+
4
+ This module contains shared tools, mock data generators, and configuration constants
5
+ used by the Fraud Model Explainability Assistant.
6
+ """
7
+
8
+ import os
9
+ import random
10
+ import warnings
11
+ from datetime import datetime, timedelta
12
+ from typing import Optional
13
+
14
+ # Suppress asyncio "Invalid file descriptor" warnings in containerized environments
15
+ # These are harmless cleanup warnings during garbage collection
16
+ warnings.filterwarnings("ignore", category=ResourceWarning)
17
+ os.environ["PYTHONWARNINGS"] = "ignore::ResourceWarning"
18
+
19
+ from strands import tool
20
+
21
+
22
+ # =============================================================================
23
+ # MOCK DATA GENERATORS
24
+ # =============================================================================
25
+ # In production, these would connect to your actual data systems
26
+ # (e.g., Snowflake, feature store, model serving infrastructure)
27
+
28
+ def generate_mock_application(app_id: str) -> dict:
29
+ """Generate realistic mock application data for demo purposes."""
30
+ random.seed(hash(app_id) % 2**32)
31
+
32
+ risk_level = random.choice(["low", "medium", "high", "very_high"])
33
+
34
+ base_data = {
35
+ "application_id": app_id,
36
+ "timestamp": (datetime.now() - timedelta(days=random.randint(0, 30))).isoformat(),
37
+ "portfolio": random.choice(["Retail Card", "Payment Solutions", "CareCredit"]),
38
+ "requested_credit_line": random.randint(500, 25000),
39
+ "fraud_score": {
40
+ "low": random.randint(150, 350),
41
+ "medium": random.randint(400, 550),
42
+ "high": random.randint(600, 750),
43
+ "very_high": random.randint(800, 950)
44
+ }[risk_level],
45
+ "fraud_score_percentile": {
46
+ "low": random.randint(5, 30),
47
+ "medium": random.randint(40, 60),
48
+ "high": random.randint(75, 90),
49
+ "very_high": random.randint(92, 99)
50
+ }[risk_level],
51
+ "decision": "FLAGGED" if risk_level in ["high", "very_high"] else "APPROVED",
52
+ "risk_level": risk_level,
53
+ }
54
+
55
+ # Features that contribute to fraud scoring
56
+ if risk_level in ["high", "very_high"]:
57
+ base_data["features"] = {
58
+ "ssn_issue_date_vs_credit_age_mismatch": random.uniform(0.7, 0.95),
59
+ "device_velocity_30d": random.randint(5, 15),
60
+ "address_type": random.choice(["CMRA", "PO_BOX", "VACANT"]),
61
+ "phone_type": random.choice(["VOIP", "PREPAID"]),
62
+ "email_domain_age_days": random.randint(1, 30),
63
+ "application_velocity_14d": random.randint(3, 8),
64
+ "identity_linkage_count": random.randint(4, 12),
65
+ "credit_inquiry_spike": True,
66
+ "synthetic_id_score": random.uniform(0.75, 0.98),
67
+ }
68
+ else:
69
+ base_data["features"] = {
70
+ "ssn_issue_date_vs_credit_age_mismatch": random.uniform(0.0, 0.2),
71
+ "device_velocity_30d": random.randint(1, 2),
72
+ "address_type": "RESIDENTIAL",
73
+ "phone_type": "POSTPAID",
74
+ "email_domain_age_days": random.randint(365, 3650),
75
+ "application_velocity_14d": random.randint(0, 1),
76
+ "identity_linkage_count": random.randint(0, 2),
77
+ "credit_inquiry_spike": False,
78
+ "synthetic_id_score": random.uniform(0.05, 0.25),
79
+ }
80
+
81
+ return base_data
82
+
83
+
84
+ # =============================================================================
85
+ # FRAUD EXPLAINABILITY TOOLS
86
+ # =============================================================================
87
+
88
+ @tool
89
+ def get_application_summary(application_id: str) -> str:
90
+ """
91
+ Retrieve basic information about a credit application including
92
+ fraud score, decision, portfolio, and timestamp.
93
+
94
+ Args:
95
+ application_id: The unique identifier for the application (e.g., "APP-12345")
96
+
97
+ Returns:
98
+ A summary of the application details and fraud assessment
99
+ """
100
+ app = generate_mock_application(application_id)
101
+
102
+ return f"""
103
+ APPLICATION SUMMARY
104
+ ==================
105
+ Application ID: {app['application_id']}
106
+ Submission Date: {app['timestamp'][:10]}
107
+ Portfolio: {app['portfolio']}
108
+ Requested Credit Line: ${app['requested_credit_line']:,}
109
+
110
+ FRAUD ASSESSMENT
111
+ ----------------
112
+ Fraud Score: {app['fraud_score']} / 1000
113
+ Risk Percentile: {app['fraud_score_percentile']}th percentile
114
+ Risk Level: {app['risk_level'].upper()}
115
+ Decision: {app['decision']}
116
+ """
117
+
118
+
119
+ @tool
120
+ def explain_fraud_score(application_id: str) -> str:
121
+ """
122
+ Get detailed SHAP-style feature attribution explanation for why an
123
+ application received its fraud score. Shows which factors contributed
124
+ most to the risk assessment.
125
+
126
+ Args:
127
+ application_id: The unique identifier for the application
128
+
129
+ Returns:
130
+ Detailed breakdown of contributing factors with impact scores
131
+ """
132
+ app = generate_mock_application(application_id)
133
+ features = app["features"]
134
+
135
+ # Simulate SHAP values (in production, these come from your model)
136
+ explanations = []
137
+
138
+ if features["ssn_issue_date_vs_credit_age_mismatch"] > 0.5:
139
+ explanations.append({
140
+ "feature": "SSN Issue Date vs Credit Age Mismatch",
141
+ "value": f"{features['ssn_issue_date_vs_credit_age_mismatch']:.0%}",
142
+ "impact": "+187 points",
143
+ "direction": "INCREASES RISK",
144
+ "explanation": "SSN was issued recently but credit file shows longer history, a key synthetic ID indicator"
145
+ })
146
+
147
+ if features["device_velocity_30d"] > 3:
148
+ explanations.append({
149
+ "feature": "Device Velocity (30 days)",
150
+ "value": f"{features['device_velocity_30d']} applications",
151
+ "impact": "+142 points",
152
+ "direction": "INCREASES RISK",
153
+ "explanation": "Same device fingerprint linked to multiple applications in short period"
154
+ })
155
+
156
+ if features["address_type"] in ["CMRA", "PO_BOX", "VACANT"]:
157
+ explanations.append({
158
+ "feature": "Address Type",
159
+ "value": features["address_type"],
160
+ "impact": "+98 points",
161
+ "direction": "INCREASES RISK",
162
+ "explanation": f"Address classified as {features['address_type']} (Commercial Mail Receiving Agency or high-risk type)"
163
+ })
164
+
165
+ if features["synthetic_id_score"] > 0.6:
166
+ explanations.append({
167
+ "feature": "Synthetic Identity Score",
168
+ "value": f"{features['synthetic_id_score']:.0%}",
169
+ "impact": "+156 points",
170
+ "direction": "INCREASES RISK",
171
+ "explanation": "Composite score from ensemble model indicates high probability of synthetic identity"
172
+ })
173
+
174
+ if features["application_velocity_14d"] > 2:
175
+ explanations.append({
176
+ "feature": "Application Velocity (14 days)",
177
+ "value": f"{features['application_velocity_14d']} applications",
178
+ "impact": "+78 points",
179
+ "direction": "INCREASES RISK",
180
+ "explanation": "Multiple credit applications submitted in short timeframe"
181
+ })
182
+
183
+ if features["email_domain_age_days"] < 60:
184
+ explanations.append({
185
+ "feature": "Email Domain Age",
186
+ "value": f"{features['email_domain_age_days']} days",
187
+ "impact": "+45 points",
188
+ "direction": "INCREASES RISK",
189
+ "explanation": "Email address created very recently"
190
+ })
191
+
192
+ if features["phone_type"] in ["VOIP", "PREPAID"]:
193
+ explanations.append({
194
+ "feature": "Phone Type",
195
+ "value": features["phone_type"],
196
+ "impact": "+62 points",
197
+ "direction": "INCREASES RISK",
198
+ "explanation": "Non-traditional phone type associated with higher fraud rates"
199
+ })
200
+
201
+ # If low risk, show protective factors
202
+ if app["risk_level"] == "low":
203
+ explanations = [
204
+ {
205
+ "feature": "Established Credit History",
206
+ "value": "12+ years",
207
+ "impact": "-120 points",
208
+ "direction": "DECREASES RISK",
209
+ "explanation": "Long credit history consistent with SSN issue date"
210
+ },
211
+ {
212
+ "feature": "Stable Contact Information",
213
+ "value": "Verified",
214
+ "impact": "-85 points",
215
+ "direction": "DECREASES RISK",
216
+ "explanation": "Phone and address verified with multiple data sources"
217
+ },
218
+ {
219
+ "feature": "Low Application Velocity",
220
+ "value": "1 in 90 days",
221
+ "impact": "-45 points",
222
+ "direction": "DECREASES RISK",
223
+ "explanation": "Normal application pattern"
224
+ }
225
+ ]
226
+
227
+ # Format output
228
+ output = f"""
229
+ FRAUD SCORE EXPLANATION
230
+ =======================
231
+ Application ID: {application_id}
232
+ Final Fraud Score: {app['fraud_score']} / 1000
233
+ Model: XGBoost Fraud Ensemble v3.2
234
+
235
+ TOP CONTRIBUTING FACTORS (ranked by impact):
236
+ --------------------------------------------
237
+ """
238
+
239
+ for i, exp in enumerate(sorted(explanations, key=lambda x: abs(int(x["impact"].split()[0])), reverse=True), 1):
240
+ output += f"""
241
+ {i}. {exp['feature']}
242
+ Value: {exp['value']}
243
+ Impact: {exp['impact']} ({exp['direction']})
244
+ β†’ {exp['explanation']}
245
+ """
246
+
247
+ return output
248
+
249
+
250
+ @tool
251
+ def compare_to_population(application_id: str, comparison_group: str = "approved") -> str:
252
+ """
253
+ Compare an application's features to the approved or denied population
254
+ to show how unusual the applicant's characteristics are.
255
+
256
+ Args:
257
+ application_id: The unique identifier for the application
258
+ comparison_group: Either "approved" or "denied" population to compare against
259
+
260
+ Returns:
261
+ Statistical comparison showing how the application differs from typical cases
262
+ """
263
+ app = generate_mock_application(application_id)
264
+ features = app["features"]
265
+
266
+ # Mock population statistics
267
+ population_stats = {
268
+ "approved": {
269
+ "ssn_credit_mismatch_mean": 0.08,
270
+ "ssn_credit_mismatch_std": 0.12,
271
+ "device_velocity_mean": 1.2,
272
+ "device_velocity_std": 0.8,
273
+ "synthetic_score_mean": 0.15,
274
+ "synthetic_score_std": 0.10,
275
+ "app_velocity_mean": 0.5,
276
+ "app_velocity_std": 0.7,
277
+ },
278
+ "denied": {
279
+ "ssn_credit_mismatch_mean": 0.72,
280
+ "ssn_credit_mismatch_std": 0.18,
281
+ "device_velocity_mean": 6.5,
282
+ "device_velocity_std": 3.2,
283
+ "synthetic_score_mean": 0.78,
284
+ "synthetic_score_std": 0.15,
285
+ "app_velocity_mean": 4.2,
286
+ "app_velocity_std": 2.1,
287
+ }
288
+ }
289
+
290
+ stats = population_stats.get(comparison_group, population_stats["approved"])
291
+
292
+ def calc_z_score(value, mean, std):
293
+ if std == 0:
294
+ return 0
295
+ return (value - mean) / std
296
+
297
+ comparisons = [
298
+ {
299
+ "feature": "SSN/Credit Age Mismatch",
300
+ "applicant_value": f"{features['ssn_issue_date_vs_credit_age_mismatch']:.0%}",
301
+ "population_mean": f"{stats['ssn_credit_mismatch_mean']:.0%}",
302
+ "z_score": calc_z_score(features['ssn_issue_date_vs_credit_age_mismatch'],
303
+ stats['ssn_credit_mismatch_mean'],
304
+ stats['ssn_credit_mismatch_std'])
305
+ },
306
+ {
307
+ "feature": "Device Velocity (30d)",
308
+ "applicant_value": str(features['device_velocity_30d']),
309
+ "population_mean": f"{stats['device_velocity_mean']:.1f}",
310
+ "z_score": calc_z_score(features['device_velocity_30d'],
311
+ stats['device_velocity_mean'],
312
+ stats['device_velocity_std'])
313
+ },
314
+ {
315
+ "feature": "Synthetic ID Score",
316
+ "applicant_value": f"{features['synthetic_id_score']:.0%}",
317
+ "population_mean": f"{stats['synthetic_score_mean']:.0%}",
318
+ "z_score": calc_z_score(features['synthetic_id_score'],
319
+ stats['synthetic_score_mean'],
320
+ stats['synthetic_score_std'])
321
+ },
322
+ {
323
+ "feature": "Application Velocity (14d)",
324
+ "applicant_value": str(features['application_velocity_14d']),
325
+ "population_mean": f"{stats['app_velocity_mean']:.1f}",
326
+ "z_score": calc_z_score(features['application_velocity_14d'],
327
+ stats['app_velocity_mean'],
328
+ stats['app_velocity_std'])
329
+ },
330
+ ]
331
+
332
+ output = f"""
333
+ POPULATION COMPARISON ANALYSIS
334
+ ==============================
335
+ Application ID: {application_id}
336
+ Comparison Group: {comparison_group.upper()} applications (last 12 months)
337
+ Sample Size: {'847,293' if comparison_group == 'approved' else '23,847'} applications
338
+
339
+ FEATURE COMPARISON:
340
+ -------------------
341
+ {"Feature":<30} {"Applicant":<15} {"Population Mean":<18} {"Z-Score":<10} {"Assessment"}
342
+ {"-"*95}
343
+ """
344
+
345
+ for comp in comparisons:
346
+ z = comp["z_score"]
347
+ if abs(z) > 3:
348
+ assessment = "⚠️ EXTREME OUTLIER"
349
+ elif abs(z) > 2:
350
+ assessment = "πŸ”Ά SIGNIFICANT DEVIATION"
351
+ elif abs(z) > 1:
352
+ assessment = "πŸ”· MILD DEVIATION"
353
+ else:
354
+ assessment = "βœ… WITHIN NORMAL"
355
+
356
+ output += f"{comp['feature']:<30} {comp['applicant_value']:<15} {comp['population_mean']:<18} {z:>+.2f}Οƒ {assessment}\n"
357
+
358
+ # Summary
359
+ extreme_count = sum(1 for c in comparisons if abs(c["z_score"]) > 2)
360
+
361
+ output += f"""
362
+ SUMMARY:
363
+ --------
364
+ {extreme_count} of {len(comparisons)} features show significant deviation (|z| > 2Οƒ) from {comparison_group} population.
365
+ """
366
+
367
+ if extreme_count >= 2:
368
+ output += f"This application's profile is statistically unusual compared to typically {comparison_group} applications."
369
+
370
+ return output
371
+
372
+
373
+ @tool
374
+ def check_fair_lending_flags(application_id: str) -> str:
375
+ """
376
+ Check for potential fair lending concerns in the fraud decision.
377
+ Reviews whether protected class proxies may have influenced the score
378
+ and provides compliance documentation.
379
+
380
+ Args:
381
+ application_id: The unique identifier for the application
382
+
383
+ Returns:
384
+ Fair lending compliance assessment and documentation
385
+ """
386
+ app = generate_mock_application(application_id)
387
+
388
+ # Mock fair lending analysis
389
+ output = f"""
390
+ FAIR LENDING COMPLIANCE REVIEW
391
+ ==============================
392
+ Application ID: {application_id}
393
+ Review Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
394
+ Model: XGBoost Fraud Ensemble v3.2
395
+
396
+ PROTECTED CLASS PROXY ANALYSIS:
397
+ -------------------------------
398
+ The following features were analyzed for potential correlation with protected characteristics:
399
+
400
+ βœ… Geography-Based Features:
401
+ - ZIP code used only for velocity calculations, not scoring
402
+ - No direct geographic risk scoring applied
403
+ - Compliant with ECOA geographic restrictions
404
+
405
+ βœ… Name-Based Features:
406
+ - No name-based features used in fraud model
407
+ - Identity verification uses SSN/DOB only
408
+
409
+ βœ… Age-Related Features:
410
+ - Credit age features measure account history, not applicant age
411
+ - SSN issuance analysis targets synthetic ID patterns, not age discrimination
412
+ - Model tested for age disparate impact: PASSED (adverse impact ratio: 0.94)
413
+
414
+ ⚠️ REVIEW ITEMS:
415
+ -----------------
416
+ """
417
+
418
+ if app["features"].get("phone_type") in ["VOIP", "PREPAID"]:
419
+ output += """
420
+ β€’ Phone Type Feature:
421
+ - VOIP/Prepaid flagged as risk factor
422
+ - Documented business justification: 73% of confirmed synthetic fraud uses VOIP
423
+ - Disparate impact testing: PASSED (ratio: 0.89)
424
+ - Alternative considered: None available with equivalent predictive power
425
+ """
426
+
427
+ if app["features"].get("address_type") in ["CMRA", "PO_BOX"]:
428
+ output += """
429
+ β€’ Address Type Feature:
430
+ - CMRA/PO Box flagged as risk factor
431
+ - Documented business justification: Required for synthetic ID detection
432
+ - Disparate impact testing: PASSED (ratio: 0.91)
433
+ - Accommodations: Manual review pathway available for legitimate CMRA users
434
+ """
435
+
436
+ output += f"""
437
+ MODEL VALIDATION STATUS:
438
+ ------------------------
439
+ Last Disparate Impact Test: 2024-11-15
440
+ Last Adverse Action Review: 2024-12-01
441
+ Model Risk Rating: LOW
442
+ SR 11-7 Compliance: COMPLIANT
443
+
444
+ ADVERSE ACTION REASON CODES:
445
+ ----------------------------
446
+ If this application is denied, the following reason codes apply:
447
+ """
448
+
449
+ if app["decision"] == "FLAGGED":
450
+ reasons = [
451
+ "FA01 - Unable to verify identity information",
452
+ "FA03 - Inconsistent application information",
453
+ "FA07 - High-risk contact information patterns",
454
+ ]
455
+ for i, reason in enumerate(reasons, 1):
456
+ output += f" {i}. {reason}\n"
457
+ else:
458
+ output += " N/A - Application approved\n"
459
+
460
+ output += """
461
+ DOCUMENTATION:
462
+ --------------
463
+ This analysis is auto-generated for compliance documentation.
464
+ Full model documentation available in Model Risk Management system.
465
+ Contact: model-governance@company.com
466
+ """
467
+
468
+ return output
469
+
470
+
471
+ @tool
472
+ def get_identity_network(application_id: str) -> str:
473
+ """
474
+ Analyze the identity linkage network for an application, showing
475
+ connections to other applications via shared attributes (device,
476
+ phone, email, address, SSN patterns).
477
+
478
+ Args:
479
+ application_id: The unique identifier for the application
480
+
481
+ Returns:
482
+ Network analysis showing linked applications and risk patterns
483
+ """
484
+ app = generate_mock_application(application_id)
485
+ features = app["features"]
486
+
487
+ linkage_count = features.get("identity_linkage_count", 0)
488
+
489
+ output = f"""
490
+ IDENTITY NETWORK ANALYSIS
491
+ =========================
492
+ Application ID: {application_id}
493
+ Analysis Date: {datetime.now().strftime('%Y-%m-%d')}
494
+
495
+ LINKAGE SUMMARY:
496
+ ----------------
497
+ Total Linked Applications: {linkage_count}
498
+ """
499
+
500
+ if linkage_count > 3:
501
+ # Generate mock linked applications for high-risk cases
502
+ random.seed(hash(application_id) % 2**32)
503
+
504
+ link_types = {
505
+ "device_fingerprint": random.randint(2, min(linkage_count, 8)),
506
+ "phone_number": random.randint(1, min(linkage_count, 4)),
507
+ "email_pattern": random.randint(1, min(linkage_count, 3)),
508
+ "address": random.randint(1, min(linkage_count, 5)),
509
+ }
510
+
511
+ output += f"""
512
+ LINKAGE BREAKDOWN:
513
+ ------------------
514
+ β€’ Device Fingerprint Links: {link_types['device_fingerprint']} applications
515
+ β€’ Phone Number Links: {link_types['phone_number']} applications
516
+ β€’ Email Pattern Links: {link_types['email_pattern']} applications
517
+ β€’ Address Links: {link_types['address']} applications
518
+
519
+ LINKED APPLICATION DETAILS:
520
+ ---------------------------
521
+ """
522
+
523
+ statuses = ["CONFIRMED_FRAUD", "FLAGGED", "DENIED", "CHARGED_OFF", "APPROVED"]
524
+ weights = [0.3, 0.25, 0.2, 0.15, 0.1] if app["risk_level"] in ["high", "very_high"] else [0.05, 0.1, 0.15, 0.1, 0.6]
525
+
526
+ for i in range(min(linkage_count, 6)):
527
+ linked_id = f"APP-{random.randint(10000, 99999)}"
528
+ link_type = random.choice(list(link_types.keys()))
529
+ status = random.choices(statuses, weights=weights)[0]
530
+ days_ago = random.randint(1, 180)
531
+
532
+ status_emoji = {
533
+ "CONFIRMED_FRAUD": "πŸ”΄",
534
+ "FLAGGED": "🟠",
535
+ "DENIED": "🟑",
536
+ "CHARGED_OFF": "πŸ”΄",
537
+ "APPROVED": "🟒"
538
+ }
539
+
540
+ output += f" {status_emoji.get(status, 'βšͺ')} {linked_id} | {link_type.replace('_', ' ').title()} | {status} | {days_ago}d ago\n"
541
+
542
+ # Risk assessment
543
+ fraud_links = sum(1 for _ in range(linkage_count) if random.random() < 0.4)
544
+
545
+ output += f"""
546
+ NETWORK RISK ASSESSMENT:
547
+ ------------------------
548
+ β€’ Confirmed Fraud in Network: {fraud_links} application(s)
549
+ β€’ Network Risk Score: {min(100, linkage_count * 12 + fraud_links * 25)}/100
550
+ β€’ Ring Pattern Detected: {"YES ⚠️" if linkage_count > 5 else "NO"}
551
+ β€’ Velocity Anomaly: {"YES ⚠️" if features.get('device_velocity_30d', 0) > 5 else "NO"}
552
+
553
+ RECOMMENDATION:
554
+ ---------------
555
+ {"⚠️ HIGH-RISK NETWORK - Manual review recommended" if linkage_count > 5 else "πŸ”Ά ELEVATED RISK - Monitor for additional activity"}
556
+ """
557
+
558
+ else:
559
+ output += """
560
+ LINKAGE BREAKDOWN:
561
+ ------------------
562
+ β€’ Device Fingerprint Links: 0-1 applications
563
+ β€’ Phone Number Links: 0 applications
564
+ β€’ Email Pattern Links: 0 applications
565
+ β€’ Address Links: 1 application (same household likely)
566
+
567
+ NETWORK RISK ASSESSMENT:
568
+ ------------------------
569
+ β€’ Network Risk Score: LOW
570
+ β€’ No suspicious patterns detected
571
+ β€’ Normal application profile
572
+
573
+ βœ… No concerning identity network patterns identified.
574
+ """
575
+
576
+ return output
577
+
578
+
579
+ @tool
580
+ def get_model_performance(model_name: str = "xgboost_fraud_v3.2", portfolio: str = "all") -> str:
581
+ """
582
+ Retrieve current performance metrics for a fraud detection model,
583
+ including precision, recall, KS statistic, and financial impact.
584
+
585
+ Args:
586
+ model_name: Name of the fraud model (default: xgboost_fraud_v3.2)
587
+ portfolio: Portfolio to filter by ("Retail Card", "Payment Solutions", "CareCredit", or "all")
588
+
589
+ Returns:
590
+ Model performance metrics and trends
591
+ """
592
+ output = f"""
593
+ MODEL PERFORMANCE DASHBOARD
594
+ ===========================
595
+ Model: {model_name}
596
+ Portfolio: {portfolio.upper()}
597
+ Reporting Period: Last 30 Days
598
+ Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
599
+
600
+ DETECTION METRICS:
601
+ ------------------
602
+ Current Prior Month Ξ” Change
603
+ Fraud Detection Rate: 87.3% 84.1% +3.2% βœ…
604
+ Precision (PPV): 34.2% 31.8% +2.4% βœ…
605
+ False Positive Rate: 2.1% 2.4% -0.3% βœ…
606
+ KS Statistic: 0.72 0.69 +0.03 βœ…
607
+ Gini Coefficient: 0.81 0.78 +0.03 βœ…
608
+ AUC-ROC: 0.91 0.89 +0.02 βœ…
609
+
610
+ FINANCIAL IMPACT:
611
+ -----------------
612
+ Current Prior Month Ξ” Change
613
+ Fraud Losses Prevented: $4.2M $3.8M +$400K βœ…
614
+ False Positive Cost: $890K $920K -$30K βœ…
615
+ Net Benefit: $3.31M $2.88M +$430K βœ…
616
+ ROI: 372% 317% +55% βœ…
617
+
618
+ VOLUME METRICS:
619
+ ---------------
620
+ Applications Scored: 1,247,832
621
+ High-Risk Flags: 26,847 (2.15%)
622
+ Manual Reviews: 8,421
623
+ Confirmed Fraud: 9,182
624
+ """
625
+
626
+ if portfolio != "all":
627
+ output += f"""
628
+ PORTFOLIO BREAKDOWN ({portfolio}):
629
+ {'='*40}
630
+ Applications: {random.randint(200000, 500000):,}
631
+ Fraud Rate: {random.uniform(0.5, 1.2):.2f}%
632
+ Detection Rate: {random.uniform(82, 92):.1f}%
633
+ """
634
+
635
+ output += """
636
+ MODEL HEALTH:
637
+ -------------
638
+ βœ… Feature Drift (PSI): 0.08 (threshold: 0.25)
639
+ βœ… Score Distribution: Stable
640
+ βœ… Latency P99: 45ms (SLA: 100ms)
641
+ ⚠️ Challenger Model: +2.1% lift in shadow mode - review scheduled
642
+
643
+ TREND ALERT:
644
+ ------------
645
+ πŸ“ˆ Synthetic ID fraud attempts up 23% MoM - model adapting well
646
+ πŸ“‰ First-party fraud stable at historical levels
647
+ """
648
+
649
+ return output
650
+
651
+
652
+ # =============================================================================
653
+ # SYSTEM PROMPT
654
+ # =============================================================================
655
+
656
+ SYSTEM_PROMPT = """
657
+ You are a Fraud Model Explainability Assistant for a major financial services company.
658
+ Your role is to help fraud analysts, data scientists, and executives understand
659
+ fraud model decisions and their implications.
660
+
661
+ You have access to tools that can:
662
+ 1. Retrieve application summaries and fraud scores
663
+ 2. Explain why applications received specific fraud scores (SHAP-style explanations)
664
+ 3. Compare applications to approved/denied populations statistically
665
+ 4. Check for fair lending compliance concerns
666
+ 5. Analyze identity networks and linkages
667
+ 6. Show model performance metrics
668
+
669
+ When answering questions:
670
+ - Be precise and data-driven
671
+ - Highlight the most important risk factors first
672
+ - Explain technical concepts in business terms when speaking to executives
673
+ - Always mention fair lending implications when relevant
674
+ - Provide actionable insights, not just data
675
+
676
+ For flagged applications, structure your response as:
677
+ 1. Quick summary (score, decision, risk level)
678
+ 2. Top contributing factors
679
+ 3. How unusual this is compared to the population
680
+ 4. Any compliance considerations
681
+ 5. Recommended next steps
682
+
683
+ Remember: Your explanations may be used in regulatory examinations and audits,
684
+ so be accurate and thorough.
685
+ """.strip()