diff --git a/src/promptforge/core/prompt.py b/src/promptforge/core/prompt.py index dbc5392..5ea7b4f 100644 --- a/src/promptforge/core/prompt.py +++ b/src/promptforge/core/prompt.py @@ -1,3 +1,5 @@ +"""Prompt model and management.""" + import hashlib import uuid from datetime import datetime @@ -10,6 +12,8 @@ from pydantic import BaseModel, Field, field_validator class VariableType(str, Enum): + """Supported variable types.""" + STRING = "string" INTEGER = "integer" FLOAT = "float" @@ -18,6 +22,8 @@ class VariableType(str, Enum): class PromptVariable(BaseModel): + """Definition of a template variable.""" + name: str type: VariableType = VariableType.STRING description: Optional[str] = None @@ -25,8 +31,17 @@ class PromptVariable(BaseModel): default: Optional[Any] = None choices: Optional[List[str]] = None + @field_validator('choices') + @classmethod + def validate_choices(cls, v, info): + if v is not None and info.data.get('type') != VariableType.CHOICE: + raise ValueError("choices only valid for CHOICE type") + return v + class ValidationRule(BaseModel): + """Validation rule for prompt output.""" + type: str pattern: Optional[str] = None json_schema: Optional[Dict[str, Any]] = None @@ -34,6 +49,8 @@ class ValidationRule(BaseModel): class Prompt(BaseModel): + """Prompt model with metadata and template.""" + id: str = Field(default_factory=lambda: str(uuid.uuid4())) name: str description: Optional[str] = None @@ -55,12 +72,20 @@ class Prompt(BaseModel): return hashlib.md5(content.encode()).hexdigest() return v + def to_dict(self, exclude_none: bool = False) -> Dict[str, Any]: + """Export prompt to dictionary.""" + data = super().model_dump(exclude_none=exclude_none) + data['created_at'] = self.created_at.isoformat() + data['updated_at'] = self.updated_at.isoformat() + return data + @classmethod def from_yaml(cls, yaml_content: str) -> "Prompt": + """Parse prompt from YAML with front matter.""" content = yaml_content.strip() if not content.startswith('---'): - metadata = {} + metadata: Dict[str, Any] = {} prompt_content = content else: parts = content[4:].split('\n---', 1) @@ -80,6 +105,7 @@ class Prompt(BaseModel): return cls(**data) def to_yaml(self) -> str: + """Export prompt to YAML front matter format.""" def var_to_dict(v): d = v.model_dump() d['type'] = v.type.value @@ -101,6 +127,14 @@ class Prompt(BaseModel): return f"---\n{yaml_str}---\n{self.content}" def save(self, prompts_dir: Path) -> Path: + """Save prompt to file. + + Args: + prompts_dir: Directory to save prompt in. + + Returns: + Path to saved file. + """ prompts_dir.mkdir(parents=True, exist_ok=True) filename = self.name.lower().replace(' ', '_').replace('/', '_') + '.yaml' filepath = prompts_dir / filename @@ -110,13 +144,15 @@ class Prompt(BaseModel): @classmethod def load(cls, filepath: Path) -> "Prompt": + """Load prompt from file.""" with open(filepath, 'r') as f: content = f.read() return cls.from_yaml(content) @classmethod def list(cls, prompts_dir: Path) -> List["Prompt"]: - prompts = [] + """List all prompts in directory.""" + prompts: List["Prompt"] = [] if not prompts_dir.exists(): return prompts for filepath in prompts_dir.glob('*.yaml'): @@ -124,4 +160,4 @@ class Prompt(BaseModel): prompts.append(cls.load(filepath)) except Exception: continue - return sorted(prompts, key=lambda p: p.name) \ No newline at end of file + return sorted(prompts, key=lambda p: p.name)