|
| 1 | +"""Prompt-asset loader (PROJECT-GENESIS.md sec. 9 Tier 5 item #36, alias Tier 9 |
| 2 | +item #78): "inline prompts -> versioned files + tests". System prompts used to be |
| 3 | +plain string constants sitting inline in rag/pipeline.py, agent/loop.py, and |
| 4 | +review/weekly.py. That makes them hard to diff, hard to reuse, and invisible to |
| 5 | +anything that wants to inspect "what does this app actually tell the model" without |
| 6 | +reading Python. This module is the only place that turns a prompt NAME into prompt |
| 7 | +TEXT; the three call sites now just call `load_prompt(name)` instead of holding |
| 8 | +their own copy of the string. |
| 9 | +
|
| 10 | +Pure/simple on purpose - no personal_llm.router import, no model call, no network - |
| 11 | +so it is trivial to unit test and safe to import from anywhere. |
| 12 | +
|
| 13 | +Loader strategy: `Path(__file__).parent`-relative read, not `importlib.resources`. |
| 14 | +Checked `pyproject.toml` first: it used plain `[tool.setuptools.packages.find]` |
| 15 | +with no `package_data`/`include_package_data`/`MANIFEST.in` entry, so a wheel |
| 16 | +build would NOT have shipped these .txt files - setuptools does not bundle |
| 17 | +non-`.py` files by default. Added the minimal fix for that |
| 18 | +(`[tool.setuptools.package-data]` -> `"personal_llm.prompts" = ["*.txt"]`) so a |
| 19 | +real wheel build includes them. Even with that fixed, this loader still reads |
| 20 | +via `Path(__file__).parent` rather than `importlib.resources`: it is one line |
| 21 | +simpler, needs no `files()`/context-manager ceremony for plain-text reads, and |
| 22 | +behaves identically in the source tree, an editable install (`pip install |
| 23 | +-e .`), and a built-and-installed wheel, since all three place `loader.py` and |
| 24 | +its sibling `.txt` files in the same directory on disk. |
| 25 | +""" |
| 26 | + |
| 27 | +from __future__ import annotations |
| 28 | + |
| 29 | +from pathlib import Path |
| 30 | + |
| 31 | +_PROMPTS_DIR = Path(__file__).parent |
| 32 | + |
| 33 | + |
| 34 | +def load_prompt(name: str) -> str: |
| 35 | + """Return the exact text of the prompt asset `{name}.txt`. |
| 36 | +
|
| 37 | + Raises FileNotFoundError with a helpful message if no such asset exists - |
| 38 | + never silently returns an empty string, since a blank system prompt is a bug |
| 39 | + that should fail loudly, not ship quietly. |
| 40 | + """ |
| 41 | + path = _PROMPTS_DIR / f"{name}.txt" |
| 42 | + if not path.is_file(): |
| 43 | + available = sorted(p.stem for p in _PROMPTS_DIR.glob("*.txt")) |
| 44 | + raise FileNotFoundError( |
| 45 | + f"No prompt asset named {name!r} (looked for {path}). " |
| 46 | + f"Available prompts: {available}" |
| 47 | + ) |
| 48 | + return path.read_text(encoding="utf-8") |
0 commit comments