All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 18s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 24s
Migration 005, Registry-Sync, Placeholder-Validation, capability-geschuetzte API, Tests und Abschlussbericht v0.1. Co-authored-by: Cursor <cursoragent@cursor.com>
82 lines
2.5 KiB
Python
82 lines
2.5 KiB
Python
"""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
|
|
),
|
|
}
|