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>
261 lines
7.4 KiB
Python
261 lines
7.4 KiB
Python
"""
|
|
Template service for high-level template management operations.
|
|
|
|
This service extends artifact service to handle PromptTemplate-specific
|
|
operations including macro analysis and dependency extraction.
|
|
"""
|
|
|
|
from typing import List, Optional
|
|
|
|
from markitect.prompts.models import ArtifactMetadata, ArtifactType
|
|
from markitect.prompts.templates.models import (
|
|
PromptTemplate,
|
|
TemplateMetadata,
|
|
)
|
|
from markitect.prompts.templates.analyzer import TemplateAnalyzer, TemplateAnalysisResult
|
|
from markitect.prompts.services.artifact_service import ArtifactService
|
|
from markitect.prompts.repositories.interfaces import ArtifactNotFoundError
|
|
|
|
|
|
class TemplateService:
|
|
"""
|
|
Service for template management operations.
|
|
|
|
Provides high-level business logic for creating and analyzing templates,
|
|
building on top of ArtifactService.
|
|
"""
|
|
|
|
def __init__(self, artifact_service: ArtifactService):
|
|
"""
|
|
Initialize service with artifact service.
|
|
|
|
Args:
|
|
artifact_service: Artifact service for persistence
|
|
"""
|
|
self.artifact_service = artifact_service
|
|
self.analyzer = TemplateAnalyzer()
|
|
|
|
def create_template(
|
|
self,
|
|
space_id: str,
|
|
name: str,
|
|
content: str,
|
|
artifact_metadata: Optional[ArtifactMetadata] = None,
|
|
template_metadata: Optional[TemplateMetadata] = None,
|
|
analyze: bool = True,
|
|
) -> PromptTemplate:
|
|
"""
|
|
Create and optionally analyze 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
|
|
analyze: Whether to analyze macros immediately
|
|
|
|
Returns:
|
|
Created template (analyzed if analyze=True)
|
|
|
|
Raises:
|
|
DuplicateArtifactError: If template already exists
|
|
MacroParsingError: If macro syntax is invalid (when analyze=True)
|
|
"""
|
|
# Create template
|
|
template = PromptTemplate.create(
|
|
space_id=space_id,
|
|
name=name,
|
|
content=content,
|
|
artifact_metadata=artifact_metadata,
|
|
template_metadata=template_metadata,
|
|
)
|
|
|
|
# Persist artifact
|
|
self.artifact_service.create_artifact(
|
|
space_id=space_id,
|
|
name=name,
|
|
content=content,
|
|
artifact_type=ArtifactType.TEMPLATE,
|
|
metadata=artifact_metadata,
|
|
)
|
|
|
|
# Analyze if requested
|
|
if analyze:
|
|
self.analyzer.analyze(template, content)
|
|
|
|
return template
|
|
|
|
def get_template(self, template_id: str, content: str) -> PromptTemplate:
|
|
"""
|
|
Retrieve template by ID.
|
|
|
|
Args:
|
|
template_id: Template identifier
|
|
content: Template content (needed to avoid storing content in DB twice)
|
|
|
|
Returns:
|
|
PromptTemplate instance
|
|
|
|
Raises:
|
|
ArtifactNotFoundError: If template doesn't exist
|
|
"""
|
|
artifact = self.artifact_service.get_artifact(template_id)
|
|
if artifact.artifact_type != ArtifactType.TEMPLATE:
|
|
raise ValueError(
|
|
f"Artifact '{template_id}' is not a template "
|
|
f"(type: {artifact.artifact_type})"
|
|
)
|
|
|
|
template = PromptTemplate.from_artifact(artifact)
|
|
# Analyze to populate macros
|
|
self.analyzer.analyze(template, content)
|
|
return template
|
|
|
|
def get_template_by_name(
|
|
self,
|
|
space_id: str,
|
|
name: str,
|
|
content: str,
|
|
) -> PromptTemplate:
|
|
"""
|
|
Retrieve template by space and name.
|
|
|
|
Args:
|
|
space_id: Space identifier
|
|
name: Template name
|
|
content: Template content
|
|
|
|
Returns:
|
|
PromptTemplate instance
|
|
|
|
Raises:
|
|
ArtifactNotFoundError: If template doesn't exist
|
|
"""
|
|
artifact = self.artifact_service.get_artifact_by_name(space_id, name)
|
|
if artifact.artifact_type != ArtifactType.TEMPLATE:
|
|
raise ValueError(
|
|
f"Artifact '{name}' is not a template "
|
|
f"(type: {artifact.artifact_type})"
|
|
)
|
|
|
|
template = PromptTemplate.from_artifact(artifact)
|
|
self.analyzer.analyze(template, content)
|
|
return template
|
|
|
|
def analyze_template(
|
|
self,
|
|
template: PromptTemplate,
|
|
content: str,
|
|
) -> TemplateAnalysisResult:
|
|
"""
|
|
Analyze template to extract macros and dependencies.
|
|
|
|
Args:
|
|
template: Template to analyze
|
|
content: Template content
|
|
|
|
Returns:
|
|
Analysis result with dependency information
|
|
|
|
Raises:
|
|
MacroParsingError: If macro syntax is invalid
|
|
"""
|
|
return self.analyzer.analyze(template, content)
|
|
|
|
def list_templates(self, space_id: str) -> List[PromptTemplate]:
|
|
"""
|
|
List all templates in a space.
|
|
|
|
Note: Templates are returned without macro analysis.
|
|
Call analyze_template() on individual templates as needed.
|
|
|
|
Args:
|
|
space_id: Space identifier
|
|
|
|
Returns:
|
|
List of templates (unanalyzed)
|
|
"""
|
|
artifacts = self.artifact_service.list_artifacts(
|
|
space_id=space_id,
|
|
artifact_type=ArtifactType.TEMPLATE,
|
|
)
|
|
|
|
templates = [PromptTemplate.from_artifact(a) for a in artifacts]
|
|
return templates
|
|
|
|
def quick_check_content(self, content: str) -> dict:
|
|
"""
|
|
Quick validation of template content.
|
|
|
|
Useful for checking content before template creation.
|
|
|
|
Args:
|
|
content: Template content to check
|
|
|
|
Returns:
|
|
Dictionary with macro counts and validation info
|
|
"""
|
|
return self.analyzer.quick_check(content)
|
|
|
|
def update_template_content(
|
|
self,
|
|
template_id: str,
|
|
new_content: str,
|
|
reanalyze: bool = True,
|
|
) -> PromptTemplate:
|
|
"""
|
|
Update template content.
|
|
|
|
Args:
|
|
template_id: Template to update
|
|
new_content: New content
|
|
reanalyze: Whether to reanalyze macros
|
|
|
|
Returns:
|
|
Updated template
|
|
|
|
Raises:
|
|
ArtifactNotFoundError: If template doesn't exist
|
|
MacroParsingError: If new content has invalid macros
|
|
"""
|
|
# Update artifact content
|
|
artifact = self.artifact_service.update_artifact_content(
|
|
template_id,
|
|
new_content,
|
|
)
|
|
|
|
# Create template from updated artifact
|
|
template = PromptTemplate.from_artifact(artifact)
|
|
|
|
# Reanalyze if requested
|
|
if reanalyze:
|
|
self.analyzer.analyze(template, new_content)
|
|
|
|
return template
|
|
|
|
def delete_template(self, template_id: str) -> bool:
|
|
"""
|
|
Delete a template.
|
|
|
|
Args:
|
|
template_id: Template to delete
|
|
|
|
Returns:
|
|
True if deleted, False if not found
|
|
"""
|
|
return self.artifact_service.delete_artifact(template_id)
|
|
|
|
def template_exists(self, space_id: str, name: str) -> bool:
|
|
"""
|
|
Check if template exists.
|
|
|
|
Args:
|
|
space_id: Space identifier
|
|
name: Template name
|
|
|
|
Returns:
|
|
True if template exists
|
|
"""
|
|
return self.artifact_service.artifact_exists(space_id, name)
|