Implement PromptTemplate models with ContentMacro parsing and analysis.
Core Features:
- PromptTemplate extending Artifact for template-specific operations
- ContentMacro model supporting REQUIRED, OPTIONAL, GENERATE kinds
- MacroParser for extracting macros from template content
- TemplateAnalyzer for dependency extraction and validation
- TemplateService for high-level template operations
- Template metadata for model hints and expected inputs
Macro Syntax:
- {{require:artifact-name}} - Required dependency
- {{optional:artifact-name}} - Optional dependency
- {{generate:template-name|param=value}} - Nested generation
Tests (38 passing):
- 18 template model tests (macros, templates, metadata)
- 20 parser tests (parsing, validation, parameters, aliases)
Implements:
- FR-2.1: PromptTemplate as content artifact with macros
- FR-2.2: ContentMacro detection and extraction
- FR-2.3: Required/Optional/Generate macro kinds
Files Created:
- markitect/prompts/templates/models.py
- markitect/prompts/templates/parser.py
- markitect/prompts/templates/analyzer.py
- markitect/prompts/services/template_service.py
- tests/unit/prompts/test_template_models.py
- tests/unit/prompts/test_macro_parser.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
194 lines
6.2 KiB
Python
194 lines
6.2 KiB
Python
"""
|
|
Template analyzer for dependency extraction.
|
|
|
|
Implements FR-2.2: TemplateAnalysis
|
|
Analyzes templates to extract and catalog content macros.
|
|
"""
|
|
|
|
from typing import List, Set
|
|
|
|
from markitect.prompts.templates.models import PromptTemplate, MacroKind
|
|
from markitect.prompts.templates.parser import MacroParser
|
|
|
|
|
|
class TemplateAnalysisResult:
|
|
"""
|
|
Result of template analysis.
|
|
|
|
Provides summary information about template dependencies.
|
|
|
|
Attributes:
|
|
template: Analyzed template
|
|
total_macros: Total number of macros found
|
|
required_count: Number of required macros
|
|
optional_count: Number of optional macros
|
|
generate_count: Number of generate macros
|
|
unique_targets: Set of unique target names
|
|
has_circular_potential: Whether template references itself
|
|
"""
|
|
|
|
def __init__(self, template: PromptTemplate):
|
|
self.template = template
|
|
self.total_macros = len(template.macros)
|
|
self.required_count = len(template.get_required_dependencies())
|
|
self.optional_count = len(template.get_optional_dependencies())
|
|
self.generate_count = len(template.get_generators())
|
|
self.unique_targets = self._get_unique_targets()
|
|
self.has_circular_potential = template.name in self.unique_targets
|
|
|
|
def _get_unique_targets(self) -> Set[str]:
|
|
"""Get set of unique target names from all macros."""
|
|
return {macro.target for macro in self.template.macros}
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert result to dictionary."""
|
|
return {
|
|
"template_id": self.template.id,
|
|
"template_name": self.template.name,
|
|
"total_macros": self.total_macros,
|
|
"required_count": self.required_count,
|
|
"optional_count": self.optional_count,
|
|
"generate_count": self.generate_count,
|
|
"unique_targets": list(self.unique_targets),
|
|
"has_circular_potential": self.has_circular_potential,
|
|
}
|
|
|
|
|
|
class TemplateAnalyzer:
|
|
"""
|
|
Analyzer for extracting dependencies from templates.
|
|
|
|
Implements FR-2.2: TemplateAnalysis
|
|
Uses MacroParser to detect and extract ContentMacros, then populates
|
|
the template's macro list and provides analysis results.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.parser = MacroParser()
|
|
|
|
def analyze(self, template: PromptTemplate, content: str) -> TemplateAnalysisResult:
|
|
"""
|
|
Analyze template content and extract macros.
|
|
|
|
Updates the template's macros list and marks it as analyzed.
|
|
|
|
Args:
|
|
template: Template to analyze
|
|
content: Template content to parse
|
|
|
|
Returns:
|
|
Analysis result with summary information
|
|
|
|
Raises:
|
|
MacroParsingError: If macro syntax is invalid
|
|
"""
|
|
# Parse macros from content
|
|
macros = self.parser.parse(content)
|
|
|
|
# Update template
|
|
template.macros = macros
|
|
template.analyzed = True
|
|
|
|
# Create and return analysis result
|
|
return TemplateAnalysisResult(template)
|
|
|
|
def quick_check(self, content: str) -> dict:
|
|
"""
|
|
Quick check of template content without full analysis.
|
|
|
|
Useful for validation before template creation.
|
|
|
|
Args:
|
|
content: Template content
|
|
|
|
Returns:
|
|
Dictionary with macro counts and basic info
|
|
"""
|
|
has_macros = self.parser.has_macros(content)
|
|
if not has_macros:
|
|
return {
|
|
"has_macros": False,
|
|
"required": 0,
|
|
"optional": 0,
|
|
"generate": 0,
|
|
"total": 0,
|
|
}
|
|
|
|
counts = self.parser.count_macros(content)
|
|
return {
|
|
"has_macros": True,
|
|
"required": counts['required'],
|
|
"optional": counts['optional'],
|
|
"generate": counts['generate'],
|
|
"total": sum(counts.values()),
|
|
}
|
|
|
|
def find_dependencies(self, template: PromptTemplate) -> dict:
|
|
"""
|
|
Extract dependency information from analyzed template.
|
|
|
|
Args:
|
|
template: Analyzed template
|
|
|
|
Returns:
|
|
Dictionary with categorized dependencies
|
|
|
|
Raises:
|
|
ValueError: If template has not been analyzed
|
|
"""
|
|
if not template.analyzed:
|
|
raise ValueError(
|
|
f"Template '{template.name}' has not been analyzed. "
|
|
"Call analyze() first."
|
|
)
|
|
|
|
return {
|
|
"required": template.get_required_dependencies(),
|
|
"optional": template.get_optional_dependencies(),
|
|
"generators": template.get_generators(),
|
|
"all_targets": list({m.target for m in template.macros}),
|
|
}
|
|
|
|
def validate_references(
|
|
self,
|
|
template: PromptTemplate,
|
|
available_artifacts: Set[str],
|
|
) -> dict:
|
|
"""
|
|
Validate that template references can be resolved.
|
|
|
|
Args:
|
|
template: Template to validate
|
|
available_artifacts: Set of available artifact names
|
|
|
|
Returns:
|
|
Dictionary with validation results:
|
|
- missing_required: List of missing required artifacts
|
|
- missing_optional: List of missing optional artifacts
|
|
- missing_generators: List of missing generator templates
|
|
- is_valid: True if all required deps are available
|
|
|
|
Raises:
|
|
ValueError: If template has not been analyzed
|
|
"""
|
|
if not template.analyzed:
|
|
raise ValueError(
|
|
f"Template '{template.name}' has not been analyzed. "
|
|
"Call analyze() first."
|
|
)
|
|
|
|
required = set(template.get_required_dependencies())
|
|
optional = set(template.get_optional_dependencies())
|
|
generators = set(template.get_generators())
|
|
|
|
missing_required = required - available_artifacts
|
|
missing_optional = optional - available_artifacts
|
|
missing_generators = generators - available_artifacts
|
|
|
|
return {
|
|
"missing_required": list(missing_required),
|
|
"missing_optional": list(missing_optional),
|
|
"missing_generators": list(missing_generators),
|
|
"is_valid": len(missing_required) == 0,
|
|
}
|