"""Placeholder extraction and validation for prompt templates.""" from __future__ import annotations import re from typing import Any from placeholder_registry import load_placeholders_for_context PLACEHOLDER_PATTERN = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}") class PlaceholderValidationError(Exception): def __init__( self, message: str, *, missing: list[str] | None = None, type_errors: list[str] | None = None, unknown: list[str] | None = None, ): super().__init__(message) self.missing = missing or [] self.type_errors = type_errors or [] self.unknown = unknown or [] def extract_placeholders(template: str) -> set[str]: return set(PLACEHOLDER_PATTERN.findall(template)) def _check_value_type(value: Any, value_type: str) -> bool: if value_type == "string": return isinstance(value, str) if value_type == "number": return isinstance(value, (int, float)) and not isinstance(value, bool) if value_type == "boolean": return isinstance(value, bool) if value_type == "object": return isinstance(value, dict) if value_type == "array": return isinstance(value, list) if value_type == "json": return True return True def validate_placeholder_input( *, template: str, context_kind: str, values: dict[str, Any], ) -> dict[str, Any]: """Validate input values against registry; return metadata including unknown placeholders.""" registry = {p["placeholder_key"]: p for p in load_placeholders_for_context(context_kind)} used = extract_placeholders(template) missing: list[str] = [] type_errors: list[str] = [] unknown = sorted(key for key in used if key not in registry) for key, meta in registry.items(): if meta["required"] and key not in values: missing.append(key) if key in values and not _check_value_type(values[key], meta["value_type"]): type_errors.append(f"{key}: expected {meta['value_type']}") if missing or type_errors: raise PlaceholderValidationError( "Placeholder validation failed", missing=missing, type_errors=type_errors, unknown=unknown, ) return { "used_placeholders": sorted(used), "unknown_placeholders": unknown, "optional_missing": sorted( key for key in registry if not registry[key]["required"] and key not in values ), }