feat(prompts): implement Phase 3 - Resolver Engine (FR-3)
Implement deterministic multi-space resolution with configurable search order. Core Features: - ResolutionContext and ResolutionResult for tracking resolution state - MultiSpaceResolutionStrategy implementing FR-3.1 search order: 1. Local InformationSpace 2. Explicitly included InformationSpaces 3. Default InformationSpace 4. Team/Shared InformationSpace - PromptResolver with macro resolution logic - ContextCompiler for assembling resolved prompts - ResolutionConfig for configurable resolution behavior Resolution Behavior: - Required macros fail if not found (FR-3.2) - Optional macros resolve to empty (FR-3.3) - Generate macros detected for deferred execution (FR-3.4) - Deterministic search order with duplicate removal - Partial compilation support for debugging Tests (31 passing): - 14 strategy tests (search order, duplicates, priority) - 9 resolver tests (required, optional, generate, multi-space) - 8 compiler tests (substitution, dependencies, digests) Implements: - FR-3.1: Deterministic resolution order - FR-3.2: Required macro validation - FR-3.3: Optional macro fallback - FR-3.4: Generate macro detection - FR-3.5: Max generation depth configuration Files Created: - markitect/prompts/resolver/models.py - markitect/prompts/resolver/strategy.py - markitect/prompts/resolver/resolver.py - markitect/prompts/resolver/compiler.py - migrations/prompts/002_create_resolution_config.sql - tests/unit/prompts/test_resolution_strategy.py - tests/unit/prompts/test_prompt_resolver.py - tests/unit/prompts/test_context_compiler.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
202
markitect/prompts/resolver/compiler.py
Normal file
202
markitect/prompts/resolver/compiler.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
Context compiler for assembling resolved prompts.
|
||||
|
||||
Compiles resolved macros into final prompt context.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from markitect.prompts.templates.models import PromptTemplate
|
||||
from markitect.prompts.resolver.models import ResolutionResult
|
||||
from markitect.prompts.models import calculate_content_digest
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompiledPrompt:
|
||||
"""
|
||||
Compiled prompt ready for execution.
|
||||
|
||||
Contains the final assembled prompt with all macros resolved.
|
||||
|
||||
Attributes:
|
||||
template_id: Source template ID
|
||||
template_name: Source template name
|
||||
content: Compiled prompt content with macros substituted
|
||||
content_digest: SHA-256 digest of compiled content
|
||||
resolution_result: Original resolution result
|
||||
dependency_digests: Map of artifact name -> content digest
|
||||
compiled_at: Compilation timestamp
|
||||
metadata: Additional metadata
|
||||
"""
|
||||
template_id: str
|
||||
template_name: str
|
||||
content: str
|
||||
content_digest: str
|
||||
resolution_result: ResolutionResult
|
||||
dependency_digests: Dict[str, str] = field(default_factory=dict)
|
||||
compiled_at: datetime = field(default_factory=datetime.utcnow)
|
||||
metadata: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"template_id": self.template_id,
|
||||
"template_name": self.template_name,
|
||||
"content_digest": self.content_digest,
|
||||
"content_length": len(self.content),
|
||||
"dependency_count": len(self.dependency_digests),
|
||||
"compiled_at": self.compiled_at.isoformat(),
|
||||
"resolution_status": self.resolution_result.status.value,
|
||||
}
|
||||
|
||||
|
||||
class ContextCompiler:
|
||||
"""
|
||||
Compiler for assembling resolved context into final prompt.
|
||||
|
||||
Takes resolution results and produces CompiledPrompt with all
|
||||
macros substituted.
|
||||
"""
|
||||
|
||||
def compile(
|
||||
self,
|
||||
template: PromptTemplate,
|
||||
template_content: str,
|
||||
resolution_result: ResolutionResult,
|
||||
) -> CompiledPrompt:
|
||||
"""
|
||||
Compile template with resolved macros into final prompt.
|
||||
|
||||
Args:
|
||||
template: Source template
|
||||
template_content: Original template content
|
||||
resolution_result: Resolution result with resolved macros
|
||||
|
||||
Returns:
|
||||
CompiledPrompt with macros substituted
|
||||
|
||||
Raises:
|
||||
ValueError: If resolution failed
|
||||
"""
|
||||
if not resolution_result.success:
|
||||
raise ValueError(
|
||||
f"Cannot compile template '{template.name}': "
|
||||
f"Resolution failed with unresolved required macros"
|
||||
)
|
||||
|
||||
# Start with original template content
|
||||
compiled_content = template_content
|
||||
|
||||
# Track dependency digests
|
||||
dependency_digests = {}
|
||||
|
||||
# Substitute each resolved macro
|
||||
for resolved in resolution_result.context.resolved_macros:
|
||||
if resolved.resolved and resolved.artifact:
|
||||
# Replace macro with resolved content
|
||||
compiled_content = compiled_content.replace(
|
||||
resolved.macro.raw_text,
|
||||
resolved.content,
|
||||
)
|
||||
# Track dependency
|
||||
dependency_digests[resolved.artifact.name] = resolved.artifact.content_digest
|
||||
|
||||
# Substitute unresolved optional macros with empty string
|
||||
for macro in resolution_result.context.unresolved_optional:
|
||||
compiled_content = compiled_content.replace(macro.raw_text, "")
|
||||
|
||||
# Calculate digest of compiled content
|
||||
content_digest = calculate_content_digest(compiled_content)
|
||||
|
||||
return CompiledPrompt(
|
||||
template_id=template.id,
|
||||
template_name=template.name,
|
||||
content=compiled_content,
|
||||
content_digest=content_digest,
|
||||
resolution_result=resolution_result,
|
||||
dependency_digests=dependency_digests,
|
||||
)
|
||||
|
||||
def compile_partial(
|
||||
self,
|
||||
template: PromptTemplate,
|
||||
template_content: str,
|
||||
resolution_result: ResolutionResult,
|
||||
placeholder: str = "[UNRESOLVED]",
|
||||
) -> CompiledPrompt:
|
||||
"""
|
||||
Compile template even with unresolved required macros.
|
||||
|
||||
Useful for debugging or preview. Unresolved required macros
|
||||
are replaced with placeholder text.
|
||||
|
||||
Args:
|
||||
template: Source template
|
||||
template_content: Original template content
|
||||
resolution_result: Resolution result (may have failures)
|
||||
placeholder: Text to use for unresolved macros
|
||||
|
||||
Returns:
|
||||
CompiledPrompt with partial resolution
|
||||
"""
|
||||
# Start with original template content
|
||||
compiled_content = template_content
|
||||
|
||||
# Track dependency digests
|
||||
dependency_digests = {}
|
||||
|
||||
# Substitute resolved macros
|
||||
for resolved in resolution_result.context.resolved_macros:
|
||||
if resolved.resolved and resolved.artifact:
|
||||
compiled_content = compiled_content.replace(
|
||||
resolved.macro.raw_text,
|
||||
resolved.content,
|
||||
)
|
||||
dependency_digests[resolved.artifact.name] = resolved.artifact.content_digest
|
||||
|
||||
# Substitute unresolved required with placeholder
|
||||
for macro in resolution_result.context.unresolved_required:
|
||||
placeholder_text = f"{placeholder}:{macro.target}"
|
||||
compiled_content = compiled_content.replace(
|
||||
macro.raw_text,
|
||||
placeholder_text,
|
||||
)
|
||||
|
||||
# Substitute unresolved optional with empty
|
||||
for macro in resolution_result.context.unresolved_optional:
|
||||
compiled_content = compiled_content.replace(macro.raw_text, "")
|
||||
|
||||
content_digest = calculate_content_digest(compiled_content)
|
||||
|
||||
return CompiledPrompt(
|
||||
template_id=template.id,
|
||||
template_name=template.name,
|
||||
content=compiled_content,
|
||||
content_digest=content_digest,
|
||||
resolution_result=resolution_result,
|
||||
dependency_digests=dependency_digests,
|
||||
metadata={"partial": "true", "placeholder": placeholder},
|
||||
)
|
||||
|
||||
def get_compilation_info(self, compiled: CompiledPrompt) -> dict:
|
||||
"""
|
||||
Get information about compilation.
|
||||
|
||||
Args:
|
||||
compiled: Compiled prompt
|
||||
|
||||
Returns:
|
||||
Dictionary with compilation metadata
|
||||
"""
|
||||
return {
|
||||
"template_id": compiled.template_id,
|
||||
"template_name": compiled.template_name,
|
||||
"content_length": len(compiled.content),
|
||||
"content_digest": compiled.content_digest,
|
||||
"dependencies": list(compiled.dependency_digests.keys()),
|
||||
"dependency_count": len(compiled.dependency_digests),
|
||||
"compiled_at": compiled.compiled_at.isoformat(),
|
||||
"is_partial": compiled.metadata.get("partial") == "true",
|
||||
}
|
||||
Reference in New Issue
Block a user