Source code for herethere.there.ai.prompts

"""Prompt sections for %%there ai."""

from collections.abc import Iterable
from dataclasses import dataclass, field
from importlib.resources import files

DEFAULT_AI_TEMPLATE_RESOURCE = "prompts/default.md"
FIX_AI_TEMPLATE_RESOURCE = "prompts/fix.md"
DEFAULT_AI_PROMPT = "default"
FIX_AI_PROMPT = "fix"


class AIPromptError(RuntimeError):
    """Raised when %%there ai prompt composition fails."""


@dataclass
class AIPromptStore:
    """Mutable prompt definitions and active prompt stack.

    Built-in prompts live in the same registry as user prompts so
    register_ai_prompt() can intentionally override any prompt section.
    """

    active_prompts: tuple[str, ...] | None = None
    registry: dict[str, str] = field(default_factory=dict)


def _read_prompt_resource(name: str) -> str:
    return (
        files("herethere.there.ai").joinpath(name).read_text(encoding="utf-8").strip()
    )


def _builtin_prompt_registry() -> dict[str, str]:
    return {
        DEFAULT_AI_PROMPT: _read_prompt_resource(DEFAULT_AI_TEMPLATE_RESOURCE),
        FIX_AI_PROMPT: _read_prompt_resource(FIX_AI_TEMPLATE_RESOURCE),
    }


_ai_prompt_store = AIPromptStore(registry=_builtin_prompt_registry())


def reset_ai_prompt_store() -> None:
    """Reset active prompt state and restore built-in prompt definitions."""
    _ai_prompt_store.active_prompts = None
    _ai_prompt_store.registry.clear()
    _ai_prompt_store.registry.update(_builtin_prompt_registry())


def _normalize_prompt_name(name: str) -> str:
    normalized = name.strip()
    if not normalized:
        raise ValueError("AI prompt name cannot be empty")
    return normalized


def _normalize_prompt_text(text: str) -> str:
    normalized = text.strip()
    if not normalized:
        raise ValueError("AI prompt cannot be empty")
    return normalized


def _split_prompt_names(value: str) -> tuple[str, ...]:
    names = []
    for part in value.split(","):
        normalized = part.strip()
        if normalized:
            names.append(normalized)
    return tuple(names)


def _dedupe_prompt_names(names: Iterable[str]) -> tuple[str, ...]:
    seen = set()
    deduped = []
    for name in names:
        normalized = _normalize_prompt_name(name)
        if normalized not in seen:
            deduped.append(normalized)
            seen.add(normalized)
    return tuple(deduped)


[docs] def register_ai_prompt(name: str, prompt: str) -> None: """Register or override a reusable prompt section.""" _ai_prompt_store.registry[_normalize_prompt_name(name)] = _normalize_prompt_text( prompt )
[docs] def list_ai_prompts() -> tuple[str, ...]: """Return registered and built-in prompt section names.""" return tuple(sorted(_ai_prompt_store.registry))
[docs] def get_ai_prompt(name: str) -> str: """Return one registered or built-in prompt section.""" normalized = _normalize_prompt_name(name) try: return _ai_prompt_store.registry[normalized] except KeyError as exc: raise AIPromptError(f"Unknown %%there ai prompt: {normalized!r}") from exc
[docs] def set_ai_prompts(*names: str, include_default: bool = True) -> None: """Set the session prompt stack used by %%there ai.""" if len(names) == 1 and "," in names[0]: names = _split_prompt_names(names[0]) prompt_names = build_ai_prompt_names(names, include_default=include_default) _ai_prompt_store.active_prompts = prompt_names
[docs] def clear_ai_prompts() -> None: """Clear session-level %%there ai prompt overrides.""" _ai_prompt_store.active_prompts = None
def build_ai_prompt_names( prompt_names: Iterable[str] | None = None, *, include_default: bool = True, ) -> tuple[str, ...]: """Build an ordered, deduplicated prompt stack.""" names = list(prompt_names or ()) if include_default: names.insert(0, DEFAULT_AI_PROMPT) deduped = _dedupe_prompt_names(names) if not deduped: raise ValueError("AI prompt stack cannot be empty") return deduped
[docs] def build_ai_template( prompt_names: Iterable[str] | None = None, *, include_default: bool = True, ) -> str: """Compose the named prompt sections into one system prompt.""" names = build_ai_prompt_names(prompt_names, include_default=include_default) return "\n\n".join(get_ai_prompt(name) for name in names)
def resolve_ai_prompt_options( prompt_names: Iterable[str] | None = None, *, include_default: bool = True, config_prompt_names: Iterable[str] | None = None, ) -> tuple[tuple[str, ...] | None, bool]: """Resolve command, session, and config prompt options in precedence order.""" if prompt_names is not None: return _dedupe_prompt_names(prompt_names), include_default session_prompts = _ai_prompt_store.active_prompts if session_prompts is not None: return session_prompts, False if config_prompt_names is not None: return _dedupe_prompt_names(config_prompt_names), True return None, True def get_ai_template( prompt_names: Iterable[str] | None = None, *, include_default: bool = True, ) -> str: if prompt_names is not None: return build_ai_template(prompt_names, include_default=include_default) session_prompts = _ai_prompt_store.active_prompts if session_prompts is not None: return build_ai_template(session_prompts, include_default=False) return build_ai_template() def build_messages( user_request: str, prompt_names: Iterable[str] | None = None, *, include_default: bool = True, ) -> list[dict[str, str]]: template = get_ai_template( prompt_names, include_default=include_default, ) return [ {"role": "system", "content": template}, {"role": "user", "content": user_request.strip()}, ]