import gradio as gr import re from reportlab.lib.pagesizes import letter from reportlab.pdfgen import canvas import bleach import logging import os from huggingface_hub import InferenceClient from retry import retry import time from datetime import datetime import base64 # Print statement to confirm script initialization print("Starting Project Closure Readiness Evaluator app...") # Set up console logging for debugging logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger() logger.handlers = [] # Clear existing handlers console_handler = logging.StreamHandler() console_handler.setLevel(logging.DEBUG) formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") console_handler.setFormatter(formatter) logger.addHandler(console_handler) # Attempt to import Salesforce library with fallback try: from simple_salesforce import Salesforce, SalesforceError SALESFORCE_AVAILABLE = True except ImportError as e: logging.error(f"Failed to import simple-salesforce: {str(e)}. Salesforce functionality will be disabled.") logging.error("Ensure 'simple-salesforce' is included in requirements.txt and installed in your Hugging Face Space.") SALESFORCE_AVAILABLE = False # Salesforce configuration (loaded from environment variables for security) SF_USERNAME = os.getenv("SF_USERNAME") SF_PASSWORD = os.getenv("SF_PASSWORD") SF_SECURITY_TOKEN = os.getenv("SF_SECURITY_TOKEN") SF_INSTANCE_URL = os.getenv("SF_INSTANCE_URL") # Validate Salesforce environment variables if not all([SF_USERNAME, SF_PASSWORD, SF_SECURITY_TOKEN, SF_INSTANCE_URL]): logging.error("One or more Salesforce environment variables are missing. Salesforce functionality will be disabled.") logging.error(f"SF_USERNAME: {SF_USERNAME if SF_USERNAME else 'Not Set'}") logging.error(f"SF_PASSWORD: {'Set' if SF_PASSWORD else 'Not Set'}") logging.error(f"SF_SECURITY_TOKEN: {'Set' if SF_SECURITY_TOKEN else 'Not Set'}") logging.error(f"SF_INSTANCE_URL: {SF_INSTANCE_URL if SF_INSTANCE_URL else 'Not Set'}") logging.error("Please set these variables in Hugging Face Space Settings > Secrets.") SALESFORCE_AVAILABLE = False # Hugging Face configuration HF_API_TOKEN = os.getenv("HF_API_TOKEN") if not HF_API_TOKEN: logging.error("Hugging Face API token (HF_API_TOKEN) not found. Hugging Face functionality will be disabled.") logging.error("Please set HF_API_TOKEN in Hugging Face Space Settings > Secrets.") HF_AVAILABLE = False else: HF_AVAILABLE = True hf_client = InferenceClient(token=HF_API_TOKEN) # Initialize Salesforce connection with retry mechanism @retry(tries=3, delay=2, backoff=2, logger=logger) def init_salesforce(): if not SALESFORCE_AVAILABLE: logging.error("Salesforce library not available. Skipping connection.") return None, "Salesforce library not available" try: logging.info("Attempting to connect to Salesforce with the following credentials:") logging.info(f"Username: {SF_USERNAME}") logging.info(f"Instance URL: {SF_INSTANCE_URL}") sf = Salesforce( username=SF_USERNAME, password=SF_PASSWORD, security_token=SF_SECURITY_TOKEN, instance_url=SF_INSTANCE_URL ) logging.info("Salesforce connected successfully") # Test read access on Project_Closure_Handover__c test_query = sf.query("SELECT Id FROM Project_Closure_Handover__c LIMIT 1") logging.info(f"Test query result (read access): {test_query}") # Test create access by attempting to describe the object and check permissions object_description = sf.Project_Closure_Handover__c.describe() logging.info(f"Object description: {object_description}") return sf, "Salesforce connected successfully" except SalesforceError as e: logging.error(f"Salesforce authentication failed: {str(e)}") logging.error("Possible issues: Incorrect credentials, IP restrictions, or insufficient permissions.") raise except Exception as e: logging.error(f"Failed to initialize Salesforce connection: {str(e)}") logging.error("Check your Salesforce org settings, network restrictions, or API access.") raise # Summarize text using Hugging Face Inference API def summarize_text(text, max_length=100, min_length=30): if not HF_AVAILABLE: logging.error("Hugging Face API not available. Returning original text.") return text if not text or text == "None": return "No summary available" try: summary = hf_client.summarization( text, model="facebook/bart-large-cnn", parameters={"max_length": max_length, "min_length": min_length} ) return summary except Exception as e: logging.error(f"Failed to summarize text with Hugging Face: {str(e)}") return text # Fallback to original text # Create Salesforce record in custom object Project_Closure_Handover__c def create_salesforce_record(score, checklist_summary, missing_summary, status, escalated, logs, qa_report, punch_list_text, open_punch_items, pdf_path=None): if not SALESFORCE_AVAILABLE: logging.error("Salesforce library not available. Skipping record creation.") return "Salesforce library not available" try: sf, connection_message = init_salesforce() if not sf: logging.error(f"Skipping Salesforce record creation due to connection failure: {connection_message}") return connection_message # Summarize checklist_summary and missing_summary using Hugging Face summarized_checklist = summarize_text(checklist_summary) summarized_missing = summarize_text(missing_summary) # Ensure inputs are properly formatted score = float(score) if score is not None else 0.0 checklist_summary = str(checklist_summary) if checklist_summary else "" summarized_checklist = str(summarized_checklist) if summarized_checklist else "" missing_summary = str(missing_summary) if missing_summary else "" summarized_missing = str(summarized_missing) if summarized_missing else "" status = str(status) if status else "" logs = str(logs) if logs else "" qa_report = str(qa_report) if qa_report else "" punch_list_text = str(punch_list_text) if punch_list_text else "" missing_documents = len(missing_summary.split(", ")) if missing_summary and missing_summary != "None" else 0 open_punch_items = int(open_punch_items) if open_punch_items is not None else 0 evaluated_at = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") # Salesforce Date/Time format alert_sent = str(bool(escalated)).lower() # Converts True/False to "true"/"false" logging.info(f"Setting Alert_Sent__c to: {alert_sent}") escalation_flag = str(bool(escalated)).lower() # Ensure this is also a proper boolean string # Create the record in Project_Closure_Handover__c without the PDF URL for now record = { "Readiness_Score__c": score, "Checklist_Summary__c": checklist_summary, "Missing_Documents__c": missing_documents, "Status__c": status, "Summarized_Missing_Items__c": summarized_missing, "Alert_Sent__c": alert_sent, "Client_PDF_Pack_URL__c": "", # Will be updated after attachment is created "Closure_Pack_URL__c": "", # Placeholder; update if you have a closure pack URL "Escalation_Flag__c": escalation_flag, "Evaluated_At__c": evaluated_at, "Logs__c": logs, "Open_Punch_Items__c": open_punch_items, "Punch_List__c": punch_list_text, "QA_Report__c": qa_report } logging.debug(f"Attempting to create Salesforce record in Project_Closure_Handover__c with data: {record}") result = sf.Project_Closure_Handover__c.create(record) logging.info(f"Successfully created Salesforce record: {result}") record_id = result.get('id') logging.info(f"Record ID: {record_id}") # Attach the PDF to the record if pdf_path exists and update the URL pdf_download_url = "" if pdf_path and os.path.exists(pdf_path): logging.info(f"Attaching PDF to Salesforce record ID: {record_id}") with open(pdf_path, "rb") as pdf_file: pdf_content = pdf_file.read() pdf_base64 = base64.b64encode(pdf_content).decode('utf-8') attachment = { "ParentId": record_id, "Name": "Readiness_Report.pdf", "Body": pdf_base64, "ContentType": "application/pdf" } attachment_result = sf.Attachment.create(attachment) logging.info(f"Successfully attached PDF to record: {attachment_result}") attachment_id = attachment_result.get('id') logging.info(f"Attachment ID: {attachment_id}") # Construct the direct download URL for the attachment pdf_download_url = f"{SF_INSTANCE_URL}/servlet/servlet.FileDownload?file={attachment_id}" logging.info(f"Generated PDF download URL: {pdf_download_url}") # Update the Project_Closure_Handover__c record with the PDF download URL update_data = { "Client_PDF_Pack_URL__c": pdf_download_url } sf.Project_Closure_Handover__c.update(record_id, update_data) logging.info(f"Updated record {record_id} with Client_PDF_Pack_URL__c: {pdf_download_url}") else: logging.warning(f"No PDF file found at {pdf_path}. Skipping attachment and URL update.") return f"Record created successfully. Record ID: {record_id}. PDF attached and URL set to: {pdf_download_url}" except SalesforceError as e: logging.error(f"Salesforce error while creating Project_Closure_Handover__c record: {str(e)}") logging.error("Possible issues: Object permissions, field-level security, validation rules, or required fields.") logging.error("Check the following in your Salesforce org:") logging.error("- Ensure the user has Create and Edit permission on Project_Closure_Handover__c.") logging.error("- Ensure the user has permission to create and read Attachments.") logging.error("- Verify field-level security for all fields in the record.") logging.error("- Check for validation rules or required fields that might be failing.") return f"Salesforce error: {str(e)}" except Exception as e: logging.error(f"Failed to create Salesforce Project_Closure_Handover__c record: {str(e)}") return f"Error creating record: {str(e)}" # Clean input to prevent injection attacks def sanitize_input(text): if not text or not isinstance(text, str): return "" return bleach.clean(text.strip()) # Rule-based completeness engine with weighted scoring def evaluate_readiness(logs, qa_report, punch_list_text): try: # Log inputs for debugging logging.info(f"Inputs - Logs: {logs}, QA Report: {qa_report}, Punch List: {punch_list_text}") # Initialize score and lists for tracking score = 0 missing_items = [] checklist_details = [] # Define weights for scoring LOGS_WEIGHT = 30 # 30% weight for logs QA_WEIGHT = 40 # 40% weight for QA report PUNCH_WEIGHT = 30 # 30% weight for punch list # Sanitize inputs logs = sanitize_input(logs) qa_report = sanitize_input(qa_report) punch_list_text = sanitize_input(punch_list_text) # Process Project Logs (30% weight) log_keywords = r"complete|handover done|done|finished|closed|successful" negative_keywords = r"issue|pending|incomplete|problem|delay" logs_pass = logs and re.search(log_keywords, logs.lower()) logs_negative = logs and re.search(negative_keywords, logs.lower()) if logs_pass and not logs_negative: score += LOGS_WEIGHT checklist_details.append("Logs: Completed") logging.info(f"Logs Check: Pass (positive keywords found, no negative keywords), Score: {score}%") elif logs_pass and logs_negative: score += LOGS_WEIGHT // 2 # Partial score for mixed signals missing_items.append("Issues detected in logs") checklist_details.append("Logs: Partially Completed (issues detected)") logging.info(f"Logs Check: Partial Pass (positive and negative keywords found), Score: {score}%") else: missing_items.append("Project Logs Incomplete") checklist_details.append("Logs: Pending") logging.info(f"Logs Check: Fail (no positive keywords or only negative keywords), Score: {score}%") # Process QA Report (40% weight) qa_keywords = r"approved|passed|cleared" qa_pass = qa_report and re.search(qa_keywords, qa_report.lower()) if qa_pass: score += QA_WEIGHT checklist_details.append("QA Report: Approved") else: missing_items.append("QA Approval Missing") checklist_details.append("QA Report: Pending") logging.info(f"QA Check: {'Pass' if qa_pass else 'Fail'}, Score so far: {score}%") # Process Punch List (30% weight) punch_keywords = r"none|resolved|closed|no issues" punch_pass = punch_list_text and re.search(punch_keywords, punch_list_text.lower()) if punch_pass: score += PUNCH_WEIGHT checklist_details.append("Punch List: Resolved") else: missing_items.append("Open Punch Points Detected") checklist_details.append("Punch List: Pending") logging.info(f"Punch List Check: {'Pass' if punch_pass else 'Fail'}, Final Score: {score}%") # Calculate open punch items open_punch_items = 0 if punch_pass else 1 # Simplified for dropdown input # Enhanced escalation logic escalated = score < 70 or open_punch_items > 2 logging.info(f"Escalation Check: Score < 70 ({score < 70}), Open Punch Items > 2 ({open_punch_items > 2}), Escalated: {escalated}") # Map "Pending" to "In Progress" for Salesforce Status__c picklist status = "Escalated" if escalated else ("Completed" if not missing_items else "In Progress") checklist_status = "Escalated" if escalated else ("Completed" if not missing_items else "Pending") # For UI display # Build summaries checklist_summary = "\n".join(checklist_details) missing_summary = "None" if not missing_items else ", ".join(missing_items) # Generate progress bar HTML color_class = "red" if score < 70 else "yellow" if score <= 90 else "green" logging.info(f"Readiness Score: {score}%, Color Class: {color_class}") progress_bar = f"""