Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python | |
| """ | |
| Fraud Model Explainability Assistant - Strands Agents | |
| An AI-powered assistant that helps fraud analysts and executives understand | |
| why specific applications were flagged as fraudulent, translating complex | |
| model outputs into actionable insights. | |
| Author: Fraud Model Data Science Team | |
| Use Cases: | |
| - Executive briefings on fraud decisions | |
| - Fair lending compliance documentation | |
| - Analyst investigation support | |
| - Model decision audit trails | |
| Production-Ready Confluence Integration (FastAPI Version) | |
| Features: | |
| - Comprehensive logging and monitoring | |
| - Error handling and recovery | |
| - Scheduled re-ingestion for keeping data fresh | |
| - Performance metrics tracking | |
| - FastAPI + uvicorn for Docker deployment | |
| Prerequisites: | |
| - Configure .env with Confluence credentials | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import warnings | |
| import logging | |
| import time | |
| import base64 | |
| from functools import lru_cache | |
| from typing import Optional, List | |
| from datetime import datetime | |
| # Suppress ResourceWarning for cleaner output | |
| warnings.filterwarnings("ignore", category=ResourceWarning) | |
| os.environ["PYTHONWARNINGS"] = "ignore::ResourceWarning" | |
| # Load environment variables from .env file | |
| try: | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| except ImportError: | |
| print("⚠ Warning: python-dotenv not installed. Install with: pip install python-dotenv") | |
| print(" Environment variables must be set manually.") | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.responses import HTMLResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from strands import Agent | |
| from strands.agent.conversation_manager import SlidingWindowConversationManager | |
| from strands.models.openai import OpenAIModel | |
| from strands.models.openai import OpenAIModel | |
| from strands.handlers.callback_handler import PrintingCallbackHandler | |
| # Telemetry | |
| from telemetry import setup_telemetry | |
| setup_telemetry() | |
| # Import confluence-ingestor | |
| from confluence_ingestor import ConfluenceRAG | |
| from confluence_ingestor.adapters.strands import ( | |
| create_confluence_search_tool, | |
| create_confluence_loader_tool, | |
| ) | |
| # Import your existing fraud tools | |
| from utils import ( | |
| get_application_summary, | |
| explain_fraud_score, | |
| compare_to_population, | |
| check_fair_lending_flags, | |
| get_identity_network, | |
| get_model_performance, | |
| SYSTEM_PROMPT as ORIGINAL_PROMPT, | |
| ) | |
| # ============================================================================= | |
| # LOGGING CONFIGURATION | |
| # ============================================================================= | |
| # Configure the root strands logger | |
| logging.getLogger("strands").setLevel(logging.DEBUG) | |
| # Add a handler to see the logs | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", | |
| handlers=[ | |
| logging.FileHandler('fraud_assistant_confluence.log'), | |
| logging.StreamHandler() | |
| ] | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # ============================================================================= | |
| # METRICS TRACKING | |
| # ============================================================================= | |
| class ConfluenceMetrics: | |
| """Track Confluence integration performance metrics.""" | |
| def __init__(self): | |
| self.search_count = 0 | |
| self.cache_hits = 0 | |
| self.cache_misses = 0 | |
| self.errors = 0 | |
| self.last_ingestion = None | |
| self.query_times = [] | |
| def record_search(self, cached: bool = False, duration: float = 0.0): | |
| """Record a search query.""" | |
| self.search_count += 1 | |
| if cached: | |
| self.cache_hits += 1 | |
| else: | |
| self.cache_misses += 1 | |
| self.query_times.append(duration) | |
| def record_error(self): | |
| """Record an error.""" | |
| self.errors += 1 | |
| def record_ingestion(self): | |
| """Record a data ingestion.""" | |
| self.last_ingestion = datetime.now() | |
| def get_stats(self) -> dict: | |
| """Get current metrics.""" | |
| return { | |
| "total_searches": self.search_count, | |
| "cache_hit_rate": ( | |
| self.cache_hits / self.search_count | |
| if self.search_count > 0 | |
| else 0.0 | |
| ), | |
| "avg_query_time": ( | |
| sum(self.query_times) / len(self.query_times) | |
| if self.query_times | |
| else 0.0 | |
| ), | |
| "errors": self.errors, | |
| "last_ingestion": str(self.last_ingestion) if self.last_ingestion else None, | |
| } | |
| # Global metrics instance | |
| _metrics = ConfluenceMetrics() | |
| # ============================================================================= | |
| # CONFLUENCE INITIALIZATION | |
| # ============================================================================= | |
| _confluence_rag: Optional[ConfluenceRAG] = None | |
| def init_confluence(): | |
| """Initialize Confluence RAG with logging and metrics.""" | |
| global _confluence_rag | |
| if _confluence_rag is None: | |
| logger.info("Initializing Confluence integration...") | |
| required_vars = ["CONFLUENCE_URL", "CONFLUENCE_EMAIL", "CONFLUENCE_API_TOKEN"] | |
| missing_vars = [var for var in required_vars if not os.getenv(var)] | |
| if missing_vars: | |
| error_msg = f"Missing required environment variables: {', '.join(missing_vars)}" | |
| logger.error(error_msg) | |
| print(f"\n❌ ERROR: {error_msg}") | |
| print("\n📝 Setup Instructions:") | |
| print(" 1. Copy .env.example to .env:") | |
| print(" cp .env.example .env") | |
| print(" 2. Edit .env with your Confluence credentials") | |
| print(" 3. See CONFLUENCE_SETUP_GUIDE.md for detailed setup instructions") | |
| print("\n⚠ App will run WITHOUT Confluence integration.\n") | |
| raise ValueError(f"Missing Confluence credentials: {', '.join(missing_vars)}") | |
| try: | |
| _confluence_rag = ConfluenceRAG.from_env( | |
| embedding_provider="huggingface", | |
| vector_store_type="chroma", | |
| ) | |
| spaces = { | |
| "Acquisitio": 10, | |
| } | |
| for space_key, max_pages in spaces.items(): | |
| try: | |
| logger.info(f"Ingesting Confluence space: {space_key}") | |
| stats = _confluence_rag.ingest_space( | |
| space_key, max_pages=max_pages, force=False | |
| ) | |
| if stats.get("skipped"): | |
| logger.info(f"{space_key}: {stats['reason']}") | |
| print(f" ✓ {space_key}: {stats['reason']}") | |
| else: | |
| logger.info(f"{space_key}: Ingested {stats['pages']} pages") | |
| print(f" ✓ {space_key}: {stats['pages']} pages indexed") | |
| try: | |
| from confluence_ingestor import ConfluenceClient | |
| client = ConfluenceClient.from_env() | |
| pages = client.load_space(space_key, max_pages=max_pages) | |
| if pages: | |
| logger.info(f"Pages in {space_key} ({len(pages)} total):") | |
| print(f" 📄 Pages in {space_key}:") | |
| for i, page in enumerate(pages, 1): | |
| logger.info(f" {i}. {page.title} (ID: {page.page_id})") | |
| print(f" {i}. {page.title}") | |
| else: | |
| logger.warning(f"No pages found in space: {space_key}") | |
| except Exception as page_list_error: | |
| logger.warning(f"Could not retrieve page titles for {space_key}: {page_list_error}") | |
| except Exception as e: | |
| logger.error(f"Failed to ingest {space_key}: {e}") | |
| print(f" ⚠ {space_key}: Failed - {e}") | |
| _metrics.record_error() | |
| _metrics.record_ingestion() | |
| logger.info("Confluence integration ready!") | |
| print("✅ Confluence integration ready!") | |
| except Exception as e: | |
| logger.error(f"Confluence initialization failed: {e}") | |
| _metrics.record_error() | |
| raise | |
| return _confluence_rag | |
| # ============================================================================= | |
| # SCHEDULED RE-INGESTION | |
| # ============================================================================= | |
| def setup_scheduled_ingestion(): | |
| """Set up scheduled Confluence re-ingestion for keeping data fresh.""" | |
| try: | |
| from apscheduler.schedulers.background import BackgroundScheduler | |
| except ImportError: | |
| logger.warning("apscheduler not installed. Scheduled ingestion disabled.") | |
| logger.warning("Install with: pip install apscheduler") | |
| return None | |
| def refresh_confluence(): | |
| """Re-ingest Confluence spaces to pick up new content.""" | |
| logger.info("Starting scheduled Confluence re-ingestion...") | |
| try: | |
| rag = init_confluence() | |
| spaces = ["Acquisitio"] | |
| for space in spaces: | |
| logger.info(f"Re-ingesting {space}...") | |
| stats = rag.ingest_space(space, max_pages=100, force=True) | |
| logger.info(f"{space}: Updated {stats['pages']} pages") | |
| _metrics.record_ingestion() | |
| logger.info("Scheduled re-ingestion completed successfully") | |
| except Exception as e: | |
| logger.error(f"Scheduled re-ingestion failed: {e}") | |
| _metrics.record_error() | |
| scheduler = BackgroundScheduler() | |
| scheduler.add_job(refresh_confluence, 'cron', hour=2) | |
| scheduler.start() | |
| logger.info("Scheduled re-ingestion enabled (runs daily at 2 AM)") | |
| return scheduler | |
| # ============================================================================= | |
| # ENHANCED SYSTEM PROMPT | |
| # ============================================================================= | |
| ENHANCED_PROMPT = """ | |
| You are a Fraud Model Explainability Assistant for a major financial services company. | |
| Your role is to help fraud analysts, data scientists, and executives understand | |
| fraud model decisions and their implications. | |
| You have access to tools that can: | |
| 1. Retrieve application summaries and fraud scores | |
| 2. Explain why applications received specific fraud scores (SHAP-style explanations) | |
| 3. Compare applications to approved/denied populations statistically | |
| 4. Check for fair lending compliance concerns | |
| 5. Analyze identity networks and linkages | |
| 6. Show model performance metrics | |
| 7. **Search company Confluence documentation** for policies, procedures, and guidelines | |
| 8. **Load full Confluence pages** to extract specific information | |
| When answering questions: | |
| - Be precise and data-driven | |
| - Highlight the most important risk factors first | |
| - Explain technical concepts in business terms when speaking to executives | |
| - Always mention fair lending implications when relevant | |
| - Provide actionable insights, not just data | |
| **CRITICAL: How to Handle Confluence Information Requests** | |
| When users ask you to "report", "list", "find", "show", or "provide" specific information from documents: | |
| 1. **FIRST**: Use the confluence_search tool to find the relevant document and identify its title | |
| 2. **SECOND**: Use the confluence_loader tool with BOTH space_key AND page_title parameters to load that specific page | |
| 3. **THIRD**: Extract and present the requested information directly in your response from the loaded content | |
| 4. **FOURTH**: Provide the Confluence page citation/link as a reference for verification | |
| For flagged applications, structure your response as: | |
| 1. Quick summary (score, decision, risk level) | |
| 2. Top contributing factors | |
| 3. How unusual this is compared to the population | |
| 4. Any compliance considerations (extract relevant policies from Confluence, then cite sources) | |
| 5. Recommended next steps (reference procedures from playbooks if available) | |
| Remember: Your explanations may be used in regulatory examinations and audits, | |
| so be accurate and thorough. | |
| """.strip() | |
| # ============================================================================= | |
| # AGENT CREATION | |
| # ============================================================================= | |
| class FilePayload(BaseModel): | |
| data: str # Base64 encoded data | |
| format: str | |
| name: str = "file" | |
| class QuestionRequest(BaseModel): | |
| question: str | |
| files: Optional[List[FilePayload]] = None | |
| _cached_agent = None | |
| def create_enhanced_agent(): | |
| """Create fraud agent with Confluence integration.""" | |
| global _cached_agent | |
| if _cached_agent is not None: | |
| return _cached_agent | |
| openai_api_key = os.environ.get("OPENAI_API_KEY") | |
| try: | |
| rag = init_confluence() | |
| search_confluence = create_confluence_search_tool(rag=rag, k=5) | |
| load_confluence_page = create_confluence_loader_tool(max_pages=10) | |
| tools = [ | |
| get_application_summary, | |
| explain_fraud_score, | |
| compare_to_population, | |
| check_fair_lending_flags, | |
| get_identity_network, | |
| get_model_performance, | |
| search_confluence, | |
| load_confluence_page, | |
| ] | |
| system_prompt = ENHANCED_PROMPT | |
| except Exception as e: | |
| logger.error(f"Confluence initialization failed: {e}") | |
| print(f"⚠ Confluence disabled: {e}") | |
| _metrics.record_error() | |
| tools = [ | |
| get_application_summary, | |
| explain_fraud_score, | |
| compare_to_population, | |
| check_fair_lending_flags, | |
| get_identity_network, | |
| get_model_performance, | |
| ] | |
| system_prompt = ORIGINAL_PROMPT | |
| if openai_api_key: | |
| model = OpenAIModel( | |
| client_args={"api_key": openai_api_key}, | |
| model_id="gpt-4o", | |
| params={"temperature": 0.1, "max_tokens": 2048}, | |
| ) | |
| # Create a conversation manager with custom window size | |
| conversation_manager = SlidingWindowConversationManager( | |
| window_size=20, # Maximum number of messages to keep | |
| should_truncate_results=True, # Enable truncating the tool result when a message is too large for the model's context window | |
| ) | |
| # The default callback handler prints text and shows tool usage | |
| _cached_agent = Agent( | |
| model=model, | |
| system_prompt=system_prompt, | |
| tools=tools, | |
| conversation_manager=conversation_manager, | |
| callback_handler=PrintingCallbackHandler() | |
| ) | |
| else: | |
| # Create a conversation manager with custom window size | |
| conversation_manager = SlidingWindowConversationManager( | |
| window_size=20, # Maximum number of messages to keep | |
| should_truncate_results=True, # Enable truncating the tool result when a message is too large for the model's context window | |
| ) | |
| # The default callback handler prints text and shows tool usage | |
| _cached_agent = Agent( | |
| system_prompt=system_prompt, | |
| tools=tools, | |
| conversation_manager=conversation_manager, | |
| callback_handler=PrintingCallbackHandler() | |
| ) | |
| return _cached_agent | |
| def query_agent(question: str, files: Optional[List[FilePayload]] = None, return_full_result: bool = False): | |
| """Process question with the enhanced agent, optionally including files.""" | |
| try: | |
| logger.info(f"Processing query: {question}") | |
| if files: | |
| logger.info(f"Query includes {len(files)} files") | |
| agent = create_enhanced_agent() | |
| # Base text content | |
| combined_text = question | |
| # List to hold image blocks | |
| image_blocks = [] | |
| if files: | |
| try: | |
| # Import necessary types and libraries inside logic to avoid top-level failures if missing | |
| import io | |
| import csv | |
| import pypdf | |
| from strands.types.content import ImageContent | |
| from strands.types.media import ImageSource | |
| # Try import python-docx | |
| try: | |
| import docx | |
| except ImportError: | |
| docx = None | |
| logger.warning("python-docx not installed. DOCX support disabled.") | |
| image_formats = {'png', 'jpeg', 'gif', 'webp', 'jpg'} | |
| for file_obj in files: | |
| try: | |
| # Decode base64 | |
| base64_data = file_obj.data | |
| if "," in base64_data: | |
| base64_data = base64_data.split(",")[1] | |
| file_bytes = base64.b64decode(base64_data) | |
| fmt = file_obj.format.lower() | |
| if fmt in image_formats: | |
| # Handle Image - Keep as rich content | |
| image_block = ImageContent( | |
| format=fmt if fmt != 'jpg' else 'jpeg', # Normalize jpg | |
| source=ImageSource(bytes=file_bytes) | |
| ) | |
| image_blocks.append({"image": image_block}) | |
| else: | |
| # Handle Document - Extract text and append to question | |
| extracted_text = "" | |
| if fmt == 'pdf': | |
| try: | |
| pdf_reader = pypdf.PdfReader(io.BytesIO(file_bytes)) | |
| for i, page in enumerate(pdf_reader.pages): | |
| try: | |
| text = page.extract_text() | |
| if text: | |
| extracted_text += text + "\n" | |
| except Exception as page_err: | |
| logger.warning(f"Failed to extract text from page {i} of {file_obj.name}: {page_err}") | |
| extracted_text += f"[Error extracting page {i+1}]\n" | |
| if not extracted_text.strip(): | |
| extracted_text = "[No text could be extracted from this PDF. It might be an image-only PDF.]" | |
| except Exception as pdf_err: | |
| logger.error(f"PDF extraction failed for {file_obj.name}: {pdf_err}") | |
| extracted_text = f"[Error extracting PDF text for {file_obj.name}: {str(pdf_err)}]" | |
| elif fmt == 'docx': | |
| if docx: | |
| try: | |
| doc = docx.Document(io.BytesIO(file_bytes)) | |
| full_text = [] | |
| for para in doc.paragraphs: | |
| full_text.append(para.text) | |
| extracted_text = '\n'.join(full_text) | |
| if not extracted_text.strip(): | |
| extracted_text = "[No text found in this DOCX file.]" | |
| except Exception as docx_err: | |
| logger.error(f"DOCX extraction failed for {file_obj.name}: {docx_err}") | |
| extracted_text = f"[Error extracting DOCX text for {file_obj.name}: {str(docx_err)}]" | |
| else: | |
| extracted_text = "[DOCX support is not available. Please install python-docx.]" | |
| elif fmt == 'csv': | |
| try: | |
| # Decode bytes to string | |
| csv_text = file_bytes.decode('utf-8', errors='replace') | |
| csv_file = io.StringIO(csv_text) | |
| csv_reader = csv.reader(csv_file) | |
| rows = [] | |
| for row in csv_reader: | |
| rows.append(','.join(row)) | |
| extracted_text = '\n'.join(rows) | |
| if not extracted_text.strip(): | |
| extracted_text = "[Empty CSV file.]" | |
| except Exception as csv_err: | |
| logger.error(f"CSV extraction failed for {file_obj.name}: {csv_err}") | |
| extracted_text = f"[Error extracting CSV text for {file_obj.name}: {str(csv_err)}]" | |
| else: | |
| # Try decoding as plain text (txt, md, html, etc) | |
| try: | |
| extracted_text = file_bytes.decode('utf-8', errors='replace') | |
| except Exception as dec_err: | |
| logger.error(f"Text decoding failed for {file_obj.name}: {dec_err}") | |
| extracted_text = f"[Error decoding text for {file_obj.name}]" | |
| # Append to combined text | |
| combined_text += f"\n\n--- Content from {file_obj.name} ---\n{extracted_text}\n-----------------------------------\n" | |
| except Exception as err: | |
| logger.error(f"Failed to process file {file_obj.name}: {err}") | |
| except ImportError as ie: | |
| logger.error(f"Missing dependency for file processing: {ie}") | |
| return "Error: Server missing dependencies (pypdf, python-docx, or strands types) for file processing." | |
| # Construct final payload | |
| message_content = [{"text": combined_text}] | |
| # Add any extracted images | |
| message_content.extend(image_blocks) | |
| # Call agent with list payload | |
| result = agent(message_content) | |
| logger.info("Query completed successfully") | |
| if return_full_result: | |
| return result | |
| return str(result) | |
| except Exception as e: | |
| logger.error(f"Query failed: {e}") | |
| _metrics.record_error() | |
| return f"Error: {str(e)}" | |
| # ============================================================================= | |
| # FASTAPI APPLICATION | |
| # ============================================================================= | |
| app = FastAPI(title="Fraud Model Explainability Assistant") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| allow_credentials=True, | |
| ) | |
| class AnswerResponse(BaseModel): | |
| answer: str | |
| metrics: dict | |
| async def index(): | |
| """Serve the main UI.""" | |
| return HTMLResponse(content=get_ui_html()) | |
| async def ask_question(request: QuestionRequest): | |
| """Process a question and return the answer.""" | |
| try: | |
| answer = query_agent(request.question, request.files) | |
| return AnswerResponse( | |
| answer=answer, | |
| metrics=_metrics.get_stats() | |
| ) | |
| except Exception as e: | |
| logger.error(f"API error: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_metrics(): | |
| """Get current performance metrics.""" | |
| return _metrics.get_stats() | |
| async def health_check(): | |
| """Health check endpoint.""" | |
| return { | |
| "status": "healthy", | |
| "confluence_initialized": _confluence_rag is not None, | |
| "metrics": _metrics.get_stats() | |
| } | |
| # ============================================================================= | |
| # HTML UI | |
| # ============================================================================= | |
| def get_ui_html() -> str: | |
| """Generate the chat UI HTML.""" | |
| example_questions = [ | |
| "Why was application APP-78432 flagged as high risk?", | |
| "Explain the fraud score for APP-12345 and compare it to approved applications", | |
| "Check fair lending compliance for APP-55555 and cite relevant policies", | |
| "Show me the identity network analysis for APP-78432", | |
| "What's the current model performance for the Retail Card portfolio?", | |
| "What does our fair lending policy say about synthetic ID detection?", | |
| "Find the model validation report for XGBoost v3.2", | |
| "What are the procedures for escalating high-risk applications?", | |
| ] | |
| examples_json = json.dumps(example_questions) | |
| return f""" | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Fraud Model Explainability Assistant</title> | |
| <style> | |
| * {{ | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| }} | |
| body {{ | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; | |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); | |
| min-height: 100vh; | |
| color: #e0e0e0; | |
| }} | |
| .container {{ | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| }} | |
| header {{ | |
| text-align: center; | |
| padding: 30px 0; | |
| border-bottom: 1px solid rgba(255,255,255,0.1); | |
| margin-bottom: 30px; | |
| }} | |
| h1 {{ | |
| font-size: 2.5rem; | |
| background: linear-gradient(90deg, #00d4ff, #7b2cbf); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: 10px; | |
| }} | |
| .subtitle {{ | |
| color: #888; | |
| font-size: 1.1rem; | |
| }} | |
| .main-content {{ | |
| display: grid; | |
| grid-template-columns: 1fr 300px; | |
| gap: 30px; | |
| }} | |
| @media (max-width: 900px) {{ | |
| .main-content {{ | |
| grid-template-columns: 1fr; | |
| }} | |
| }} | |
| .chat-section {{ | |
| background: rgba(255,255,255,0.05); | |
| border-radius: 16px; | |
| padding: 20px; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| }} | |
| .input-area {{ | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| }} | |
| textarea {{ | |
| flex: 1; | |
| padding: 15px; | |
| border: 2px solid rgba(255,255,255,0.1); | |
| border-radius: 12px; | |
| background: rgba(0,0,0,0.3); | |
| color: #fff; | |
| font-size: 1rem; | |
| resize: vertical; | |
| min-height: 80px; | |
| transition: border-color 0.3s; | |
| }} | |
| textarea:focus {{ | |
| outline: none; | |
| border-color: #00d4ff; | |
| }} | |
| button {{ | |
| padding: 15px 30px; | |
| background: linear-gradient(135deg, #00d4ff, #7b2cbf); | |
| color: white; | |
| border: none; | |
| border-radius: 12px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| }} | |
| button:hover:not(:disabled) {{ | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 30px rgba(0,212,255,0.3); | |
| }} | |
| button:disabled {{ | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| }} | |
| .response-area {{ | |
| background: rgba(0,0,0,0.3); | |
| border-radius: 12px; | |
| padding: 20px; | |
| min-height: 400px; | |
| max-height: 600px; | |
| overflow-y: auto; | |
| }} | |
| .response-area pre {{ | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| font-family: 'Fira Code', 'Consolas', monospace; | |
| font-size: 0.9rem; | |
| line-height: 1.6; | |
| }} | |
| .loading {{ | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| padding: 40px; | |
| color: #00d4ff; | |
| }} | |
| .spinner {{ | |
| width: 24px; | |
| height: 24px; | |
| border: 3px solid rgba(0,212,255,0.2); | |
| border-top-color: #00d4ff; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| }} | |
| @keyframes spin {{ | |
| to {{ transform: rotate(360deg); }} | |
| }} | |
| .sidebar {{ | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| }} | |
| .card {{ | |
| background: rgba(255,255,255,0.05); | |
| border-radius: 12px; | |
| padding: 20px; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| }} | |
| .card h3 {{ | |
| color: #00d4ff; | |
| margin-bottom: 15px; | |
| font-size: 1rem; | |
| }} | |
| .example-btn {{ | |
| display: block; | |
| width: 100%; | |
| padding: 10px; | |
| margin-bottom: 8px; | |
| background: rgba(0,0,0,0.3); | |
| color: #ccc; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| border-radius: 8px; | |
| font-size: 0.85rem; | |
| text-align: left; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| }} | |
| .example-btn:hover {{ | |
| background: rgba(0,212,255,0.1); | |
| border-color: #00d4ff; | |
| color: #fff; | |
| }} | |
| .metrics {{ | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| }} | |
| .metric {{ | |
| background: rgba(0,0,0,0.3); | |
| padding: 12px; | |
| border-radius: 8px; | |
| text-align: center; | |
| }} | |
| .metric-value {{ | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| color: #00d4ff; | |
| }} | |
| .metric-label {{ | |
| font-size: 0.75rem; | |
| color: #888; | |
| margin-top: 4px; | |
| }} | |
| .features {{ | |
| list-style: none; | |
| }} | |
| .features li {{ | |
| padding: 8px 0; | |
| border-bottom: 1px solid rgba(255,255,255,0.05); | |
| font-size: 0.9rem; | |
| }} | |
| .features li:last-child {{ | |
| border-bottom: none; | |
| }} | |
| .features li::before {{ | |
| content: "✓ "; | |
| color: #00d4ff; | |
| }} | |
| footer {{ | |
| text-align: center; | |
| padding: 30px; | |
| color: #666; | |
| font-size: 0.9rem; | |
| margin-top: 40px; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>🔍 Fraud Model Explainability Assistant</h1> | |
| <p class="subtitle">Production-Ready with Confluence Integration</p> | |
| </header> | |
| <div class="main-content"> | |
| <div class="chat-section"> | |
| <div class="input-area"> | |
| <textarea | |
| id="questionInput" | |
| placeholder="Ask a question about fraud models, applications, or policies..." | |
| onkeydown="if(event.key === 'Enter' && !event.shiftKey) {{ event.preventDefault(); askQuestion(); }}" | |
| ></textarea> | |
| </div> | |
| <div class="controls-area" style="display: flex; gap: 10px; margin-bottom: 20px; align-items: center;"> | |
| <label for="fileInput" style="cursor: pointer; display: flex; align-items: center; gap: 5px; color: #00d4ff; font-size: 0.9rem;"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg> | |
| Add File | |
| </label> | |
| <input type="file" id="fileInput" accept="image/*, .pdf, .csv, .doc, .docx, .xls, .xlsx, .txt, .md, .html" style="display: none;" onchange="updateFileLabel()"> | |
| <span id="fileName" style="color: #888; font-size: 0.8rem; margin-right: auto;"></span> | |
| <button id="askBtn" onclick="askQuestion()">🔍 Analyze</button> | |
| </div> | |
| <div class="response-area" id="responseArea"> | |
| <pre id="responseText">Welcome! Ask me about: | |
| • Application fraud scores and explanations | |
| • Fair lending compliance checks | |
| • Identity network analysis | |
| • Model performance metrics | |
| • Confluence documentation and policies | |
| Enter your question above and click "Analyze" to get started.</pre> | |
| </div> | |
| </div> | |
| <div class="sidebar"> | |
| <div class="card"> | |
| <h3>📊 Performance Metrics</h3> | |
| <div class="metrics"> | |
| <div class="metric"> | |
| <div class="metric-value" id="totalSearches">0</div> | |
| <div class="metric-label">Searches</div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-value" id="cacheRate">0%</div> | |
| <div class="metric-label">Cache Rate</div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-value" id="avgTime">0s</div> | |
| <div class="metric-label">Avg Time</div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-value" id="errors">0</div> | |
| <div class="metric-label">Errors</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>💡 Example Questions</h3> | |
| <div id="examplesContainer"></div> | |
| </div> | |
| <div class="card"> | |
| <h3>✨ Production Features</h3> | |
| <ul class="features"> | |
| <li>Structured logging</li> | |
| <li>Performance tracking</li> | |
| <li>Error monitoring</li> | |
| <li>Daily auto-refresh (2 AM)</li> | |
| <li>Confluence integration</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <footer> | |
| Powered by Strands Agents + Confluence Integration • FastAPI Backend | |
| </footer> | |
| </div> | |
| <script> | |
| const examples = {examples_json}; | |
| // Populate examples | |
| const examplesContainer = document.getElementById('examplesContainer'); | |
| examples.slice(0, 5).forEach(q => {{ | |
| const btn = document.createElement('button'); | |
| btn.className = 'example-btn'; | |
| btn.textContent = q.length > 50 ? q.substring(0, 50) + '...' : q; | |
| btn.title = q; | |
| btn.onclick = () => {{ | |
| document.getElementById('questionInput').value = q; | |
| askQuestion(); | |
| }}; | |
| examplesContainer.appendChild(btn); | |
| }}); | |
| function updateFileLabel() {{ | |
| const input = document.getElementById('fileInput'); | |
| const fileName = document.getElementById('fileName'); | |
| if (input.files && input.files.length > 0) {{ | |
| fileName.textContent = input.files[0].name; | |
| }} else {{ | |
| fileName.textContent = ""; | |
| }} | |
| }} | |
| async function convertFileToBase64(file) {{ | |
| return new Promise((resolve, reject) => {{ | |
| const reader = new FileReader(); | |
| reader.onload = () => resolve(reader.result); | |
| reader.onerror = error => reject(error); | |
| reader.readAsDataURL(file); | |
| }}); | |
| }} | |
| async function askQuestion() {{ | |
| const input = document.getElementById('questionInput'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const btn = document.getElementById('askBtn'); | |
| const responseArea = document.getElementById('responseArea'); | |
| const question = input.value.trim(); | |
| if (!question && (!fileInput.files || fileInput.files.length === 0)) return; | |
| // UI updates | |
| input.disabled = true; | |
| btn.disabled = true; | |
| btn.textContent = '⏳ Processing...'; | |
| responseArea.innerHTML = '<div class="loading"><div class="spinner"></div>Analyzing request...</div>'; | |
| try {{ | |
| const payload = {{ | |
| question: question, | |
| files: [] | |
| }}; | |
| // Handle file upload | |
| if (fileInput.files && fileInput.files.length > 0) {{ | |
| const file = fileInput.files[0]; | |
| const base64Data = await convertFileToBase64(file); | |
| // Determine format | |
| let format = "txt"; // default | |
| // Try from type | |
| if (file.type) {{ | |
| const subtype = file.type.split('/')[1]; | |
| if (subtype) format = subtype; | |
| }} | |
| // Try from extension (more reliable for docs) | |
| const nameParts = file.name.split('.'); | |
| if (nameParts.length > 1) {{ | |
| format = nameParts[nameParts.length - 1].toLowerCase(); | |
| }} | |
| payload.files.push({{ | |
| data: base64Data, | |
| format: format, | |
| name: file.name | |
| }}); | |
| }} | |
| const response = await fetch('/api/ask', {{ | |
| method: 'POST', | |
| headers: {{ | |
| 'Content-Type': 'application/json' | |
| }}, | |
| body: JSON.stringify(payload) | |
| }}); | |
| const data = await response.json(); | |
| if (response.ok) {{ | |
| responseArea.innerHTML = '<pre id="responseText"></pre>'; | |
| document.getElementById('responseText').textContent = data.answer; | |
| // Update metrics | |
| if (data.metrics) {{ | |
| document.getElementById('totalSearches').textContent = data.metrics.total_searches || 0; | |
| document.getElementById('cacheRate').textContent = | |
| ((data.metrics.cache_hit_rate || 0) * 100).toFixed(0) + '%'; | |
| document.getElementById('avgTime').textContent = | |
| (data.metrics.avg_query_time || 0).toFixed(2) + 's'; | |
| document.getElementById('errors').textContent = data.metrics.errors || 0; | |
| }} | |
| }} else {{ | |
| responseArea.innerHTML = `<pre style="color: #ff6b6b">Error: ${{data.detail || 'Unknown error occurred'}}</pre>`; | |
| }} | |
| }} catch (error) {{ | |
| responseArea.innerHTML = `<pre style="color: #ff6b6b">Network Error: ${{error.message}}</pre>`; | |
| }} finally {{ | |
| input.disabled = false; | |
| btn.disabled = false; | |
| btn.textContent = '🔍 Analyze'; | |
| input.focus(); | |
| fileInput.value = ""; | |
| updateFileLabel(); | |
| }} | |
| }} | |
| // Fetch initial metrics | |
| async function fetchMetrics() {{ | |
| try {{ | |
| const response = await fetch('/api/metrics'); | |
| const metrics = await response.json(); | |
| document.getElementById('totalSearches').textContent = metrics.total_searches || 0; | |
| document.getElementById('cacheRate').textContent = | |
| ((metrics.cache_hit_rate || 0) * 100).toFixed(0) + '%'; | |
| document.getElementById('avgTime').textContent = | |
| (metrics.avg_query_time || 0).toFixed(2) + 's'; | |
| document.getElementById('errors').textContent = metrics.errors || 0; | |
| }} catch (e) {{ | |
| console.log('Could not fetch metrics:', e); | |
| }} | |
| }} | |
| fetchMetrics(); | |
| setInterval(fetchMetrics, 30000); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # ============================================================================= | |
| # MAIN ENTRYPOINT | |
| # ============================================================================= | |
| if __name__ == "__main__": | |
| import uvicorn | |
| # Pre-initialize Confluence | |
| try: | |
| init_confluence() | |
| except Exception as e: | |
| logger.error(f"Confluence initialization failed: {e}") | |
| print(f"Warning: Confluence initialization failed: {e}") | |
| print("App will run without Confluence integration.") | |
| # Set up scheduled re-ingestion | |
| scheduler = setup_scheduled_ingestion() | |
| # Launch FastAPI with uvicorn | |
| logger.info("Launching FastAPI server...") | |
| print("\n" + "=" * 60) | |
| print("Fraud Model Explainability Assistant") | |
| print(" - FastAPI backend on port 7860") | |
| print(" - Confluence integration enabled") | |
| print(" - Scheduled refresh at 2 AM daily") | |
| print("=" * 60 + "\n") | |
| uvicorn.run( | |
| app, | |
| host="0.0.0.0", | |
| port=7860, | |
| reload=False | |
| ) | |