Files
markitect-main/markitect/prompts/templates/models.py
tegwick e6840fe696 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>
2026-02-08 22:34:22 +01:00

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),
)