Initial commit: Conflict Checker
Cross-discipline design-contradiction checker for construction drawing sets. Standalone tool broken out from Iron_Bid; a pipeline stage may later fold back into Iron_Bid. Pipeline: PDF->images -> per-sheet assertion extraction -> deterministic clustering by location -> per-cluster reasoning -> report. Includes CLI (cli/run_check.py) and web UI (backend/main.py). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
email_sender.py - Sends a completion notification for a conflict check.
|
||||
|
||||
Plain stdlib smtplib, all config from env (see config.py). Mirrors IronBid's
|
||||
graceful behavior: if SMTP is not configured, or a send fails, it logs and
|
||||
returns without ever crashing the pipeline.
|
||||
"""
|
||||
|
||||
import os
|
||||
import ssl
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from backend import config
|
||||
|
||||
|
||||
def _smtp_ready() -> bool:
|
||||
return bool(config.SMTP_HOST and config.SMTP_USER and config.SMTP_PASSWORD)
|
||||
|
||||
|
||||
def _send(msg: EmailMessage) -> bool:
|
||||
try:
|
||||
if config.SMTP_USE_SSL:
|
||||
ctx = ssl.create_default_context()
|
||||
with smtplib.SMTP_SSL(config.SMTP_HOST, config.SMTP_PORT, context=ctx) as s:
|
||||
s.login(config.SMTP_USER, config.SMTP_PASSWORD)
|
||||
s.send_message(msg)
|
||||
else:
|
||||
with smtplib.SMTP(config.SMTP_HOST, config.SMTP_PORT) as s:
|
||||
if config.SMTP_USE_TLS:
|
||||
s.starttls(context=ssl.create_default_context())
|
||||
s.login(config.SMTP_USER, config.SMTP_PASSWORD)
|
||||
s.send_message(msg)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[Email] Failed to send: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def send_conflict_report(
|
||||
recipient_email: str,
|
||||
report: Dict,
|
||||
results_url: str = "",
|
||||
attachments: Optional[List[str]] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Email the conflict-check summary to recipient_email, with the report files
|
||||
attached. Returns True if sent, False if skipped/failed.
|
||||
"""
|
||||
if not recipient_email:
|
||||
return False
|
||||
if not _smtp_ready():
|
||||
print(f"[Email] SMTP not configured - skipping notification to {recipient_email}")
|
||||
return False
|
||||
|
||||
s = report.get("summary", {})
|
||||
source = report.get("source", "drawing set")
|
||||
sev = s.get("by_severity", {})
|
||||
found = s.get("conflicts_found", 0)
|
||||
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = f"Conflict Checker - {source} - {found} conflict(s) found"
|
||||
msg["From"] = config.SMTP_FROM or config.SMTP_USER
|
||||
msg["To"] = recipient_email
|
||||
|
||||
lines = [
|
||||
"Your conflict check is complete.",
|
||||
"",
|
||||
f"Drawing set: {source}",
|
||||
f"Sheets: {s.get('sheets_analyzed', 0)} ({', '.join(s.get('disciplines', [])) or 'none'})",
|
||||
f"Facts checked: {s.get('assertions_extracted', 0)} across {s.get('clusters_checked', 0)} location clusters",
|
||||
"",
|
||||
"RESULT",
|
||||
f" Conflicts found: {found}",
|
||||
f" High: {sev.get('high', 0)}",
|
||||
f" Medium: {sev.get('medium', 0)}",
|
||||
f" Low: {sev.get('low', 0)}",
|
||||
]
|
||||
by_stage = s.get("by_stage")
|
||||
if by_stage:
|
||||
lines += [
|
||||
"",
|
||||
"QAQC REVIEW",
|
||||
f" Full-set QAQC issues: {by_stage.get('qaqc', 0)}",
|
||||
f" Code / ADA issues: {by_stage.get('code', 0)}",
|
||||
f" Constructability: {by_stage.get('constructability', 0)}",
|
||||
f" Consolidated issues: {by_stage.get('validated', 0)}",
|
||||
f" RFIs drafted: {by_stage.get('rfis', 0)}",
|
||||
]
|
||||
if s.get("cost_usd") is not None:
|
||||
lines += ["", f"LLM cost: ${s['cost_usd']:.4f} over {s.get('llm_calls', 0)} calls"]
|
||||
mu = s.get("models_used", {})
|
||||
if mu:
|
||||
lines += ["", "AI MODELS"]
|
||||
if mu.get("vision"):
|
||||
lines += [f" Vision: {', '.join(mu['vision'])}"]
|
||||
if mu.get("text_local"):
|
||||
lines += [f" Text local: {', '.join(mu['text_local'])}"]
|
||||
if mu.get("text_cloud"):
|
||||
label = "Text cloud" if not mu.get("text_local") else "Text cloud (fallback)"
|
||||
lines += [f" {label}: {', '.join(mu['text_cloud'])}"]
|
||||
if mu.get("fallback_count"):
|
||||
lines += [f" Local->cloud fallbacks: {mu['fallback_count']}"]
|
||||
if results_url:
|
||||
lines += ["", "VIEW FULL REPORT", f" {results_url}"]
|
||||
lines += ["", "Generated by Conflict Checker"]
|
||||
msg.set_content("\n".join(lines))
|
||||
|
||||
for path in attachments or []:
|
||||
if not path or not os.path.isfile(path):
|
||||
continue
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
name = os.path.basename(path)
|
||||
subtype = "json" if name.endswith(".json") else "plain"
|
||||
maintype = "application" if name.endswith(".json") else "text"
|
||||
msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=name)
|
||||
|
||||
if _send(msg):
|
||||
print(f"[Email] Sent conflict report to {recipient_email}")
|
||||
return True
|
||||
return False
|
||||
Reference in New Issue
Block a user