CoolFace
Apppublic

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

sourceHugging Faceupdated 8mo agoView on Hugging Face
0likes
analysisreport.py1167 linesDownload Raw Back to root
1# imports2from sqlalchemy import create_engine3import pandas as pd4from openai import OpenAI5from llmeventhandler import EventHandler6from fastapi import APIRouter, HTTPException, Request7from config import OPENAI_API_KEY, DB_CONNECTION_STR, DATA_TABLE, PREDICTION_FLAG, ANALYSIS_TABLE, ASSISTANT_ID, PII_COLUMNS8from pydantic import BaseModel, ValidationError, Field9from typing import Optional, List, Dict, Any, Union10from pydantic import BaseModel, Field as PydanticField11import logging12from localLLM import LocalLLM13from fastapi.responses import StreamingResponse14import json15import asyncio16from functools import lru_cache17import threading18from hybrid_ultra_fast_aml import get_hybrid_system19from hybrid_template_llm_aml import get_hybrid_system as get_template_llm_system20 21 22def mask_pii_value(value: str) -> str:23    """24    Mask a PII value by keeping first 2 and last 2 characters, replacing middle with ***25    Handles all string lengths appropriately.26    27    Args:28        value: The value to mask29        30    Returns:31        Masked value with format: first 2 chars + *** + last 2 chars32        For very short values, appropriate masking is applied33    """34    if not value or not isinstance(value, str):35        return value36    37    value = str(value).strip()38    length = len(value)39    40    # Handle edge cases for very short values41    if length == 0:42        return value43    elif length == 1:44        return "*"45    elif length == 2:46        return "**"47    elif length == 3:48        return value[0] + "**"49    elif length == 4:50        return value[0] + "**" + value[-1]51    elif length == 5:52        return value[0] + "***" + value[-1]53    54    # For values with length >= 6, keep first 2 and last 2, mask the middle55    return value[:2] + "***" + value[-2:]56 57 58def mask_pii_data(data: Union[Dict[str, Any], List[Dict[str, Any]]], pii_columns: List[str]) -> Union[Dict[str, Any], List[Dict[str, Any]]]:59    """60    Mask PII data in a dictionary or list of dictionaries based on provided column names61    62    Args:63        data: Dictionary or list of dictionaries containing the data64        pii_columns: List of column names to mask65        66    Returns:67        Data with masked PII values68    """69    if not pii_columns:70        return data71    72    # Normalize column names (strip whitespace)73    pii_columns = [col.strip() for col in pii_columns if col.strip()]74    75    if not pii_columns:76        return data77    78    def mask_dict(d: Dict[str, Any]) -> Dict[str, Any]:79        """Mask PII in a single dictionary"""80        masked = d.copy()81        82        # Mask direct fields83        for col in pii_columns:84            if col in masked and masked[col] is not None:85                masked[col] = mask_pii_value(str(masked[col]))86        87        # Handle FilteredTransactions (JSON string)88        if 'FilteredTransactions' in masked and masked['FilteredTransactions']:89            try:90                transactions = json.loads(masked['FilteredTransactions']) if isinstance(masked['FilteredTransactions'], str) else masked['FilteredTransactions']91                if isinstance(transactions, list):92                    masked_transactions = []93                    for trans in transactions:94                        masked_trans = trans.copy() if isinstance(trans, dict) else trans95                        if isinstance(trans, dict):96                            for col in pii_columns:97                                if col in masked_trans and masked_trans[col] is not None:98                                    masked_trans[col] = mask_pii_value(str(masked_trans[col]))99                        masked_transactions.append(masked_trans)100                    masked['FilteredTransactions'] = json.dumps(masked_transactions) if isinstance(masked['FilteredTransactions'], str) else masked_transactions101            except (json.JSONDecodeError, TypeError):102                # If parsing fails, leave as is103                pass104        105        # Handle PreviousAlerts (list of dictionaries)106        if 'PreviousAlerts' in masked and isinstance(masked['PreviousAlerts'], list):107            masked_alerts = []108            for alert in masked['PreviousAlerts']:109                if isinstance(alert, dict):110                    masked_alert = alert.copy()111                    for col in pii_columns:112                        if col in masked_alert and masked_alert[col] is not None:113                            masked_alert[col] = mask_pii_value(str(masked_alert[col]))114                    masked_alerts.append(masked_alert)115                else:116                    masked_alerts.append(alert)117            masked['PreviousAlerts'] = masked_alerts118        119        # Handle Counterparties (list of dictionaries)120        if 'Counterparties' in masked and isinstance(masked['Counterparties'], list):121            masked_counterparties = []122            for cp in masked['Counterparties']:123                if isinstance(cp, dict):124                    masked_cp = cp.copy()125                    for col in pii_columns:126                        if col in masked_cp and masked_cp[col] is not None:127                            masked_cp[col] = mask_pii_value(str(masked_cp[col]))128                    masked_counterparties.append(masked_cp)129                else:130                    masked_counterparties.append(cp)131            masked['Counterparties'] = masked_counterparties132        133        # Handle BranchQueries (dictionary)134        if 'BranchQueries' in masked and isinstance(masked['BranchQueries'], dict):135            masked_queries = masked['BranchQueries'].copy()136            # Mask PII in Requested and Response text fields137            for key in ['Requested', 'Response']:138                if key in masked_queries and masked_queries[key]:139                    # Simple text masking - could be enhanced to detect PII patterns140                    text = str(masked_queries[key])141                    # For now, we'll mask known PII patterns in text142                    # This is a simple approach - could be enhanced with regex143                    for col in pii_columns:144                        # Try to find and mask the column value in text145                        # This is a basic implementation146                        pass147            masked['BranchQueries'] = masked_queries148        149        return masked150    151    # Handle list of dictionaries152    if isinstance(data, list):153        return [mask_dict(item) if isinstance(item, dict) else item for item in data]154    155    # Handle single dictionary156    if isinstance(data, dict):157        return mask_dict(data)158    159    return data160 161 162# analysis alert request model - all fields optional to handle systems with missing data163class AnalysisAlertRequest(BaseModel):164    AlertID: Optional[int] = Field(None, description="Unique alert identifier", example=1)165    FocusColumnValue: Optional[str] = Field(None, description="Customer/entity identifier", example="100001")166    KYCMonthlyIncome: Optional[str] = Field(None, description="Monthly income range", example="85,000 PKR")167    KYCNoOfCredits: Optional[str] = Field(None, description="Expected number of credits", example="3-5")168    KYCNoOfDebits: Optional[str] = Field(None, description="Expected number of debits", example="8-12")169    KYCRiskCategoryValue: Optional[str] = Field(None, description="KYC risk category", example="Low")170    KYCValueOfCredits: Optional[str] = Field(None, description="Expected value of credits", example="150,000 - 200,000 PKR")171    KYCValueOfDebits: Optional[str] = Field(None, description="Expected value of debits", example="80,000 - 120,000 PKR")172    OccupationValue: Optional[str] = Field(None, description="Customer occupation", example="Private Employee")173    FilteredTransactions: Optional[str] = Field(None, description="JSON array string with transaction details", example='[{"CUSTOMERID":"100001","TRANSACTIONAMOUNT":150000.0,"CURRENCY":"PKR"}]')174    ScenarioName: Optional[str] = Field(None, description="Alert scenario name", example="Unusually large installment")175    STRCount: Optional[int] = Field(0, description="Count of previous STRs", example=0)176    STRScenarioHistory: Optional[str] = Field("", description="History of STR scenarios", example="")177    # Additional optional fields from the JSON payload178    CustomerName: Optional[str] = Field(None, description="Customer full name", example="Muhammad Bilal Sheikh")179    CUSTOMERID: Optional[str] = Field(None, description="Customer ID", example="100001")180    BranchID: Optional[str] = Field(None, description="Branch identifier", example="KHI-DHA")181    Country: Optional[str] = Field(None, description="Customer country", example="Pakistan")182    CustomerType: Optional[str] = Field(None, description="Customer type", example="Retail")183    CustomerStatus: Optional[str] = Field(None, description="Customer status", example="Retail Customer")184    CreatedDate: Optional[str] = Field(None, description="Account creation date", example="2020-01-15")185    RelationshipStartDate: Optional[str] = Field(None, description="Relationship start date", example="2020-01-15")186    RiskScore: Optional[str] = Field(None, description="Risk score", example="4.2")187    PreviousAlerts: Optional[List[Dict[str, Any]]] = Field([], description="Array of previous alert objects", example=[])188    Counterparties: Optional[List[Dict[str, Any]]] = Field([], description="Array of counterparty objects", example=[])189    BranchQueries: Optional[Dict[str, Any]] = Field(None, description="Branch query request/response", example={"Requested": "Verification required...", "Response": "Customer stated..."})190    191    class Config:192        json_schema_extra = {193            "example": {194                "AlertID": 1,195                "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}]',196                "FocusColumnValue": "100001",197                "KYCMonthlyIncome": "85,000 PKR",198                "KYCNoOfCredits": "3-5",199                "KYCNoOfDebits": "8-12",200                "KYCRiskCategoryValue": "Low",201                "KYCValueOfCredits": "150,000 - 200,000 PKR",202                "KYCValueOfDebits": "80,000 - 120,000 PKR",203                "OccupationValue": "Private Employee",204                "STRCount": 0,205                "STRScenarioHistory": "",206                "ScenarioName": "Unusually large installment",207                "CustomerName": "Muhammad Bilal Sheikh",208                "CUSTOMERID": "100001",209                "BranchID": "KHI-DHA",210                "Country": "Pakistan",211                "CustomerType": "Retail",212                "CustomerStatus": "Retail Customer",213                "CreatedDate": "2020-01-15",214                "RelationshipStartDate": "2020-01-15",215                "RiskScore": "4.2",216                "PreviousAlerts": [],217                "Counterparties": [],218                "BranchQueries": {219                    "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.",220                    "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."221                }222            }223        }224 225# Response models226class AnalysisItem(BaseModel):227    AlertID: int = Field(..., description="Alert identifier", example=2)228    FocusColumnValue: str = Field(..., description="Customer/entity identifier", example="100002")229    analysis: str = Field(..., description="Generated AML analysis report", example="AML Investigation Report...")230    response_time_ms: float = Field(..., description="Response time in milliseconds", example=1234.56)231    method: str = Field(..., description="Analysis method used", example="hybrid_template_only")232    model: Optional[str] = Field(None, description="LLM model used", example="granite3.1-moe:3b")233    thinking: Optional[str] = Field(None, description="Thinking/reasoning from audit mode (when audit=true)")234 235class AnalysisResponse(BaseModel):236    status: int = Field(..., description="HTTP status code", example=200)237    message: str = Field(..., description="Response message", example="Success")238    data: List[AnalysisItem] = Field(..., description="List of analysis results")239    240    class Config:241        json_schema_extra = {242            "example": {243                "status": 200,244                "message": "Success",245                "data": [246                    {247                        "AlertID": 2,248                        "FocusColumnValue": "100002",249                        "analysis": "AML Investigation Report - TMS Case: Structuring / Smurfing activity\n\nCustomer Name: Ayesha Malik\n...",250                        "response_time_ms": 1234.56,251                        "method": "hybrid_template_only",252                        "model": "granite3.1-moe:3b"253                    }254                ]255            }256        }257 258# api route instance259router = APIRouter()260 261# **Setup logging** (add this section to configure the logging)262logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[263    logging.FileHandler("analysisapi.log"),264    logging.StreamHandler()265])266 267logger = logging.getLogger(__name__)268 269# Global LLM instance for performance optimization270_global_llm = None271_llm_lock = threading.Lock()272 273def get_global_llm():274    """Get or create global LLM instance with thread safety"""275    global _global_llm276    if _global_llm is None:277        with _llm_lock:278            if _global_llm is None:279                _global_llm = LocalLLM()280    return _global_llm281 282# Template caching for faster response generation283@lru_cache(maxsize=100)284def get_analysis_template(scenario_name: Optional[str], kyc_info: str, transaction_data: Optional[str], 285                          str_count: Optional[int], str_history: Optional[str]) -> str:286    """Cached template generation for analysis prompts"""287    # Handle None or empty values288    scenario_name = scenario_name or "Unknown Scenario"289    transaction_data = transaction_data or "No transaction data available"290    str_count = str_count if str_count is not None else 0291    str_history = str_history or "No previous STR scenarios"292    kyc_info = kyc_info or "KYC profile not available"293    294    return (295        f"You are an AML analyst. An alert has been raised regarding potential suspicious activity involving the scenario: {scenario_name}.\n\n"296        "Conclusion Up Front\n"297        "- Provide a clear conclusion on whether the case can be closed or warrants escalation/STR.\n"298        "- Include a brief rationale highlighting red flags or justifications for closure.\n\n"299 300        "Initial Risk\n"301        f"- Alerted Scenario / Rule ID: {scenario_name}\n\n"302 303        "Reviewed Period\n"304        "- Define the transaction review period based on the data provided.\n\n"305 306        "Transaction Review\n"307        f"- Transaction Data:\n{transaction_data}\n"308        "- Summarize the number of transactions, total values in USD, dates, involved parties, locations, and any remarks indicating suspicious behavior.\n\n"309 310        "Research on Focus\n"311        f"- KYC Profile:\n{kyc_info}\n"312        "- Include Name, CIF, ID, DOB/Age, Country, Employment, Job Title, Monthly Income, Risk Level, Relationship Start Date, and any name screening results.\n\n"313 314        "Research on Parties / Counterparties Involved\n"315        "- Assess the profile of counterparties involved in the transactions.\n"316        "- Include any name screening or risk database results.\n\n"317 318        "History Related Cases\n"319        f"- Alerts raised Previously: {str_count}\n"320        f"- STR raised Scenarios: {str_history}\n"321        "- Summarize only if there are relevant concerns or patterns found.\n\n"322 323        "RFI / Enquiry Branch Observation\n"324        "- Summarize any branch or RM input, if available.\n\n"325 326        "Risk Search\n"327        "- Detail outcomes of internal or external screening and risk searches.\n"328        "- Mention any matches with internal watchlists, fraud portals, or regulatory flags.\n\n"329 330        "At the end no need for suggestion or conclusion, just return headings I have mentioned above. All the headings and their content must be present in the response.\n"331    )332 333# analysis class334class AnalysisProcessor:335    # constructor336    def __init__(self, db_connection_str, client, assistant_id):337        self.db_connection_str = db_connection_str338        # Only create database engine if connection string is provided and not empty339        if db_connection_str:340            try:341                self.engine = create_engine(self.db_connection_str)342                logger.info("Database engine initialized successfully")343            except Exception as e:344                logger.warning(f"Database connection not available: {e}. Running without database.")345                self.engine = None346        else:347            self.engine = None348            logger.info("Database connection string not provided. Running without database.")349        350        # Create a new thread for the conversation only if client is provided351        self.client = client352        if client is not None:353            try:354                self.thread = client.beta.threads.create()355            except Exception as e:356                logger.warning(f"Failed to create OpenAI thread: {e}. Running without OpenAI thread.")357                self.thread = None358        else:359            self.thread = None360        361        self.assistant_id = assistant_id or ""362        logger.info(f"Initialized AnalysisProcessor (assistant_id: {self.assistant_id}, db_available: {self.engine is not None}, client_available: {self.client is not None})")363 364 365    # query database for retrival of data366    def query_data(self, table_name, prediction_value):367        """368        Queries the database for records with Prediction = 1 and returns it as a DataFrame.369        """370        try:371            # Skip database operations if engine is not available372            if self.engine is None:373                logger.info(f"Database not configured. Skipping query from table {table_name}")374                return pd.DataFrame()  # Return empty DataFrame375            376            logger.info(f"Querying data from table: {table_name} with Prediction = {prediction_value}")377 378            query = f"SELECT * FROM {table_name} WHERE Prediction = {prediction_value}"  # Adjust table name as needed379            df = pd.read_sql(query, self.engine)380            logger.info(f"Retrieved {len(df)} rows from the database")381            return df382        except Exception as e:383            logger.error(f"Error querying data: {e}")384            # Return empty DataFrame instead of raising exception385            logger.warning(f"Returning empty DataFrame due to database error")386            return pd.DataFrame()387    388    # return kyc data in string389    def return_kyc_profile(self, customer_data):390        try:391            logger.debug("Generating KYC profile string")392 393            # Safely get values with defaults for missing fields394            kyc_monthly = customer_data.get('KYCMonthlyIncome', 'Not available')395            kyc_numcredits = customer_data.get('KYCNoOfCredits', 'Not available')396            kyc_numdebits = customer_data.get('KYCNoOfDebits', 'Not available')397            kyc_riskcategory = customer_data.get('KYCRiskCategoryValue', 'Not available')398            kyc_valuecredits = customer_data.get('KYCValueOfCredits', 'Not available')399            kyc_valuedebits = customer_data.get('KYCValueOfDebits', 'Not available')400            kyc_occupation = customer_data.get('OccupationValue', 'Not available')401            402            # Get additional customer info if available403            customer_name = customer_data.get('CustomerName', customer_data.get('CUSTOMERID', 'Not available'))404            customer_id = customer_data.get('CUSTOMERID', 'Not available')405            country = customer_data.get('Country', 'Not available')406            relationship_start = customer_data.get('RelationshipStartDate', 'Not available')407            risk_score = customer_data.get('RiskScore', 'Not available')408 409            return f'''410        Customer Name: - {customer_name}411        Customer ID: - {customer_id}412        Country: - {country}413        Relationship Start Date: - {relationship_start}414        Monthly Income Range: - {kyc_monthly}415        Occupation:- {kyc_occupation}416        Risk Category: - {kyc_riskcategory}417        Risk Score: - {risk_score}418        Expected Number of Credits: - {kyc_numcredits}419        Expected Total Value of Credits: -  {kyc_valuecredits}420        Expected Number of Debits: - {kyc_numdebits}421        Expected Total Value of Debits: - {kyc_valuedebits}422        '''423        except Exception as e:424                logger.error(f"Error generating KYC profile: {e}")425                # Return a minimal profile if there's an error426                return "KYC Profile: Data not available"427    428    # Send a comprehensive message to the assistant429    def send_message_to_assistant(self, transaction_data, scenario, prev_alerts, kyc_profile, str_scenarios_history):430        # Construct the full query for the assistant431        try:432            logger.info(f"Sending message to assistant for transaction: {transaction_data}")433 434            # Handle None or empty values435            scenario = scenario or "Unknown Scenario"436            transaction_data = transaction_data or "No transaction data available"437            prev_alerts = prev_alerts if prev_alerts is not None else 0438            str_scenarios_history = str_scenarios_history or "No previous STR scenarios"439            kyc_profile = kyc_profile or "KYC profile not available"440 441            full_query = (442                f"You are an AML analyst. An alert has been raised regarding potential suspicious activity involving the scenario: {scenario}.\n\n"443                "Conclusion Up Front\n"444                "- Provide a clear conclusion on whether the case can be closed or warrants escalation/STR.\n"445                "- Include a brief rationale highlighting red flags or justifications for closure.\n\n"446 447                "Initial Risk\n"448                f"- Alerted Scenario / Rule ID: {scenario}\n\n"449 450                "Reviewed Period\n"451                "- Define the transaction review period based on the data provided.\n\n"452 453                "Transaction Review\n"454                f"- Transaction Data:\n{transaction_data}\n"455                "- Summarize the number of transactions, total values in USD, dates, involved parties, locations, and any remarks indicating suspicious behavior.\n\n"456 457                "Research on Focus\n"458                f"- KYC Profile:\n{kyc_profile}\n"459                "- Include Name, CIF, ID, DOB/Age, Country, Employment, Job Title, Monthly Income, Risk Level, Relationship Start Date, and any name screening results.\n\n"460 461                "Research on Parties / Counterparties Involved\n"462                "- Assess the profile of counterparties involved in the transactions.\n"463                "- Include any name screening or risk database results.\n\n"464 465                "History Related Cases\n"466                f"- Alerts raised Previously: {prev_alerts}\n"467                f"- STR raised Scenarios: {str_scenarios_history}\n"468                "- Summarize only if there are relevant concerns or patterns found.\n\n"469 470                "RFI / Enquiry Branch Observation\n"471                "- Summarize any branch or RM input, if available.\n\n"472 473                "Risk Search\n"474                "- Detail outcomes of internal or external screening and risk searches.\n"475                "- Mention any matches with internal watchlists, fraud portals, or regulatory flags.\n\n"476 477                "At the end no need for suggestion or conclusion, just return headings I have mentioned above. All the headings and their content must be present in the response.\n"478 479            )480 481            # Send the message482            message = self.client.beta.threads.messages.create(483                thread_id=self.thread.id,484                role="user",485                content=full_query486            )487            logger.info("Message sent to assistant successfully")488 489            # Initialize a variable to store the final JSON response490            response_data = {}491            # Instantiate the event handler492            event_handler = EventHandler()493            # Stream the response494            with self.client.beta.threads.runs.stream(495                thread_id=self.thread.id,496                assistant_id=self.assistant_id,  # Use your existing assistant ID497                instructions="You are an AML Analyst. Be assertive in explanation. Don't use 'could', 'can'. Suggest actions instead. Your input prompt will be transaction details in XML. Analyze the details and explain the transaction in context of the document and categorize it in Scenario strictly within document mentioned scenarios only, but don't mention it is coming from the document explicitly in response. Just mention which Scenario the transaction lies in and its explanation with quoted transaction in the same paragraph where explanation is.",498                event_handler=event_handler,499            ) as stream:500                stream.until_done()  # Complete streaming501 502            response_data = event_handler.get_final_response()503            logger.info(f"Response received from assistant: {response_data}")504 505            return response_data506 507        except Exception as e:508            logger.error(f"Error sending message to assistant: {e}")509            raise510 511    def send_message_direct(self, transaction_data, scenario, prev_alerts, kyc_profile, str_scenarios_history):512        """513        Send transaction details directly to OpenAI's latest model without threads or agents.514        """515        try:516            logger.info(f"Sending direct request to OpenAI for transaction: {transaction_data}")517 518            # Handle None or empty values519            scenario = scenario or "Unknown Scenario"520            transaction_data = transaction_data or "No transaction data available"521            prev_alerts = prev_alerts if prev_alerts is not None else 0522            str_scenarios_history = str_scenarios_history or "No previous STR scenarios"523            kyc_profile = kyc_profile or "KYC profile not available"524 525            # Construct the query (same format you had for the threaded version)526            full_query = (527                f"You are an AML analyst. An alert has been raised regarding potential suspicious activity involving the scenario: {scenario}.\n\n"528                "Conclusion Up Front\n"529                "- Provide a clear conclusion on whether the case can be closed or warrants escalation/STR.\n"530                "- Include a brief rationale highlighting red flags or justifications for closure.\n\n"531 532                "Initial Risk\n"533                f"- Alerted Scenario / Rule ID: {scenario}\n\n"534 535                "Reviewed Period\n"536                "- Define the transaction review period based on the data provided.\n\n"537 538                "Transaction Review\n"539                f"- Transaction Data:\n{transaction_data}\n"540                "- Summarize the number of transactions, total values in USD, dates, involved parties, locations, and any remarks indicating suspicious behavior.\n\n"541 542                "Research on Focus\n"543                f"- KYC Profile:\n{kyc_profile}\n"544                "- Include Name, CIF, ID, DOB/Age, Country, Employment, Job Title, Monthly Income, Risk Level, Relationship Start Date, and any name screening results.\n\n"545 546                "Research on Parties / Counterparties Involved\n"547                "- Assess the profile of counterparties involved in the transactions.\n"548                "- Include any name screening or risk database results.\n\n"549 550                "History Related Cases\n"551                f"- Alerts raised Previously: {prev_alerts}\n"552                f"- STR raised Scenarios: {str_scenarios_history}\n"553                "- Summarize only if there are relevant concerns or patterns found.\n\n"554 555                "RFI / Enquiry Branch Observation\n"556                "- Summarize any branch or RM input, if available.\n\n"557 558                "Risk Search\n"559                "- Detail outcomes of internal or external screening and risk searches.\n"560                "- Mention any matches with internal watchlists, fraud portals, or regulatory flags.\n\n"561 562                "At the end no need for suggestion or conclusion, just return headings I have mentioned above. All the headings and their content must be present in the response.\n"563            )564 565            # Direct API call566            response = self.client.chat.completions.create(567                model="gpt-5-mini",  # or "gpt-4.1-mini" / "gpt-4.1-turbo" depending on your need568                messages=[569                    {"role": "system", "content": "You are an AML Analyst. Be assertive in explanation. Don't use 'could', 'can'. Suggest actions instead."},570                    {"role": "user", "content": full_query}571                ],572                temperature=0,   # deterministic output573            )574 575            # Extract the text output576            answer = response.choices[0].message.content577            logger.info(f"Direct response received: {answer}")578 579            return answer580 581        except Exception as e:582            logger.error(f"Error in send_message_direct: {e}")583            raise584    585    # convert text analysis to html586    def convert_to_html(self, response):587        # Placeholder conversion function (e.g., using an HTML template library)588        # Convert response content to HTML as needed589        return f"<html><body>{response}</body></html>"590    591    # store analysis in the database592    def store_analysis_data(self, df, responses, table_name):593        # Skip database operations if engine is not available594        if self.engine is None:595            logger.info(f"Database not configured. Skipping storage to table {table_name}")596            return True  # Return True to indicate "success" (no error, just skipped)597        598        # Prepare data for storage599        data_to_store = []600        for i, response in enumerate(responses):601            # Convert response to HTML format602            html_analysis = self.convert_to_html(response)603 604            # Gather necessary fields from df and the HTML response - safely get values with defaults605            data_to_store.append({606                "AlertID": df.get('AlertID', pd.Series([0])).iloc[i] if 'AlertID' in df.columns else 0,607                "FocusColumnValue": df.get('FocusColumnValue', pd.Series([""])).iloc[i] if 'FocusColumnValue' in df.columns else "",608                "html_analysis": html_analysis609            })610 611        # Convert the data to a DataFrame to store in the database612        store_df = pd.DataFrame(data_to_store)613        try:614            self.insert_data(table_name, store_df)615            return True616        except Exception as e:617            logger.error(f"Error storing data: {e}")618            return False619 620    # insert the data into database621    def insert_data(self, table_name, df):622        try:623            # Skip database operations if engine is not available624            if self.engine is None:625                logger.info(f"Database not configured. Skipping insert into table {table_name}")626                return627            628            # Insert the DataFrame into the specified table in the database629            df.to_sql(table_name, con=self.engine, if_exists='append', index=False)630            logger.info(f"Data inserted into table {table_name}")631        except Exception as e:632            logger.error(f"Error inserting data into {table_name}: {e}")633            raise634 635 636    # run the analysis637    def get_analysis(self):638        try:639            logger.info("Running analysis")640 641            data_table_name = DATA_TABLE642            predicition_flag = PREDICTION_FLAG643            store_table_name = ANALYSIS_TABLE644            df = self.query_data(data_table_name, predicition_flag)645            responses = []646            for i in range(len(df)):647                kyc_info = self.return_kyc_profile(df.iloc[i])648 649                # Send the full query to the assistant - safely get values with defaults650                response = self.send_message_to_assistant(651                    df.get('FilteredTransactions', pd.Series([None])).iloc[i] if 'FilteredTransactions' in df.columns else None,652                    df.get('ScenarioName', pd.Series([None])).iloc[i] if 'ScenarioName' in df.columns else None,653                    df.get('STRCount', pd.Series([0])).iloc[i] if 'STRCount' in df.columns else 0,654                    kyc_info,655                    df.get('STRScenarioHistory', pd.Series([""])).iloc[i] if 'STRScenarioHistory' in df.columns else ""656                )657                responses.append(response)658            659            # Store the responses in the table660            success = self.store_analysis_data(df, responses, store_table_name)661            # Return a JSON response based on the success662            if success:663                logger.info("Analysis completed and stored successfully")664 665                return {"status": 200, "message": "Data stored successfully"}666            else:667                logger.error("Failed to store analysis data")668 669                return {"status": "error", "message": "Failed to store data"}670            671        except Exception as e:672            logger.error(f"Error running analysis: {e}")673            return {"status": "error", "message": str(e)}674 675    676    # return the analysis in json677    def get_analysis_json(self, json_input):678        try:679            logger.info("Processing JSON input for analysis")680 681            df = json_input682            responses = []683            for i in range(len(df)):684                kyc_info = self.return_kyc_profile(df.iloc[i])685 686                # Send the full query to the assistant - safely get values with defaults687                response = self.send_message_direct(688                    df.get('FilteredTransactions', pd.Series([None])).iloc[i] if 'FilteredTransactions' in df.columns else None,689                    df.get('ScenarioName', pd.Series([None])).iloc[i] if 'ScenarioName' in df.columns else None,690                    df.get('STRCount', pd.Series([0])).iloc[i] if 'STRCount' in df.columns else 0,691                    kyc_info,692                    df.get('STRScenarioHistory', pd.Series([""])).iloc[i] if 'STRScenarioHistory' in df.columns else ""693                )694                logger.info(f"Response from assistant: {response}")695 696 697                cleaned_response = response.replace("Text(annotations=[], value='**')", "") if response else "Analysis not available"698 699                responses.append({700                    "AlertID": int(df.get('AlertID', pd.Series([0])).iloc[i]) if 'AlertID' in df.columns else 0,701                    "FocusColumnValue": str(df.get('FocusColumnValue', pd.Series([""])).iloc[i]) if 'FocusColumnValue' in df.columns else "",702                    "analysis": cleaned_response703                })704            705            logger.info("Returning analysis in JSON format")706 707            # Return the responses as a list of JSON objects708            return {"status": 200, "message": "Success", "data": responses}709        except Exception as e:710            logger.error(f"Error processing JSON input for analysis: {e}")711 712            # Return an error JSON response if conversion fails713            return {"status": "error", "message": str(e)}714 715    # return the analysis in json using local LLM (non-streaming) - OPTIMIZED716    def get_analysis_json_local_llm(self, json_input):717        try:718            logger.info("Processing JSON input for analysis using local LLM (OPTIMIZED)")719 720            df = json_input721            responses = []722            723            # Use global LLM instance for better performance724            local_llm = get_global_llm()725            726            for i in range(len(df)):727                kyc_info = self.return_kyc_profile(df.iloc[i])728 729                # Use cached template for faster generation - safely get values with defaults730                full_query = get_analysis_template(731                    df.get('ScenarioName', pd.Series([None])).iloc[i] if 'ScenarioName' in df.columns else None,732                    kyc_info,733                    df.get('FilteredTransactions', pd.Series([None])).iloc[i] if 'FilteredTransactions' in df.columns else None,734                    df.get('STRCount', pd.Series([0])).iloc[i] if 'STRCount' in df.columns else 0,735                    df.get('STRScenarioHistory', pd.Series([""])).iloc[i] if 'STRScenarioHistory' in df.columns else ""736                )737 738                # Use local LLM to generate response (non-streaming) with optimized settings739                response = local_llm.generate_response(740                    full_query, 741                    stream=False,742                    temperature=0.1,  # Lower temperature for faster, more deterministic responses743                    top_p=0.9,744                    num_predict=150  # Limit response length for speed745                )746                logger.info(f"Response from local LLM: {response[:100] if response else 'No response'}...")  # Log only first 100 chars747 748                cleaned_response = response.replace("Text(annotations=[], value='**')", "") if response else "Analysis not available"749 750                responses.append({751                    "AlertID": int(df.get('AlertID', pd.Series([0])).iloc[i]) if 'AlertID' in df.columns else 0,752                    "FocusColumnValue": str(df.get('FocusColumnValue', pd.Series([""])).iloc[i]) if 'FocusColumnValue' in df.columns else "",753                    "analysis": cleaned_response754                })755            756            logger.info("Returning analysis in JSON format using local LLM (OPTIMIZED)")757 758            return {"status": 200, "message": "Success", "data": responses}759        except Exception as e:760            logger.error(f"Error processing JSON input for analysis using local LLM: {e}")761            return {"status": "error", "message": str(e)}762 763    # return the analysis in json using local LLM (streaming) - OPTIMIZED764    def get_analysis_json_local_llm_streaming(self, json_input):765        try:766            logger.info("Processing JSON input for analysis using local LLM (streaming OPTIMIZED)")767 768            df = json_input769            770            # Use global LLM instance for better performance771            local_llm = get_global_llm()772            773            for i in range(len(df)):774                kyc_info = self.return_kyc_profile(df.iloc[i])775 776                # Use cached template for faster generation - safely get values with defaults777                full_query = get_analysis_template(778                    df.get('ScenarioName', pd.Series([None])).iloc[i] if 'ScenarioName' in df.columns else None,779                    kyc_info,780                    df.get('FilteredTransactions', pd.Series([None])).iloc[i] if 'FilteredTransactions' in df.columns else None,781                    df.get('STRCount', pd.Series([0])).iloc[i] if 'STRCount' in df.columns else 0,782                    df.get('STRScenarioHistory', pd.Series([""])).iloc[i] if 'STRScenarioHistory' in df.columns else ""783                )784 785                # Use local LLM to generate streaming response with optimized settings786                response_stream = local_llm.generate_response(787                    full_query, 788                    stream=True,789                    temperature=0.1,  # Lower temperature for faster responses790                    top_p=0.9,791                    num_predict=150  # Limit response length for speed792                )793                794                # Yield the streaming response795                yield {796                    "AlertID": int(df.get('AlertID', pd.Series([0])).iloc[i]) if 'AlertID' in df.columns else 0,797                    "FocusColumnValue": str(df.get('FocusColumnValue', pd.Series([""])).iloc[i]) if 'FocusColumnValue' in df.columns else "",798                    "analysis": response_stream799                }800            801            logger.info("Completed streaming analysis using local LLM (OPTIMIZED)")802 803        except Exception as e:804            logger.error(f"Error processing JSON input for analysis using local LLM (streaming): {e}")805            yield {"status": "error", "message": str(e)}806 807    # return the analysis in json using hybrid ultra-fast system (MILLISECOND RESPONSES)808    def get_analysis_json_ultra_fast(self, json_input):809        try:810            logger.info("Processing JSON input for analysis using HYBRID ULTRA-FAST system")811 812            df = json_input813            responses = []814            815            # Use hybrid system for ultra-fast responses816            hybrid_system = get_hybrid_system()817            818            for i in range(len(df)):819                # Convert DataFrame row to dictionary820                alert_data = df.iloc[i].to_dict()821                822                # Generate ultra-fast analysis823                result = hybrid_system.analyze_ultra_fast(alert_data)824                825                responses.append({826                    "AlertID": result["AlertID"],827                    "FocusColumnValue": result["FocusColumnValue"],828                    "analysis": result["analysis"],829                    "response_time_ms": result["response_time_ms"],830                    "risk_level": result["risk_level"],831                    "method": result["method"]832                })833            834            logger.info(f"Returning ULTRA-FAST analysis in JSON format - Average response time: {sum(r['response_time_ms'] for r in responses)/len(responses):.2f}ms")835 836            return {"status": 200, "message": "Success", "data": responses}837        except Exception as e:838            logger.error(f"Error processing JSON input for ULTRA-FAST analysis: {e}")839            return {"status": "error", "message": str(e)}840 841    # return the analysis in json using hybrid ultra-fast system (streaming)842    def get_analysis_json_ultra_fast_streaming(self, json_input):843        try:844            logger.info("Processing JSON input for analysis using HYBRID ULTRA-FAST streaming")845 846            df = json_input847            848            # Use hybrid system for ultra-fast responses849            hybrid_system = get_hybrid_system()850            851            for i in range(len(df)):852                # Convert DataFrame row to dictionary853                alert_data = df.iloc[i].to_dict()854                855                # Generate ultra-fast analysis856                result = hybrid_system.analyze_ultra_fast(alert_data)857                858                # Yield the ultra-fast streaming response859                yield {860                    "AlertID": result["AlertID"],861                    "FocusColumnValue": result["FocusColumnValue"],862                    "analysis": result["analysis"],863                    "response_time_ms": result["response_time_ms"],864                    "risk_level": result["risk_level"],865                    "method": result["method"]866                }867            868            logger.info("Completed ULTRA-FAST streaming analysis")869 870        except Exception as e:871            logger.error(f"Error processing JSON input for ULTRA-FAST streaming analysis: {e}")872            yield {"status": "error", "message": str(e)}873 874    # return the analysis in json using hybrid template+LLM system (TEMPLATES FOR DATA + LLM FOR ANALYSIS)875    def get_analysis_json_hybrid_template_llm(self, json_input, use_cloud: bool = False, llm_on_server: bool = False, url: str = "", evaluation: bool = False, audit: bool = False):876        try:877            method_name = "HYBRID TEMPLATE+LLM (CLOUD)" if use_cloud else "HYBRID TEMPLATE+LLM"878            logger.info(f"Processing JSON input for analysis using {method_name} system")879 880            df = json_input881            responses = []882 883            # Use hybrid template+LLM system884            hybrid_system = get_template_llm_system()885 886            for i in range(len(df)):887                # Convert DataFrame row to dictionary888                alert_data = df.iloc[i].to_dict()889 890                # Generate hybrid analysis (templates for data + LLM for analysis)891                # Use cloud method if flag is set, otherwise use local method892                if use_cloud:893                    result = hybrid_system.analyze_hybrid_cloud(alert_data, evaluation=evaluation, audit=audit)894                else:895                    result = hybrid_system.analyze_hybrid(alert_data, llm_on_server=llm_on_server, url=url, evaluation=evaluation, audit=audit)896 897                item = {898                    "AlertID": result["AlertID"],899                    "FocusColumnValue": result["FocusColumnValue"],900                    "analysis": result["analysis"],901                    "response_time_ms": result["response_time_ms"],902                    "method": result["method"],903                    "model": result.get("model", "N/A")904                }905                if result.get("thinking") is not None:906                    item["thinking"] = result["thinking"]907                responses.append(item)908 909            logger.info(f"Returning {method_name} analysis in JSON format - Average response time: {sum(r['response_time_ms'] for r in responses)/len(responses):.2f}ms")910 911            return {"status": 200, "message": "Success", "data": responses}912        except Exception as e:913            logger.error(f"Error processing JSON input for {method_name} analysis: {e}")914            return {"status": "error", "message": str(e)}915 916    # return the analysis in json using hybrid template+LLM system (streaming)917    def get_analysis_json_hybrid_template_llm_streaming(self, json_input):918        try:919            logger.info("Processing JSON input for analysis using HYBRID TEMPLATE+LLM streaming")920 921            df = json_input922 923            # Use hybrid template+LLM system924            hybrid_system = get_template_llm_system()925 926            for i in range(len(df)):927                # Convert DataFrame row to dictionary928                alert_data = df.iloc[i].to_dict()929 930                # Generate hybrid analysis (templates for data + LLM for analysis)931                result = hybrid_system.analyze_hybrid(alert_data)932 933                # Yield the hybrid streaming response934                yield {935                    "AlertID": result["AlertID"],936                    "FocusColumnValue": result["FocusColumnValue"],937                    "analysis": result["analysis"],938                    "response_time_ms": result["response_time_ms"],939                    "method": result["method"],940                    "model": result.get("model", "N/A")941                }942 943            logger.info("Completed HYBRID TEMPLATE+LLM streaming analysis")944 945        except Exception as e:946            logger.error(f"Error processing JSON input for HYBRID TEMPLATE+LLM streaming analysis: {e}")947            yield {"status": "error", "message": str(e)}948 949# api route for generating analysis and returning json response950@router.post(951    "/api/ai-service/generateamlanalysisoai",952    response_model=AnalysisResponse,953    summary="Generate AML Analysis (OpenAI)",954    description="Generate AML analysis using OpenAI GPT models. Requires OPENAI_API_KEY and ASSISTANT_ID configuration."955)956async def generate_analysis(alert_data: Union[AnalysisAlertRequest, List[AnalysisAlertRequest]]):957    try:958        logger.info("API call to /generateamlanalysisoai started")959 960        # Handle both single object and array of objects961        if isinstance(alert_data, list):962            json_data = [item.model_dump(exclude_none=True) for item in alert_data]963            df = pd.DataFrame(json_data)964        else:965            json_data = alert_data.model_dump(exclude_none=True)966            df = pd.DataFrame([json_data])967        db_connection_str = DB_CONNECTION_STR968        api_key = OPENAI_API_KEY969        client = OpenAI(api_key=api_key)970        analysis = AnalysisProcessor(db_connection_str, client, ASSISTANT_ID)971        972        logger.info("API call to /generateamlanalysisoai completed successfully")973 974        return analysis.get_analysis_json(df)975    976    except ValidationError as e:977        # Handle Pydantic validation errors and return an appropriate response978        logger.error(f"Error in /generateamlanalysisoai validation error: {e.errors()}")979        raise HTTPException(status_code=422, detail=f"Validation error: {e.errors()}")980    except Exception as e:981        logger.error(f"Error in /generateamlanalysisoai: {e}")982        raise HTTPException(status_code=500, detail="An error occurred while generating AML analysis using oai")983 984 985# Extended model for hybrid analysis with Cloud flags986class AnalysisAlertRequestWithFlags(AnalysisAlertRequest):987    Cloud: Optional[bool] = Field(False, description="Use cloud LLM (OpenRouter) instead of local Ollama. Set to true to use OpenRouter API.", example=False)988    llm_on_server: Optional[bool] = Field(False, description="Use LLM on remote server. Set to true to use a remote Ollama server instead of local.", example=False)989    url: Optional[str] = Field("", description="URL of remote Ollama server. Required if llm_on_server is true. Example: http://remote-ollama:11434", example="http://remote-ollama:11434")990    anonymous: Optional[bool] = Field(False, description="Mask PII (Personally Identifiable Information) in the data. When true, PII columns specified in PII_COLUMNS environment variable will be masked.", example=False)991    evaluation: Optional[bool] = Field(False, description="When true, evaluate the generated analysis and fix mistakes using an evaluator agent (cloud or local/remote Ollama).", example=False)992    audit: Optional[bool] = Field(False, description="When true, use a thinking model for generation and include 'thinking' in the response (cloud: OPENROUTER_THINKING_MODEL, local: OLLAMA_THINKING_MODEL).", example=False)993    994    class Config:995        json_schema_extra = {996            "example": {997                "AlertID": 1,998                "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}]',999                "FocusColumnValue": "100001",1000                "KYCMonthlyIncome": "85,000 PKR",1001                "KYCNoOfCredits": "3-5",1002                "KYCNoOfDebits": "8-12",1003                "KYCRiskCategoryValue": "Low",1004                "KYCValueOfCredits": "150,000 - 200,000 PKR",1005                "KYCValueOfDebits": "80,000 - 120,000 PKR",1006                "OccupationValue": "Private Employee",1007                "STRCount": 0,1008                "STRScenarioHistory": "",1009                "ScenarioName": "Unusually large installment",1010                "CustomerName": "Muhammad Bilal Sheikh",1011                "CUSTOMERID": "100001",1012                "BranchID": "KHI-DHA",1013                "Country": "Pakistan",1014                "CustomerType": "Retail",1015                "CustomerStatus": "Retail Customer",1016                "CreatedDate": "2020-01-15",1017                "RelationshipStartDate": "2020-01-15",1018                "RiskScore": "4.2",1019                "PreviousAlerts": [],1020                "Counterparties": [],1021                "BranchQueries": {1022                    "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.",1023                    "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."1024                },1025                "Cloud": False,1026                "llm_on_server": False,1027                "url": "",1028                "anonymous": False,1029                "evaluation": False,1030                "audit": False1031            }1032        }1033 1034# api route for generating analysis and returning json response using HYBRID TEMPLATE+LLM system (TEMPLATES FOR DATA + LLM FOR ANALYSIS)1035@router.post(1036    "/api/ai-service/generateamlanalysis",1037    response_model=AnalysisResponse,1038    summary="Generate AML Analysis (Hybrid Template+LLM)",1039    description="""1040    Generate AML analysis using Hybrid Template+LLM system.1041    1042    **LLM Options:**1043    - **Local LLM (default)**: Set `Cloud: false` and `llm_on_server: false` - Uses local Ollama1044    - **Cloud LLM**: Set `Cloud: true` - Uses OpenRouter API (requires OPENROUTER_API_KEY)1045    - **Remote LLM**: Set `llm_on_server: true` and provide `url` - Uses remote Ollama server1046    1047    **Note**: Only one LLM option should be enabled at a time. If multiple are set, priority is: Cloud > llm_on_server > Local1048    1049    **PII Masking:**1050    - Set `anonymous: true` to mask Personally Identifiable Information (PII) in the data1051    - PII values are masked by keeping first 2 and last 2 characters, replacing middle with `***`1052    - Example: `"Muhammad Bilal Sheikh"` → `"Mu***kh"`, `"100001"` → `"10***01"`1053    - Masking applies to direct fields and nested structures (FilteredTransactions, PreviousAlerts, Counterparties)1054    - PII columns are configurable via `PII_COLUMNS` environment variable (comma-separated list)1055    - Default PII columns: `CustomerName`, `CUSTOMERID`, `FocusColumnValue`, `IDENTITYNUMBERS`, `ACCOUNTID`, `LOANID`1056    - The masking function handles all string lengths appropriately1057    1058    **Evaluation (evaluation: true):**1059    - When `evaluation: true`, the generated report is evaluated by an evaluator agent (cloud or local/remote Ollama)1060    - The LLM then decides whether corrections are needed; if YES, a fix step is applied; if NO, the original report is returned1061    - Works with both Cloud and Local/Remote LLM1062    1063    **Audit (audit: true):**1064    - When `audit: true`, a thinking model is used for generation (cloud: OPENROUTER_THINKING_MODEL, default moonshotai/kimi-k2-thinking; local: OLLAMA_THINKING_MODEL, default deepseek-r1:8b)1065    - The response includes an optional `thinking` field with the model's reasoning/reasoning_content when available1066    """,1067    response_description="Analysis results with generated AML report; optional 'thinking' when audit=true"1068)1069async def generate_aml_analysis(alert_data: Union[AnalysisAlertRequestWithFlags, List[AnalysisAlertRequestWithFlags]]):1070    try:1071        logger.info("API call to /generateamlanalysis started")1072 1073        # Extract flags and handle both single object and array1074        use_cloud = False1075        llm_on_server = False1076        url = ""1077        anonymous = False1078        evaluation = False1079        audit = False1080        _exclude = {'Cloud', 'llm_on_server', 'url', 'anonymous', 'evaluation', 'audit'}1081        1082        if isinstance(alert_data, list):1083            if len(alert_data) > 0:1084                use_cloud = alert_data[0].Cloud if alert_data[0].Cloud is not None else False1085                llm_on_server = alert_data[0].llm_on_server if alert_data[0].llm_on_server is not None else False1086                url = alert_data[0].url if alert_data[0].url else ""1087                anonymous = alert_data[0].anonymous if alert_data[0].anonymous is not None else False1088                evaluation = alert_data[0].evaluation if getattr(alert_data[0], 'evaluation', None) is not None else False1089                audit = alert_data[0].audit if getattr(alert_data[0], 'audit', None) is not None else False1090            json_data = [item.model_dump(exclude_none=True, exclude=_exclude) for item in alert_data]1091            1092            # Apply PII masking if anonymous flag is true1093            if anonymous:1094                json_data = mask_pii_data(json_data, PII_COLUMNS)1095            1096            df = pd.DataFrame(json_data)1097        else:1098            use_cloud = alert_data.Cloud if alert_data.Cloud is not None else False1099            llm_on_server = alert_data.llm_on_server if alert_data.llm_on_server is not None else False1100            url = alert_data.url if alert_data.url else ""1101            anonymous = alert_data.anonymous if alert_data.anonymous is not None else False1102            evaluation = alert_data.evaluation if getattr(alert_data, 'evaluation', None) is not None else False1103            audit = alert_data.audit if getattr(alert_data, 'audit', None) is not None else False1104            json_data = alert_data.model_dump(exclude_none=True, exclude=_exclude)1105            1106            # Apply PII masking if anonymous flag is true1107            if anonymous:1108                json_data = mask_pii_data(json_data, PII_COLUMNS)1109            1110            df = pd.DataFrame([json_data])1111        # Use None for database connection since it's not needed for hybrid template LLM1112        db_connection_str = None1113        analysis = AnalysisProcessor(db_connection_str, None, None)1114        1115        logger.info(f"API call to /generateamlanalysis completed successfully (Cloud: {use_cloud}, llm_on_server: {llm_on_server}, url: {url}, anonymous: {anonymous}, evaluation: {evaluation}, audit: {audit})")1116 1117        return analysis.get_analysis_json_hybrid_template_llm(df, use_cloud=use_cloud, llm_on_server=llm_on_server, url=url, evaluation=evaluation, audit=audit)1118 1119    except ValidationError as e:1120        logger.error(f"Error in /generateamlanalysis validation error: {e.errors()}")1121        raise HTTPException(status_code=422, detail=f"Validation error: {e.errors()}")1122    except Exception as e:1123        logger.error(f"Error in /generateamlanalysis: {e}")1124        raise HTTPException(status_code=500, detail="An error occurred while generating analysis")1125 1126# api route for generating analysis and returning streaming json response using HYBRID TEMPLATE+LLM system1127@router.post("/api/ai-service/generateamlanalysisstreaming")1128async def generate_aml_analysis_streaming(alert_data: Union[AnalysisAlertRequest, List[AnalysisAlertRequest]]):1129    try:1130        logger.info("API call to /generateamlanalysisstreaming started")1131 1132        # Handle both single object and array of objects1133        if isinstance(alert_data, list):1134            json_data = [item.model_dump(exclude_none=True) for item in alert_data]1135            df = pd.DataFrame(json_data)1136        else:1137            json_data = alert_data.model_dump(exclude_none=True)1138            df = pd.DataFrame([json_data])1139        # Use None for database connection since it's not needed for hybrid template LLM1140        db_connection_str = None1141        analysis = AnalysisProcessor(db_connection_str, None, None)1142        1143        logger.info("API call to /generateamlanalysisstreaming completed successfully")1144 1145        # Create a generator function for hybrid template+LLM streaming response1146        async def generate_hybrid_template_llm_stream():1147            try:1148                for response_chunk in analysis.get_analysis_json_hybrid_template_llm_streaming(df):1149                    yield f"data: {json.dumps(response_chunk)}\n\n"1150            except Exception as e:1151                logger.error(f"Error in hybrid template+LLM streaming response: {e}")1152                yield f"data: {json.dumps({'status': 'error', 'message': str(e)})}\n\n"1153 1154        return StreamingResponse(1155            generate_hybrid_template_llm_stream(),1156            media_type="text/plain",1157            headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}1158        )1159    1160    except ValidationError as e:1161        logger.error(f"Error in /generateamlanalysisstreaming validation error: {e.errors()}")1162        raise HTTPException(status_code=422, detail=f"Validation error: {e.errors()}")1163    except Exception as e:1164        logger.error(f"Error in /generateamlanalysisstreaming: {e}")1165        raise HTTPException(status_code=500, detail="An error occurred while generating analysis streaming")1166 1167