langchain-core 1.0.0a1__py3-none-any.whl → 1.0.0a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of langchain-core might be problematic. Click here for more details.
- langchain_core/_api/beta_decorator.py +17 -40
- langchain_core/_api/deprecation.py +20 -7
- langchain_core/_api/path.py +19 -2
- langchain_core/_import_utils.py +7 -0
- langchain_core/agents.py +10 -6
- langchain_core/callbacks/base.py +28 -15
- langchain_core/callbacks/manager.py +81 -69
- langchain_core/callbacks/usage.py +4 -2
- langchain_core/chat_history.py +29 -21
- langchain_core/document_loaders/base.py +34 -9
- langchain_core/document_loaders/langsmith.py +3 -0
- langchain_core/documents/base.py +35 -10
- langchain_core/documents/transformers.py +4 -2
- langchain_core/embeddings/fake.py +8 -5
- langchain_core/env.py +2 -3
- langchain_core/example_selectors/base.py +12 -0
- langchain_core/exceptions.py +7 -0
- langchain_core/globals.py +17 -28
- langchain_core/indexing/api.py +57 -45
- langchain_core/indexing/base.py +5 -8
- langchain_core/indexing/in_memory.py +23 -3
- langchain_core/language_models/__init__.py +6 -2
- langchain_core/language_models/_utils.py +28 -4
- langchain_core/language_models/base.py +33 -21
- langchain_core/language_models/chat_models.py +103 -29
- langchain_core/language_models/fake_chat_models.py +5 -7
- langchain_core/language_models/llms.py +54 -20
- langchain_core/load/dump.py +2 -3
- langchain_core/load/load.py +15 -1
- langchain_core/load/serializable.py +38 -43
- langchain_core/memory.py +7 -3
- langchain_core/messages/__init__.py +7 -17
- langchain_core/messages/ai.py +41 -34
- langchain_core/messages/base.py +16 -7
- langchain_core/messages/block_translators/__init__.py +10 -8
- langchain_core/messages/block_translators/anthropic.py +3 -1
- langchain_core/messages/block_translators/bedrock.py +3 -1
- langchain_core/messages/block_translators/bedrock_converse.py +3 -1
- langchain_core/messages/block_translators/google_genai.py +3 -1
- langchain_core/messages/block_translators/google_vertexai.py +3 -1
- langchain_core/messages/block_translators/groq.py +3 -1
- langchain_core/messages/block_translators/langchain_v0.py +3 -136
- langchain_core/messages/block_translators/ollama.py +3 -1
- langchain_core/messages/block_translators/openai.py +252 -10
- langchain_core/messages/content.py +26 -124
- langchain_core/messages/human.py +2 -13
- langchain_core/messages/system.py +2 -6
- langchain_core/messages/tool.py +34 -14
- langchain_core/messages/utils.py +189 -74
- langchain_core/output_parsers/base.py +5 -2
- langchain_core/output_parsers/json.py +4 -4
- langchain_core/output_parsers/list.py +7 -22
- langchain_core/output_parsers/openai_functions.py +3 -0
- langchain_core/output_parsers/openai_tools.py +6 -1
- langchain_core/output_parsers/pydantic.py +4 -0
- langchain_core/output_parsers/string.py +5 -1
- langchain_core/output_parsers/xml.py +19 -19
- langchain_core/outputs/chat_generation.py +18 -7
- langchain_core/outputs/generation.py +14 -3
- langchain_core/outputs/llm_result.py +8 -1
- langchain_core/prompt_values.py +10 -4
- langchain_core/prompts/base.py +6 -11
- langchain_core/prompts/chat.py +88 -60
- langchain_core/prompts/dict.py +16 -8
- langchain_core/prompts/few_shot.py +9 -11
- langchain_core/prompts/few_shot_with_templates.py +5 -1
- langchain_core/prompts/image.py +12 -5
- langchain_core/prompts/loading.py +2 -2
- langchain_core/prompts/message.py +5 -6
- langchain_core/prompts/pipeline.py +13 -8
- langchain_core/prompts/prompt.py +22 -8
- langchain_core/prompts/string.py +18 -10
- langchain_core/prompts/structured.py +7 -2
- langchain_core/rate_limiters.py +2 -2
- langchain_core/retrievers.py +7 -6
- langchain_core/runnables/base.py +387 -246
- langchain_core/runnables/branch.py +11 -28
- langchain_core/runnables/config.py +20 -17
- langchain_core/runnables/configurable.py +34 -19
- langchain_core/runnables/fallbacks.py +20 -13
- langchain_core/runnables/graph.py +48 -38
- langchain_core/runnables/graph_ascii.py +40 -17
- langchain_core/runnables/graph_mermaid.py +54 -25
- langchain_core/runnables/graph_png.py +27 -31
- langchain_core/runnables/history.py +55 -58
- langchain_core/runnables/passthrough.py +44 -21
- langchain_core/runnables/retry.py +44 -23
- langchain_core/runnables/router.py +9 -8
- langchain_core/runnables/schema.py +9 -0
- langchain_core/runnables/utils.py +53 -90
- langchain_core/stores.py +19 -31
- langchain_core/sys_info.py +9 -8
- langchain_core/tools/base.py +36 -27
- langchain_core/tools/convert.py +25 -14
- langchain_core/tools/simple.py +36 -8
- langchain_core/tools/structured.py +25 -12
- langchain_core/tracers/base.py +2 -2
- langchain_core/tracers/context.py +5 -1
- langchain_core/tracers/core.py +110 -46
- langchain_core/tracers/evaluation.py +22 -26
- langchain_core/tracers/event_stream.py +97 -42
- langchain_core/tracers/langchain.py +12 -3
- langchain_core/tracers/langchain_v1.py +10 -2
- langchain_core/tracers/log_stream.py +56 -17
- langchain_core/tracers/root_listeners.py +4 -20
- langchain_core/tracers/run_collector.py +6 -16
- langchain_core/tracers/schemas.py +5 -1
- langchain_core/utils/aiter.py +14 -6
- langchain_core/utils/env.py +3 -0
- langchain_core/utils/function_calling.py +46 -20
- langchain_core/utils/interactive_env.py +6 -2
- langchain_core/utils/iter.py +12 -5
- langchain_core/utils/json.py +12 -3
- langchain_core/utils/json_schema.py +156 -40
- langchain_core/utils/loading.py +5 -1
- langchain_core/utils/mustache.py +25 -16
- langchain_core/utils/pydantic.py +38 -9
- langchain_core/utils/utils.py +25 -9
- langchain_core/vectorstores/base.py +7 -20
- langchain_core/vectorstores/in_memory.py +20 -14
- langchain_core/vectorstores/utils.py +18 -12
- langchain_core/version.py +1 -1
- langchain_core-1.0.0a3.dist-info/METADATA +77 -0
- langchain_core-1.0.0a3.dist-info/RECORD +181 -0
- langchain_core/beta/__init__.py +0 -1
- langchain_core/beta/runnables/__init__.py +0 -1
- langchain_core/beta/runnables/context.py +0 -448
- langchain_core-1.0.0a1.dist-info/METADATA +0 -106
- langchain_core-1.0.0a1.dist-info/RECORD +0 -184
- {langchain_core-1.0.0a1.dist-info → langchain_core-1.0.0a3.dist-info}/WHEEL +0 -0
- {langchain_core-1.0.0a1.dist-info → langchain_core-1.0.0a3.dist-info}/entry_points.txt +0 -0
|
@@ -17,12 +17,17 @@ from typing import (
|
|
|
17
17
|
Optional,
|
|
18
18
|
Union,
|
|
19
19
|
cast,
|
|
20
|
+
get_args,
|
|
21
|
+
get_origin,
|
|
20
22
|
)
|
|
21
23
|
|
|
22
24
|
from pydantic import BaseModel
|
|
23
25
|
from pydantic.v1 import BaseModel as BaseModelV1
|
|
24
|
-
from
|
|
26
|
+
from pydantic.v1 import Field as Field_v1
|
|
27
|
+
from pydantic.v1 import create_model as create_model_v1
|
|
28
|
+
from typing_extensions import TypedDict, is_typeddict
|
|
25
29
|
|
|
30
|
+
import langchain_core
|
|
26
31
|
from langchain_core._api import beta, deprecated
|
|
27
32
|
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage
|
|
28
33
|
from langchain_core.utils.json_schema import dereference_refs
|
|
@@ -146,6 +151,9 @@ def _convert_pydantic_to_openai_function(
|
|
|
146
151
|
of the schema will be used.
|
|
147
152
|
rm_titles: Whether to remove titles from the schema. Defaults to True.
|
|
148
153
|
|
|
154
|
+
Raises:
|
|
155
|
+
TypeError: If the model is not a Pydantic model.
|
|
156
|
+
|
|
149
157
|
Returns:
|
|
150
158
|
The function description.
|
|
151
159
|
"""
|
|
@@ -217,10 +225,8 @@ def _convert_python_function_to_openai_function(
|
|
|
217
225
|
Returns:
|
|
218
226
|
The OpenAI function description.
|
|
219
227
|
"""
|
|
220
|
-
from langchain_core.tools.base import create_schema_from_function
|
|
221
|
-
|
|
222
228
|
func_name = _get_python_function_name(function)
|
|
223
|
-
model = create_schema_from_function(
|
|
229
|
+
model = langchain_core.tools.base.create_schema_from_function(
|
|
224
230
|
func_name,
|
|
225
231
|
function,
|
|
226
232
|
filter_args=(),
|
|
@@ -261,9 +267,6 @@ def _convert_any_typed_dicts_to_pydantic(
|
|
|
261
267
|
visited: dict,
|
|
262
268
|
depth: int = 0,
|
|
263
269
|
) -> type:
|
|
264
|
-
from pydantic.v1 import Field as Field_v1
|
|
265
|
-
from pydantic.v1 import create_model as create_model_v1
|
|
266
|
-
|
|
267
270
|
if type_ in visited:
|
|
268
271
|
return visited[type_]
|
|
269
272
|
if depth >= _MAX_TYPED_DICT_RECURSION:
|
|
@@ -323,12 +326,15 @@ def _format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
|
|
|
323
326
|
Args:
|
|
324
327
|
tool: The tool to format.
|
|
325
328
|
|
|
329
|
+
Raises:
|
|
330
|
+
ValueError: If the tool call schema is not supported.
|
|
331
|
+
|
|
326
332
|
Returns:
|
|
327
333
|
The function description.
|
|
328
334
|
"""
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
335
|
+
is_simple_oai_tool = (
|
|
336
|
+
isinstance(tool, langchain_core.tools.simple.Tool) and not tool.args_schema
|
|
337
|
+
)
|
|
332
338
|
if tool.tool_call_schema and not is_simple_oai_tool:
|
|
333
339
|
if isinstance(tool.tool_call_schema, dict):
|
|
334
340
|
return _convert_json_schema_to_openai_function(
|
|
@@ -429,8 +435,6 @@ def convert_to_openai_function(
|
|
|
429
435
|
'description' and 'parameters' keys are now optional. Only 'name' is
|
|
430
436
|
required and guaranteed to be part of the output.
|
|
431
437
|
"""
|
|
432
|
-
from langchain_core.tools import BaseTool
|
|
433
|
-
|
|
434
438
|
# an Anthropic format tool
|
|
435
439
|
if isinstance(function, dict) and all(
|
|
436
440
|
k in function for k in ("name", "input_schema")
|
|
@@ -470,7 +474,7 @@ def convert_to_openai_function(
|
|
|
470
474
|
oai_function = cast(
|
|
471
475
|
"dict", _convert_typed_dict_to_openai_function(cast("type", function))
|
|
472
476
|
)
|
|
473
|
-
elif isinstance(function, BaseTool):
|
|
477
|
+
elif isinstance(function, langchain_core.tools.base.BaseTool):
|
|
474
478
|
oai_function = cast("dict", _format_tool_to_openai_function(function))
|
|
475
479
|
elif callable(function):
|
|
476
480
|
oai_function = cast(
|
|
@@ -515,6 +519,7 @@ _WellKnownOpenAITools = (
|
|
|
515
519
|
"mcp",
|
|
516
520
|
"image_generation",
|
|
517
521
|
"web_search_preview",
|
|
522
|
+
"web_search",
|
|
518
523
|
)
|
|
519
524
|
|
|
520
525
|
|
|
@@ -575,7 +580,8 @@ def convert_to_openai_tool(
|
|
|
575
580
|
|
|
576
581
|
Added support for OpenAI's image generation built-in tool.
|
|
577
582
|
"""
|
|
578
|
-
|
|
583
|
+
# Import locally to prevent circular import
|
|
584
|
+
from langchain_core.tools import Tool # noqa: PLC0415
|
|
579
585
|
|
|
580
586
|
if isinstance(tool, dict):
|
|
581
587
|
if tool.get("type") in _WellKnownOpenAITools:
|
|
@@ -601,7 +607,20 @@ def convert_to_json_schema(
|
|
|
601
607
|
*,
|
|
602
608
|
strict: Optional[bool] = None,
|
|
603
609
|
) -> dict[str, Any]:
|
|
604
|
-
"""Convert a schema representation to a JSON schema.
|
|
610
|
+
"""Convert a schema representation to a JSON schema.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
schema: The schema to convert.
|
|
614
|
+
strict: If True, model output is guaranteed to exactly match the JSON Schema
|
|
615
|
+
provided in the function definition. If None, ``strict`` argument will not
|
|
616
|
+
be included in function definition.
|
|
617
|
+
|
|
618
|
+
Raises:
|
|
619
|
+
ValueError: If the input is not a valid OpenAI-format tool.
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
A JSON schema representation of the input schema.
|
|
623
|
+
"""
|
|
605
624
|
openai_tool = convert_to_openai_tool(schema, strict=strict)
|
|
606
625
|
if (
|
|
607
626
|
not isinstance(openai_tool, dict)
|
|
@@ -671,8 +690,10 @@ def tool_example_to_messages(
|
|
|
671
690
|
from pydantic import BaseModel, Field
|
|
672
691
|
from langchain_openai import ChatOpenAI
|
|
673
692
|
|
|
693
|
+
|
|
674
694
|
class Person(BaseModel):
|
|
675
695
|
'''Information about a person.'''
|
|
696
|
+
|
|
676
697
|
name: Optional[str] = Field(..., description="The name of the person")
|
|
677
698
|
hair_color: Optional[str] = Field(
|
|
678
699
|
..., description="The color of the person's hair if known"
|
|
@@ -681,6 +702,7 @@ def tool_example_to_messages(
|
|
|
681
702
|
..., description="Height in METERS"
|
|
682
703
|
)
|
|
683
704
|
|
|
705
|
+
|
|
684
706
|
examples = [
|
|
685
707
|
(
|
|
686
708
|
"The ocean is vast and blue. It's more than 20,000 feet deep.",
|
|
@@ -696,9 +718,7 @@ def tool_example_to_messages(
|
|
|
696
718
|
messages = []
|
|
697
719
|
|
|
698
720
|
for txt, tool_call in examples:
|
|
699
|
-
messages.extend(
|
|
700
|
-
tool_example_to_messages(txt, [tool_call])
|
|
701
|
-
)
|
|
721
|
+
messages.extend(tool_example_to_messages(txt, [tool_call]))
|
|
702
722
|
|
|
703
723
|
"""
|
|
704
724
|
messages: list[BaseMessage] = [HumanMessage(content=input)]
|
|
@@ -816,8 +836,14 @@ def _recursive_set_additional_properties_false(
|
|
|
816
836
|
if isinstance(schema, dict):
|
|
817
837
|
# Check if 'required' is a key at the current level or if the schema is empty,
|
|
818
838
|
# in which case additionalProperties still needs to be specified.
|
|
819
|
-
if
|
|
820
|
-
"
|
|
839
|
+
if (
|
|
840
|
+
"required" in schema
|
|
841
|
+
or ("properties" in schema and not schema["properties"])
|
|
842
|
+
# Since Pydantic 2.11, it will always add `additionalProperties: True`
|
|
843
|
+
# for arbitrary dictionary schemas
|
|
844
|
+
# See: https://pydantic.dev/articles/pydantic-v2-11-release#changes
|
|
845
|
+
# If it is already set to True, we need override it to False
|
|
846
|
+
or "additionalProperties" in schema
|
|
821
847
|
):
|
|
822
848
|
schema["additionalProperties"] = False
|
|
823
849
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"""Utilities for working with interactive environments."""
|
|
2
2
|
|
|
3
|
+
import sys
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
def is_interactive_env() -> bool:
|
|
5
|
-
"""Determine if running within IPython or Jupyter.
|
|
6
|
-
import sys
|
|
7
|
+
"""Determine if running within IPython or Jupyter.
|
|
7
8
|
|
|
9
|
+
Returns:
|
|
10
|
+
True if running in an interactive environment, False otherwise.
|
|
11
|
+
"""
|
|
8
12
|
return hasattr(sys, "ps2")
|
langchain_core/utils/iter.py
CHANGED
|
@@ -8,14 +8,13 @@ from types import TracebackType
|
|
|
8
8
|
from typing import (
|
|
9
9
|
Any,
|
|
10
10
|
Generic,
|
|
11
|
+
Literal,
|
|
11
12
|
Optional,
|
|
12
13
|
TypeVar,
|
|
13
14
|
Union,
|
|
14
15
|
overload,
|
|
15
16
|
)
|
|
16
17
|
|
|
17
|
-
from typing_extensions import Literal
|
|
18
|
-
|
|
19
18
|
T = TypeVar("T")
|
|
20
19
|
|
|
21
20
|
|
|
@@ -31,7 +30,7 @@ class NoLock:
|
|
|
31
30
|
exc_val: Optional[BaseException],
|
|
32
31
|
exc_tb: Optional[TracebackType],
|
|
33
32
|
) -> Literal[False]:
|
|
34
|
-
"""
|
|
33
|
+
"""Return False (exception not suppressed)."""
|
|
35
34
|
return False
|
|
36
35
|
|
|
37
36
|
|
|
@@ -173,7 +172,11 @@ class Tee(Generic[T]):
|
|
|
173
172
|
return self._children[item]
|
|
174
173
|
|
|
175
174
|
def __iter__(self) -> Iterator[Iterator[T]]:
|
|
176
|
-
"""Return an iterator over the child iterators.
|
|
175
|
+
"""Return an iterator over the child iterators.
|
|
176
|
+
|
|
177
|
+
Yields:
|
|
178
|
+
The child iterators.
|
|
179
|
+
"""
|
|
177
180
|
yield from self._children
|
|
178
181
|
|
|
179
182
|
def __enter__(self) -> "Tee[T]":
|
|
@@ -186,7 +189,11 @@ class Tee(Generic[T]):
|
|
|
186
189
|
exc_val: Optional[BaseException],
|
|
187
190
|
exc_tb: Optional[TracebackType],
|
|
188
191
|
) -> Literal[False]:
|
|
189
|
-
"""Close all child iterators.
|
|
192
|
+
"""Close all child iterators.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
False (exception not suppressed).
|
|
196
|
+
"""
|
|
190
197
|
self.close()
|
|
191
198
|
return False
|
|
192
199
|
|
langchain_core/utils/json.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
|
-
from typing import Any, Callable
|
|
7
|
+
from typing import Any, Callable, Union
|
|
8
8
|
|
|
9
9
|
from langchain_core.exceptions import OutputParserException
|
|
10
10
|
|
|
@@ -19,13 +19,16 @@ def _replace_new_line(match: re.Match[str]) -> str:
|
|
|
19
19
|
return match.group(1) + value + match.group(3)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def _custom_parser(multiline_string: str) -> str:
|
|
22
|
+
def _custom_parser(multiline_string: Union[str, bytes, bytearray]) -> str:
|
|
23
23
|
r"""Custom parser for multiline strings.
|
|
24
24
|
|
|
25
25
|
The LLM response for `action_input` may be a multiline
|
|
26
26
|
string containing unescaped newlines, tabs or quotes. This function
|
|
27
27
|
replaces those characters with their escaped counterparts.
|
|
28
28
|
(newlines in JSON must be double-escaped: `\\n`).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The modified string with escaped newlines, tabs and quotes.
|
|
29
32
|
"""
|
|
30
33
|
if isinstance(multiline_string, (bytes, bytearray)):
|
|
31
34
|
multiline_string = multiline_string.decode()
|
|
@@ -98,7 +101,7 @@ def parse_partial_json(s: str, *, strict: bool = False) -> Any:
|
|
|
98
101
|
# If we're still inside a string at the end of processing,
|
|
99
102
|
# we need to close the string.
|
|
100
103
|
if is_inside_string:
|
|
101
|
-
if escaped: #
|
|
104
|
+
if escaped: # Remove unterminated escape character
|
|
102
105
|
new_chars.pop()
|
|
103
106
|
new_chars.append('"')
|
|
104
107
|
|
|
@@ -187,6 +190,12 @@ def parse_and_check_json_markdown(text: str, expected_keys: list[str]) -> dict:
|
|
|
187
190
|
except json.JSONDecodeError as e:
|
|
188
191
|
msg = f"Got invalid JSON object. Error: {e}"
|
|
189
192
|
raise OutputParserException(msg) from e
|
|
193
|
+
if not isinstance(json_obj, dict):
|
|
194
|
+
error_message = (
|
|
195
|
+
f"Expected JSON object (dict), but got: {type(json_obj).__name__}. "
|
|
196
|
+
)
|
|
197
|
+
raise OutputParserException(error_message, llm_output=text)
|
|
198
|
+
|
|
190
199
|
for key in expected_keys:
|
|
191
200
|
if key not in json_obj:
|
|
192
201
|
msg = (
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from copy import deepcopy
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Optional
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from collections.abc import Sequence
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def _retrieve_ref(path: str, schema: dict) -> dict:
|
|
12
|
+
def _retrieve_ref(path: str, schema: dict) -> Union[list, dict]:
|
|
13
13
|
components = path.split("/")
|
|
14
14
|
if components[0] != "#":
|
|
15
15
|
msg = (
|
|
@@ -17,9 +17,12 @@ def _retrieve_ref(path: str, schema: dict) -> dict:
|
|
|
17
17
|
"with #."
|
|
18
18
|
)
|
|
19
19
|
raise ValueError(msg)
|
|
20
|
-
out = schema
|
|
20
|
+
out: Union[list, dict] = schema
|
|
21
21
|
for component in components[1:]:
|
|
22
22
|
if component in out:
|
|
23
|
+
if isinstance(out, list):
|
|
24
|
+
msg = f"Reference '{path}' not found."
|
|
25
|
+
raise KeyError(msg)
|
|
23
26
|
out = out[component]
|
|
24
27
|
elif component.isdigit():
|
|
25
28
|
index = int(component)
|
|
@@ -36,6 +39,31 @@ def _retrieve_ref(path: str, schema: dict) -> dict:
|
|
|
36
39
|
return deepcopy(out)
|
|
37
40
|
|
|
38
41
|
|
|
42
|
+
def _process_dict_properties(
|
|
43
|
+
properties: dict[str, Any],
|
|
44
|
+
full_schema: dict[str, Any],
|
|
45
|
+
processed_refs: set[str],
|
|
46
|
+
skip_keys: Sequence[str],
|
|
47
|
+
*,
|
|
48
|
+
shallow_refs: bool,
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
"""Process dictionary properties, recursing into nested structures."""
|
|
51
|
+
result: dict[str, Any] = {}
|
|
52
|
+
for key, value in properties.items():
|
|
53
|
+
if key in skip_keys:
|
|
54
|
+
# Skip recursion for specified keys, just copy the value as-is
|
|
55
|
+
result[key] = deepcopy(value)
|
|
56
|
+
elif isinstance(value, (dict, list)):
|
|
57
|
+
# Recursively process nested objects and arrays
|
|
58
|
+
result[key] = _dereference_refs_helper(
|
|
59
|
+
value, full_schema, processed_refs, skip_keys, shallow_refs
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
# Copy primitive values directly
|
|
63
|
+
result[key] = value
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
|
|
39
67
|
def _dereference_refs_helper(
|
|
40
68
|
obj: Any,
|
|
41
69
|
full_schema: dict[str, Any],
|
|
@@ -43,51 +71,87 @@ def _dereference_refs_helper(
|
|
|
43
71
|
skip_keys: Sequence[str],
|
|
44
72
|
shallow_refs: bool, # noqa: FBT001
|
|
45
73
|
) -> Any:
|
|
46
|
-
"""
|
|
74
|
+
"""Dereference JSON Schema $ref objects, handling both pure and mixed references.
|
|
47
75
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- if shallow_refs=False: deep-inline all nested refs
|
|
76
|
+
This function processes JSON Schema objects containing $ref properties by resolving
|
|
77
|
+
the references and merging any additional properties. It handles:
|
|
51
78
|
|
|
52
|
-
|
|
79
|
+
- Pure $ref objects: {"$ref": "#/path/to/definition"}
|
|
80
|
+
- Mixed $ref objects: {"$ref": "#/path", "title": "Custom Title", ...}
|
|
81
|
+
- Circular references by breaking cycles and preserving non-ref properties
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
obj: The object to process (can be dict, list, or primitive)
|
|
85
|
+
full_schema: The complete schema containing all definitions
|
|
86
|
+
processed_refs: Set tracking currently processing refs (for cycle detection)
|
|
87
|
+
skip_keys: Keys under which to skip recursion
|
|
88
|
+
shallow_refs: If True, only break cycles; if False, deep-inline all refs
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The object with $ref properties resolved and merged with other properties.
|
|
53
92
|
"""
|
|
54
93
|
if processed_refs is None:
|
|
55
94
|
processed_refs = set()
|
|
56
95
|
|
|
57
|
-
# 1
|
|
58
|
-
if isinstance(obj, dict) and "$ref" in
|
|
96
|
+
# Case 1: Object contains a $ref property (pure or mixed with additional properties)
|
|
97
|
+
if isinstance(obj, dict) and "$ref" in obj:
|
|
59
98
|
ref_path = obj["$ref"]
|
|
60
|
-
|
|
99
|
+
additional_properties = {
|
|
100
|
+
key: value for key, value in obj.items() if key != "$ref"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Detect circular reference: if we're already processing this $ref,
|
|
104
|
+
# return only the additional properties to break the cycle
|
|
61
105
|
if ref_path in processed_refs:
|
|
62
|
-
return
|
|
63
|
-
|
|
106
|
+
return _process_dict_properties(
|
|
107
|
+
additional_properties,
|
|
108
|
+
full_schema,
|
|
109
|
+
processed_refs,
|
|
110
|
+
skip_keys,
|
|
111
|
+
shallow_refs=shallow_refs,
|
|
112
|
+
)
|
|
64
113
|
|
|
65
|
-
#
|
|
66
|
-
|
|
114
|
+
# Mark this reference as being processed (for cycle detection)
|
|
115
|
+
processed_refs.add(ref_path)
|
|
67
116
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
117
|
+
# Fetch and recursively resolve the referenced object
|
|
118
|
+
referenced_object = deepcopy(_retrieve_ref(ref_path, full_schema))
|
|
119
|
+
resolved_reference = _dereference_refs_helper(
|
|
120
|
+
referenced_object, full_schema, processed_refs, skip_keys, shallow_refs
|
|
71
121
|
)
|
|
72
122
|
|
|
123
|
+
# Clean up: remove from processing set before returning
|
|
73
124
|
processed_refs.remove(ref_path)
|
|
74
|
-
return result
|
|
75
125
|
|
|
76
|
-
|
|
126
|
+
# Pure $ref case: no additional properties, return resolved reference directly
|
|
127
|
+
if not additional_properties:
|
|
128
|
+
return resolved_reference
|
|
129
|
+
|
|
130
|
+
# Mixed $ref case: merge resolved reference with additional properties
|
|
131
|
+
# Additional properties take precedence over resolved properties
|
|
132
|
+
merged_result = {}
|
|
133
|
+
if isinstance(resolved_reference, dict):
|
|
134
|
+
merged_result.update(resolved_reference)
|
|
135
|
+
|
|
136
|
+
# Process additional properties and merge them (they override resolved ones)
|
|
137
|
+
processed_additional = _process_dict_properties(
|
|
138
|
+
additional_properties,
|
|
139
|
+
full_schema,
|
|
140
|
+
processed_refs,
|
|
141
|
+
skip_keys,
|
|
142
|
+
shallow_refs=shallow_refs,
|
|
143
|
+
)
|
|
144
|
+
merged_result.update(processed_additional)
|
|
145
|
+
|
|
146
|
+
return merged_result
|
|
147
|
+
|
|
148
|
+
# Case 2: Regular dictionary without $ref - process all properties
|
|
77
149
|
if isinstance(obj, dict):
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# do not recurse under this key
|
|
82
|
-
out[k] = deepcopy(v)
|
|
83
|
-
elif isinstance(v, (dict, list)):
|
|
84
|
-
out[k] = _dereference_refs_helper(
|
|
85
|
-
v, full_schema, processed_refs, skip_keys, shallow_refs
|
|
86
|
-
)
|
|
87
|
-
else:
|
|
88
|
-
out[k] = v
|
|
89
|
-
return out
|
|
150
|
+
return _process_dict_properties(
|
|
151
|
+
obj, full_schema, processed_refs, skip_keys, shallow_refs=shallow_refs
|
|
152
|
+
)
|
|
90
153
|
|
|
154
|
+
# Case 3: List - recursively process each item
|
|
91
155
|
if isinstance(obj, list):
|
|
92
156
|
return [
|
|
93
157
|
_dereference_refs_helper(
|
|
@@ -96,6 +160,7 @@ def _dereference_refs_helper(
|
|
|
96
160
|
for item in obj
|
|
97
161
|
]
|
|
98
162
|
|
|
163
|
+
# Case 4: Primitive value (string, number, boolean, null) - return unchanged
|
|
99
164
|
return obj
|
|
100
165
|
|
|
101
166
|
|
|
@@ -105,16 +170,67 @@ def dereference_refs(
|
|
|
105
170
|
full_schema: Optional[dict] = None,
|
|
106
171
|
skip_keys: Optional[Sequence[str]] = None,
|
|
107
172
|
) -> dict:
|
|
108
|
-
"""
|
|
173
|
+
"""Resolve and inline JSON Schema $ref references in a schema object.
|
|
174
|
+
|
|
175
|
+
This function processes a JSON Schema and resolves all $ref references by replacing
|
|
176
|
+
them with the actual referenced content. It handles both simple references and
|
|
177
|
+
complex cases like circular references and mixed $ref objects that contain
|
|
178
|
+
additional properties alongside the $ref.
|
|
109
179
|
|
|
110
180
|
Args:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
181
|
+
schema_obj: The JSON Schema object or fragment to process. This can be a
|
|
182
|
+
complete schema or just a portion of one.
|
|
183
|
+
full_schema: The complete schema containing all definitions that $refs might
|
|
184
|
+
point to. If not provided, defaults to schema_obj (useful when the
|
|
185
|
+
schema is self-contained).
|
|
186
|
+
skip_keys: Controls recursion behavior and reference resolution depth:
|
|
187
|
+
- If None (default): Only recurse under '$defs' and use shallow reference
|
|
188
|
+
resolution (break cycles but don't deep-inline nested refs)
|
|
189
|
+
- If provided (even as []): Recurse under all keys and use deep reference
|
|
190
|
+
resolution (fully inline all nested references)
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
A new dictionary with all $ref references resolved and inlined. The original
|
|
194
|
+
schema_obj is not modified.
|
|
195
|
+
|
|
196
|
+
Examples:
|
|
197
|
+
Basic reference resolution:
|
|
198
|
+
>>> schema = {
|
|
199
|
+
... "type": "object",
|
|
200
|
+
... "properties": {"name": {"$ref": "#/$defs/string_type"}},
|
|
201
|
+
... "$defs": {"string_type": {"type": "string"}},
|
|
202
|
+
... }
|
|
203
|
+
>>> result = dereference_refs(schema)
|
|
204
|
+
>>> result["properties"]["name"] # {"type": "string"}
|
|
205
|
+
|
|
206
|
+
Mixed $ref with additional properties:
|
|
207
|
+
>>> schema = {
|
|
208
|
+
... "properties": {
|
|
209
|
+
... "name": {"$ref": "#/$defs/base", "description": "User name"}
|
|
210
|
+
... },
|
|
211
|
+
... "$defs": {"base": {"type": "string", "minLength": 1}},
|
|
212
|
+
... }
|
|
213
|
+
>>> result = dereference_refs(schema)
|
|
214
|
+
>>> result["properties"]["name"]
|
|
215
|
+
# {"type": "string", "minLength": 1, "description": "User name"}
|
|
216
|
+
|
|
217
|
+
Handling circular references:
|
|
218
|
+
>>> schema = {
|
|
219
|
+
... "properties": {"user": {"$ref": "#/$defs/User"}},
|
|
220
|
+
... "$defs": {
|
|
221
|
+
... "User": {
|
|
222
|
+
... "type": "object",
|
|
223
|
+
... "properties": {"friend": {"$ref": "#/$defs/User"}},
|
|
224
|
+
... }
|
|
225
|
+
... },
|
|
226
|
+
... }
|
|
227
|
+
>>> result = dereference_refs(schema) # Won't cause infinite recursion
|
|
228
|
+
|
|
229
|
+
Note:
|
|
230
|
+
- Circular references are handled gracefully by breaking cycles
|
|
231
|
+
- Mixed $ref objects (with both $ref and other properties) are supported
|
|
232
|
+
- Additional properties in mixed $refs override resolved properties
|
|
233
|
+
- The $defs section is preserved in the output by default
|
|
118
234
|
"""
|
|
119
235
|
full = full_schema or schema_obj
|
|
120
236
|
keys_to_skip = list(skip_keys) if skip_keys is not None else ["$defs"]
|
langchain_core/utils/loading.py
CHANGED
|
@@ -19,7 +19,11 @@ def try_load_from_hub(
|
|
|
19
19
|
*args: Any, # noqa: ARG001
|
|
20
20
|
**kwargs: Any, # noqa: ARG001
|
|
21
21
|
) -> Any:
|
|
22
|
-
"""[DEPRECATED] Try to load from the old Hub.
|
|
22
|
+
"""[DEPRECATED] Try to load from the old Hub.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
None always, indicating that we shouldn't load from the old hub.
|
|
26
|
+
"""
|
|
23
27
|
warnings.warn(
|
|
24
28
|
"Loading from the deprecated github-based Hub is no longer supported. "
|
|
25
29
|
"Please use the new LangChain Hub at https://smith.langchain.com/hub instead.",
|
langchain_core/utils/mustache.py
CHANGED
|
@@ -18,7 +18,7 @@ from typing import (
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
|
-
from
|
|
21
|
+
from typing import TypeAlias
|
|
22
22
|
|
|
23
23
|
logger = logging.getLogger(__name__)
|
|
24
24
|
|
|
@@ -82,7 +82,7 @@ def l_sa_check(
|
|
|
82
82
|
"""
|
|
83
83
|
# If there is a newline, or the previous tag was a standalone
|
|
84
84
|
if literal.find("\n") != -1 or is_standalone:
|
|
85
|
-
padding = literal.
|
|
85
|
+
padding = literal.rsplit("\n", maxsplit=1)[-1]
|
|
86
86
|
|
|
87
87
|
# If all the characters since the last newline are spaces
|
|
88
88
|
# Then the next tag could be a standalone
|
|
@@ -214,17 +214,22 @@ def tokenize(
|
|
|
214
214
|
def_rdel: The default right delimiter
|
|
215
215
|
("}}" by default, as in spec compliant mustache)
|
|
216
216
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
217
|
+
Yields:
|
|
218
|
+
Mustache tags in the form of a tuple (tag_type, tag_key)
|
|
219
|
+
where tag_type is one of:
|
|
220
|
+
|
|
221
|
+
* literal
|
|
222
|
+
* section
|
|
223
|
+
* inverted section
|
|
224
|
+
* end
|
|
225
|
+
* partial
|
|
226
|
+
* no escape
|
|
227
|
+
|
|
228
|
+
and tag_key is either the key or in the case of a literal tag,
|
|
229
|
+
the literal itself.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
ChevronError: If there is a syntax error in the template.
|
|
228
233
|
"""
|
|
229
234
|
global _CURRENT_LINE, _LAST_TAG_LINE
|
|
230
235
|
_CURRENT_LINE = 1
|
|
@@ -326,7 +331,7 @@ def tokenize(
|
|
|
326
331
|
|
|
327
332
|
|
|
328
333
|
def _html_escape(string: str) -> str:
|
|
329
|
-
"""HTML
|
|
334
|
+
"""Return the HTML-escaped string with these characters escaped: ``" & < >``."""
|
|
330
335
|
html_codes = {
|
|
331
336
|
'"': """,
|
|
332
337
|
"<": "<",
|
|
@@ -349,7 +354,7 @@ def _get_key(
|
|
|
349
354
|
def_ldel: str,
|
|
350
355
|
def_rdel: str,
|
|
351
356
|
) -> Any:
|
|
352
|
-
"""
|
|
357
|
+
"""Return a key from the current scope."""
|
|
353
358
|
# If the key is a dot
|
|
354
359
|
if key == ".":
|
|
355
360
|
# Then just return the current scope
|
|
@@ -407,7 +412,11 @@ def _get_key(
|
|
|
407
412
|
|
|
408
413
|
|
|
409
414
|
def _get_partial(name: str, partials_dict: Mapping[str, str]) -> str:
|
|
410
|
-
"""Load a partial.
|
|
415
|
+
"""Load a partial.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
The partial.
|
|
419
|
+
"""
|
|
411
420
|
try:
|
|
412
421
|
# Maybe the partial is in the dictionary
|
|
413
422
|
return partials_dict[name]
|