Files
markitect-main/markitect/prompts/execution/models.py
tegwick 36c20f37d0
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
feat(llm): extract adapter layer for standalone llm-connect package (S1+S2)
Stage 1 — Decouple:
- Move RunConfig + LLMResponse to markitect/llm/models.py (canonical)
- Move LLMAdapter + Mock/ErrorLLMAdapter to markitect/llm/adapter.py
- markitect/prompts/execution/models.py and llm_adapter.py become re-export shims
- All 4 adapters + factory.py updated to import from markitect.llm.*
- Parameterize app_name in toml_config.py (resolve_llm, get_default_layers,
  get_preference_layers): paths and env var now derived from app_name arg
- Add tests/test_llm_isolation.py: 7 isolation + backward-compat tests

Stage 2 — Extract:
- Standalone llm-connect package created at ~/llm-connect/
- All 18 llm files copied; markitect.* imports replaced with llm_connect.*
- LLMError base inlined in llm_connect/exceptions.py (no markitect dep)
- llm-connect installed into markitect-venv; declared in pyproject.toml

Smoke test: markitect llm-check succeeds (live Gemini API call).
Backward compat: markitect.prompts.execution.{models,llm_adapter} still work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:04:50 +01:00

228 lines
7.1 KiB
Python

"""
Models for prompt execution.
Implements FR-4: PromptRun Lifecycle
Defines execution stages, run configurations, and input bundles.
"""
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, Any, List, Optional
from enum import Enum
from markitect.prompts.models import calculate_bundle_digest
from markitect.llm.models import RunConfig, LLMResponse # canonical; re-exported here
class ExecutionStage(Enum):
"""
Execution lifecycle stages.
Implements FR-4.1: PromptRun execution stages
"""
PENDING = "pending" # Not started
ANALYSIS = "analysis" # Template analysis
COMPILATION = "compilation" # Context compilation
PROCESSING = "processing" # LLM execution
COMPLETE = "complete" # Successfully finished
FAILED = "failed" # Execution failed
class RunStatus(Enum):
"""Overall status of a run."""
PENDING = "pending"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
SKIPPED = "skipped" # Skipped due to identical InputBundleHash
@dataclass
class InputBundle:
"""
Complete input context for execution.
Implements FR-4.3: InputBundleHash calculation
The InputBundle captures all inputs that affect execution output,
enabling idempotent execution through content-based hashing.
Attributes:
template_digest: SHA-256 digest of template content
dependency_digests: Map of dependency name -> digest
resolution_config_hash: Hash of resolution configuration
model_config: Model configuration
compilation_options: Compilation settings
"""
template_digest: str
dependency_digests: Dict[str, str]
resolution_config_hash: str
model_config: Dict[str, Any]
compilation_options: Dict[str, Any] = field(default_factory=dict)
def calculate_hash(self) -> str:
"""
Calculate deterministic hash of input bundle.
Implements FR-4.3: InputBundleHash calculation
Components (sorted for determinism):
1. Template content digest
2. Sorted dependency digests by name
3. Resolution configuration hash
4. Model settings (name, temperature, etc.)
5. Compilation options
Returns:
SHA-256 hash of complete input bundle
"""
components = {
"template": self.template_digest,
"dependencies": ":".join(
f"{k}={v}" for k, v in sorted(self.dependency_digests.items())
),
"resolution_config": self.resolution_config_hash,
"model": ":".join(
f"{k}={v}" for k, v in sorted(self.model_config.items())
),
"compilation": ":".join(
f"{k}={v}" for k, v in sorted(self.compilation_options.items())
),
}
return calculate_bundle_digest(components)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"template_digest": self.template_digest,
"dependency_digests": self.dependency_digests,
"resolution_config_hash": self.resolution_config_hash,
"model_config": self.model_config,
"compilation_options": self.compilation_options,
"input_bundle_hash": self.calculate_hash(),
}
@dataclass
class PromptRun:
"""
Record of a prompt template execution.
Implements FR-4: PromptRun Lifecycle
Tracks complete execution state through all stages:
Analysis → Compilation → Processing → Complete/Failed
Attributes:
id: Unique run identifier
template_id: ID of template being executed
input_bundle_hash: Hash of input bundle for idempotency
status: Overall run status
stage: Current execution stage
parent_run_id: Parent run ID (for nested generators)
depth: Nesting depth (0 for top-level)
config: Execution configuration
started_at: Execution start time
completed_at: Execution completion time
error_message: Error message if failed
metadata: Additional run metadata
"""
id: str
template_id: str
input_bundle_hash: str
status: RunStatus = RunStatus.PENDING
stage: ExecutionStage = ExecutionStage.PENDING
parent_run_id: Optional[str] = None
depth: int = 0
config: RunConfig = field(default_factory=RunConfig)
started_at: datetime = field(default_factory=datetime.utcnow)
completed_at: Optional[datetime] = None
error_message: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
@classmethod
def create(
cls,
template_id: str,
input_bundle_hash: str,
config: Optional[RunConfig] = None,
parent_run_id: Optional[str] = None,
depth: int = 0,
) -> "PromptRun":
"""
Create a new run.
Args:
template_id: Template being executed
input_bundle_hash: Hash of input bundle
config: Execution configuration
parent_run_id: Parent run ID for nested execution
depth: Nesting depth
Returns:
New PromptRun instance
"""
return cls(
id=str(uuid.uuid4()),
template_id=template_id,
input_bundle_hash=input_bundle_hash,
config=config or RunConfig(),
parent_run_id=parent_run_id,
depth=depth,
)
def advance_stage(self, stage: ExecutionStage) -> None:
"""
Advance to next execution stage.
Args:
stage: New stage
"""
self.stage = stage
if stage == ExecutionStage.PROCESSING:
self.status = RunStatus.RUNNING
def mark_complete(self) -> None:
"""Mark run as successfully completed."""
self.stage = ExecutionStage.COMPLETE
self.status = RunStatus.SUCCESS
self.completed_at = datetime.utcnow()
def mark_failed(self, error: str) -> None:
"""
Mark run as failed.
Args:
error: Error message
"""
self.stage = ExecutionStage.FAILED
self.status = RunStatus.FAILED
self.error_message = error
self.completed_at = datetime.utcnow()
def mark_skipped(self) -> None:
"""Mark run as skipped (identical hash exists)."""
self.status = RunStatus.SKIPPED
self.completed_at = datetime.utcnow()
def is_complete(self) -> bool:
"""Check if run is complete."""
return self.status in (RunStatus.SUCCESS, RunStatus.FAILED, RunStatus.SKIPPED)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"id": self.id,
"template_id": self.template_id,
"input_bundle_hash": self.input_bundle_hash,
"status": self.status.value,
"stage": self.stage.value,
"parent_run_id": self.parent_run_id,
"depth": self.depth,
"config": self.config.to_dict(),
"started_at": self.started_at.isoformat(),
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
"error_message": self.error_message,
}