chrisjcc commited on
Commit
27f4650
Β·
verified Β·
1 Parent(s): 73ddec7

Fraud Model Explainability Assistant - Strands Agents

Browse files
Files changed (1) hide show
  1. app.py +837 -0
app.py ADDED
@@ -0,0 +1,837 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from datetime import datetime, timedelta
20
+ from typing import Optional
21
+ import gradio as gr
22
+ from strands import Agent, tool
23
+ from strands.models.openai import OpenAIModel
24
+
25
+
26
+ # =============================================================================
27
+ # MOCK DATA GENERATORS
28
+ # =============================================================================
29
+ # In production, these would connect to your actual data systems
30
+ # (e.g., Snowflake, feature store, model serving infrastructure)
31
+
32
+ def generate_mock_application(app_id: str) -> dict:
33
+ """Generate realistic mock application data for demo purposes."""
34
+ random.seed(hash(app_id) % 2**32)
35
+
36
+ risk_level = random.choice(["low", "medium", "high", "very_high"])
37
+
38
+ base_data = {
39
+ "application_id": app_id,
40
+ "timestamp": (datetime.now() - timedelta(days=random.randint(0, 30))).isoformat(),
41
+ "portfolio": random.choice(["Retail Card", "Payment Solutions", "CareCredit"]),
42
+ "requested_credit_line": random.randint(500, 25000),
43
+ "fraud_score": {
44
+ "low": random.randint(150, 350),
45
+ "medium": random.randint(400, 550),
46
+ "high": random.randint(600, 750),
47
+ "very_high": random.randint(800, 950)
48
+ }[risk_level],
49
+ "fraud_score_percentile": {
50
+ "low": random.randint(5, 30),
51
+ "medium": random.randint(40, 60),
52
+ "high": random.randint(75, 90),
53
+ "very_high": random.randint(92, 99)
54
+ }[risk_level],
55
+ "decision": "FLAGGED" if risk_level in ["high", "very_high"] else "APPROVED",
56
+ "risk_level": risk_level,
57
+ }
58
+
59
+ # Features that contribute to fraud scoring
60
+ if risk_level in ["high", "very_high"]:
61
+ base_data["features"] = {
62
+ "ssn_issue_date_vs_credit_age_mismatch": random.uniform(0.7, 0.95),
63
+ "device_velocity_30d": random.randint(5, 15),
64
+ "address_type": random.choice(["CMRA", "PO_BOX", "VACANT"]),
65
+ "phone_type": random.choice(["VOIP", "PREPAID"]),
66
+ "email_domain_age_days": random.randint(1, 30),
67
+ "application_velocity_14d": random.randint(3, 8),
68
+ "identity_linkage_count": random.randint(4, 12),
69
+ "credit_inquiry_spike": True,
70
+ "synthetic_id_score": random.uniform(0.75, 0.98),
71
+ }
72
+ else:
73
+ base_data["features"] = {
74
+ "ssn_issue_date_vs_credit_age_mismatch": random.uniform(0.0, 0.2),
75
+ "device_velocity_30d": random.randint(1, 2),
76
+ "address_type": "RESIDENTIAL",
77
+ "phone_type": "POSTPAID",
78
+ "email_domain_age_days": random.randint(365, 3650),
79
+ "application_velocity_14d": random.randint(0, 1),
80
+ "identity_linkage_count": random.randint(0, 2),
81
+ "credit_inquiry_spike": False,
82
+ "synthetic_id_score": random.uniform(0.05, 0.25),
83
+ }
84
+
85
+ return base_data
86
+
87
+
88
+ # =============================================================================
89
+ # FRAUD EXPLAINABILITY TOOLS
90
+ # =============================================================================
91
+
92
+ @tool
93
+ def get_application_summary(application_id: str) -> str:
94
+ """
95
+ Retrieve basic information about a credit application including
96
+ fraud score, decision, portfolio, and timestamp.
97
+
98
+ Args:
99
+ application_id: The unique identifier for the application (e.g., "APP-12345")
100
+
101
+ Returns:
102
+ A summary of the application details and fraud assessment
103
+ """
104
+ app = generate_mock_application(application_id)
105
+
106
+ return f"""
107
+ APPLICATION SUMMARY
108
+ ==================
109
+ Application ID: {app['application_id']}
110
+ Submission Date: {app['timestamp'][:10]}
111
+ Portfolio: {app['portfolio']}
112
+ Requested Credit Line: ${app['requested_credit_line']:,}
113
+
114
+ FRAUD ASSESSMENT
115
+ ----------------
116
+ Fraud Score: {app['fraud_score']} / 1000
117
+ Risk Percentile: {app['fraud_score_percentile']}th percentile
118
+ Risk Level: {app['risk_level'].upper()}
119
+ Decision: {app['decision']}
120
+ """
121
+
122
+
123
+ @tool
124
+ def explain_fraud_score(application_id: str) -> str:
125
+ """
126
+ Get detailed SHAP-style feature attribution explanation for why an
127
+ application received its fraud score. Shows which factors contributed
128
+ most to the risk assessment.
129
+
130
+ Args:
131
+ application_id: The unique identifier for the application
132
+
133
+ Returns:
134
+ Detailed breakdown of contributing factors with impact scores
135
+ """
136
+ app = generate_mock_application(application_id)
137
+ features = app["features"]
138
+
139
+ # Simulate SHAP values (in production, these come from your model)
140
+ explanations = []
141
+
142
+ if features["ssn_issue_date_vs_credit_age_mismatch"] > 0.5:
143
+ explanations.append({
144
+ "feature": "SSN Issue Date vs Credit Age Mismatch",
145
+ "value": f"{features['ssn_issue_date_vs_credit_age_mismatch']:.0%}",
146
+ "impact": "+187 points",
147
+ "direction": "INCREASES RISK",
148
+ "explanation": "SSN was issued recently but credit file shows longer history, a key synthetic ID indicator"
149
+ })
150
+
151
+ if features["device_velocity_30d"] > 3:
152
+ explanations.append({
153
+ "feature": "Device Velocity (30 days)",
154
+ "value": f"{features['device_velocity_30d']} applications",
155
+ "impact": "+142 points",
156
+ "direction": "INCREASES RISK",
157
+ "explanation": "Same device fingerprint linked to multiple applications in short period"
158
+ })
159
+
160
+ if features["address_type"] in ["CMRA", "PO_BOX", "VACANT"]:
161
+ explanations.append({
162
+ "feature": "Address Type",
163
+ "value": features["address_type"],
164
+ "impact": "+98 points",
165
+ "direction": "INCREASES RISK",
166
+ "explanation": f"Address classified as {features['address_type']} (Commercial Mail Receiving Agency or high-risk type)"
167
+ })
168
+
169
+ if features["synthetic_id_score"] > 0.6:
170
+ explanations.append({
171
+ "feature": "Synthetic Identity Score",
172
+ "value": f"{features['synthetic_id_score']:.0%}",
173
+ "impact": "+156 points",
174
+ "direction": "INCREASES RISK",
175
+ "explanation": "Composite score from ensemble model indicates high probability of synthetic identity"
176
+ })
177
+
178
+ if features["application_velocity_14d"] > 2:
179
+ explanations.append({
180
+ "feature": "Application Velocity (14 days)",
181
+ "value": f"{features['application_velocity_14d']} applications",
182
+ "impact": "+78 points",
183
+ "direction": "INCREASES RISK",
184
+ "explanation": "Multiple credit applications submitted in short timeframe"
185
+ })
186
+
187
+ if features["email_domain_age_days"] < 60:
188
+ explanations.append({
189
+ "feature": "Email Domain Age",
190
+ "value": f"{features['email_domain_age_days']} days",
191
+ "impact": "+45 points",
192
+ "direction": "INCREASES RISK",
193
+ "explanation": "Email address created very recently"
194
+ })
195
+
196
+ if features["phone_type"] in ["VOIP", "PREPAID"]:
197
+ explanations.append({
198
+ "feature": "Phone Type",
199
+ "value": features["phone_type"],
200
+ "impact": "+62 points",
201
+ "direction": "INCREASES RISK",
202
+ "explanation": "Non-traditional phone type associated with higher fraud rates"
203
+ })
204
+
205
+ # If low risk, show protective factors
206
+ if app["risk_level"] == "low":
207
+ explanations = [
208
+ {
209
+ "feature": "Established Credit History",
210
+ "value": "12+ years",
211
+ "impact": "-120 points",
212
+ "direction": "DECREASES RISK",
213
+ "explanation": "Long credit history consistent with SSN issue date"
214
+ },
215
+ {
216
+ "feature": "Stable Contact Information",
217
+ "value": "Verified",
218
+ "impact": "-85 points",
219
+ "direction": "DECREASES RISK",
220
+ "explanation": "Phone and address verified with multiple data sources"
221
+ },
222
+ {
223
+ "feature": "Low Application Velocity",
224
+ "value": "1 in 90 days",
225
+ "impact": "-45 points",
226
+ "direction": "DECREASES RISK",
227
+ "explanation": "Normal application pattern"
228
+ }
229
+ ]
230
+
231
+ # Format output
232
+ output = f"""
233
+ FRAUD SCORE EXPLANATION
234
+ =======================
235
+ Application ID: {application_id}
236
+ Final Fraud Score: {app['fraud_score']} / 1000
237
+ Model: XGBoost Fraud Ensemble v3.2
238
+
239
+ TOP CONTRIBUTING FACTORS (ranked by impact):
240
+ --------------------------------------------
241
+ """
242
+
243
+ for i, exp in enumerate(sorted(explanations, key=lambda x: abs(int(x["impact"].split()[0])), reverse=True), 1):
244
+ output += f"""
245
+ {i}. {exp['feature']}
246
+ Value: {exp['value']}
247
+ Impact: {exp['impact']} ({exp['direction']})
248
+ β†’ {exp['explanation']}
249
+ """
250
+
251
+ return output
252
+
253
+
254
+ @tool
255
+ def compare_to_population(application_id: str, comparison_group: str = "approved") -> str:
256
+ """
257
+ Compare an application's features to the approved or denied population
258
+ to show how unusual the applicant's characteristics are.
259
+
260
+ Args:
261
+ application_id: The unique identifier for the application
262
+ comparison_group: Either "approved" or "denied" population to compare against
263
+
264
+ Returns:
265
+ Statistical comparison showing how the application differs from typical cases
266
+ """
267
+ app = generate_mock_application(application_id)
268
+ features = app["features"]
269
+
270
+ # Mock population statistics
271
+ population_stats = {
272
+ "approved": {
273
+ "ssn_credit_mismatch_mean": 0.08,
274
+ "ssn_credit_mismatch_std": 0.12,
275
+ "device_velocity_mean": 1.2,
276
+ "device_velocity_std": 0.8,
277
+ "synthetic_score_mean": 0.15,
278
+ "synthetic_score_std": 0.10,
279
+ "app_velocity_mean": 0.5,
280
+ "app_velocity_std": 0.7,
281
+ },
282
+ "denied": {
283
+ "ssn_credit_mismatch_mean": 0.72,
284
+ "ssn_credit_mismatch_std": 0.18,
285
+ "device_velocity_mean": 6.5,
286
+ "device_velocity_std": 3.2,
287
+ "synthetic_score_mean": 0.78,
288
+ "synthetic_score_std": 0.15,
289
+ "app_velocity_mean": 4.2,
290
+ "app_velocity_std": 2.1,
291
+ }
292
+ }
293
+
294
+ stats = population_stats.get(comparison_group, population_stats["approved"])
295
+
296
+ def calc_z_score(value, mean, std):
297
+ if std == 0:
298
+ return 0
299
+ return (value - mean) / std
300
+
301
+ comparisons = [
302
+ {
303
+ "feature": "SSN/Credit Age Mismatch",
304
+ "applicant_value": f"{features['ssn_issue_date_vs_credit_age_mismatch']:.0%}",
305
+ "population_mean": f"{stats['ssn_credit_mismatch_mean']:.0%}",
306
+ "z_score": calc_z_score(features['ssn_issue_date_vs_credit_age_mismatch'],
307
+ stats['ssn_credit_mismatch_mean'],
308
+ stats['ssn_credit_mismatch_std'])
309
+ },
310
+ {
311
+ "feature": "Device Velocity (30d)",
312
+ "applicant_value": str(features['device_velocity_30d']),
313
+ "population_mean": f"{stats['device_velocity_mean']:.1f}",
314
+ "z_score": calc_z_score(features['device_velocity_30d'],
315
+ stats['device_velocity_mean'],
316
+ stats['device_velocity_std'])
317
+ },
318
+ {
319
+ "feature": "Synthetic ID Score",
320
+ "applicant_value": f"{features['synthetic_id_score']:.0%}",
321
+ "population_mean": f"{stats['synthetic_score_mean']:.0%}",
322
+ "z_score": calc_z_score(features['synthetic_id_score'],
323
+ stats['synthetic_score_mean'],
324
+ stats['synthetic_score_std'])
325
+ },
326
+ {
327
+ "feature": "Application Velocity (14d)",
328
+ "applicant_value": str(features['application_velocity_14d']),
329
+ "population_mean": f"{stats['app_velocity_mean']:.1f}",
330
+ "z_score": calc_z_score(features['application_velocity_14d'],
331
+ stats['app_velocity_mean'],
332
+ stats['app_velocity_std'])
333
+ },
334
+ ]
335
+
336
+ output = f"""
337
+ POPULATION COMPARISON ANALYSIS
338
+ ==============================
339
+ Application ID: {application_id}
340
+ Comparison Group: {comparison_group.upper()} applications (last 12 months)
341
+ Sample Size: {'847,293' if comparison_group == 'approved' else '23,847'} applications
342
+
343
+ FEATURE COMPARISON:
344
+ -------------------
345
+ {"Feature":<30} {"Applicant":<15} {"Population Mean":<18} {"Z-Score":<10} {"Assessment"}
346
+ {"-"*95}
347
+ """
348
+
349
+ for comp in comparisons:
350
+ z = comp["z_score"]
351
+ if abs(z) > 3:
352
+ assessment = "⚠️ EXTREME OUTLIER"
353
+ elif abs(z) > 2:
354
+ assessment = "πŸ”Ά SIGNIFICANT DEVIATION"
355
+ elif abs(z) > 1:
356
+ assessment = "πŸ”· MILD DEVIATION"
357
+ else:
358
+ assessment = "βœ… WITHIN NORMAL"
359
+
360
+ output += f"{comp['feature']:<30} {comp['applicant_value']:<15} {comp['population_mean']:<18} {z:>+.2f}Οƒ {assessment}\n"
361
+
362
+ # Summary
363
+ extreme_count = sum(1 for c in comparisons if abs(c["z_score"]) > 2)
364
+
365
+ output += f"""
366
+ SUMMARY:
367
+ --------
368
+ {extreme_count} of {len(comparisons)} features show significant deviation (|z| > 2Οƒ) from {comparison_group} population.
369
+ """
370
+
371
+ if extreme_count >= 2:
372
+ output += f"This application's profile is statistically unusual compared to typically {comparison_group} applications."
373
+
374
+ return output
375
+
376
+
377
+ @tool
378
+ def check_fair_lending_flags(application_id: str) -> str:
379
+ """
380
+ Check for potential fair lending concerns in the fraud decision.
381
+ Reviews whether protected class proxies may have influenced the score
382
+ and provides compliance documentation.
383
+
384
+ Args:
385
+ application_id: The unique identifier for the application
386
+
387
+ Returns:
388
+ Fair lending compliance assessment and documentation
389
+ """
390
+ app = generate_mock_application(application_id)
391
+
392
+ # Mock fair lending analysis
393
+ output = f"""
394
+ FAIR LENDING COMPLIANCE REVIEW
395
+ ==============================
396
+ Application ID: {application_id}
397
+ Review Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
398
+ Model: XGBoost Fraud Ensemble v3.2
399
+
400
+ PROTECTED CLASS PROXY ANALYSIS:
401
+ -------------------------------
402
+ The following features were analyzed for potential correlation with protected characteristics:
403
+
404
+ βœ… Geography-Based Features:
405
+ - ZIP code used only for velocity calculations, not scoring
406
+ - No direct geographic risk scoring applied
407
+ - Compliant with ECOA geographic restrictions
408
+
409
+ βœ… Name-Based Features:
410
+ - No name-based features used in fraud model
411
+ - Identity verification uses SSN/DOB only
412
+
413
+ βœ… Age-Related Features:
414
+ - Credit age features measure account history, not applicant age
415
+ - SSN issuance analysis targets synthetic ID patterns, not age discrimination
416
+ - Model tested for age disparate impact: PASSED (adverse impact ratio: 0.94)
417
+
418
+ ⚠️ REVIEW ITEMS:
419
+ -----------------
420
+ """
421
+
422
+ if app["features"].get("phone_type") in ["VOIP", "PREPAID"]:
423
+ output += """
424
+ β€’ Phone Type Feature:
425
+ - VOIP/Prepaid flagged as risk factor
426
+ - Documented business justification: 73% of confirmed synthetic fraud uses VOIP
427
+ - Disparate impact testing: PASSED (ratio: 0.89)
428
+ - Alternative considered: None available with equivalent predictive power
429
+ """
430
+
431
+ if app["features"].get("address_type") in ["CMRA", "PO_BOX"]:
432
+ output += """
433
+ β€’ Address Type Feature:
434
+ - CMRA/PO Box flagged as risk factor
435
+ - Documented business justification: Required for synthetic ID detection
436
+ - Disparate impact testing: PASSED (ratio: 0.91)
437
+ - Accommodations: Manual review pathway available for legitimate CMRA users
438
+ """
439
+
440
+ output += f"""
441
+ MODEL VALIDATION STATUS:
442
+ ------------------------
443
+ Last Disparate Impact Test: 2024-11-15
444
+ Last Adverse Action Review: 2024-12-01
445
+ Model Risk Rating: LOW
446
+ SR 11-7 Compliance: COMPLIANT
447
+
448
+ ADVERSE ACTION REASON CODES:
449
+ ----------------------------
450
+ If this application is denied, the following reason codes apply:
451
+ """
452
+
453
+ if app["decision"] == "FLAGGED":
454
+ reasons = [
455
+ "FA01 - Unable to verify identity information",
456
+ "FA03 - Inconsistent application information",
457
+ "FA07 - High-risk contact information patterns",
458
+ ]
459
+ for i, reason in enumerate(reasons, 1):
460
+ output += f" {i}. {reason}\n"
461
+ else:
462
+ output += " N/A - Application approved\n"
463
+
464
+ output += """
465
+ DOCUMENTATION:
466
+ --------------
467
+ This analysis is auto-generated for compliance documentation.
468
+ Full model documentation available in Model Risk Management system.
469
+ Contact: model-governance@company.com
470
+ """
471
+
472
+ return output
473
+
474
+
475
+ @tool
476
+ def get_identity_network(application_id: str) -> str:
477
+ """
478
+ Analyze the identity linkage network for an application, showing
479
+ connections to other applications via shared attributes (device,
480
+ phone, email, address, SSN patterns).
481
+
482
+ Args:
483
+ application_id: The unique identifier for the application
484
+
485
+ Returns:
486
+ Network analysis showing linked applications and risk patterns
487
+ """
488
+ app = generate_mock_application(application_id)
489
+ features = app["features"]
490
+
491
+ linkage_count = features.get("identity_linkage_count", 0)
492
+
493
+ output = f"""
494
+ IDENTITY NETWORK ANALYSIS
495
+ =========================
496
+ Application ID: {application_id}
497
+ Analysis Date: {datetime.now().strftime('%Y-%m-%d')}
498
+
499
+ LINKAGE SUMMARY:
500
+ ----------------
501
+ Total Linked Applications: {linkage_count}
502
+ """
503
+
504
+ if linkage_count > 3:
505
+ # Generate mock linked applications for high-risk cases
506
+ random.seed(hash(application_id) % 2**32)
507
+
508
+ link_types = {
509
+ "device_fingerprint": random.randint(2, min(linkage_count, 8)),
510
+ "phone_number": random.randint(1, min(linkage_count, 4)),
511
+ "email_pattern": random.randint(1, min(linkage_count, 3)),
512
+ "address": random.randint(1, min(linkage_count, 5)),
513
+ }
514
+
515
+ output += f"""
516
+ LINKAGE BREAKDOWN:
517
+ ------------------
518
+ β€’ Device Fingerprint Links: {link_types['device_fingerprint']} applications
519
+ β€’ Phone Number Links: {link_types['phone_number']} applications
520
+ β€’ Email Pattern Links: {link_types['email_pattern']} applications
521
+ β€’ Address Links: {link_types['address']} applications
522
+
523
+ LINKED APPLICATION DETAILS:
524
+ ---------------------------
525
+ """
526
+
527
+ statuses = ["CONFIRMED_FRAUD", "FLAGGED", "DENIED", "CHARGED_OFF", "APPROVED"]
528
+ 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]
529
+
530
+ for i in range(min(linkage_count, 6)):
531
+ linked_id = f"APP-{random.randint(10000, 99999)}"
532
+ link_type = random.choice(list(link_types.keys()))
533
+ status = random.choices(statuses, weights=weights)[0]
534
+ days_ago = random.randint(1, 180)
535
+
536
+ status_emoji = {
537
+ "CONFIRMED_FRAUD": "πŸ”΄",
538
+ "FLAGGED": "🟠",
539
+ "DENIED": "🟑",
540
+ "CHARGED_OFF": "πŸ”΄",
541
+ "APPROVED": "🟒"
542
+ }
543
+
544
+ output += f" {status_emoji.get(status, 'βšͺ')} {linked_id} | {link_type.replace('_', ' ').title()} | {status} | {days_ago}d ago\n"
545
+
546
+ # Risk assessment
547
+ fraud_links = sum(1 for _ in range(linkage_count) if random.random() < 0.4)
548
+
549
+ output += f"""
550
+ NETWORK RISK ASSESSMENT:
551
+ ------------------------
552
+ β€’ Confirmed Fraud in Network: {fraud_links} application(s)
553
+ β€’ Network Risk Score: {min(100, linkage_count * 12 + fraud_links * 25)}/100
554
+ β€’ Ring Pattern Detected: {"YES ⚠️" if linkage_count > 5 else "NO"}
555
+ β€’ Velocity Anomaly: {"YES ⚠️" if features.get('device_velocity_30d', 0) > 5 else "NO"}
556
+
557
+ RECOMMENDATION:
558
+ ---------------
559
+ {"⚠️ HIGH-RISK NETWORK - Manual review recommended" if linkage_count > 5 else "πŸ”Ά ELEVATED RISK - Monitor for additional activity"}
560
+ """
561
+
562
+ else:
563
+ output += """
564
+ LINKAGE BREAKDOWN:
565
+ ------------------
566
+ β€’ Device Fingerprint Links: 0-1 applications
567
+ β€’ Phone Number Links: 0 applications
568
+ β€’ Email Pattern Links: 0 applications
569
+ β€’ Address Links: 1 application (same household likely)
570
+
571
+ NETWORK RISK ASSESSMENT:
572
+ ------------------------
573
+ β€’ Network Risk Score: LOW
574
+ β€’ No suspicious patterns detected
575
+ β€’ Normal application profile
576
+
577
+ βœ… No concerning identity network patterns identified.
578
+ """
579
+
580
+ return output
581
+
582
+
583
+ @tool
584
+ def get_model_performance(model_name: str = "xgboost_fraud_v3.2", portfolio: str = "all") -> str:
585
+ """
586
+ Retrieve current performance metrics for a fraud detection model,
587
+ including precision, recall, KS statistic, and financial impact.
588
+
589
+ Args:
590
+ model_name: Name of the fraud model (default: xgboost_fraud_v3.2)
591
+ portfolio: Portfolio to filter by ("Retail Card", "Payment Solutions", "CareCredit", or "all")
592
+
593
+ Returns:
594
+ Model performance metrics and trends
595
+ """
596
+ output = f"""
597
+ MODEL PERFORMANCE DASHBOARD
598
+ ===========================
599
+ Model: {model_name}
600
+ Portfolio: {portfolio.upper()}
601
+ Reporting Period: Last 30 Days
602
+ Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
603
+
604
+ DETECTION METRICS:
605
+ ------------------
606
+ Current Prior Month Ξ” Change
607
+ Fraud Detection Rate: 87.3% 84.1% +3.2% βœ…
608
+ Precision (PPV): 34.2% 31.8% +2.4% βœ…
609
+ False Positive Rate: 2.1% 2.4% -0.3% βœ…
610
+ KS Statistic: 0.72 0.69 +0.03 βœ…
611
+ Gini Coefficient: 0.81 0.78 +0.03 βœ…
612
+ AUC-ROC: 0.91 0.89 +0.02 βœ…
613
+
614
+ FINANCIAL IMPACT:
615
+ -----------------
616
+ Current Prior Month Ξ” Change
617
+ Fraud Losses Prevented: $4.2M $3.8M +$400K βœ…
618
+ False Positive Cost: $890K $920K -$30K βœ…
619
+ Net Benefit: $3.31M $2.88M +$430K βœ…
620
+ ROI: 372% 317% +55% βœ…
621
+
622
+ VOLUME METRICS:
623
+ ---------------
624
+ Applications Scored: 1,247,832
625
+ High-Risk Flags: 26,847 (2.15%)
626
+ Manual Reviews: 8,421
627
+ Confirmed Fraud: 9,182
628
+ """
629
+
630
+ if portfolio != "all":
631
+ output += f"""
632
+ PORTFOLIO BREAKDOWN ({portfolio}):
633
+ {'='*40}
634
+ Applications: {random.randint(200000, 500000):,}
635
+ Fraud Rate: {random.uniform(0.5, 1.2):.2f}%
636
+ Detection Rate: {random.uniform(82, 92):.1f}%
637
+ """
638
+
639
+ output += """
640
+ MODEL HEALTH:
641
+ -------------
642
+ βœ… Feature Drift (PSI): 0.08 (threshold: 0.25)
643
+ βœ… Score Distribution: Stable
644
+ βœ… Latency P99: 45ms (SLA: 100ms)
645
+ ⚠️ Challenger Model: +2.1% lift in shadow mode - review scheduled
646
+
647
+ TREND ALERT:
648
+ ------------
649
+ πŸ“ˆ Synthetic ID fraud attempts up 23% MoM - model adapting well
650
+ πŸ“‰ First-party fraud stable at historical levels
651
+ """
652
+
653
+ return output
654
+
655
+
656
+ # =============================================================================
657
+ # SYSTEM PROMPT
658
+ # =============================================================================
659
+
660
+ SYSTEM_PROMPT = """
661
+ You are a Fraud Model Explainability Assistant for a major financial services company.
662
+ Your role is to help fraud analysts, data scientists, and executives understand
663
+ fraud model decisions and their implications.
664
+
665
+ You have access to tools that can:
666
+ 1. Retrieve application summaries and fraud scores
667
+ 2. Explain why applications received specific fraud scores (SHAP-style explanations)
668
+ 3. Compare applications to approved/denied populations statistically
669
+ 4. Check for fair lending compliance concerns
670
+ 5. Analyze identity networks and linkages
671
+ 6. Show model performance metrics
672
+
673
+ When answering questions:
674
+ - Be precise and data-driven
675
+ - Highlight the most important risk factors first
676
+ - Explain technical concepts in business terms when speaking to executives
677
+ - Always mention fair lending implications when relevant
678
+ - Provide actionable insights, not just data
679
+
680
+ For flagged applications, structure your response as:
681
+ 1. Quick summary (score, decision, risk level)
682
+ 2. Top contributing factors
683
+ 3. How unusual this is compared to the population
684
+ 4. Any compliance considerations
685
+ 5. Recommended next steps
686
+
687
+ Remember: Your explanations may be used in regulatory examinations and audits,
688
+ so be accurate and thorough.
689
+ """.strip()
690
+
691
+
692
+ # =============================================================================
693
+ # AGENT SETUP
694
+ # =============================================================================
695
+
696
+ def create_agent():
697
+ """Create and configure the Strands fraud explainability agent."""
698
+ openai_api_key = os.environ.get('OPENAI_API_KEY')
699
+
700
+ tools = [
701
+ get_application_summary,
702
+ explain_fraud_score,
703
+ compare_to_population,
704
+ check_fair_lending_flags,
705
+ get_identity_network,
706
+ get_model_performance,
707
+ ]
708
+
709
+ if openai_api_key:
710
+ model = OpenAIModel(
711
+ client_args={"api_key": openai_api_key},
712
+ model_id="gpt-4o",
713
+ params={"temperature": 0.1, "max_tokens": 2048}
714
+ )
715
+ return Agent(model=model, system_prompt=SYSTEM_PROMPT, tools=tools)
716
+ else:
717
+ # Default to Bedrock
718
+ return Agent(system_prompt=SYSTEM_PROMPT, tools=tools)
719
+
720
+
721
+ def query(question: str) -> str:
722
+ """Process a question using the fraud explainability agent."""
723
+ try:
724
+ agent = create_agent()
725
+ result = agent(question)
726
+ return str(result)
727
+ except Exception as e:
728
+ return f"Error processing question: {str(e)}"
729
+
730
+
731
+ # =============================================================================
732
+ # GRADIO INTERFACE
733
+ # =============================================================================
734
+
735
+ def process_question(question: str) -> str:
736
+ """Wrapper for Gradio interface."""
737
+ return query(question)
738
+
739
+
740
+ # Custom CSS for professional appearance
741
+ custom_css = """
742
+ .gradio-container {
743
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
744
+ }
745
+ .output-text {
746
+ font-family: 'Consolas', 'Monaco', monospace;
747
+ font-size: 14px;
748
+ line-height: 1.5;
749
+ }
750
+ """
751
+
752
+ # Create the interface
753
+ with gr.Blocks(css=custom_css, title="Fraud Model Explainability Assistant") as iface:
754
+ gr.Markdown("""
755
+ # πŸ” Fraud Model Explainability Assistant
756
+
757
+ An AI-powered assistant that helps you understand fraud model decisions,
758
+ investigate flagged applications, and ensure fair lending compliance.
759
+
760
+ **Capabilities:**
761
+ - Explain why applications were flagged (SHAP-style feature attribution)
762
+ - Compare applications to approved/denied populations
763
+ - Analyze identity networks and linkages
764
+ - Check fair lending compliance
765
+ - Review model performance metrics
766
+ """)
767
+
768
+ with gr.Row():
769
+ with gr.Column(scale=2):
770
+ question_input = gr.Textbox(
771
+ label="Ask a Question",
772
+ placeholder="e.g., Why was application APP-12345 flagged as high risk?",
773
+ lines=3
774
+ )
775
+ submit_btn = gr.Button("πŸ” Analyze", variant="primary")
776
+
777
+ with gr.Row():
778
+ output = gr.Textbox(
779
+ label="Analysis Results",
780
+ lines=25,
781
+ show_copy_button=True
782
+ )
783
+
784
+ gr.Markdown("### πŸ’‘ Example Questions")
785
+
786
+ examples = gr.Examples(
787
+ examples=[
788
+ ["Why was application APP-78432 flagged as high risk?"],
789
+ ["Explain the fraud score for APP-12345 and compare it to approved applications"],
790
+ ["Check fair lending compliance for application APP-55555"],
791
+ ["Show me the identity network analysis for APP-78432"],
792
+ ["What's the current model performance for the Retail Card portfolio?"],
793
+ ["I need to present APP-99999 to the CCO. Give me a complete risk summary with compliance review."],
794
+ ["Compare APP-12345 to the denied population and explain if this looks like synthetic ID fraud"],
795
+ ],
796
+ inputs=question_input
797
+ )
798
+
799
+ submit_btn.click(fn=process_question, inputs=question_input, outputs=output)
800
+ question_input.submit(fn=process_question, inputs=question_input, outputs=output)
801
+
802
+ gr.Markdown("""
803
+ ---
804
+ *Powered by Amazon Strands Agents SDK | Demo with synthetic data*
805
+
806
+ **Note:** This demo uses mock data. In production, tools would connect to:
807
+ - Feature Store / Data Warehouse
808
+ - Model Serving Infrastructure
809
+ - SHAP/Model Interpretation Services
810
+ - Compliance Documentation Systems
811
+ """)
812
+
813
+
814
+ # =============================================================================
815
+ # MAIN
816
+ # =============================================================================
817
+
818
+ if __name__ == "__main__":
819
+ import sys
820
+
821
+ if "--demo" in sys.argv:
822
+ print("\n" + "="*70)
823
+ print("FRAUD MODEL EXPLAINABILITY ASSISTANT - DEMO")
824
+ print("="*70 + "\n")
825
+
826
+ test_questions = [
827
+ "Why was application APP-78432 flagged as high risk?",
828
+ "What's the model performance for Retail Card?",
829
+ ]
830
+
831
+ for q in test_questions:
832
+ print(f"Question: {q}")
833
+ print("-" * 50)
834
+ print(query(q))
835
+ print("\n")
836
+ else:
837
+ iface.launch()