libs/core/langchain_core/prompts/string.py PYTHON 400 lines View on github.com → Search inside
1"""`BasePrompt` schema definition."""23from __future__ import annotations45import warnings6from abc import ABC, abstractmethod7from string import Formatter8from typing import TYPE_CHECKING, Any, Literal, cast910from pydantic import BaseModel, create_model11from typing_extensions import override1213from langchain_core.prompt_values import PromptValue, StringPromptValue14from langchain_core.prompts.base import BasePromptTemplate15from langchain_core.utils import get_colored_text, mustache16from langchain_core.utils.formatting import formatter17from langchain_core.utils.interactive_env import is_interactive_env1819if TYPE_CHECKING:20    from collections.abc import Callable, Sequence2122try:23    from jinja2 import meta24    from jinja2.sandbox import SandboxedEnvironment2526    _HAS_JINJA2 = True27except ImportError:28    _HAS_JINJA2 = False2930PromptTemplateFormat = Literal["f-string", "mustache", "jinja2"]313233def jinja2_formatter(template: str, /, **kwargs: Any) -> str:34    """Format a template using jinja2.3536    !!! warning "Security"3738        As of LangChain 0.0.329, this method uses Jinja2's `SandboxedEnvironment` by39        default. However, this sandboxing should be treated as a best-effort approach40        rather than a guarantee of security.4142        Do not accept jinja2 templates from untrusted sources as they may lead43        to arbitrary Python code execution.4445        [More information.](https://jinja.palletsprojects.com/en/3.1.x/sandbox/)4647    Args:48        template: The template string.49        **kwargs: The variables to format the template with.5051    Returns:52        The formatted string.5354    Raises:55        ImportError: If jinja2 is not installed.56    """57    if not _HAS_JINJA2:58        msg = (59            "jinja2 not installed, which is needed to use the jinja2_formatter. "60            "Please install it with `pip install jinja2`."61            "Please be cautious when using jinja2 templates. "62            "Do not expand jinja2 templates using unverified or user-controlled "63            "inputs as that can result in arbitrary Python code execution."64        )65        raise ImportError(msg)6667    # Use Jinja2's SandboxedEnvironment which blocks access to dunder attributes68    # (e.g., __class__, __globals__) to prevent sandbox escapes.69    # Note: regular attribute access (e.g., {{obj.attr}}) and method calls are70    # still allowed. This is a best-effort measure  do not use with untrusted71    # templates.72    return SandboxedEnvironment().from_string(template).render(**kwargs)737475def validate_jinja2(template: str, input_variables: list[str]) -> None:76    """Validate that the input variables are valid for the template.7778    Issues a warning if missing or extra variables are found.7980    Args:81        template: The template string.82        input_variables: The input variables.83    """84    input_variables_set = set(input_variables)85    valid_variables = _get_jinja2_variables_from_template(template)86    missing_variables = valid_variables - input_variables_set87    extra_variables = input_variables_set - valid_variables8889    warning_message = ""90    if missing_variables:91        warning_message += f"Missing variables: {missing_variables} "9293    if extra_variables:94        warning_message += f"Extra variables: {extra_variables}"9596    if warning_message:97        warnings.warn(warning_message.strip(), stacklevel=7)9899100def _get_jinja2_variables_from_template(template: str) -> set[str]:101    if not _HAS_JINJA2:102        msg = (103            "jinja2 not installed, which is needed to use the jinja2_formatter. "104            "Please install it with `pip install jinja2`."105        )106        raise ImportError(msg)107    env = SandboxedEnvironment()108    ast = env.parse(template)109    return meta.find_undeclared_variables(ast)110111112def mustache_formatter(template: str, /, **kwargs: Any) -> str:113    """Format a template using mustache.114115    Args:116        template: The template string.117        **kwargs: The variables to format the template with.118119    Returns:120        The formatted string.121    """122    return mustache.render(template, kwargs)123124125def mustache_template_vars(126    template: str,127) -> set[str]:128    """Get the top-level variables from a mustache template.129130    For nested variables like `{{person.name}}`, only the top-level key (`person`) is131    returned.132133    Args:134        template: The template string.135136    Returns:137        The top-level variables from the template.138    """139    variables: set[str] = set()140    section_depth = 0141    for type_, key in mustache.tokenize(template):142        if type_ == "end":143            section_depth -= 1144        elif (145            type_ in {"variable", "section", "inverted section", "no escape"}146            and key != "."147            and section_depth == 0148        ):149            variables.add(key.split(".")[0])150        if type_ in {"section", "inverted section"}:151            section_depth += 1152    return variables153154155Defs = dict[str, "Defs"]156157158def mustache_schema(template: str) -> type[BaseModel]:159    """Get the variables from a mustache template.160161    Args:162        template: The template string.163164    Returns:165        The variables from the template as a Pydantic model.166    """167    fields = {}168    prefix: tuple[str, ...] = ()169    section_stack: list[tuple[str, ...]] = []170    for type_, key in mustache.tokenize(template):171        if key == ".":172            continue173        if type_ == "end":174            if section_stack:175                prefix = section_stack.pop()176        elif type_ in {"section", "inverted section"}:177            section_stack.append(prefix)178            prefix += tuple(key.split("."))179            fields[prefix] = False180        elif type_ in {"variable", "no escape"}:181            fields[prefix + tuple(key.split("."))] = True182183    for fkey, fval in fields.items():184        fields[fkey] = fval and not any(185            is_subsequence(fkey, k) for k in fields if k != fkey186        )187    defs: Defs = {}  # None means leaf node188    while fields:189        field, is_leaf = fields.popitem()190        current = defs191        for part in field[:-1]:192            current = current.setdefault(part, {})193        current.setdefault(field[-1], "" if is_leaf else {})  # type: ignore[arg-type]194    return _create_model_recursive("PromptInput", defs)195196197def _create_model_recursive(name: str, defs: Defs) -> type[BaseModel]:198    return cast(199        "type[BaseModel]",200        create_model(  # type: ignore[call-overload]201            name,202            **{203                k: (_create_model_recursive(k, v), None) if v else (type(v), None)204                for k, v in defs.items()205            },206        ),207    )208209210DEFAULT_FORMATTER_MAPPING: dict[str, Callable[..., str]] = {211    "f-string": formatter.format,212    "mustache": mustache_formatter,213    "jinja2": jinja2_formatter,214}215216DEFAULT_VALIDATOR_MAPPING: dict[str, Callable] = {217    "f-string": formatter.validate_input_variables,218    "jinja2": validate_jinja2,219}220221222def _parse_f_string_fields(template: str) -> list[tuple[str, str | None]]:223    fields: list[tuple[str, str | None]] = []224    for _, field_name, format_spec, _ in Formatter().parse(template):225        if field_name is not None:226            fields.append((field_name, format_spec))227    return fields228229230def validate_f_string_template(template: str) -> list[str]:231    """Validate an f-string template and return its input variables."""232    input_variables = set()233    for var, format_spec in _parse_f_string_fields(template):234        if "." in var or "[" in var or "]" in var:235            msg = (236                f"Invalid variable name {var!r} in f-string template. "237                f"Variable names cannot contain attribute "238                f"access (.) or indexing ([])."239            )240            raise ValueError(msg)241242        if var.isdigit():243            msg = (244                f"Invalid variable name {var!r} in f-string template. "245                f"Variable names cannot be all digits as they are interpreted "246                f"as positional arguments."247            )248            raise ValueError(msg)249250        if format_spec and ("{" in format_spec or "}" in format_spec):251            msg = (252                "Invalid format specifier in f-string template. "253                "Nested replacement fields are not allowed."254            )255            raise ValueError(msg)256257        input_variables.add(var)258259    return sorted(input_variables)260261262def check_valid_template(263    template: str, template_format: str, input_variables: list[str]264) -> None:265    """Check that template string is valid.266267    Args:268        template: The template string.269        template_format: The template format.270271            Should be one of `'f-string'` or `'jinja2'`.272        input_variables: The input variables.273274    Raises:275        ValueError: If the template format is not supported.276        ValueError: If the prompt schema is invalid.277    """278    try:279        validator_func = DEFAULT_VALIDATOR_MAPPING[template_format]280    except KeyError as exc:281        msg = (282            f"Invalid template format {template_format!r}, should be one of"283            f" {list(DEFAULT_FORMATTER_MAPPING)}."284        )285        raise ValueError(msg) from exc286    if template_format == "f-string":287        validate_f_string_template(template)288    try:289        validator_func(template, input_variables)290    except (KeyError, IndexError) as exc:291        msg = (292            "Invalid prompt schema; check for mismatched or missing input parameters"293            f" from {input_variables}."294        )295        raise ValueError(msg) from exc296297298def get_template_variables(template: str, template_format: str) -> list[str]:299    """Get the variables from the template.300301    Args:302        template: The template string.303        template_format: The template format.304305            Should be one of `'f-string'`, `'mustache'` or `'jinja2'`.306307    Returns:308        The variables from the template.309310    Raises:311        ValueError: If the template format is not supported.312    """313    input_variables: list[str] | set[str]314    if template_format == "jinja2":315        # Get the variables for the template316        input_variables = sorted(_get_jinja2_variables_from_template(template))317    elif template_format == "f-string":318        input_variables = validate_f_string_template(template)319    elif template_format == "mustache":320        input_variables = mustache_template_vars(template)321    else:322        msg = f"Unsupported template format: {template_format}"323        raise ValueError(msg)324325    return sorted(input_variables)326327328class StringPromptTemplate(BasePromptTemplate, ABC):329    """String prompt that exposes the format method, returning a prompt."""330331    @classmethod332    def get_lc_namespace(cls) -> list[str]:333        """Get the namespace of the LangChain object.334335        Returns:336            `["langchain", "prompts", "base"]`337        """338        return ["langchain", "prompts", "base"]339340    def format_prompt(self, **kwargs: Any) -> PromptValue:341        """Format the prompt with the inputs.342343        Args:344            **kwargs: Any arguments to be passed to the prompt template.345346        Returns:347            A formatted string.348        """349        return StringPromptValue(text=self.format(**kwargs))350351    async def aformat_prompt(self, **kwargs: Any) -> PromptValue:352        """Async format the prompt with the inputs.353354        Args:355            **kwargs: Any arguments to be passed to the prompt template.356357        Returns:358            A formatted string.359        """360        return StringPromptValue(text=await self.aformat(**kwargs))361362    @override363    @abstractmethod364    def format(self, **kwargs: Any) -> str: ...365366    def pretty_repr(367        self,368        html: bool = False,  # noqa: FBT001,FBT002369    ) -> str:370        """Get a pretty representation of the prompt.371372        Args:373            html: Whether to return an HTML-formatted string.374375        Returns:376            A pretty representation of the prompt.377        """378        # TODO: handle partials379        dummy_vars = {380            input_var: "{" + f"{input_var}" + "}" for input_var in self.input_variables381        }382        if html:383            dummy_vars = {384                k: get_colored_text(v, "yellow") for k, v in dummy_vars.items()385            }386        return self.format(**dummy_vars)387388    def pretty_print(self) -> None:389        """Print a pretty representation of the prompt."""390        print(self.pretty_repr(html=is_interactive_env()))  # noqa: T201391392393def is_subsequence(child: Sequence, parent: Sequence) -> bool:394    """Return `True` if child is subsequence of parent."""395    if len(child) == 0 or len(parent) == 0:396        return False397    if len(parent) < len(child):398        return False399    return all(child[i] == parent[i] for i in range(len(child)))

Code quality findings 6

Ensure functions have docstrings for documentation
missing-docstring
def mustache_template_vars(
Ensure functions have docstrings for documentation
missing-docstring
def check_valid_template(
Ensure functions have docstrings for documentation
missing-docstring
def format(self, **kwargs: Any) -> str: ...
Ensure functions have docstrings for documentation
missing-docstring
def pretty_repr(
Use logging module for better control and configurability
print-statement
def pretty_print(self) -> None:
Use logging module for better control and configurability
print-statement
print(self.pretty_repr(html=is_interactive_env())) # noqa: T201

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.