prompt_template/scripts/validate.py

304 lines
11 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
Prompt Template Validator
Validiert JSON- und Markdown-Templates gegen definierte Schemas.
"""
import argparse
import json
import os
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple
# JSON Schema für JSON-Templates
JSON_SCHEMA = {
"required": ["name", "template", "variables"],
"properties": {
"name": {"type": "string", "minLength": 1},
"version": {"type": "string", "pattern": r"^\d+\.\d+$"},
"description": {"type": "string"},
"role": {"type": "string"},
"template": {"type": "string", "minLength": 1},
"variables": {"type": "object"},
"tags": {"type": "array", "items": {"type": "string"}},
"language": {"type": "string", "enum": ["de", "en", "fr", "es", "any"]},
},
}
# Muster für Markdown-Templates
MD_REQUIRED_SECTIONS = ["Template", "Variablen"]
def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
"""Validiert ein JSON-Template."""
errors = []
try:
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
except json.JSONDecodeError as e:
return False, [f"❌ JSON Syntax Error: {e}"]
except Exception as e:
return False, [f"❌ Datei kann nicht gelesen werden: {e}"]
# Schemas validieren
if not isinstance(data, dict):
return False, ["❌ Root muss ein Object sein"]
# Required Felder
for field in JSON_SCHEMA.get("required", []):
if field not in data:
errors.append(f"❌ Fehlendes Pflichtfeld: '{field}'")
# Feld-Typen validieren
for field, schema in JSON_SCHEMA.get("properties", {}).items():
if field in data:
field_type = schema.get("type")
if field_type == "string" and not isinstance(data[field], str):
errors.append(f"❌ Feld '{field}' muss ein String sein")
elif field_type == "object" and not isinstance(data[field], dict):
errors.append(f"❌ Feld '{field}' muss ein Object sein")
elif field_type == "array" and not isinstance(data[field], list):
errors.append(f"❌ Feld '{field}' muss ein Array sein")
# Pattern validieren
if "pattern" in schema and isinstance(data[field], str):
pattern = schema["pattern"]
# Sicherstellen dass Pattern ^...$ hat für fullmatch
if not (pattern.startswith("^") and pattern.endswith("$")):
fullmatch_pattern = f"^{pattern}$"
else:
fullmatch_pattern = pattern
if not re.fullmatch(fullmatch_pattern, data[field]):
errors.append(
f"❌ Feld '{field}' entspricht nicht dem Pattern: {pattern}"
)
# Enum validieren
if "enum" in schema and isinstance(data[field], str):
if data[field] not in schema["enum"]:
errors.append(
f"❌ Feld '{field}' hat ungültigen Wert: '{data[field]}'. Erwartet: {schema['enum']}"
)
# Template prüfen
if "template" in data:
template = data["template"]
if not isinstance(template, str):
errors.append("❌ Template muss ein String sein")
else:
# Variablen im Template prüfen
if "variables" in data:
template_vars = set(re.findall(r"\{(\w+)\}", template))
defined_vars = set(data["variables"].keys())
undefined_vars = template_vars - defined_vars
if undefined_vars:
errors.append(
f"❌ Undefinierte Variablen im Template: {', '.join(undefined_vars)}"
)
# Variablen validieren
if "variables" in data:
variables = data["variables"]
if not isinstance(variables, dict):
errors.append("❌ Variables muss ein Object sein")
else:
for var_name, var_schema in variables.items():
if not isinstance(var_schema, dict):
errors.append(f"❌ Variable '{var_name}' muss ein Object sein")
continue
if "type" not in var_schema:
errors.append(f"❌ Variable '{var_name}' benötigt ein 'type' Feld")
if var_schema.get("type") == "enum":
if "values" not in var_schema:
errors.append(
f"❌ Enum Variable '{var_name}' benötigt 'values' Array"
)
elif not var_schema.get("values"):
errors.append(
f"❌ Enum Variable '{var_name}' hat leeres 'values' Array"
)
return len(errors) == 0, errors
def validate_md_template(filepath: Path) -> Tuple[bool, List[str]]:
"""Validiert ein Markdown-Template."""
errors = []
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
except Exception as e:
return False, [f"❌ Datei kann nicht gelesen werden: {e}"]
# Mindestlänge
if len(content.strip()) < 50:
errors.append("⚠️ Warnung: Template zu kurz (mind. 50 Zeichen)")
# Titel prüfen
if not content.startswith("# "):
errors.append("❌ Fehlender Titel (erwartet: # Titel)")
# Pflichtabschnitte prüfen
for section in MD_REQUIRED_SECTIONS:
if f"## {section}" not in content and f"# {section}" not in content:
errors.append(f"❌ Fehlender Abschnitt: {section}")
# Variablen-Tabelle prüfen
if "Variablen" in content:
var_section_start = content.find("## Variablen")
if var_section_start == -1:
var_section_start = content.find("# Variablen")
if var_section_start != -1:
var_section = content[var_section_start : var_section_start + 500]
if "| Variable |" not in var_section:
errors.append("❌ Variablen-Tabelle nicht im korrekten Format")
# Template-Block prüfen
if "Template" in content:
template_section = content.find("## Template")
if template_section == -1:
template_section = content.find("# Template")
if template_section != -1:
template_block_start = content.find("```", template_section)
if template_block_start != -1:
close_fence = content.find("```", template_block_start + 3)
if close_fence != -1:
template_content = content[template_block_start + 3:close_fence].strip()
if "{" not in template_content or "}" not in template_content:
errors.append(
"⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden"
)
else:
errors.append("❌ Kein schließender Code-Block gefunden")
else:
errors.append("❌ Kein Code-Block für Template gefunden")
else:
errors.append("❌ Kein Template-Abschnitt gefunden")
return len([e for e in errors if e.startswith("")]) == 0, errors
def validate_template(filepath: Path) -> Tuple[bool, List[str]]:
"""Validiert ein Template basierend auf der Dateiendung."""
if filepath.suffix.lower() == ".json":
return validate_json_template(filepath)
elif filepath.suffix.lower() == ".md":
return validate_md_template(filepath)
else:
return False, [f"❌ Unsupported file type: {filepath.suffix}"]
ALLOWED_DIRS = {"system", "user", "custom", "categories"}
def find_templates(directory: Path) -> List[Path]:
"""Findet alle Template-Dateien in einem Verzeichnis Baum."""
templates = []
for root, dirs, files in os.walk(directory):
rel = os.path.relpath(root, directory)
if rel != "." and rel.split(os.sep)[0] not in ALLOWED_DIRS:
dirs.clear() # Skip nicht-erlaubte Subdirs
continue
for file in files:
if file.endswith((".json", ".md")):
templates.append(Path(root) / file)
return templates
def main():
parser = argparse.ArgumentParser(
description="Validiert Prompt-Templates",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Beispiele:
python validate.py templates/system/code_reviewer.json
python validate.py --all
python validate.py --type-json
""",
)
parser.add_argument("path", nargs="?", help="Pfad zum Template oder Verzeichnis")
parser.add_argument("--all", action="store_true", help="Alle Templates validieren")
type_group = parser.add_mutually_exclusive_group()
type_group.add_argument(
"--type-json", action="store_true", help="Nur JSON-Templates validieren"
)
type_group.add_argument(
"--type-md", action="store_true", help="Nur Markdown-Templates validieren"
)
args = parser.parse_args()
base_dir = Path(__file__).parent.parent
if args.all:
# Alle Templates finden
templates = find_templates(base_dir / "templates")
if args.type_json:
templates = [t for t in templates if t.suffix == ".json"]
if args.type_md:
templates = [t for t in templates if t.suffix == ".md"]
elif args.path:
path = Path(args.path)
if path.is_dir():
templates = find_templates(path)
if args.type_json:
templates = [t for t in templates if t.suffix == ".json"]
if args.type_md:
templates = [t for t in templates if t.suffix == ".md"]
else:
templates = [path]
else:
print("❌ Bitte Pfad angeben oder --all verwenden")
print("Beispiel: python validate.py --all")
sys.exit(1)
if not templates:
print("❌ Keine Templates gefunden")
sys.exit(1)
# Validierung
total = len(templates)
valid = 0
invalid = 0
print(f"\n{'=' * 60}")
print(f"Validiere {total} Template(s)...\n")
for template_path in sorted(templates):
is_valid, errors = validate_template(template_path)
# Make path relative to base_dir, handling both absolute and relative paths
try:
rel_path = str(template_path.relative_to(base_dir))
except ValueError:
# If template_path is not under base_dir, use absolute path
rel_path = str(template_path)
if is_valid:
print(f"{rel_path}")
valid += 1
else:
print(f"{rel_path}")
for error in errors:
print(f" {error}")
invalid += 1
print(f"\n{'=' * 60}")
print(f"Ergebnis: {valid} ✅ | {invalid} ❌ | {total} Total")
if invalid > 0:
sys.exit(2) # Validierungsfehler
sys.exit(0)
if __name__ == "__main__":
main()