268 lines
9.4 KiB
Python
268 lines
9.4 KiB
Python
|
|
#!/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",
|
||
|
|
"patternProperties": {
|
||
|
|
r"^.+$": {
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"type": {"type": "string", "enum": ["string", "number", "enum", "boolean"]},
|
||
|
|
"required": {"type": "boolean"},
|
||
|
|
"default": {},
|
||
|
|
"description": {"type": "string"},
|
||
|
|
"values": {"type": "array"},
|
||
|
|
},
|
||
|
|
"required": ["type"],
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"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"]
|
||
|
|
MD_VARIABLE_PATTERN = re.compile(r"\|\s*(\w+)\s*\|\s*(\w+)\s*\|")
|
||
|
|
|
||
|
|
|
||
|
|
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):
|
||
|
|
if not re.match(schema["pattern"], data[field]):
|
||
|
|
errors.append(f"❌ Feld '{field}' entspricht nicht dem Pattern: {schema['pattern']}")
|
||
|
|
|
||
|
|
# 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" and "values" not in var_schema:
|
||
|
|
errors.append(f"❌ Enum Variable '{var_name}' benötigt '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("❌ 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_start = content.find("```")
|
||
|
|
if template_start == -1:
|
||
|
|
errors.append("❌ Kein Code-Block für Template gefunden")
|
||
|
|
else:
|
||
|
|
# Prüfe ob Variablen im Template sind
|
||
|
|
template_content = content[template_start:]
|
||
|
|
if "{" not in template_content or "}" not in template_content:
|
||
|
|
errors.append("⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden")
|
||
|
|
|
||
|
|
return len(errors) == 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}"]
|
||
|
|
|
||
|
|
|
||
|
|
def find_templates(directory: Path) -> List[Path]:
|
||
|
|
"""Findet alle Template-Dateien in einem Verzeichnis Baum."""
|
||
|
|
templates = []
|
||
|
|
for root, _, files in os.walk(directory):
|
||
|
|
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 --json templates/
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
parser.add_argument("path", nargs="?", help="Pfad zum Template oder Verzeichnis")
|
||
|
|
parser.add_argument("--all", action="store_true", help="Alle Templates validieren")
|
||
|
|
parser.add_argument("--json", action="store_true", help="Nur JSON-Templates validieren")
|
||
|
|
parser.add_argument("--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 not args.json:
|
||
|
|
templates += find_templates(base_dir / "categories")
|
||
|
|
|
||
|
|
if args.json:
|
||
|
|
templates = [t for t in templates if t.suffix == ".json"]
|
||
|
|
if args.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.json:
|
||
|
|
templates = [t for t in templates if t.suffix == ".json"]
|
||
|
|
if args.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)
|
||
|
|
rel_path = str(template_path.relative_to(base_dir))
|
||
|
|
|
||
|
|
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")
|
||
|
|
|
||
|
|
sys.exit(0 if invalid == 0 else 1)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|