feat(prompts): implement Phase 2 - Templates & Macros (FR-2)
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>
This commit is contained in:
275
markitect/prompts/templates/models.py
Normal file
275
markitect/prompts/templates/models.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
Template models for Prompt Dependency Resolution.
|
||||
|
||||
Defines PromptTemplate and ContentMacro models for template-based
|
||||
prompt generation with dependency resolution.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Any, List, Optional
|
||||
from enum import Enum
|
||||
|
||||
from markitect.prompts.models import Artifact, ArtifactType, ArtifactMetadata
|
||||
|
||||
|
||||
class MacroKind(Enum):
|
||||
"""Types of content macros supported in templates."""
|
||||
REQUIRED = "required" # Must be resolved, fail if missing
|
||||
OPTIONAL = "optional" # Resolve if available, empty if missing
|
||||
GENERATE = "generate" # Trigger generator template execution
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContentMacro:
|
||||
"""
|
||||
Content macro extracted from a template.
|
||||
|
||||
Implements FR-2.3: ContentMacro kinds (Required, Optional, Generate)
|
||||
|
||||
Syntax: {{<kind>:<target>[|<param1>=<value1>|<param2>=<value2>...]}}
|
||||
|
||||
Examples:
|
||||
{{require:glossary}}
|
||||
{{optional:technical-constraints}}
|
||||
{{generate:code-examples|language=python|framework=fastapi}}
|
||||
|
||||
Attributes:
|
||||
kind: Type of macro (required, optional, generate)
|
||||
target: Name of artifact or template to resolve
|
||||
parameters: Optional parameters for macro resolution
|
||||
raw_text: Original macro text from template
|
||||
line_number: Line number where macro appears (for error reporting)
|
||||
"""
|
||||
kind: MacroKind
|
||||
target: str
|
||||
parameters: Dict[str, str] = field(default_factory=dict)
|
||||
raw_text: str = ""
|
||||
line_number: int = 0
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of macro."""
|
||||
params = ''.join(f"|{k}={v}" for k, v in self.parameters.items())
|
||||
return f"{{{{{self.kind.value}:{self.target}{params}}}}}"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert macro to dictionary for serialization."""
|
||||
return {
|
||||
"kind": self.kind.value,
|
||||
"target": self.target,
|
||||
"parameters": self.parameters,
|
||||
"raw_text": self.raw_text,
|
||||
"line_number": self.line_number,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ContentMacro":
|
||||
"""Create macro from dictionary."""
|
||||
return cls(
|
||||
kind=MacroKind(data["kind"]),
|
||||
target=data["target"],
|
||||
parameters=data.get("parameters", {}),
|
||||
raw_text=data.get("raw_text", ""),
|
||||
line_number=data.get("line_number", 0),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateMetadata:
|
||||
"""
|
||||
Extended metadata specific to prompt templates.
|
||||
|
||||
Attributes:
|
||||
purpose: Description of what the template generates
|
||||
model_hints: Suggested model parameters
|
||||
expected_inputs: Documentation of expected required/optional inputs
|
||||
output_type: Expected type of generated content
|
||||
"""
|
||||
purpose: Optional[str] = None
|
||||
model_hints: Dict[str, Any] = field(default_factory=dict)
|
||||
expected_inputs: List[str] = field(default_factory=list)
|
||||
output_type: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization."""
|
||||
return {
|
||||
"purpose": self.purpose,
|
||||
"model_hints": self.model_hints,
|
||||
"expected_inputs": self.expected_inputs,
|
||||
"output_type": self.output_type,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "TemplateMetadata":
|
||||
"""Create from dictionary."""
|
||||
return cls(
|
||||
purpose=data.get("purpose"),
|
||||
model_hints=data.get("model_hints", {}),
|
||||
expected_inputs=data.get("expected_inputs", []),
|
||||
output_type=data.get("output_type"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PromptTemplate:
|
||||
"""
|
||||
Prompt template with content macros.
|
||||
|
||||
Extends Artifact with template-specific functionality.
|
||||
Implements FR-2: PromptTemplate Definition
|
||||
|
||||
A PromptTemplate is an Artifact of type TEMPLATE containing ContentMacros
|
||||
that reference other artifacts or trigger nested generation.
|
||||
|
||||
Attributes:
|
||||
artifact: Underlying artifact
|
||||
template_metadata: Template-specific metadata
|
||||
macros: Extracted content macros
|
||||
analyzed: Whether template has been analyzed
|
||||
"""
|
||||
artifact: Artifact
|
||||
template_metadata: TemplateMetadata = field(default_factory=TemplateMetadata)
|
||||
macros: List[ContentMacro] = field(default_factory=list)
|
||||
analyzed: bool = False
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
space_id: str,
|
||||
name: str,
|
||||
content: str,
|
||||
artifact_metadata: Optional[ArtifactMetadata] = None,
|
||||
template_metadata: Optional[TemplateMetadata] = None,
|
||||
) -> "PromptTemplate":
|
||||
"""
|
||||
Create a new template.
|
||||
|
||||
Args:
|
||||
space_id: ID of containing space
|
||||
name: Template name
|
||||
content: Template content with macros
|
||||
artifact_metadata: General artifact metadata
|
||||
template_metadata: Template-specific metadata
|
||||
|
||||
Returns:
|
||||
New PromptTemplate instance
|
||||
"""
|
||||
artifact = Artifact.create(
|
||||
space_id=space_id,
|
||||
name=name,
|
||||
content=content,
|
||||
artifact_type=ArtifactType.TEMPLATE,
|
||||
metadata=artifact_metadata,
|
||||
)
|
||||
|
||||
return cls(
|
||||
artifact=artifact,
|
||||
template_metadata=template_metadata or TemplateMetadata(),
|
||||
macros=[],
|
||||
analyzed=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_artifact(cls, artifact: Artifact) -> "PromptTemplate":
|
||||
"""
|
||||
Create template from existing artifact.
|
||||
|
||||
Args:
|
||||
artifact: Artifact to wrap
|
||||
|
||||
Returns:
|
||||
PromptTemplate instance
|
||||
|
||||
Raises:
|
||||
ValueError: If artifact is not of type TEMPLATE
|
||||
"""
|
||||
if artifact.artifact_type != ArtifactType.TEMPLATE:
|
||||
raise ValueError(
|
||||
f"Artifact must be of type TEMPLATE, got {artifact.artifact_type}"
|
||||
)
|
||||
|
||||
return cls(
|
||||
artifact=artifact,
|
||||
template_metadata=TemplateMetadata(),
|
||||
macros=[],
|
||||
analyzed=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
"""Get template ID."""
|
||||
return self.artifact.id
|
||||
|
||||
@property
|
||||
def space_id(self) -> str:
|
||||
"""Get space ID."""
|
||||
return self.artifact.space_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get template name."""
|
||||
return self.artifact.name
|
||||
|
||||
@property
|
||||
def content_digest(self) -> str:
|
||||
"""Get content digest."""
|
||||
return self.artifact.content_digest
|
||||
|
||||
def get_required_dependencies(self) -> List[str]:
|
||||
"""
|
||||
Get list of required artifact names.
|
||||
|
||||
Returns:
|
||||
List of artifact names from required macros
|
||||
"""
|
||||
return [
|
||||
macro.target
|
||||
for macro in self.macros
|
||||
if macro.kind == MacroKind.REQUIRED
|
||||
]
|
||||
|
||||
def get_optional_dependencies(self) -> List[str]:
|
||||
"""
|
||||
Get list of optional artifact names.
|
||||
|
||||
Returns:
|
||||
List of artifact names from optional macros
|
||||
"""
|
||||
return [
|
||||
macro.target
|
||||
for macro in self.macros
|
||||
if macro.kind == MacroKind.OPTIONAL
|
||||
]
|
||||
|
||||
def get_generators(self) -> List[str]:
|
||||
"""
|
||||
Get list of generator template names.
|
||||
|
||||
Returns:
|
||||
List of template names from generate macros
|
||||
"""
|
||||
return [
|
||||
macro.target
|
||||
for macro in self.macros
|
||||
if macro.kind == MacroKind.GENERATE
|
||||
]
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert template to dictionary for serialization."""
|
||||
return {
|
||||
"artifact": self.artifact.to_dict(),
|
||||
"template_metadata": self.template_metadata.to_dict(),
|
||||
"macros": [macro.to_dict() for macro in self.macros],
|
||||
"analyzed": self.analyzed,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "PromptTemplate":
|
||||
"""Create template from dictionary."""
|
||||
return cls(
|
||||
artifact=Artifact.from_dict(data["artifact"]),
|
||||
template_metadata=TemplateMetadata.from_dict(
|
||||
data.get("template_metadata", {})
|
||||
),
|
||||
macros=[ContentMacro.from_dict(m) for m in data.get("macros", [])],
|
||||
analyzed=data.get("analyzed", False),
|
||||
)
|
||||
Reference in New Issue
Block a user