Files
2026-07-13 12:24:33 +08:00

135 lines
4.2 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Prompt placeholder expansion helpers for ``lmcache query``."""
# Standard
from pathlib import Path
from typing import Optional
import re
PLACEHOLDER = re.compile(r"\{(\w+)\}")
BUILTIN_DOCUMENT_PATHS = {
"lmcache": (
Path(__file__).resolve().parent.parent.parent / "documents" / "lmcache.txt"
),
}
class PromptBuilder:
"""Build a complete prompt from CLI inputs."""
def __init__(self, prompt: str, documents_args: Optional[list[str]] = None) -> None:
self._complete_prompt = self.expand_prompt(prompt, documents_args or [])
@property
def complete_prompt(self) -> str:
"""Return the expanded prompt from constructor inputs."""
return self._complete_prompt
def expand_prompt(
self,
prompt: str,
documents_args: Optional[list[str]] = None,
) -> str:
"""Expand placeholders and return the complete prompt text.
Args:
prompt: The prompt template string with {name} placeholders.
documents_args: Optional list of document paths or NAME=PATH strings.
Returns:
The expanded prompt string.
"""
docs_args = documents_args or []
documents, appended_text = resolve_documents(prompt, docs_args)
for key in _unique_placeholders(prompt):
if key not in documents:
unknown_documents(key)
complete_prompt = PLACEHOLDER.sub(
lambda match: documents[match.group(1)], prompt
)
if appended_text:
complete_prompt = (
f"{complete_prompt}\n{appended_text}"
if complete_prompt
else appended_text
)
return complete_prompt
def resolve_documents(
prompt_template: str, documents_args: list[str]
) -> tuple[dict[str, str], str]:
"""Resolve ``--documents`` args into placeholder mapping and trailing text.
Args:
prompt_template: The prompt string containing placeholders.
documents_args: List of document arguments from the CLI.
Returns:
A tuple containing a dictionary mapping placeholder names to document
content and a string containing any appended document text.
"""
placeholders = _unique_placeholders(prompt_template)
documents: dict[str, str] = {}
plain_docs: list[str] = []
for item in documents_args:
if "=" not in item:
plain_docs.append(_read_document_file(item))
continue
name, path = [x.strip() for x in item.split("=", 1)]
if not name:
raise ValueError(f"Invalid --documents {item!r}; empty name")
documents[name] = _read_document_file(path, name=name)
unresolved = [key for key in placeholders if key not in documents]
for idx, key in enumerate(unresolved[: len(plain_docs)]):
documents[key] = plain_docs[idx]
appended_docs = plain_docs[len(unresolved) :]
for key in placeholders:
if key in documents:
continue
builtin_path = BUILTIN_DOCUMENT_PATHS.get(key)
if builtin_path is None:
continue
documents[key] = _read_document_file(str(builtin_path), name=key)
return documents, "\n".join(appended_docs).strip()
def unknown_documents(key: str) -> None:
"""Raise an error for a missing documents placeholder.
Args:
key: The name of the missing placeholder.
Raises:
ValueError: Always raised with a descriptive message.
"""
raise ValueError(
f"Unknown documents {key!r}. Define it with --documents {key}=PATH "
)
def _read_document_file(path: str, *, name: Optional[str] = None) -> str:
file_path = Path(path).expanduser()
if not file_path.is_file():
if name is not None:
raise ValueError(f"documents file not found for {name!r}: {file_path}")
raise ValueError(f"documents file not found: {file_path}")
return file_path.read_text(encoding="utf-8", errors="replace")
def _unique_placeholders(prompt_template: str) -> list[str]:
seen: set[str] = set()
keys: list[str] = []
for match in PLACEHOLDER.finditer(prompt_template):
key = match.group(1)
if key in seen:
continue
seen.add(key)
keys.append(key)
return keys