|
|
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("Starting Project Closure Readiness Evaluator app...") |
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.DEBUG) |
|
|
logger = logging.getLogger() |
|
|
logger.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) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
@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_query = sf.query("SELECT Id FROM Project_Closure_Handover__c LIMIT 1") |
|
|
logging.info(f"Test query result (read access): {test_query}") |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
summarized_checklist = summarize_text(checklist_summary) |
|
|
summarized_missing = summarize_text(missing_summary) |
|
|
|
|
|
|
|
|
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") |
|
|
alert_sent = str(bool(escalated)).lower() |
|
|
logging.info(f"Setting Alert_Sent__c to: {alert_sent}") |
|
|
escalation_flag = str(bool(escalated)).lower() |
|
|
|
|
|
|
|
|
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": "", |
|
|
"Closure_Pack_URL__c": "", |
|
|
"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}") |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
pdf_download_url = f"{SF_INSTANCE_URL}/servlet/servlet.FileDownload?file={attachment_id}" |
|
|
logging.info(f"Generated PDF download URL: {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)}" |
|
|
|
|
|
|
|
|
def sanitize_input(text): |
|
|
if not text or not isinstance(text, str): |
|
|
return "" |
|
|
return bleach.clean(text.strip()) |
|
|
|
|
|
|
|
|
def evaluate_readiness(logs, qa_report, punch_list_text): |
|
|
try: |
|
|
|
|
|
logging.info(f"Inputs - Logs: {logs}, QA Report: {qa_report}, Punch List: {punch_list_text}") |
|
|
|
|
|
|
|
|
score = 0 |
|
|
missing_items = [] |
|
|
checklist_details = [] |
|
|
|
|
|
|
|
|
LOGS_WEIGHT = 30 |
|
|
QA_WEIGHT = 40 |
|
|
PUNCH_WEIGHT = 30 |
|
|
|
|
|
|
|
|
logs = sanitize_input(logs) |
|
|
qa_report = sanitize_input(qa_report) |
|
|
punch_list_text = sanitize_input(punch_list_text) |
|
|
|
|
|
|
|
|
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 |
|
|
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}%") |
|
|
|
|
|
|
|
|
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}%") |
|
|
|
|
|
|
|
|
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}%") |
|
|
|
|
|
|
|
|
open_punch_items = 0 if punch_pass else 1 |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
checklist_summary = "\n".join(checklist_details) |
|
|
missing_summary = "None" if not missing_items else ", ".join(missing_items) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
def generate_pdf(score, checklist_summary, missing_summary, checklist_status, logs, qa_report, punch_list_text): |
|
|
try: |
|
|
|
|
|
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 "" |
|
|
|
|
|
|
|
|
checklist_summary = checklist_summary.encode('ascii', 'ignore').decode('ascii') |
|
|
checklist_status = checklist_status.encode('ascii', 'ignore').decode('ascii') |
|
|
|
|
|
|
|
|
pdf_path = "readiness_report.pdf" |
|
|
logging.info(f"Creating PDF at {pdf_path}") |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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() |