Files
markitect-main/markitect/prompts/services/template_service.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

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)