Source code for promptdb.files

"""File helpers for :mod:`promptdb`.

Purpose:
    Load prompts from plain-text or structured files and write prompt specs or
    resolved versions back to disk.

Design:
    Plain-text files are convenient for authoring simple prompts, while JSON and
    YAML files can capture the full :class:`~promptdb.domain.PromptSpec` shape.

Attributes:
    load_prompt_file: Create a prompt spec from a file.
    save_prompt_spec: Write a prompt specification to a file.
    write_version_bundle: Write a version view to a file.

Examples:
    .. code-block:: python

        spec = load_prompt_file('prompts/demo.yaml')
        save_prompt_spec(spec, 'build/demo.json')
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import yaml

from promptdb.domain import (
    ChatMessage,
    MessageRole,
    PromptKind,
    PromptMetadata,
    PromptSpec,
    PromptVersionView,
)

_STRUCTURED_SUFFIXES = {".json", ".yaml", ".yml"}
_TEXT_SUFFIXES = {".txt", ".md", ".prompt", ".jinja", ".mustache"}


def _normalize_structured_payload(payload: dict[str, Any], source_path: Path) -> PromptSpec:
    """Normalize a structured prompt payload into a prompt spec.

    Args:
        payload: Parsed mapping payload.
        source_path: Source file path.

    Returns:
        PromptSpec: Validated prompt specification.

    Raises:
        ValueError: If the payload shape is invalid.

    Examples:
        >>> payload = {'kind': 'string', 'template': 'Hi {name}'}
        >>> spec = _normalize_structured_payload(payload, Path('x.yaml'))
        >>> spec.kind.value
        'string'
    """
    data = dict(payload)
    metadata_payload = dict(data.get("metadata") or {})
    metadata_payload.setdefault("source_path", str(source_path))
    data["metadata"] = PromptMetadata.model_validate(metadata_payload)
    return PromptSpec.model_validate(data)


[docs] def load_prompt_file( path: str | Path, *, kind: PromptKind | None = None, message_role: MessageRole = MessageRole.HUMAN, ) -> PromptSpec: """Load a prompt spec from a plain-text or structured file. Args: path: File path. kind: Prompt kind for plain-text files. Structured files can omit this. message_role: Message role for plain-text chat prompts. Returns: PromptSpec: Loaded prompt specification. Raises: FileNotFoundError: If the file is missing. ValueError: If ``kind`` is omitted for plain-text files. Examples: .. code-block:: python spec = load_prompt_file('prompts/demo.txt', kind=PromptKind.STRING) spec = load_prompt_file('prompts/demo.yaml') """ file_path = Path(path) suffix = file_path.suffix.lower() if suffix in _STRUCTURED_SUFFIXES: text = file_path.read_text(encoding="utf-8") payload = json.loads(text) if suffix == ".json" else yaml.safe_load(text) if not isinstance(payload, dict): raise ValueError("Structured prompt files must contain a mapping payload.") return _normalize_structured_payload(payload, file_path) if kind is None: raise ValueError("Plain-text prompt files require an explicit kind.") body = file_path.read_text(encoding="utf-8") metadata = PromptMetadata(title=file_path.stem, source_path=str(file_path)) if kind is PromptKind.STRING: return PromptSpec(kind=kind, template=body, metadata=metadata) messages = [ChatMessage(role=message_role, template=body)] return PromptSpec(kind=kind, messages=messages, metadata=metadata)
[docs] def save_prompt_spec(spec: PromptSpec, path: str | Path) -> Path: """Write a prompt spec to JSON or YAML. Args: spec: Prompt specification. path: Output file path. Returns: Path: Output path. Raises: ValueError: If the suffix is unsupported. OSError: If writing fails. Examples: .. code-block:: python save_prompt_spec(spec, 'build/demo.yaml') """ output_path = Path(path) output_path.parent.mkdir(parents=True, exist_ok=True) payload = spec.model_dump(mode="json", exclude_none=True, exclude_computed_fields=True) suffix = output_path.suffix.lower() if suffix == ".json": output_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") return output_path if suffix in {".yaml", ".yml"}: output_path.write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8") return output_path raise ValueError("Prompt specs can only be saved as .json, .yaml, or .yml.")
[docs] def write_version_bundle(version: PromptVersionView, path: str | Path) -> Path: """Write a version bundle to a file. Args: version: Prompt version. path: Output file path. Returns: Path: Output path. Raises: OSError: If writing fails. Examples: .. code-block:: python write_version_bundle(version, 'build/version.json') """ output_path = Path(path) output_path.parent.mkdir(parents=True, exist_ok=True) payload = version.model_dump(mode="json", exclude_none=True) suffix = output_path.suffix.lower() if suffix in {".yaml", ".yml"}: output_path.write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8") else: output_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") return output_path