- ContentMacro: add __post_init__ to auto-derive raw_text when built
programmatically, preventing str.replace("", X) corruption
- MacroParser: add @{target} shorthand syntax support mapped to REQUIRED kind,
updating parse, has_macros, count_macros, and find_macro_positions
- Artifact: store content in model and SQLite DB, replace resolver placeholder
with actual artifact content, add migration for existing databases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
281 lines
8.4 KiB
Python
281 lines
8.4 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 __post_init__(self) -> None:
|
|
"""Auto-derive raw_text when built programmatically."""
|
|
if not self.raw_text:
|
|
self.raw_text = f"@{{{self.target}}}"
|
|
|
|
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),
|
|
)
|