syedkhizarrayaz/BM-AI-Analysis-And-Alert-Prioritization-Agent
0
1import streamlit as st2import requests3import json4from typing import List, Dict, Any5import pandas as pd6from datetime import datetime7 8# Page configuration9st.set_page_config(10 page_title="AML AI Analysis & Alert Prioritization Demo",11 page_icon="๐",12 layout="wide",13 initial_sidebar_state="expanded"14)15 16# Custom CSS for beautiful styling17st.markdown("""18 <style>19 .main-header {20 font-size: 2.5rem;21 font-weight: bold;22 color: #1f77b4;23 text-align: center;24 margin-bottom: 2rem;25 padding: 1rem;26 background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);27 -webkit-background-clip: text;28 -webkit-text-fill-color: transparent;29 }30 .stButton>button {31 width: 100%;32 background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);33 color: white;34 font-weight: bold;35 border-radius: 10px;36 padding: 0.5rem 1rem;37 border: none;38 transition: all 0.3s;39 }40 .stButton>button:hover {41 transform: translateY(-2px);42 box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);43 }44 .success-box {45 padding: 1rem;46 border-radius: 10px;47 background-color: #d4edda;48 border: 1px solid #c3e6cb;49 margin: 1rem 0;50 }51 .error-box {52 padding: 1rem;53 border-radius: 10px;54 background-color: #f8d7da;55 border: 1px solid #f5c6cb;56 margin: 1rem 0;57 }58 .info-box {59 padding: 1rem;60 border-radius: 10px;61 background-color: #d1ecf1;62 border: 1px solid #bee5eb;63 margin: 1rem 0;64 }65 .metric-card {66 background: white;67 padding: 1rem;68 border-radius: 10px;69 box-shadow: 0 2px 4px rgba(0,0,0,0.1);70 margin: 0.5rem 0;71 }72 </style>73""", unsafe_allow_html=True)74 75# Default API Base URL76DEFAULT_API_BASE_URL = "https://syedkhizarrayaz-bm-ai-analysis-and-alert-priorit-2625d69.hf.space"77 78# Example data for Prediction API79PREDICTION_EXAMPLE = [80 {81 "AlertID": 1001,82 "FocusColumnValue": "PK-42101-1234567-1",83 "AlertScore": 85.5,84 "CreateDate": "2025-06-15T10:30:00",85 "riskLevel": "Low",86 "MatchDetails": '{"id": "PK-42101-1234567-1", "scenario": "Unusually large installment", "score": 85.5, "riskLevel": "Low"}',87 "MatchInfoJson": '[{"ID": "PK-42101-1234567-1", "TRANSACTIONAMOUNT": 150000, "CURRENCY": "PKR", "INSTALLMENTNUMBER": 1}, {"ID": "PK-42101-1234567-1", "TRANSACTIONAMOUNT": 180000, "CURRENCY": "PKR", "INSTALLMENTNUMBER": 2}, {"ID": "PK-42101-1234567-1", "TRANSACTIONAMOUNT": 200000, "CURRENCY": "PKR", "INSTALLMENTNUMBER": 3}]',88 "ScenarioName": "Unusually large installment",89 "workflow": "Unassigned"90 },91 {92 "AlertID": 1002,93 "FocusColumnValue": "PK-35202-9876543-2",94 "AlertScore": 92.0,95 "CreateDate": "2025-06-20T14:15:00",96 "riskLevel": "High",97 "MatchDetails": '{"id": "PK-35202-9876543-2", "scenario": "Structuring / Smurfing activity", "score": 92.0, "riskLevel": "High"}',98 "MatchInfoJson": '[{"ID": "PK-35202-9876543-2", "TRANSACTIONAMOUNT": 9500, "CURRENCY": "PKR", "TRANSACTIONTYPE": "Cash Deposit"}, {"ID": "PK-35202-9876543-2", "TRANSACTIONAMOUNT": 9800, "CURRENCY": "PKR", "TRANSACTIONTYPE": "Cash Deposit"}, {"ID": "PK-35202-9876543-2", "TRANSACTIONAMOUNT": 9200, "CURRENCY": "PKR", "TRANSACTIONTYPE": "Cash Deposit"}, {"ID": "PK-35202-9876543-2", "TRANSACTIONAMOUNT": 9600, "CURRENCY": "PKR", "TRANSACTIONTYPE": "Cash Deposit"}, {"ID": "PK-35202-9876543-2", "TRANSACTIONAMOUNT": 9400, "CURRENCY": "PKR", "TRANSACTIONTYPE": "Cash Deposit"}, {"ID": "PK-35202-9876543-2", "TRANSACTIONAMOUNT": 9900, "CURRENCY": "PKR", "TRANSACTIONTYPE": "Cash Deposit"}, {"ID": "PK-35202-9876543-2", "TRANSACTIONAMOUNT": 9100, "CURRENCY": "PKR", "TRANSACTIONTYPE": "Cash Deposit"}]',99 "ScenarioName": "Structuring / Smurfing activity",100 "workflow": "Unassigned"101 },102 {103 "AlertID": 1003,104 "FocusColumnValue": "PK-37405-5551234-3",105 "AlertScore": 78.3,106 "CreateDate": "2025-06-25T09:45:00",107 "riskLevel": "Medium",108 "MatchDetails": '{"id": "PK-37405-5551234-3", "scenario": "Rapid movement of funds", "score": 78.3, "riskLevel": "Medium"}',109 "MatchInfoJson": '[{"ID": "PK-37405-5551234-3", "TRANSACTIONAMOUNT": 250000, "CURRENCY": "PKR", "TRANSACTIONTYPE": "Wire Transfer", "COUNTERPARTYACCOUNT": "AE987654321098765432"}, {"ID": "PK-37405-5551234-3", "TRANSACTIONAMOUNT": 300000, "CURRENCY": "PKR", "TRANSACTIONTYPE": "Wire Transfer", "COUNTERPARTYACCOUNT": "GB12ABCD123456789012"}]',110 "ScenarioName": "Rapid movement of funds",111 "workflow": "Unassigned"112 }113]114 115# Example data for Analysis API116ANALYSIS_EXAMPLE = [117 {118 "AlertID": 1,119 "FilteredTransactions": '[{"CUSTOMERID":"100001","IDENTITYNUMBERS":"42101-1234567-1","LOANID":"LN2024001","ACCOUNTID":"AC100001","CREATEDDATE":"2025-06-01 10:15:00","TRANSACTIONAMOUNT":150000.0,"CURRENCY":"PKR","INSTALLMENTNUMBER":1,"EXCESSAMOUNT":0.0},{"CUSTOMERID":"100001","IDENTITYNUMBERS":"42101-1234567-1","LOANID":"LN2024001","ACCOUNTID":"AC100001","CREATEDDATE":"2025-06-15 14:20:00","TRANSACTIONAMOUNT":180000.0,"CURRENCY":"PKR","INSTALLMENTNUMBER":2,"EXCESSAMOUNT":0.0},{"CUSTOMERID":"100001","IDENTITYNUMBERS":"42101-1234567-1","LOANID":"LN2024001","ACCOUNTID":"AC100001","CREATEDDATE":"2025-06-28 16:45:00","TRANSACTIONAMOUNT":200000.0,"CURRENCY":"PKR","INSTALLMENTNUMBER":3,"EXCESSAMOUNT":0.0}]',120 "FocusColumnValue": "100001",121 "KYCMonthlyIncome": "85,000 PKR",122 "KYCNoOfCredits": "3-5",123 "KYCNoOfDebits": "8-12",124 "KYCRiskCategoryValue": "Low",125 "KYCValueOfCredits": "150,000 - 200,000 PKR",126 "KYCValueOfDebits": "80,000 - 120,000 PKR",127 "OccupationValue": "Private Employee",128 "STRCount": 0,129 "STRScenarioHistory": "",130 "ScenarioName": "Unusually large installment",131 "CustomerName": "Muhammad Bilal Sheikh",132 "CUSTOMERID": "100001",133 "BranchID": "KHI-DHA",134 "Country": "Pakistan",135 "CustomerType": "Retail",136 "CustomerStatus": "Retail Customer",137 "CreatedDate": "2020-01-15",138 "RelationshipStartDate": "2020-01-15",139 "RiskScore": "4.2",140 "PreviousAlerts": [],141 "Counterparties": [],142 "BranchQueries": {143 "Requested": "Verification required for loan installments totaling 530,000 PKR within one month, significantly exceeding declared monthly income of 85,000 PKR. Please confirm source of funds and provide documentation for additional income sources.",144 "Response": "Customer stated installments are from remittances received from brother working in UAE, family savings from wedding expenses, and advance salary from employer for Eid holidays. Customer provided remittance receipts and employer letter."145 }146 },147 {148 "AlertID": 2,149 "FilteredTransactions": '[{"CUSTOMERID":"100002","IDENTITYNUMBERS":"35202-9876543-2","LOANID":"","ACCOUNTID":"AC100002","CREATEDDATE":"2025-06-02 09:30:00","TRANSACTIONAMOUNT":9500.0,"CURRENCY":"PKR","TRANSACTIONTYPE":"Cash Deposit","COUNTERPARTYACCOUNT":"","EXCESSAMOUNT":0.0},{"CUSTOMERID":"100002","IDENTITYNUMBERS":"35202-9876543-2","LOANID":"","ACCOUNTID":"AC100002","CREATEDDATE":"2025-06-02 11:30:00","TRANSACTIONAMOUNT":9800.0,"CURRENCY":"PKR","TRANSACTIONTYPE":"Cash Deposit","COUNTERPARTYACCOUNT":"","EXCESSAMOUNT":0.0},{"CUSTOMERID":"100002","IDENTITYNUMBERS":"35202-9876543-2","LOANID":"","ACCOUNTID":"AC100002","CREATEDDATE":"2025-06-02 13:30:00","TRANSACTIONAMOUNT":9200.0,"CURRENCY":"PKR","TRANSACTIONTYPE":"Cash Deposit","COUNTERPARTYACCOUNT":"","EXCESSAMOUNT":0.0},{"CUSTOMERID":"100002","IDENTITYNUMBERS":"35202-9876543-2","LOANID":"","ACCOUNTID":"AC100002","CREATEDDATE":"2025-06-02 15:30:00","TRANSACTIONAMOUNT":9600.0,"CURRENCY":"PKR","TRANSACTIONTYPE":"Cash Deposit","COUNTERPARTYACCOUNT":"","EXCESSAMOUNT":0.0},{"CUSTOMERID":"100002","IDENTITYNUMBERS":"35202-9876543-2","LOANID":"","ACCOUNTID":"AC100002","CREATEDDATE":"2025-06-02 17:30:00","TRANSACTIONAMOUNT":9400.0,"CURRENCY":"PKR","TRANSACTIONTYPE":"Cash Deposit","COUNTERPARTYACCOUNT":"","EXCESSAMOUNT":0.0},{"CUSTOMERID":"100002","IDENTITYNUMBERS":"35202-9876543-2","LOANID":"","ACCOUNTID":"AC100002","CREATEDDATE":"2025-06-02 19:30:00","TRANSACTIONAMOUNT":9900.0,"CURRENCY":"PKR","TRANSACTIONTYPE":"Cash Deposit","COUNTERPARTYACCOUNT":"","EXCESSAMOUNT":0.0},{"CUSTOMERID":"100002","IDENTITYNUMBERS":"35202-9876543-2","LOANID":"","ACCOUNTID":"AC100002","CREATEDDATE":"2025-06-02 21:30:00","TRANSACTIONAMOUNT":9100.0,"CURRENCY":"PKR","TRANSACTIONTYPE":"Cash Deposit","COUNTERPARTYACCOUNT":"","EXCESSAMOUNT":0.0},{"CUSTOMERID":"100002","IDENTITYNUMBERS":"35202-9876543-2","LOANID":"","ACCOUNTID":"AC100002","CREATEDDATE":"2025-06-03 10:15:00","TRANSACTIONAMOUNT":450000.0,"CURRENCY":"PKR","TRANSACTIONTYPE":"Wire Transfer","COUNTERPARTYACCOUNT":"AE123456789012345678","EXCESSAMOUNT":0.0},{"CUSTOMERID":"100002","IDENTITYNUMBERS":"35202-9876543-2","LOANID":"","ACCOUNTID":"AC100002","CREATEDDATE":"2025-06-03 14:30:00","TRANSACTIONAMOUNT":500000.0,"CURRENCY":"PKR","TRANSACTIONTYPE":"Wire Transfer","COUNTERPARTYACCOUNT":"GB29NWBK60161331926819","EXCESSAMOUNT":0.0},{"CUSTOMERID":"100002","IDENTITYNUMBERS":"35202-9876543-2","LOANID":"","ACCOUNTID":"AC100002","CREATEDDATE":"2025-06-03 16:45:00","TRANSACTIONAMOUNT":56525.0,"CURRENCY":"PKR","TRANSACTIONTYPE":"Local Transfer","COUNTERPARTYACCOUNT":"PK36SCBL0000001123456702","EXCESSAMOUNT":0.0}]',150 "FocusColumnValue": "100002",151 "KYCMonthlyIncome": "55,000 PKR",152 "KYCNoOfCredits": "7",153 "KYCNoOfDebits": "6",154 "KYCRiskCategoryValue": "Medium",155 "KYCValueOfCredits": "66,500 PKR",156 "KYCValueOfDebits": "56,525 PKR",157 "OccupationValue": "Textile Trader",158 "STRCount": 2,159 "STRScenarioHistory": "Large Cash Deposits, Rapid Fund Transfers",160 "ScenarioName": "Structuring / Smurfing activity",161 "CustomerName": "Ayesha Malik",162 "CUSTOMERID": "100002",163 "BranchID": "LHR-GUL",164 "Country": "Pakistan",165 "CustomerType": "Retail",166 "CustomerStatus": "Retail Customer",167 "CreatedDate": "2019-03-20",168 "RelationshipStartDate": "2019-03-20",169 "RiskScore": "8.5",170 "PreviousAlerts": [171 {172 "AlertName": "Large Cash Deposits",173 "Description": "1,200,000 PKR deposited in cash across multiple transactions in single day",174 "BranchExplanation": "Proceeds from sale of commercial property in Faisalabad",175 "Documentation": "",176 "RiskEscalation": ""177 },178 {179 "AlertName": "Rapid Fund Transfers",180 "Description": "950,000 PKR transferred to multiple accounts in Dubai and UK within 48 hours of deposit",181 "BranchExplanation": "Payment for textile machinery imports and supplier advance",182 "Documentation": "",183 "RiskEscalation": "Risk score escalated due to rapid fund movement to multiple high-risk jurisdictions without proper trade documentation."184 }185 ],186 "Counterparties": [187 {188 "Name": "Al-Madina Textile Machinery LLC",189 "AccountID": "AE123456789012345678",190 "Country": "United Arab Emirates",191 "Jurisdiction": "Dubai",192 "TransactionAmount": 450000.0,193 "Currency": "PKR",194 "TransactionDate": "2025-06-03 10:15:00",195 "TransactionType": "Wire Transfer",196 "Relationship": "Supplier",197 "RiskLevel": "Medium",198 "ScreeningResult": "No adverse media found"199 },200 {201 "Name": "Manchester Textile Imports Ltd",202 "AccountID": "GB29NWBK60161331926819",203 "Country": "United Kingdom",204 "Jurisdiction": "Manchester",205 "TransactionAmount": 500000.0,206 "Currency": "PKR",207 "TransactionDate": "2025-06-03 14:30:00",208 "TransactionType": "Wire Transfer",209 "Relationship": "Business Partner",210 "RiskLevel": "High",211 "ScreeningResult": "Flagged for enhanced due diligence - multiple transactions with high-risk jurisdictions"212 },213 {214 "Name": "Ahmed Textiles Faisalabad",215 "AccountID": "PK36SCBL0000001123456702",216 "Country": "Pakistan",217 "Jurisdiction": "Faisalabad",218 "TransactionAmount": 56525.0,219 "Currency": "PKR",220 "TransactionDate": "2025-06-03 16:45:00",221 "TransactionType": "Local Transfer",222 "Relationship": "Local Supplier",223 "RiskLevel": "Low",224 "ScreeningResult": "Verified local business entity"225 }226 ],227 "BranchQueries": {228 "Requested": "Multiple cash deposits totaling 66,500 PKR made in same day, each below 10,000 PKR threshold. Pattern suggests potential structuring to avoid CTR reporting. Please verify legitimate business purpose and provide sales invoices or receipts.",229 "Response": "Customer explained these are daily cash collections from retail textile sales at Anarkali Bazaar. Customer provided daily sales register and GST invoices. Stated pattern is normal for cash-based business operations."230 }231 }232]233 234def call_prediction_api(data: List[Dict[str, Any]], api_base_url: str = DEFAULT_API_BASE_URL) -> Dict[str, Any]:235 """Call the prediction API - handles single alert per request"""236 url = f"{api_base_url}/api/ai-service/predictalertpriority"237 results = []238 errors = []239 240 # Process each alert individually since API expects a single object241 for i, alert_data in enumerate(data):242 try:243 response = requests.post(url, json=alert_data, timeout=120)244 response.raise_for_status()245 try:246 result_data = response.json()247 results.append(result_data)248 except json.JSONDecodeError:249 results.append({"raw_response": response.text, "alert_index": i})250 except requests.exceptions.Timeout:251 errors.append(f"Alert {i+1} (ID: {alert_data.get('AlertID', 'N/A')}): Request timed out.")252 except requests.exceptions.RequestException as e:253 error_msg = str(e)254 if hasattr(e, 'response') and e.response is not None:255 try:256 error_detail = e.response.json()257 error_msg = f"{error_msg}\nDetails: {json.dumps(error_detail, indent=2)}"258 except:259 error_msg = f"{error_msg}\nResponse: {e.response.text[:500]}"260 errors.append(f"Alert {i+1} (ID: {alert_data.get('AlertID', 'N/A')}): {error_msg}")261 262 if errors and not results:263 return {"success": False, "error": "\n".join(errors)}264 elif errors:265 return {"success": True, "data": results, "errors": errors, "partial": True}266 else:267 # Combine all results into a single response format268 combined_data = []269 for result in results:270 if isinstance(result, dict) and "data" in result:271 combined_data.extend(result["data"] if isinstance(result["data"], list) else [result["data"]])272 else:273 combined_data.append(result)274 275 return {"success": True, "data": {"status": 200, "message": "Success", "data": combined_data}}276 277def call_analysis_api(data: List[Dict[str, Any]], api_base_url: str = DEFAULT_API_BASE_URL, 278 cloud: bool = False, llm_on_server: bool = False, 279 url: str = "", anonymous: bool = False,280 audit: bool = False, evaluation: bool = False) -> Dict[str, Any]:281 """Call the analysis API with flags"""282 try:283 # Add flags to each alert data object284 data_with_flags = []285 for alert in data:286 alert_copy = alert.copy()287 alert_copy['Cloud'] = cloud288 alert_copy['llm_on_server'] = llm_on_server289 alert_copy['url'] = url290 alert_copy['anonymous'] = anonymous291 alert_copy['audit'] = audit292 alert_copy['evaluation'] = evaluation293 data_with_flags.append(alert_copy)294 295 api_url = f"{api_base_url}/api/ai-service/generateamlanalysis"296 response = requests.post(api_url, json=data_with_flags, timeout=300)297 response.raise_for_status()298 try:299 result_data = response.json()300 except json.JSONDecodeError:301 result_data = {"raw_response": response.text}302 return {"success": True, "data": result_data}303 except requests.exceptions.Timeout:304 return {"success": False, "error": "Request timed out. The analysis may take longer. Please try again."}305 except requests.exceptions.RequestException as e:306 error_msg = str(e)307 if hasattr(e, 'response') and e.response is not None:308 try:309 error_detail = e.response.json()310 error_msg = f"{error_msg}\nDetails: {json.dumps(error_detail, indent=2)}"311 except:312 error_msg = f"{error_msg}\nResponse: {e.response.text[:500]}"313 return {"success": False, "error": error_msg}314 315def display_prediction_result(result: Dict[str, Any]):316 """Display prediction results in a beautiful format"""317 if result.get("success"):318 data = result.get("data", {})319 320 # Handle partial success321 if result.get("partial") and result.get("errors"):322 st.warning("โ ๏ธ Some alerts processed successfully, but some had errors:")323 for error in result.get("errors", []):324 st.error(error)325 326 # Extract predictions from response327 if isinstance(data, dict) and "data" in data:328 predictions = data["data"] if isinstance(data["data"], list) else [data["data"]]329 elif isinstance(data, list):330 # Handle list of response objects331 all_predictions = []332 for item in data:333 if isinstance(item, dict) and "data" in item:334 preds = item["data"] if isinstance(item["data"], list) else [item["data"]]335 all_predictions.extend(preds)336 else:337 all_predictions.append(item)338 predictions = all_predictions339 else:340 predictions = [data]341 342 st.success("โ
Prediction completed successfully!")343 344 for pred in predictions:345 if isinstance(pred, dict):346 with st.expander(f"Alert ID: {pred.get('AlertID', 'N/A')} - {pred.get('STRScenario', pred.get('ScenarioName', 'Unknown'))}", expanded=True):347 col1, col2, col3 = st.columns(3)348 with col1:349 st.metric("Alert ID", pred.get('AlertID', 'N/A'))350 st.metric("Focus Column", pred.get('FocusColumnValue', 'N/A'))351 with col2:352 prediction = pred.get('Prediction', 'N/A')353 risk_color = {354 'High': '๐ด',355 'Medium': '๐ก',356 'Low': '๐ข'357 }.get(prediction, 'โช')358 st.metric("Prediction", f"{risk_color} {prediction}")359 st.metric("Scenario", pred.get('STRScenario', pred.get('ScenarioName', 'N/A')))360 with col3:361 if isinstance(data, dict):362 st.metric("Status", data.get('status', 'N/A'))363 st.metric("Message", data.get('message', 'Success'))364 else:365 st.metric("Status", "Success")366 st.metric("Message", "Processed")367 else:368 st.error(f"โ Error: {result.get('error', 'Unknown error')}")369 370def display_analysis_result(result: Dict[str, Any]):371 """Display analysis results - showing only the analysis text"""372 if result.get("success"):373 data = result.get("data", {})374 375 # Handle different response formats376 if isinstance(data, dict):377 if "data" in data:378 analyses = data["data"] if isinstance(data["data"], list) else [data["data"]]379 elif "Analysis" in data:380 analyses = data["Analysis"] if isinstance(data["Analysis"], list) else [data["Analysis"]]381 else:382 analyses = [data]383 elif isinstance(data, list):384 analyses = data385 else:386 analyses = [data]387 388 st.success("โ
Analysis completed successfully!")389 390 for analysis in analyses:391 alert_id = analysis.get('AlertID', analysis.get('Alert ID', 'N/A'))392 customer_name = analysis.get('CustomerName', analysis.get('Customer Name', 'N/A'))393 394 # Show basic info in header395 col1, col2 = st.columns([3, 1])396 with col1:397 st.subheader(f"๐ Analysis Report - Alert ID: {alert_id}")398 with col2:399 if customer_name and customer_name != 'N/A':400 st.caption(f"Customer: {customer_name}")401 402 # Display thinking separately if present (from audit mode)403 thinking_text = analysis.get('thinking')404 if thinking_text:405 st.markdown("---")406 st.subheader("๐ง Thinking Process (Audit Mode)")407 st.caption("This section shows the model's reasoning process when Audit Mode is enabled.")408 409 # Display thinking in a separate styled text area410 st.text_area(411 "Model Reasoning",412 value=str(thinking_text),413 height=400,414 disabled=True,415 label_visibility="visible",416 key=f"thinking_{alert_id}",417 help="This is the model's internal reasoning process. Scroll to view the full thinking."418 )419 420 # Display the analysis text421 analysis_text = analysis.get('analysis', analysis.get('Analysis', analysis.get('AnalysisReport', '')))422 423 if analysis_text:424 st.markdown("---")425 st.subheader("๐ Analysis Report")426 427 # Format the text for better display428 import html429 import re430 431 text = str(analysis_text)432 escaped_text = html.escape(text)433 434 # Split into lines and format435 lines = escaped_text.split('\n')436 formatted_lines = []437 in_list = False438 439 for i, line in enumerate(lines):440 line_stripped = line.strip()441 442 # Skip empty lines but add spacing443 if not line_stripped:444 if formatted_lines and not formatted_lines[-1].endswith('<br>'):445 formatted_lines.append('<br>')446 continue447 448 # Detect main headers (like "AML Investigation Report", "Alert Summary", etc.)449 if (line_stripped.endswith(':') and len(line_stripped) < 60 and 450 not line_stripped.startswith(' ') and451 (line_stripped.isupper() or 452 any(keyword in line_stripped.lower() for keyword in ['report', 'summary', 'analysis', 'conclusion', 'recommendation', 'assessment', 'background', 'pattern', 'flow']))):453 formatted_lines.append(f'<h3 style="color: #ffffff; margin: 25px 0 15px 0; font-weight: 700; font-size: 20px; border-bottom: 2px solid rgba(102, 126, 234, 0.6); padding-bottom: 8px;">{line_stripped}</h3>')454 455 # Detect sub-headers (short lines ending with colon)456 elif line_stripped.endswith(':') and len(line_stripped) < 50 and not line_stripped.startswith(' '):457 formatted_lines.append(f'<h4 style="color: #ffffff; margin: 18px 0 10px 0; font-weight: 600; font-size: 16px; color: #a8b5ff;">{line_stripped}</h4>')458 459 # Detect bold text markers460 elif line_stripped.startswith('**') and line_stripped.endswith('**'):461 bold_text = line_stripped.replace('**', '')462 formatted_lines.append(f'<p style="color: #ffffff; margin: 12px 0; font-weight: 600; font-size: 16px;">{bold_text}</p>')463 464 # Detect list items (lines starting with - or โข)465 elif line_stripped.startswith('-') or line_stripped.startswith('โข'):466 if not in_list:467 formatted_lines.append('<ul style="color: #ffffff; margin: 10px 0; padding-left: 25px;">')468 in_list = True469 list_text = line_stripped.lstrip('-โข').strip()470 formatted_lines.append(f'<li style="margin: 8px 0; line-height: 1.8;">{list_text}</li>')471 472 # Regular paragraph text473 else:474 if in_list:475 formatted_lines.append('</ul>')476 in_list = False477 # Check if it's a key-value pair (like "Customer Name: ...")478 if ':' in line_stripped and len(line_stripped.split(':')) == 2:479 key, value = line_stripped.split(':', 1)480 key = key.strip()481 value = value.strip()482 formatted_lines.append(483 f'<p style="color: #ffffff; margin: 10px 0; line-height: 1.8;">'484 f'<span style="font-weight: 600; color: #a8b5ff;">{key}:</span> '485 f'<span>{value}</span></p>'486 )487 else:488 formatted_lines.append(f'<p style="color: #ffffff; margin: 10px 0; line-height: 1.8;">{line_stripped}</p>')489 490 # Close any open list491 if in_list:492 formatted_lines.append('</ul>')493 494 formatted_html = '\n'.join(formatted_lines)495 496 # Display in a styled container with transparent background and white text497 st.markdown(498 f"""499 <div style='500 background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%);501 padding: 30px; 502 border-radius: 12px; 503 border-left: 4px solid #667eea;504 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 505 line-height: 1.8; 506 color: #ffffff; 507 font-size: 15px;508 max-height: 800px;509 overflow-y: auto;510 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);511 '>512 {formatted_html}513 </div>514 """, 515 unsafe_allow_html=True516 )517 else:518 st.warning("No analysis text found in the response.")519 520 # Show metadata in a collapsible section521 with st.expander("๐ View Metadata", expanded=False):522 col1, col2, col3 = st.columns(3)523 with col1:524 st.metric("Response Time", f"{analysis.get('response_time_ms', 0):.2f} ms")525 st.metric("Method", analysis.get('method', 'N/A'))526 with col2:527 st.metric("Model", analysis.get('model', 'N/A'))528 st.metric("Alert ID", alert_id)529 with col3:530 st.metric("Focus Column", analysis.get('FocusColumnValue', 'N/A'))531 else:532 st.error(f"โ Error: {result.get('error', 'Unknown error')}")533 534def main():535 # Header536 st.markdown('<h1 class="main-header">๐ AML AI Analysis & Alert Prioritization Demo</h1>', unsafe_allow_html=True)537 538 # Sidebar539 with st.sidebar:540 st.header("โ๏ธ Configuration")541 st.info("This demo showcases the AML AI Analysis and Alert Prioritization system. Use the tabs below to test different APIs.")542 st.markdown("---")543 544 # API Base URL Configuration545 st.markdown("### ๐ API Configuration")546 api_base_url = st.text_input(547 "API Base URL",548 value=DEFAULT_API_BASE_URL,549 help="Base URL for the API endpoints",550 key="api_base_url"551 )552 st.markdown("---")553 st.markdown("### ๐ก API Endpoints")554 st.code(f"{api_base_url}/api/ai-service/predictalertpriority")555 st.code(f"{api_base_url}/api/ai-service/generateamlanalysis")556 557 # Main tabs558 tab1, tab2 = st.tabs(["๐ฏ Alert Priority Prediction", "๐ AML Analysis Generation"])559 560 # Tab 1: Prediction API561 with tab1:562 st.header("๐ฏ Alert Priority Prediction API")563 st.markdown("Predict whether an alert should be escalated or closed using the ML model.")564 565 # Preload button566 col1, col2 = st.columns([1, 4])567 with col1:568 if st.button("๐ฅ Load Example Data", key="load_pred_example"):569 st.session_state.prediction_data = PREDICTION_EXAMPLE570 st.success("Example data loaded!")571 572 # Data input573 st.subheader("๐ Input Data")574 575 # Number of alerts576 num_alerts = st.number_input("Number of Alerts", min_value=1, max_value=10, value=1, key="num_pred_alerts")577 578 # Initialize session state579 if 'prediction_data' not in st.session_state:580 st.session_state.prediction_data = [{} for _ in range(num_alerts)]581 582 # Form for each alert583 alerts_data = []584 for i in range(num_alerts):585 with st.expander(f"Alert {i+1}", expanded=(i == 0)):586 alert_data = {}587 588 col1, col2 = st.columns(2)589 with col1:590 alert_data['AlertID'] = st.number_input(f"Alert ID", value=st.session_state.prediction_data[i].get('AlertID', 1001) if i < len(st.session_state.prediction_data) else 1001, key=f"pred_alert_id_{i}")591 alert_data['FocusColumnValue'] = st.text_input(f"Focus Column Value", value=st.session_state.prediction_data[i].get('FocusColumnValue', 'PK-42101-1234567-1') if i < len(st.session_state.prediction_data) else 'PK-42101-1234567-1', key=f"pred_focus_{i}")592 alert_data['AlertScore'] = st.number_input(f"Alert Score", value=float(st.session_state.prediction_data[i].get('AlertScore', 85.5)) if i < len(st.session_state.prediction_data) else 85.5, key=f"pred_score_{i}")593 alert_data['CreateDate'] = st.text_input(f"Create Date (ISO format)", value=st.session_state.prediction_data[i].get('CreateDate', '2025-06-15T10:30:00') if i < len(st.session_state.prediction_data) else '2025-06-15T10:30:00', key=f"pred_date_{i}")594 with col2:595 alert_data['riskLevel'] = st.selectbox(f"Risk Level", ['Low', 'Medium', 'High'], index=['Low', 'Medium', 'High'].index(st.session_state.prediction_data[i].get('riskLevel', 'Low')) if i < len(st.session_state.prediction_data) and st.session_state.prediction_data[i].get('riskLevel') in ['Low', 'Medium', 'High'] else 0, key=f"pred_risk_{i}")596 alert_data['ScenarioName'] = st.text_input(f"Scenario Name", value=st.session_state.prediction_data[i].get('ScenarioName', 'Unusually large installment') if i < len(st.session_state.prediction_data) else 'Unusually large installment', key=f"pred_scenario_{i}")597 alert_data['workflow'] = st.text_input(f"Workflow", value=st.session_state.prediction_data[i].get('workflow', 'Unassigned') if i < len(st.session_state.prediction_data) else 'Unassigned', key=f"pred_workflow_{i}")598 599 alert_data['MatchDetails'] = st.text_area(f"Match Details (JSON string)", value=st.session_state.prediction_data[i].get('MatchDetails', '') if i < len(st.session_state.prediction_data) else '', key=f"pred_match_details_{i}", height=100)600 alert_data['MatchInfoJson'] = st.text_area(f"Match Info JSON (JSON array string)", value=st.session_state.prediction_data[i].get('MatchInfoJson', '') if i < len(st.session_state.prediction_data) else '', key=f"pred_match_info_{i}", height=100)601 602 alerts_data.append(alert_data)603 604 # Call API button605 if st.button("๐ Predict Alert Priority", type="primary", key="call_pred_api"):606 # Get API base URL from sidebar (stored in session state)607 api_url = st.session_state.get('api_base_url', DEFAULT_API_BASE_URL)608 with st.spinner("Calling prediction API..."):609 result = call_prediction_api(alerts_data, api_url)610 st.session_state.prediction_result = result611 612 # Display results613 if 'prediction_result' in st.session_state:614 st.markdown("---")615 st.subheader("๐ Results")616 display_prediction_result(st.session_state.prediction_result)617 618 # Tab 2: Analysis API619 with tab2:620 st.header("๐ AML Analysis Generation API")621 st.markdown("Generate comprehensive AML analysis using Hybrid Template+LLM system.")622 623 # Preload button624 col1, col2 = st.columns([1, 4])625 with col1:626 if st.button("๐ฅ Load Example Data", key="load_analysis_example"):627 st.session_state.analysis_data = ANALYSIS_EXAMPLE628 st.success("Example data loaded!")629 630 # Data input631 st.subheader("๐ Input Data")632 633 # Number of alerts634 num_alerts_analysis = st.number_input("Number of Alerts", min_value=1, max_value=10, value=1, key="num_analysis_alerts")635 636 # Initialize session state637 if 'analysis_data' not in st.session_state:638 st.session_state.analysis_data = [{} for _ in range(num_alerts_analysis)]639 640 # Form for each alert641 alerts_analysis_data = []642 for i in range(num_alerts_analysis):643 with st.expander(f"Alert {i+1}", expanded=(i == 0)):644 alert_data = {}645 646 # Basic Information647 st.markdown("#### Basic Information")648 col1, col2, col3 = st.columns(3)649 with col1:650 alert_data['AlertID'] = st.number_input(f"Alert ID", value=st.session_state.analysis_data[i].get('AlertID', 1) if i < len(st.session_state.analysis_data) else 1, key=f"analysis_alert_id_{i}")651 alert_data['FocusColumnValue'] = st.text_input(f"Focus Column Value", value=st.session_state.analysis_data[i].get('FocusColumnValue', '100001') if i < len(st.session_state.analysis_data) else '100001', key=f"analysis_focus_{i}")652 alert_data['ScenarioName'] = st.text_input(f"Scenario Name", value=st.session_state.analysis_data[i].get('ScenarioName', 'Unusually large installment') if i < len(st.session_state.analysis_data) else 'Unusually large installment', key=f"analysis_scenario_{i}")653 with col2:654 alert_data['CustomerName'] = st.text_input(f"Customer Name", value=st.session_state.analysis_data[i].get('CustomerName', '') if i < len(st.session_state.analysis_data) else '', key=f"analysis_customer_name_{i}")655 alert_data['CUSTOMERID'] = st.text_input(f"Customer ID", value=st.session_state.analysis_data[i].get('CUSTOMERID', '') if i < len(st.session_state.analysis_data) else '', key=f"analysis_customer_id_{i}")656 alert_data['BranchID'] = st.text_input(f"Branch ID", value=st.session_state.analysis_data[i].get('BranchID', '') if i < len(st.session_state.analysis_data) else '', key=f"analysis_branch_{i}")657 with col3:658 alert_data['Country'] = st.text_input(f"Country", value=st.session_state.analysis_data[i].get('Country', 'Pakistan') if i < len(st.session_state.analysis_data) else 'Pakistan', key=f"analysis_country_{i}")659 alert_data['CustomerType'] = st.text_input(f"Customer Type", value=st.session_state.analysis_data[i].get('CustomerType', 'Retail') if i < len(st.session_state.analysis_data) else 'Retail', key=f"analysis_customer_type_{i}")660 alert_data['RiskScore'] = st.text_input(f"Risk Score", value=st.session_state.analysis_data[i].get('RiskScore', '4.2') if i < len(st.session_state.analysis_data) else '4.2', key=f"analysis_risk_score_{i}")661 662 # KYC Information663 st.markdown("#### KYC Information")664 col1, col2 = st.columns(2)665 with col1:666 alert_data['KYCMonthlyIncome'] = st.text_input(f"KYC Monthly Income", value=st.session_state.analysis_data[i].get('KYCMonthlyIncome', '85,000 PKR') if i < len(st.session_state.analysis_data) else '85,000 PKR', key=f"analysis_kyc_income_{i}")667 alert_data['KYCNoOfCredits'] = st.text_input(f"KYC No. of Credits", value=st.session_state.analysis_data[i].get('KYCNoOfCredits', '3-5') if i < len(st.session_state.analysis_data) else '3-5', key=f"analysis_kyc_credits_{i}")668 alert_data['KYCNoOfDebits'] = st.text_input(f"KYC No. of Debits", value=st.session_state.analysis_data[i].get('KYCNoOfDebits', '8-12') if i < len(st.session_state.analysis_data) else '8-12', key=f"analysis_kyc_debits_{i}")669 alert_data['KYCRiskCategoryValue'] = st.selectbox(f"KYC Risk Category", ['Low', 'Medium', 'High'], index=['Low', 'Medium', 'High'].index(st.session_state.analysis_data[i].get('KYCRiskCategoryValue', 'Low')) if i < len(st.session_state.analysis_data) and st.session_state.analysis_data[i].get('KYCRiskCategoryValue') in ['Low', 'Medium', 'High'] else 0, key=f"analysis_kyc_risk_{i}")670 with col2:671 alert_data['KYCValueOfCredits'] = st.text_input(f"KYC Value of Credits", value=st.session_state.analysis_data[i].get('KYCValueOfCredits', '150,000 - 200,000 PKR') if i < len(st.session_state.analysis_data) else '150,000 - 200,000 PKR', key=f"analysis_kyc_val_credits_{i}")672 alert_data['KYCValueOfDebits'] = st.text_input(f"KYC Value of Debits", value=st.session_state.analysis_data[i].get('KYCValueOfDebits', '80,000 - 120,000 PKR') if i < len(st.session_state.analysis_data) else '80,000 - 120,000 PKR', key=f"analysis_kyc_val_debits_{i}")673 alert_data['OccupationValue'] = st.text_input(f"Occupation", value=st.session_state.analysis_data[i].get('OccupationValue', 'Private Employee') if i < len(st.session_state.analysis_data) else 'Private Employee', key=f"analysis_occupation_{i}")674 alert_data['STRCount'] = st.number_input(f"STR Count", value=st.session_state.analysis_data[i].get('STRCount', 0) if i < len(st.session_state.analysis_data) else 0, key=f"analysis_str_count_{i}")675 676 # Transactions677 st.markdown("#### Transactions")678 alert_data['FilteredTransactions'] = st.text_area(f"Filtered Transactions (JSON array string)", value=st.session_state.analysis_data[i].get('FilteredTransactions', '[]') if i < len(st.session_state.analysis_data) else '[]', key=f"analysis_transactions_{i}", height=150)679 680 # Additional fields681 st.markdown("#### Additional Information")682 alert_data['STRScenarioHistory'] = st.text_input(f"STR Scenario History", value=st.session_state.analysis_data[i].get('STRScenarioHistory', '') if i < len(st.session_state.analysis_data) else '', key=f"analysis_str_history_{i}")683 alert_data['CreatedDate'] = st.text_input(f"Created Date", value=st.session_state.analysis_data[i].get('CreatedDate', '2020-01-15') if i < len(st.session_state.analysis_data) else '2020-01-15', key=f"analysis_created_date_{i}")684 alert_data['RelationshipStartDate'] = st.text_input(f"Relationship Start Date", value=st.session_state.analysis_data[i].get('RelationshipStartDate', '2020-01-15') if i < len(st.session_state.analysis_data) else '2020-01-15', key=f"analysis_relationship_date_{i}")685 alert_data['CustomerStatus'] = st.text_input(f"Customer Status", value=st.session_state.analysis_data[i].get('CustomerStatus', 'Retail Customer') if i < len(st.session_state.analysis_data) else 'Retail Customer', key=f"analysis_customer_status_{i}")686 687 # Previous Alerts (JSON editor)688 st.markdown("#### Previous Alerts (JSON)")689 prev_alerts_str = st.text_area(f"Previous Alerts (JSON array)", value=json.dumps(st.session_state.analysis_data[i].get('PreviousAlerts', []), indent=2) if i < len(st.session_state.analysis_data) and st.session_state.analysis_data[i].get('PreviousAlerts') else '[]', key=f"analysis_prev_alerts_{i}", height=100)690 try:691 alert_data['PreviousAlerts'] = json.loads(prev_alerts_str)692 except:693 alert_data['PreviousAlerts'] = []694 695 # Counterparties (JSON editor)696 st.markdown("#### Counterparties (JSON)")697 counterparties_str = st.text_area(f"Counterparties (JSON array)", value=json.dumps(st.session_state.analysis_data[i].get('Counterparties', []), indent=2) if i < len(st.session_state.analysis_data) and st.session_state.analysis_data[i].get('Counterparties') else '[]', key=f"analysis_counterparties_{i}", height=100)698 try:699 alert_data['Counterparties'] = json.loads(counterparties_str)700 except:701 alert_data['Counterparties'] = []702 703 # Branch Queries (JSON editor)704 st.markdown("#### Branch Queries (JSON)")705 branch_queries_str = st.text_area(f"Branch Queries (JSON object)", value=json.dumps(st.session_state.analysis_data[i].get('BranchQueries', {}), indent=2) if i < len(st.session_state.analysis_data) and st.session_state.analysis_data[i].get('BranchQueries') else '{}', key=f"analysis_branch_queries_{i}", height=100)706 try:707 alert_data['BranchQueries'] = json.loads(branch_queries_str)708 except:709 alert_data['BranchQueries'] = {}710 711 alerts_analysis_data.append(alert_data)712 713 # LLM Configuration Flags714 st.markdown("---")715 st.subheader("โ๏ธ LLM Configuration")716 st.info("Configure the LLM options for analysis generation. Only one LLM option should be enabled at a time.")717 718 col1, col2 = st.columns(2)719 with col1:720 use_cloud = st.checkbox(721 "โ๏ธ Use Cloud LLM (OpenRouter)",722 value=True,723 help="Use OpenRouter API for LLM. Requires OPENROUTER_API_KEY to be configured on the server.",724 key="analysis_cloud"725 )726 llm_on_server = st.checkbox(727 "๐ Use Remote LLM Server",728 value=False,729 help="Use a remote Ollama server instead of Cloud. Requires URL to be provided.",730 key="analysis_llm_on_server"731 )732 with col2:733 remote_url = st.text_input(734 "๐ Remote LLM Server URL",735 value="",736 help="URL of remote Ollama server (e.g., http://remote-ollama:11434). Required if 'Use Remote LLM Server' is enabled.",737 key="analysis_remote_url",738 disabled=not st.session_state.get('analysis_llm_on_server', False)739 )740 anonymous = st.checkbox(741 "๐ Mask PII (Anonymous Mode)",742 value=False,743 help="Mask Personally Identifiable Information in the data. PII values will be masked by keeping first 2 and last 2 characters.",744 key="analysis_anonymous"745 )746 747 # Additional Flags748 st.markdown("---")749 st.subheader("๐ง Advanced Options")750 col1, col2 = st.columns(2)751 with col1:752 audit = st.checkbox(753 "๐ Audit Mode",754 value=False,755 help="Use a thinking model for generation and include 'thinking' reasoning in the response. Cloud: uses OPENROUTER_THINKING_MODEL, Local: uses OLLAMA_THINKING_MODEL.",756 key="analysis_audit"757 )758 with col2:759 evaluation = st.checkbox(760 "โ
Evaluation Mode",761 value=False,762 help="Evaluate the generated analysis and fix mistakes using an evaluator agent. Works with both Cloud and Local/Remote LLM.",763 key="analysis_evaluation"764 )765 766 # Show warning if multiple LLM options are selected767 if use_cloud and llm_on_server:768 st.warning("โ ๏ธ Multiple LLM options selected. Priority: Cloud > Remote Server")769 770 # Show info if neither is selected (defaults to Cloud)771 if not use_cloud and not llm_on_server:772 st.info("โน๏ธ Defaulting to Cloud LLM mode (OpenRouter)")773 use_cloud = True774 775 # Call API button776 if st.button("๐ Generate AML Analysis", type="primary", key="call_analysis_api"):777 # Get API base URL from sidebar (stored in session state)778 api_url = st.session_state.get('api_base_url', DEFAULT_API_BASE_URL)779 with st.spinner("Generating AML analysis... This may take a few moments."):780 result = call_analysis_api(781 alerts_analysis_data, 782 api_url,783 cloud=use_cloud,784 llm_on_server=llm_on_server,785 url=remote_url,786 anonymous=anonymous,787 audit=audit,788 evaluation=evaluation789 )790 st.session_state.analysis_result = result791 792 # Display results793 if 'analysis_result' in st.session_state:794 st.markdown("---")795 st.subheader("๐ Results")796 display_analysis_result(st.session_state.analysis_result)797 798if __name__ == "__main__":799 main()800 