CheddahBot/cheddahbot/email_templates.py

92 lines
2.7 KiB
Python

"""Email template loader and renderer for client delivery emails."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from pathlib import Path
import yaml
log = logging.getLogger(__name__)
_TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "skills" / "email_templates"
@dataclass
class EmailTemplate:
"""Parsed email template with subject and body."""
task_type: str
subject_template: str
body_template: str
def load_template(task_type: str, templates_dir: Path | str = _TEMPLATES_DIR) -> EmailTemplate | None:
"""Load an email template matching the given task type.
Searches skills/email_templates/ for .md files with matching task_type in frontmatter.
"""
templates_dir = Path(templates_dir)
if not templates_dir.exists():
log.warning("Email templates directory not found: %s", templates_dir)
return None
for md_file in templates_dir.glob("*.md"):
template = _parse_template_file(md_file)
if template and template.task_type.lower() == task_type.lower():
log.info("Loaded email template '%s' for task type '%s'", md_file.name, task_type)
return template
log.warning("No email template found for task type '%s'", task_type)
return None
def _parse_template_file(path: Path) -> EmailTemplate | None:
"""Parse a template .md file with YAML frontmatter."""
text = path.read_text(encoding="utf-8")
# Split frontmatter from body
if not text.startswith("---"):
return None
parts = text.split("---", 2)
if len(parts) < 3:
return None
try:
meta = yaml.safe_load(parts[1]) or {}
except yaml.YAMLError:
log.warning("Invalid YAML frontmatter in %s", path.name)
return None
task_type = meta.get("task_type", "")
subject = meta.get("subject", "")
body = parts[2].strip()
if not task_type or not body:
return None
return EmailTemplate(task_type=task_type, subject_template=subject, body_template=body)
def render_template(template: EmailTemplate, context: dict) -> tuple[str, str]:
"""Render a template with the given context dict.
Returns (subject, body). Uses str.format_map() for placeholder substitution.
Unknown placeholders are left as-is.
"""
subject = _safe_format(template.subject_template, context)
body = _safe_format(template.body_template, context)
return subject, body
def _safe_format(template: str, context: dict) -> str:
"""Format a string template, leaving unknown placeholders intact."""
class SafeDict(dict):
def __missing__(self, key: str) -> str:
return "{" + key + "}"
return template.format_map(SafeDict(context))