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.

Files changed (131) hide show
  1. langchain_core/_api/beta_decorator.py +17 -40
  2. langchain_core/_api/deprecation.py +20 -7
  3. langchain_core/_api/path.py +19 -2
  4. langchain_core/_import_utils.py +7 -0
  5. langchain_core/agents.py +10 -6
  6. langchain_core/callbacks/base.py +28 -15
  7. langchain_core/callbacks/manager.py +81 -69
  8. langchain_core/callbacks/usage.py +4 -2
  9. langchain_core/chat_history.py +29 -21
  10. langchain_core/document_loaders/base.py +34 -9
  11. langchain_core/document_loaders/langsmith.py +3 -0
  12. langchain_core/documents/base.py +35 -10
  13. langchain_core/documents/transformers.py +4 -2
  14. langchain_core/embeddings/fake.py +8 -5
  15. langchain_core/env.py +2 -3
  16. langchain_core/example_selectors/base.py +12 -0
  17. langchain_core/exceptions.py +7 -0
  18. langchain_core/globals.py +17 -28
  19. langchain_core/indexing/api.py +57 -45
  20. langchain_core/indexing/base.py +5 -8
  21. langchain_core/indexing/in_memory.py +23 -3
  22. langchain_core/language_models/__init__.py +6 -2
  23. langchain_core/language_models/_utils.py +28 -4
  24. langchain_core/language_models/base.py +33 -21
  25. langchain_core/language_models/chat_models.py +103 -29
  26. langchain_core/language_models/fake_chat_models.py +5 -7
  27. langchain_core/language_models/llms.py +54 -20
  28. langchain_core/load/dump.py +2 -3
  29. langchain_core/load/load.py +15 -1
  30. langchain_core/load/serializable.py +38 -43
  31. langchain_core/memory.py +7 -3
  32. langchain_core/messages/__init__.py +7 -17
  33. langchain_core/messages/ai.py +41 -34
  34. langchain_core/messages/base.py +16 -7
  35. langchain_core/messages/block_translators/__init__.py +10 -8
  36. langchain_core/messages/block_translators/anthropic.py +3 -1
  37. langchain_core/messages/block_translators/bedrock.py +3 -1
  38. langchain_core/messages/block_translators/bedrock_converse.py +3 -1
  39. langchain_core/messages/block_translators/google_genai.py +3 -1
  40. langchain_core/messages/block_translators/google_vertexai.py +3 -1
  41. langchain_core/messages/block_translators/groq.py +3 -1
  42. langchain_core/messages/block_translators/langchain_v0.py +3 -136
  43. langchain_core/messages/block_translators/ollama.py +3 -1
  44. langchain_core/messages/block_translators/openai.py +252 -10
  45. langchain_core/messages/content.py +26 -124
  46. langchain_core/messages/human.py +2 -13
  47. langchain_core/messages/system.py +2 -6
  48. langchain_core/messages/tool.py +34 -14
  49. langchain_core/messages/utils.py +189 -74
  50. langchain_core/output_parsers/base.py +5 -2
  51. langchain_core/output_parsers/json.py +4 -4
  52. langchain_core/output_parsers/list.py +7 -22
  53. langchain_core/output_parsers/openai_functions.py +3 -0
  54. langchain_core/output_parsers/openai_tools.py +6 -1
  55. langchain_core/output_parsers/pydantic.py +4 -0
  56. langchain_core/output_parsers/string.py +5 -1
  57. langchain_core/output_parsers/xml.py +19 -19
  58. langchain_core/outputs/chat_generation.py +18 -7
  59. langchain_core/outputs/generation.py +14 -3
  60. langchain_core/outputs/llm_result.py +8 -1
  61. langchain_core/prompt_values.py +10 -4
  62. langchain_core/prompts/base.py +6 -11
  63. langchain_core/prompts/chat.py +88 -60
  64. langchain_core/prompts/dict.py +16 -8
  65. langchain_core/prompts/few_shot.py +9 -11
  66. langchain_core/prompts/few_shot_with_templates.py +5 -1
  67. langchain_core/prompts/image.py +12 -5
  68. langchain_core/prompts/loading.py +2 -2
  69. langchain_core/prompts/message.py +5 -6
  70. langchain_core/prompts/pipeline.py +13 -8
  71. langchain_core/prompts/prompt.py +22 -8
  72. langchain_core/prompts/string.py +18 -10
  73. langchain_core/prompts/structured.py +7 -2
  74. langchain_core/rate_limiters.py +2 -2
  75. langchain_core/retrievers.py +7 -6
  76. langchain_core/runnables/base.py +387 -246
  77. langchain_core/runnables/branch.py +11 -28
  78. langchain_core/runnables/config.py +20 -17
  79. langchain_core/runnables/configurable.py +34 -19
  80. langchain_core/runnables/fallbacks.py +20 -13
  81. langchain_core/runnables/graph.py +48 -38
  82. langchain_core/runnables/graph_ascii.py +40 -17
  83. langchain_core/runnables/graph_mermaid.py +54 -25
  84. langchain_core/runnables/graph_png.py +27 -31
  85. langchain_core/runnables/history.py +55 -58
  86. langchain_core/runnables/passthrough.py +44 -21
  87. langchain_core/runnables/retry.py +44 -23
  88. langchain_core/runnables/router.py +9 -8
  89. langchain_core/runnables/schema.py +9 -0
  90. langchain_core/runnables/utils.py +53 -90
  91. langchain_core/stores.py +19 -31
  92. langchain_core/sys_info.py +9 -8
  93. langchain_core/tools/base.py +36 -27
  94. langchain_core/tools/convert.py +25 -14
  95. langchain_core/tools/simple.py +36 -8
  96. langchain_core/tools/structured.py +25 -12
  97. langchain_core/tracers/base.py +2 -2
  98. langchain_core/tracers/context.py +5 -1
  99. langchain_core/tracers/core.py +110 -46
  100. langchain_core/tracers/evaluation.py +22 -26
  101. langchain_core/tracers/event_stream.py +97 -42
  102. langchain_core/tracers/langchain.py +12 -3
  103. langchain_core/tracers/langchain_v1.py +10 -2
  104. langchain_core/tracers/log_stream.py +56 -17
  105. langchain_core/tracers/root_listeners.py +4 -20
  106. langchain_core/tracers/run_collector.py +6 -16
  107. langchain_core/tracers/schemas.py +5 -1
  108. langchain_core/utils/aiter.py +14 -6
  109. langchain_core/utils/env.py +3 -0
  110. langchain_core/utils/function_calling.py +46 -20
  111. langchain_core/utils/interactive_env.py +6 -2
  112. langchain_core/utils/iter.py +12 -5
  113. langchain_core/utils/json.py +12 -3
  114. langchain_core/utils/json_schema.py +156 -40
  115. langchain_core/utils/loading.py +5 -1
  116. langchain_core/utils/mustache.py +25 -16
  117. langchain_core/utils/pydantic.py +38 -9
  118. langchain_core/utils/utils.py +25 -9
  119. langchain_core/vectorstores/base.py +7 -20
  120. langchain_core/vectorstores/in_memory.py +20 -14
  121. langchain_core/vectorstores/utils.py +18 -12
  122. langchain_core/version.py +1 -1
  123. langchain_core-1.0.0a3.dist-info/METADATA +77 -0
  124. langchain_core-1.0.0a3.dist-info/RECORD +181 -0
  125. langchain_core/beta/__init__.py +0 -1
  126. langchain_core/beta/runnables/__init__.py +0 -1
  127. langchain_core/beta/runnables/context.py +0 -448
  128. langchain_core-1.0.0a1.dist-info/METADATA +0 -106
  129. langchain_core-1.0.0a1.dist-info/RECORD +0 -184
  130. {langchain_core-1.0.0a1.dist-info → langchain_core-1.0.0a3.dist-info}/WHEEL +0 -0
  131. {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 typing_extensions import TypedDict, get_args, get_origin, is_typeddict
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
- from langchain_core.tools import simple
330
-
331
- is_simple_oai_tool = isinstance(tool, simple.Tool) and not tool.args_schema
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
- from langchain_core.tools import Tool
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 "required" in schema or (
820
- "properties" in schema and not schema["properties"]
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")
@@ -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
- """Exception not handled."""
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
 
@@ -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: # Remoe unterminated escape character
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
- """Inline every pure {'$ref':...}.
74
+ """Dereference JSON Schema $ref objects, handling both pure and mixed references.
47
75
 
48
- But:
49
- - if shallow_refs=True: only break cycles, do not inline nested refs
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
- Also skip recursion under any key in skip_keys.
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) Pure $ref node?
58
- if isinstance(obj, dict) and "$ref" in set(obj.keys()):
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
- # cycle?
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
- processed_refs.add(ref_path)
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
- # grab + copy the target
66
- target = deepcopy(_retrieve_ref(ref_path, full_schema))
114
+ # Mark this reference as being processed (for cycle detection)
115
+ processed_refs.add(ref_path)
67
116
 
68
- # deep inlining: recurse into everything
69
- result = _dereference_refs_helper(
70
- target, full_schema, processed_refs, skip_keys, shallow_refs
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
- # 2) Not a pure-$ref: recurse, skipping any keys in skip_keys
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
- out: dict[str, Any] = {}
79
- for k, v in obj.items():
80
- if k in skip_keys:
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
- """Try to substitute $refs in JSON Schema.
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
- schema_obj: The fragment to dereference.
112
- full_schema: The complete schema (defaults to schema_obj).
113
- skip_keys:
114
- - If None (the default), we skip recursion under '$defs' *and* only
115
- shallow-inline refs.
116
- - If provided (even as an empty list), we will recurse under every key and
117
- deep-inline all refs.
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"]
@@ -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.",
@@ -18,7 +18,7 @@ from typing import (
18
18
  )
19
19
 
20
20
  if TYPE_CHECKING:
21
- from typing_extensions import TypeAlias
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.split("\n")[-1]
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
- Returns:
218
- A generator of mustache tags in the form of a tuple (tag_type, tag_key)
219
- Where tag_type is one of:
220
- * literal
221
- * section
222
- * inverted section
223
- * end
224
- * partial
225
- * no escape
226
- And tag_key is either the key or in the case of a literal tag,
227
- the literal itself.
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 escape all of these " & < >."""
334
+ """Return the HTML-escaped string with these characters escaped: ``" & < >``."""
330
335
  html_codes = {
331
336
  '"': "&quot;",
332
337
  "<": "&lt;",
@@ -349,7 +354,7 @@ def _get_key(
349
354
  def_ldel: str,
350
355
  def_rdel: str,
351
356
  ) -> Any:
352
- """Get a key from the current scope."""
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]