mirror of
https://github.com/sstent/AICycling_mcp.git
synced 2026-02-06 14:32:11 +00:00
workng - split out prompt temapltes
This commit is contained in:
155
template_validator.py
Normal file
155
template_validator.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TemplateValidator:
|
||||
"""Validates template syntax, inheritance, and versioning."""
|
||||
|
||||
def __init__(self, templates_dir: str):
|
||||
self.templates_dir = Path(templates_dir)
|
||||
self.components_dir = self.templates_dir / "base"
|
||||
self.versions_dir = self.templates_dir / "versions"
|
||||
self.legacy_mappings = {'main_agent_system_prompt.txt': 'base/system_prompts/main_agent.txt'}
|
||||
|
||||
def parse_frontmatter(self, content: str) -> dict:
|
||||
"""Parse YAML frontmatter from template content."""
|
||||
frontmatter = {}
|
||||
if content.startswith("---\n"):
|
||||
end = content.find("\n---\n")
|
||||
if end != -1:
|
||||
try:
|
||||
frontmatter = yaml.safe_load(content[4:end])
|
||||
except yaml.YAMLError as e:
|
||||
raise ValueError(f"Invalid YAML frontmatter: {e}")
|
||||
content = content[end + 5:]
|
||||
return frontmatter, content
|
||||
|
||||
def validate_syntax(self, template_path: Path) -> bool:
|
||||
"""Validate extends/includes syntax in frontmatter."""
|
||||
with open(template_path, 'r') as f:
|
||||
frontmatter, _ = self.parse_frontmatter(f.read())
|
||||
|
||||
extends = frontmatter.get('extends')
|
||||
includes = frontmatter.get('includes', [])
|
||||
|
||||
if extends and not isinstance(extends, str):
|
||||
raise ValueError("extends must be a string")
|
||||
|
||||
if not isinstance(includes, list):
|
||||
raise ValueError("includes must be a list")
|
||||
|
||||
for inc in includes:
|
||||
if not isinstance(inc, str):
|
||||
raise ValueError("Each include must be a string")
|
||||
|
||||
return True
|
||||
|
||||
def detect_inheritance_cycle(self, template_name: str, visited: set = None) -> bool:
|
||||
"""Detect cycles in inheritance chain."""
|
||||
if visited is None:
|
||||
visited = set()
|
||||
|
||||
if template_name in visited:
|
||||
return True # Cycle detected
|
||||
|
||||
visited.add(template_name)
|
||||
template_path = self._find_template(template_name)
|
||||
if not template_path:
|
||||
return False
|
||||
|
||||
with open(template_path, 'r') as f:
|
||||
frontmatter, _ = self.parse_frontmatter(f.read())
|
||||
|
||||
extends = frontmatter.get('extends')
|
||||
if extends:
|
||||
if self.detect_inheritance_cycle(extends, visited):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def validate_components_exist(self, template_path: Path) -> bool:
|
||||
"""Check if all included components exist."""
|
||||
with open(template_path, 'r') as f:
|
||||
frontmatter, _ = self.parse_frontmatter(f.read())
|
||||
|
||||
includes = frontmatter.get('includes', [])
|
||||
for inc in includes:
|
||||
comp_path = self.components_dir / inc
|
||||
if not comp_path.exists():
|
||||
raise FileNotFoundError(f"Component '{inc}' not found")
|
||||
|
||||
return True
|
||||
|
||||
def validate_version(self, version_str: str) -> bool:
|
||||
"""Validate semantic version format."""
|
||||
import re
|
||||
if re.match(r'^v?\d+\.\d+\.\d+$', version_str):
|
||||
return True
|
||||
raise ValueError(f"Invalid version format: {version_str}")
|
||||
|
||||
def validate_backward_compatibility(self, template_path: Path) -> bool:
|
||||
"""Ensure template can be loaded as plain if no frontmatter."""
|
||||
with open(template_path, 'r') as f:
|
||||
content = f.read()
|
||||
try:
|
||||
frontmatter, body = self.parse_frontmatter(content)
|
||||
# If no frontmatter, it's compatible
|
||||
return True
|
||||
except ValueError:
|
||||
# No frontmatter, plain template
|
||||
return True
|
||||
|
||||
def _find_template(self, name: str) -> Path | None:
|
||||
"""Find template path, handling versions."""
|
||||
# Check legacy mappings first
|
||||
if name in self.legacy_mappings:
|
||||
name = self.legacy_mappings[name]
|
||||
# Handle versioned
|
||||
if '@' in name:
|
||||
name_part, version = name.rsplit('@', 1)
|
||||
ver_path = self.versions_dir / name_part / f"{version}.txt"
|
||||
if ver_path.exists():
|
||||
return ver_path
|
||||
# Handle subdir paths like workflows/xxx.txt or base/yyy.txt
|
||||
if '/' in name:
|
||||
path = self.templates_dir / name
|
||||
if path.exists():
|
||||
return path
|
||||
# Plain name
|
||||
path = self.templates_dir / f"{name}.txt"
|
||||
if path.exists():
|
||||
return path
|
||||
return None
|
||||
|
||||
def full_validate(self, template_name: str) -> dict:
|
||||
"""Perform full validation and return report."""
|
||||
template_path = self._find_template(template_name)
|
||||
if not template_path:
|
||||
raise FileNotFoundError(f"Template '{template_name}' not found")
|
||||
|
||||
errors = []
|
||||
try:
|
||||
self.validate_syntax(template_path)
|
||||
except ValueError as e:
|
||||
errors.append(str(e))
|
||||
|
||||
try:
|
||||
self.validate_components_exist(template_path)
|
||||
except FileNotFoundError as e:
|
||||
errors.append(str(e))
|
||||
|
||||
if self.detect_inheritance_cycle(template_name):
|
||||
errors.append("Inheritance cycle detected")
|
||||
|
||||
version_str = template_name.split('@')[-1] if '@' in template_name else None
|
||||
if version_str:
|
||||
try:
|
||||
self.validate_version(version_str)
|
||||
except ValueError as e:
|
||||
errors.append(str(e))
|
||||
|
||||
self.validate_backward_compatibility(template_path)
|
||||
|
||||
return {"valid": len(errors) == 0, "errors": errors}
|
||||
Reference in New Issue
Block a user