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>
276 lines
8.2 KiB
Python
276 lines
8.2 KiB
Python
"""
|
|
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),
|
|
)
|