Download app.py from Dineshpopuri/Project_Closure_Handover: direct link, hf CLI and curl.
- Browser
- Download file 21 kB
-
https://huggingface.co/spaces/Dineshpopuri/Project_Closure_Handover/resolve/main/app.py
- Command line
-
hf download hf://spaces/Dineshpopuri/Project_Closure_Handover/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Dineshpopuri/Project_Closure_Handover/resolve/main/app.py
21 kB
| 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 | |
| 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""" | |
| <div class="progress-container"> | |
| <div class="progress-bar {color_class}" style="width: {score}%;"> | |
| {score}% | |
| </div> | |
| </div> | |
| """ | |
| return (score, checklist_summary, missing_summary, status, progress_bar, escalated, logs, qa_report, punch_list_text, open_punch_items, checklist_status) | |
| except Exception as e: | |
| logging.error(f"Error in evaluate_readiness: {str(e)}") | |
| raise | |
| # Generate PDF report with signature slots | |
| def generate_pdf(score, checklist_summary, missing_summary, checklist_status, logs, qa_report, punch_list_text): | |
| try: | |
| # Sanitize inputs | |
| logging.info("Sanitizing inputs for PDF generation") | |
| score = str(float(score)) if score is not None else "0" | |
| checklist_summary = str(checklist_summary) if checklist_summary is not None else "" | |
| checklist_status = str(checklist_status) if checklist_status is not None else "" | |
| # Remove non-ASCII characters | |
| checklist_summary = checklist_summary.encode('ascii', 'ignore').decode('ascii') | |
| checklist_status = checklist_status.encode('ascii', 'ignore').decode('ascii') | |
| # Define the temporary file path | |
| pdf_path = "readiness_report.pdf" | |
| logging.info(f"Creating PDF at {pdf_path}") | |
| # Create the PDF | |
| c = canvas.Canvas(pdf_path, pagesize=letter) | |
| c.setFont("Times-Bold", 16) | |
| c.drawString(50, 750, "Project Closure Readiness Report") | |
| c.setFont("Times-Roman", 12) | |
| c.drawString(50, 720, f"Readiness Score: {score}%") | |
| c.drawString(50, 700, f"Status: {checklist_status}") | |
| c.drawString(50, 680, "Checklist Summary:") | |
| y = 660 | |
| BOTTOM_MARGIN = 50 | |
| for line in checklist_summary.split("\n"): | |
| if y < BOTTOM_MARGIN: | |
| c.showPage() | |
| c.setFont("Times-Roman", 12) | |
| y = 750 | |
| c.drawString(50, y, line) | |
| y -= 20 | |
| if y - 40 < BOTTOM_MARGIN: | |
| c.showPage() | |
| c.setFont("Times-Roman", 12) | |
| y = 750 | |
| c.drawString(50, y - 40, "Stakeholder Signature: ____________________") | |
| if y - 60 < BOTTOM_MARGIN: | |
| c.showPage() | |
| c.setFont("Times-Roman", 12) | |
| y = 750 | |
| c.drawString(50, y - 60, "Date: ____________________") | |
| c.save() | |
| # Confirm the file exists | |
| if not os.path.exists(pdf_path): | |
| logging.error(f"PDF file {pdf_path} was not created") | |
| raise FileNotFoundError(f"PDF file {pdf_path} was not created") | |
| logging.info(f"PDF generated successfully at {pdf_path}") | |
| return pdf_path, "PDF generation completed. Click the link to download." | |
| except Exception as e: | |
| logging.error(f"Error in generate_pdf: {str(e)}") | |
| raise | |
| # Gradio interface with updated UI | |
| with gr.Blocks(css=""" | |
| .progress-container { background-color: #f0f0f0; width: 100%; height: 20px; border-radius: 5px; overflow: hidden; position: relative; } | |
| .progress-bar { height: 100%; text-align: center; line-height: 20px; color: #000; font-size: 12px; } | |
| .progress-bar.red { background-color: #FF0000; } | |
| .progress-bar.yellow { background-color: #FFFF00; } | |
| .progress-bar.green { background-color: #00FF00; } | |
| """) as demo: | |
| gr.Markdown( | |
| """ | |
| # Project Closure Readiness Evaluator | |
| Evaluate project readiness, generate a PDF report with signature slots. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| logs_input = gr.Textbox(label="Project Logs", lines=5, placeholder="Enter project logs (e.g., 'Project complete, handover done')") | |
| qa_input = gr.Dropdown( | |
| label="QA Report", | |
| choices=["Approved", "Cleared", "Pending", "Not Started", "Rejected"], | |
| value="Pending", | |
| allow_custom_value=False | |
| ) | |
| punch_input = gr.Dropdown( | |
| label="Punch List", | |
| choices=["None", "Resolved", "Closed", "No Issues", "Open Items"], | |
| value="Open Items", | |
| allow_custom_value=False | |
| ) | |
| submit_btn = gr.Button("Evaluate and Generate PDF") | |
| with gr.Column(scale=3): | |
| score_output = gr.Number(label="Readiness Score (%)") | |
| progress_output = gr.HTML(label="Alert Indicator: Progress") | |
| gr.Markdown("Color-coded readiness: Red (<70%), Yellow (70-90%), Green (>90%)") | |
| status_output = gr.Textbox(label="Overall Status") | |
| with gr.Group(): | |
| gr.Markdown("### Handover Summary") | |
| checklist_output = gr.Textbox(label="Checklist Summary") | |
| missing_output = gr.Textbox(label="Missing Items") | |
| open_punch_items_output = gr.Number(label="Open Punch Items (Debug)") | |
| pdf_output = gr.File(label="Download PDF Report", type="filepath", interactive=False) | |
| pdf_debug = gr.Textbox(label="PDF Debug Output") | |
| # Chain the evaluation, PDF generation, and Salesforce record creation | |
| submit_btn.click( | |
| fn=evaluate_readiness, | |
| inputs=[logs_input, qa_input, punch_input], | |
| outputs=[ | |
| score_output, checklist_output, missing_output, status_output, progress_output, | |
| gr.State(), gr.State(), gr.State(), gr.State(), open_punch_items_output, status_output | |
| ] | |
| ).then( | |
| fn=generate_pdf, | |
| inputs=[score_output, checklist_output, missing_output, status_output, gr.State(), gr.State(), gr.State()], | |
| outputs=[pdf_output, pdf_debug] | |
| ).then( | |
| fn=create_salesforce_record, | |
| inputs=[ | |
| score_output, checklist_output, missing_output, status_output, | |
| gr.State(), gr.State(), gr.State(), gr.State(), open_punch_items_output, pdf_output | |
| ], | |
| outputs=None | |
| ) | |
| demo.launch() |