Source code for dartwork_mpl.prompt

"""Prompt guide file management module.

Provides helper functions for finding, reading, listing, and copying
the prompt guide Markdown files bundled with the dartwork package.
"""

from __future__ import annotations

__all__ = [
    "copy_prompt",
    "find_template",
    "get_prompt",
    "list_prompts",
    "prompt_path",
]

import json
import warnings
from pathlib import Path
from shutil import copy2

from ._helpers import create_parent_path

_PROMPT_DIR: Path = Path(__file__).parent / "asset/prompt"

# Canonical Markdown prompt corpus (T5). ``list_prompts`` warns when a
# ``.md`` file outside this set appears next to it. The YAML
# anti-pattern catalog (``02-anti-patterns.yaml``) and the
# ``05-templates/`` subdirectory are separate surfaces with their own
# loaders and are intentionally not listed here.
_CANONICAL_PROMPTS: frozenset[str] = frozenset(
    {"00-index", "01-policy", "03-recipes"}
)


[docs] def prompt_path(name: str) -> Path: """Get the absolute path to a prompt guide file. Parameters ---------- name : str Name of the prompt guide to retrieve (e.g., ``'00-index'``, ``'01-policy'``, ``'03-recipes'``). Returns ------- Path Path to the prompt guide Markdown file. Raises ------ ValueError If the specified guide cannot be found in the library. """ path: Path = _PROMPT_DIR / f"{name}.md" if not path.exists(): raise ValueError(f"Prompt guide not found: {name}") return path
[docs] def get_prompt(name: str) -> str: """Read a prompt guide file and return its full content as a string. Parameters ---------- name : str Name of the prompt guide to read. Returns ------- str The Markdown content of the prompt guide. """ path = prompt_path(name) return path.read_text(encoding="utf-8")
[docs] def list_prompts() -> list[str]: """List the bundled prompt guide files. The canonical corpus is exactly four entries — ``00-index``, ``01-policy``, ``02-anti-patterns`` (YAML, not surfaced here), ``03-recipes`` — plus the ``05-templates/`` subdirectory listed separately. If extra ``*.md`` files appear (stale leftovers, an in-progress addition, etc.), this function still returns them but emits a :class:`UserWarning` so drift surfaces immediately. Returns ------- list[str] Sorted list of available prompt guide names. """ if not _PROMPT_DIR.exists(): return [] found = sorted([p.stem for p in _PROMPT_DIR.glob("*.md")]) unexpected = [name for name in found if name not in _CANONICAL_PROMPTS] if unexpected: warnings.warn( "Unexpected prompt guide(s) found alongside the canonical " f"corpus: {unexpected}. The canonical set is " f"{sorted(_CANONICAL_PROMPTS)}. Either fold the content into " "an existing canonical file or extend _CANONICAL_PROMPTS in " "dartwork_mpl/prompt.py.", UserWarning, stacklevel=2, ) return found
_TEMPLATE_INDEX_PATH: Path = _PROMPT_DIR / "05-templates" / "_index.json" def find_template(intent: str, top_k: int = 5) -> list[dict[str, object]]: """Rank the bundled AI plot templates against a free-text intent. Mirrors the MCP ``find_template`` tool so the same ranking is reachable natively from Python without the MCP server. Each template's metadata text (``use_case`` + ``data_shape`` + ``difficulty`` + ``tags``) is scanned for occurrences of every whitespace-separated lowercase token in ``intent``; the count of matched tokens is the score. Parameters ---------- intent : str Free-text description, e.g. ``"horizontal bar comparison"``. top_k : int, optional Maximum number of matches to return, by default 5. Returns ------- list[dict] Each match is ``{"template_id", "score", **metadata}``. Empty list when ``intent`` is blank or no template overlaps. """ if not _TEMPLATE_INDEX_PATH.exists(): return [] index = json.loads(_TEMPLATE_INDEX_PATH.read_text(encoding="utf-8")) tokens = [t for t in intent.lower().split() if t] if not tokens: return [] scored: list[tuple[int, str, dict[str, object]]] = [] for template_id, meta in index.items(): haystack = " ".join( [ str(meta.get("use_case", "")), str(meta.get("data_shape", "")), str(meta.get("difficulty", "")), " ".join(meta.get("tags", [])), ] ).lower() score = sum(1 for t in tokens if t in haystack) if score > 0: scored.append((score, template_id, meta)) scored.sort(key=lambda item: (-item[0], item[1])) return [ {"template_id": template_id, "score": score, **meta} for score, template_id, meta in scored[:top_k] ]
[docs] def copy_prompt(name: str, destination: str | Path) -> Path: """Copy a bundled prompt guide file to the specified destination. Parameters ---------- name : str Name of the prompt guide to copy. destination : str | Path Destination path. If a directory, the file is copied with its original name (``name.md``). If a file path, that name is used. Returns ------- Path Absolute path of the newly copied file. Raises ------ ValueError If the source prompt guide cannot be found. """ source_path = prompt_path(name) dest_path = Path(destination) if dest_path.is_dir() or (not dest_path.exists() and not dest_path.suffix): dest_path = dest_path / f"{name}.md" create_parent_path(dest_path) copy2(source_path, dest_path) return dest_path