CoolFace
Apppublic

syedkhizarrayaz/BM-AI-Analysis-And-Alert-Prioritization-Agent

sourceHugging Faceupdated 8mo agoView on Hugging Face
0likes
streamlit_demo.py800 linesDownload Raw Back to root
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