92 lines
2.7 KiB
Python
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))
|