Source code for promptdb.client

"""Ergonomic Python client for prompt registration, resolution, and rendering.

:class:`PromptClient` is the main entry point for application code. It wraps
:class:`~promptdb.service.PromptService` and adds compact reference parsing,
file-based registration, and a :class:`~promptdb.domain.ResolvedPrompt`
wrapper with render and LangChain helpers.

Creating a client::

    from promptdb import PromptClient

    # Reads PROMPTDB_* env vars (database URL, blob root, storage backend)
    client = PromptClient.from_env()

    # Or with explicit settings
    from promptdb import AppSettings
    client = PromptClient.from_env(AppSettings(
        database_url="postgresql://user:pass@localhost/promptdb",
    ))

Registering prompts — three approaches::

    # 1. From inline text
    client.register_text(
        namespace="support", name="triage",
        template="Hello {name}", kind=PromptKind.STRING,
        alias="production",
    )

    # 2. From a PromptSpec object
    client.register_spec(namespace="support", name="triage", spec=spec)

    # 3. From a YAML/JSON/text file
    client.register_file(path="prompts/triage.yaml",
                         namespace="support", name="triage")

Resolving and rendering::

    resolved = client.get("support/triage:production")  # ResolvedPrompt
    text = resolved.render_text({"name": "Will"})
    lc = resolved.as_langchain()                        # LangChain prompt
    value = resolved.invoke({"name": "Will"})           # LangChain invoke

Selectors: ``latest``, ``production``, ``rev:2``, ``2026.04.01.1``, or a UUID.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

from promptdb.api import build_service
from promptdb.db import create_all
from promptdb.domain import (
    ChatMessage,
    MessageRole,
    PromptKind,
    PromptMetadata,
    PromptRef,
    PromptRegistration,
    PromptSpec,
    PromptVersionView,
    ResolvedPrompt,
    TemplateFormat,
)
from promptdb.files import load_prompt_file, write_version_bundle
from promptdb.service import PromptService
from promptdb.settings import AppSettings


[docs] class PromptClient: """Developer-friendly facade over :class:`~promptdb.service.PromptService`. Args: service: Prompt service instance. Returns: PromptClient: Local prompt client. Raises: None. Examples: .. code-block:: python client = PromptClient.from_env() resolved = client.get("support/triage:latest") """
[docs] def __init__(self, service: PromptService) -> None: """Initialize the client with a prompt service instance.""" self.service = service
[docs] @classmethod def from_env(cls, settings: AppSettings | None = None) -> PromptClient: """Create a client from environment-backed settings. Args: settings: Optional explicit settings. Returns: PromptClient: Configured client. Raises: ValueError: If the storage backend is misconfigured. Examples: >>> settings = AppSettings(database_url='sqlite:///:memory:') >>> isinstance(PromptClient.from_env(settings), PromptClient) True """ resolved_settings = settings or AppSettings() create_all(resolved_settings.database_url) return cls(build_service(resolved_settings))
@staticmethod def _coerce_ref(ref: PromptRef | str) -> PromptRef: """Normalize a prompt reference value. Args: ref: Prompt reference model or compact reference string. Returns: PromptRef: Normalized prompt reference. Raises: ValueError: If the string cannot be parsed. Examples: >>> PromptClient._coerce_ref('support/triage:latest').selector 'latest' """ if isinstance(ref, PromptRef): return ref return PromptRef.parse(ref)
[docs] def register_spec( self, *, namespace: str, name: str, spec: PromptSpec, created_by: str | None = None, alias: str | None = "latest", ) -> PromptVersionView: """Register a prompt spec. Args: namespace: Prompt namespace. name: Prompt name. spec: Prompt specification. created_by: Optional creator identifier. alias: Alias to move after creation. Returns: PromptVersionView: Stored prompt version. Raises: LookupError: If alias movement fails. Examples: .. code-block:: python version = client.register_spec(namespace='support', name='triage', spec=spec) """ return self.service.register( PromptRegistration( namespace=namespace, name=name, spec=spec, created_by=created_by, alias=alias, ) )
[docs] def register_text( self, *, namespace: str, name: str, template: str, kind: PromptKind = PromptKind.STRING, alias: str | None = "latest", created_by: str | None = None, metadata: PromptMetadata | None = None, template_format: TemplateFormat = TemplateFormat.FSTRING, partial_variables: dict[str, Any] | None = None, role: MessageRole = MessageRole.HUMAN, ) -> PromptVersionView: """Register a prompt directly from text. Args: namespace: Prompt namespace. name: Prompt name. template: Root template or message template. kind: Prompt kind. alias: Alias to move after registration. created_by: Optional creator identifier. metadata: Optional prompt metadata. template_format: Template engine. partial_variables: Stored partial variables. role: Chat role when ``kind`` is ``chat``. Returns: PromptVersionView: Stored prompt version. Raises: ValueError: If the prompt shape is invalid. Examples: .. code-block:: python version = client.register_text( namespace='support', name='triage', template='Hello {name}', ) """ if kind is PromptKind.STRING: spec = PromptSpec( kind=kind, template=template, template_format=template_format, partial_variables=partial_variables or {}, metadata=metadata or PromptMetadata(title=name), ) else: spec = PromptSpec( kind=kind, messages=[ChatMessage(role=role, template=template)], template_format=template_format, partial_variables=partial_variables or {}, metadata=metadata or PromptMetadata(title=name), ) return self.register_spec( namespace=namespace, name=name, spec=spec, created_by=created_by, alias=alias, )
[docs] def register_file( self, *, path: str | Path, namespace: str, name: str, kind: PromptKind | None = None, alias: str | None = "latest", created_by: str | None = None, message_role: MessageRole = MessageRole.HUMAN, user_version: str | None = None, ) -> PromptVersionView: """Register a prompt from a text or structured file. Args: path: Input file path. namespace: Prompt namespace. name: Prompt name. kind: Prompt kind for plain-text files. Ignored for structured spec files. alias: Alias to move after registration. created_by: Optional creator identifier. message_role: Chat role for plain-text chat prompt files. user_version: Optional user-facing version label override. Returns: PromptVersionView: Stored prompt version. Raises: FileNotFoundError: If the file does not exist. ValueError: If ``kind`` is omitted for plain-text files. Examples: .. code-block:: python version = client.register_file( path='prompts/triage.yaml', namespace='support', name='triage', ) """ spec = load_prompt_file(path, kind=kind, message_role=message_role) if user_version is not None: spec.metadata.user_version = user_version if spec.metadata.title is None: spec.metadata.title = name return self.register_spec( namespace=namespace, name=name, spec=spec, created_by=created_by, alias=alias, )
[docs] def resolve(self, ref: PromptRef | str) -> PromptVersionView: """Resolve a prompt reference. Args: ref: Prompt reference or compact string. Returns: PromptVersionView: Resolved prompt version. Raises: LookupError: If resolution fails. Examples: >>> client = PromptClient.from_env(AppSettings(database_url='sqlite:///:memory:')) >>> version = client.register_text(namespace='x', name='y', template='Hi {name}') >>> client.resolve('x/y:latest').version_id == version.version_id True """ return self.service.resolve(self._coerce_ref(ref))
[docs] def get(self, ref: PromptRef | str) -> ResolvedPrompt: """Resolve and wrap a prompt reference. Args: ref: Prompt reference or compact string. Returns: ResolvedPrompt: Wrapped resolved prompt. Raises: LookupError: If resolution fails. Examples: >>> client = PromptClient.from_env(AppSettings(database_url='sqlite:///:memory:')) >>> _ = client.register_text(namespace='x', name='y', template='Hi {name}') >>> client.get('x/y:latest').render_text({'name': 'Will'}) 'Hi Will' """ return self.resolve(ref).wrap()
[docs] def render(self, ref: PromptRef | str, variables: dict[str, Any]) -> Any: """Render a prompt reference directly. Args: ref: Prompt reference or compact string. variables: Runtime variables. Returns: Any: Render result model. Raises: LookupError: If resolution fails. Examples: >>> client = PromptClient.from_env(AppSettings(database_url='sqlite:///:memory:')) >>> _ = client.register_text(namespace='x', name='y', template='Hi {name}') >>> client.render('x/y:latest', {'name': 'Will'}).text 'Hi Will' """ return self.service.render(self._coerce_ref(ref), variables)
[docs] def list_versions(self) -> list[PromptVersionView]: """List all stored versions. Args: None. Returns: list[PromptVersionView]: Stored prompt versions. Raises: None. Examples: >>> client = PromptClient.from_env(AppSettings(database_url='sqlite:///:memory:')) >>> client.list_versions() [] """ return self.service.list_versions()
[docs] def export_to_file(self, ref: PromptRef | str, path: str | Path) -> Path: """Resolve and export a version bundle to a file. Args: ref: Prompt reference or compact string. path: Output file path. Returns: Path: Written file path. Raises: LookupError: If resolution fails. OSError: If writing fails. Examples: .. code-block:: python client.export_to_file('support/triage:production', 'build/triage.json') """ return write_version_bundle(self.resolve(ref), path)
[docs] def export_file(self, ref: PromptRef | str, path: str | Path) -> Path: """Resolve and export a version bundle to a file. Args: ref: Prompt reference or compact string. path: Output file path. Returns: Path: Written file path. Raises: LookupError: If resolution fails. OSError: If writing fails. Examples: .. code-block:: python client.export_file('support/triage:production', 'build/triage.json') """ return self.export_to_file(ref, path)