"""Load prompts from files and write specs and version bundles to disk.
Supported input formats:
- **YAML / JSON** (``.yaml``, ``.yml``, ``.json``) — parsed as a full
:class:`~promptdb.domain.PromptSpec` with kind, messages, metadata, etc.
- **Plain text** (``.txt``, ``.md``, ``.prompt``, ``.jinja``, ``.mustache``)
— the file body becomes the template. You must specify ``kind`` explicitly.
Loading a structured file::
from promptdb.files import load_prompt_file
spec = load_prompt_file("prompts/support_classifier.yaml")
print(spec.kind) # PromptKind.CHAT
print(spec.messages) # [ChatMessage(...), ...]
Loading a plain-text file::
spec = load_prompt_file(
"prompts/answerer.md", kind=PromptKind.STRING,
)
Saving a spec back to disk::
from promptdb.files import save_prompt_spec
save_prompt_spec(spec, "build/classifier.yaml") # YAML
save_prompt_spec(spec, "build/classifier.json") # JSON
Exporting a full version bundle (includes version_id, revision, aliases)::
from promptdb.files import write_version_bundle
write_version_bundle(version_view, "build/classifier.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