Skip to content

Commit 5e0a870

Browse files
Merge branch 'master' into update-exa-search-types-v2
2 parents 5d4d659 + d22df94 commit 5e0a870

File tree

9 files changed

+340
-40
lines changed

9 files changed

+340
-40
lines changed

libs/core/langchain_core/prompts/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from pydantic import BaseModel, ConfigDict, Field, model_validator
1616
from typing_extensions import Self, override
1717

18+
from langchain_core._api import deprecated
1819
from langchain_core.exceptions import ErrorCode, create_message
1920
from langchain_core.load import dumpd
2021
from langchain_core.output_parsers.base import BaseOutputParser # noqa: TC001
@@ -350,6 +351,12 @@ def dict(self, **kwargs: Any) -> dict:
350351
prompt_dict["_type"] = self._prompt_type
351352
return prompt_dict
352353

354+
@deprecated(
355+
since="1.2.21",
356+
removal="2.0.0",
357+
alternative="Use `dumpd`/`dumps` from `langchain_core.load` to serialize "
358+
"prompts and `load`/`loads` to deserialize them.",
359+
)
353360
def save(self, file_path: Path | str) -> None:
354361
"""Save the prompt.
355362

libs/core/langchain_core/prompts/chat.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from typing_extensions import Self, override
2424

25+
from langchain_core._api import deprecated
2526
from langchain_core.messages import (
2627
AIMessage,
2728
AnyMessage,
@@ -1305,6 +1306,12 @@ def _prompt_type(self) -> str:
13051306
"""Name of prompt type. Used for serialization."""
13061307
return "chat"
13071308

1309+
@deprecated(
1310+
since="1.2.21",
1311+
removal="2.0.0",
1312+
alternative="Use `dumpd`/`dumps` from `langchain_core.load` to serialize "
1313+
"prompts and `load`/`loads` to deserialize them.",
1314+
)
13081315
def save(self, file_path: Path | str) -> None:
13091316
"""Save prompt to file.
13101317

libs/core/langchain_core/prompts/few_shot.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
)
1313
from typing_extensions import override
1414

15+
from langchain_core._api import deprecated
1516
from langchain_core.example_selectors import BaseExampleSelector
1617
from langchain_core.messages import BaseMessage, get_buffer_string
1718
from langchain_core.prompts.chat import BaseChatPromptTemplate
@@ -237,6 +238,12 @@ def _prompt_type(self) -> str:
237238
"""Return the prompt type key."""
238239
return "few_shot"
239240

241+
@deprecated(
242+
since="1.2.21",
243+
removal="2.0.0",
244+
alternative="Use `dumpd`/`dumps` from `langchain_core.load` to serialize "
245+
"prompts and `load`/`loads` to deserialize them.",
246+
)
240247
def save(self, file_path: Path | str) -> None:
241248
"""Save the prompt template to a file.
242249

libs/core/langchain_core/prompts/few_shot_with_templates.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pydantic import ConfigDict, model_validator
77
from typing_extensions import Self
88

9+
from langchain_core._api import deprecated
910
from langchain_core.example_selectors import BaseExampleSelector
1011
from langchain_core.prompts.prompt import PromptTemplate
1112
from langchain_core.prompts.string import (
@@ -215,6 +216,12 @@ def _prompt_type(self) -> str:
215216
"""Return the prompt type key."""
216217
return "few_shot_with_templates"
217218

219+
@deprecated(
220+
since="1.2.21",
221+
removal="2.0.0",
222+
alternative="Use `dumpd`/`dumps` from `langchain_core.load` to serialize "
223+
"prompts and `load`/`loads` to deserialize them.",
224+
)
218225
def save(self, file_path: Path | str) -> None:
219226
"""Save the prompt to a file.
220227

libs/core/langchain_core/prompts/loading.py

Lines changed: 107 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import yaml
99

10+
from langchain_core._api import deprecated
1011
from langchain_core.output_parsers.string import StrOutputParser
1112
from langchain_core.prompts.base import BasePromptTemplate
1213
from langchain_core.prompts.chat import ChatPromptTemplate
@@ -17,11 +18,51 @@
1718
logger = logging.getLogger(__name__)
1819

1920

20-
def load_prompt_from_config(config: dict) -> BasePromptTemplate:
21+
def _validate_path(path: Path) -> None:
22+
"""Reject absolute paths and ``..`` traversal components.
23+
24+
Args:
25+
path: The path to validate.
26+
27+
Raises:
28+
ValueError: If the path is absolute or contains ``..`` components.
29+
"""
30+
if path.is_absolute():
31+
msg = (
32+
f"Path '{path}' is absolute. Absolute paths are not allowed "
33+
f"when loading prompt configurations to prevent path traversal "
34+
f"attacks. Use relative paths instead, or pass "
35+
f"`allow_dangerous_paths=True` if you trust the input."
36+
)
37+
raise ValueError(msg)
38+
if ".." in path.parts:
39+
msg = (
40+
f"Path '{path}' contains '..' components. Directory traversal "
41+
f"sequences are not allowed when loading prompt configurations. "
42+
f"Use direct relative paths instead, or pass "
43+
f"`allow_dangerous_paths=True` if you trust the input."
44+
)
45+
raise ValueError(msg)
46+
47+
48+
@deprecated(
49+
since="1.2.21",
50+
removal="2.0.0",
51+
alternative="Use `dumpd`/`dumps` from `langchain_core.load` to serialize "
52+
"prompts and `load`/`loads` to deserialize them.",
53+
)
54+
def load_prompt_from_config(
55+
config: dict, *, allow_dangerous_paths: bool = False
56+
) -> BasePromptTemplate:
2157
"""Load prompt from config dict.
2258
2359
Args:
2460
config: Dict containing the prompt configuration.
61+
allow_dangerous_paths: If ``False`` (default), file paths in the
62+
config (such as ``template_path``, ``examples``, and
63+
``example_prompt_path``) are validated to reject absolute paths
64+
and directory traversal (``..``) sequences. Set to ``True`` only
65+
if you trust the source of the config.
2566
2667
Returns:
2768
A `PromptTemplate` object.
@@ -38,10 +79,12 @@ def load_prompt_from_config(config: dict) -> BasePromptTemplate:
3879
raise ValueError(msg)
3980

4081
prompt_loader = type_to_loader_dict[config_type]
41-
return prompt_loader(config)
82+
return prompt_loader(config, allow_dangerous_paths=allow_dangerous_paths)
4283

4384

44-
def _load_template(var_name: str, config: dict) -> dict:
85+
def _load_template(
86+
var_name: str, config: dict, *, allow_dangerous_paths: bool = False
87+
) -> dict:
4588
"""Load template from the path if applicable."""
4689
# Check if template_path exists in config.
4790
if f"{var_name}_path" in config:
@@ -51,6 +94,8 @@ def _load_template(var_name: str, config: dict) -> dict:
5194
raise ValueError(msg)
5295
# Pop the template path from the config.
5396
template_path = Path(config.pop(f"{var_name}_path"))
97+
if not allow_dangerous_paths:
98+
_validate_path(template_path)
5499
# Load the template.
55100
if template_path.suffix == ".txt":
56101
template = template_path.read_text(encoding="utf-8")
@@ -61,12 +106,14 @@ def _load_template(var_name: str, config: dict) -> dict:
61106
return config
62107

63108

64-
def _load_examples(config: dict) -> dict:
109+
def _load_examples(config: dict, *, allow_dangerous_paths: bool = False) -> dict:
65110
"""Load examples if necessary."""
66111
if isinstance(config["examples"], list):
67112
pass
68113
elif isinstance(config["examples"], str):
69114
path = Path(config["examples"])
115+
if not allow_dangerous_paths:
116+
_validate_path(path)
70117
with path.open(encoding="utf-8") as f:
71118
if path.suffix == ".json":
72119
examples = json.load(f)
@@ -92,11 +139,17 @@ def _load_output_parser(config: dict) -> dict:
92139
return config
93140

94141

95-
def _load_few_shot_prompt(config: dict) -> FewShotPromptTemplate:
142+
def _load_few_shot_prompt(
143+
config: dict, *, allow_dangerous_paths: bool = False
144+
) -> FewShotPromptTemplate:
96145
"""Load the "few shot" prompt from the config."""
97146
# Load the suffix and prefix templates.
98-
config = _load_template("suffix", config)
99-
config = _load_template("prefix", config)
147+
config = _load_template(
148+
"suffix", config, allow_dangerous_paths=allow_dangerous_paths
149+
)
150+
config = _load_template(
151+
"prefix", config, allow_dangerous_paths=allow_dangerous_paths
152+
)
100153
# Load the example prompt.
101154
if "example_prompt_path" in config:
102155
if "example_prompt" in config:
@@ -105,19 +158,30 @@ def _load_few_shot_prompt(config: dict) -> FewShotPromptTemplate:
105158
"be specified."
106159
)
107160
raise ValueError(msg)
108-
config["example_prompt"] = load_prompt(config.pop("example_prompt_path"))
161+
example_prompt_path = Path(config.pop("example_prompt_path"))
162+
if not allow_dangerous_paths:
163+
_validate_path(example_prompt_path)
164+
config["example_prompt"] = load_prompt(
165+
example_prompt_path, allow_dangerous_paths=allow_dangerous_paths
166+
)
109167
else:
110-
config["example_prompt"] = load_prompt_from_config(config["example_prompt"])
168+
config["example_prompt"] = load_prompt_from_config(
169+
config["example_prompt"], allow_dangerous_paths=allow_dangerous_paths
170+
)
111171
# Load the examples.
112-
config = _load_examples(config)
172+
config = _load_examples(config, allow_dangerous_paths=allow_dangerous_paths)
113173
config = _load_output_parser(config)
114174
return FewShotPromptTemplate(**config)
115175

116176

117-
def _load_prompt(config: dict) -> PromptTemplate:
177+
def _load_prompt(
178+
config: dict, *, allow_dangerous_paths: bool = False
179+
) -> PromptTemplate:
118180
"""Load the prompt template from config."""
119181
# Load the template from disk if necessary.
120-
config = _load_template("template", config)
182+
config = _load_template(
183+
"template", config, allow_dangerous_paths=allow_dangerous_paths
184+
)
121185
config = _load_output_parser(config)
122186

123187
template_format = config.get("template_format", "f-string")
@@ -134,12 +198,28 @@ def _load_prompt(config: dict) -> PromptTemplate:
134198
return PromptTemplate(**config)
135199

136200

137-
def load_prompt(path: str | Path, encoding: str | None = None) -> BasePromptTemplate:
201+
@deprecated(
202+
since="1.2.21",
203+
removal="2.0.0",
204+
alternative="Use `dumpd`/`dumps` from `langchain_core.load` to serialize "
205+
"prompts and `load`/`loads` to deserialize them.",
206+
)
207+
def load_prompt(
208+
path: str | Path,
209+
encoding: str | None = None,
210+
*,
211+
allow_dangerous_paths: bool = False,
212+
) -> BasePromptTemplate:
138213
"""Unified method for loading a prompt from LangChainHub or local filesystem.
139214
140215
Args:
141216
path: Path to the prompt file.
142217
encoding: Encoding of the file.
218+
allow_dangerous_paths: If ``False`` (default), file paths referenced
219+
inside the loaded config (such as ``template_path``, ``examples``,
220+
and ``example_prompt_path``) are validated to reject absolute paths
221+
and directory traversal (``..``) sequences. Set to ``True`` only
222+
if you trust the source of the config.
143223
144224
Returns:
145225
A `PromptTemplate` object.
@@ -154,11 +234,16 @@ def load_prompt(path: str | Path, encoding: str | None = None) -> BasePromptTemp
154234
"instead."
155235
)
156236
raise RuntimeError(msg)
157-
return _load_prompt_from_file(path, encoding)
237+
return _load_prompt_from_file(
238+
path, encoding, allow_dangerous_paths=allow_dangerous_paths
239+
)
158240

159241

160242
def _load_prompt_from_file(
161-
file: str | Path, encoding: str | None = None
243+
file: str | Path,
244+
encoding: str | None = None,
245+
*,
246+
allow_dangerous_paths: bool = False,
162247
) -> BasePromptTemplate:
163248
"""Load prompt from file."""
164249
# Convert file to a Path object.
@@ -174,10 +259,14 @@ def _load_prompt_from_file(
174259
msg = f"Got unsupported file type {file_path.suffix}"
175260
raise ValueError(msg)
176261
# Load the prompt from the config now.
177-
return load_prompt_from_config(config)
262+
return load_prompt_from_config(config, allow_dangerous_paths=allow_dangerous_paths)
178263

179264

180-
def _load_chat_prompt(config: dict) -> ChatPromptTemplate:
265+
def _load_chat_prompt(
266+
config: dict,
267+
*,
268+
allow_dangerous_paths: bool = False, # noqa: ARG001
269+
) -> ChatPromptTemplate:
181270
"""Load chat prompt from config."""
182271
messages = config.pop("messages")
183272
template = messages[0]["prompt"].pop("template") if messages else None
@@ -190,7 +279,7 @@ def _load_chat_prompt(config: dict) -> ChatPromptTemplate:
190279
return ChatPromptTemplate.from_template(template=template, **config)
191280

192281

193-
type_to_loader_dict: dict[str, Callable[[dict], BasePromptTemplate]] = {
282+
type_to_loader_dict: dict[str, Callable[..., BasePromptTemplate]] = {
194283
"prompt": _load_prompt,
195284
"few_shot": _load_few_shot_prompt,
196285
"chat": _load_chat_prompt,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""langchain-core version information and utilities."""
22

3-
VERSION = "1.2.21"
3+
VERSION = "1.2.22"

libs/core/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ classifiers = [
2121
"Topic :: Software Development :: Libraries :: Python Modules",
2222
]
2323

24-
version = "1.2.21"
24+
version = "1.2.22"
2525
requires-python = ">=3.10.0,<4.0.0"
2626
dependencies = [
2727
"langsmith>=0.3.45,<1.0.0",

0 commit comments

Comments
 (0)