langchain-core 0.3.76__py3-none-any.whl → 0.3.77__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 (49) hide show
  1. langchain_core/_api/beta_decorator.py +6 -5
  2. langchain_core/_api/deprecation.py +11 -11
  3. langchain_core/callbacks/base.py +17 -11
  4. langchain_core/callbacks/manager.py +2 -2
  5. langchain_core/callbacks/usage.py +2 -2
  6. langchain_core/chat_history.py +26 -16
  7. langchain_core/document_loaders/langsmith.py +1 -1
  8. langchain_core/indexing/api.py +31 -31
  9. langchain_core/language_models/chat_models.py +4 -2
  10. langchain_core/language_models/fake_chat_models.py +5 -2
  11. langchain_core/language_models/llms.py +3 -1
  12. langchain_core/load/serializable.py +1 -1
  13. langchain_core/messages/ai.py +22 -10
  14. langchain_core/messages/base.py +30 -16
  15. langchain_core/messages/chat.py +4 -1
  16. langchain_core/messages/function.py +9 -5
  17. langchain_core/messages/human.py +11 -4
  18. langchain_core/messages/modifier.py +1 -0
  19. langchain_core/messages/system.py +9 -2
  20. langchain_core/messages/tool.py +27 -16
  21. langchain_core/messages/utils.py +92 -83
  22. langchain_core/outputs/chat_generation.py +10 -6
  23. langchain_core/prompt_values.py +6 -2
  24. langchain_core/prompts/chat.py +6 -3
  25. langchain_core/prompts/few_shot.py +4 -1
  26. langchain_core/runnables/base.py +14 -13
  27. langchain_core/runnables/graph.py +4 -1
  28. langchain_core/runnables/graph_ascii.py +1 -1
  29. langchain_core/runnables/graph_mermaid.py +27 -10
  30. langchain_core/runnables/retry.py +35 -18
  31. langchain_core/stores.py +6 -6
  32. langchain_core/tools/base.py +7 -5
  33. langchain_core/tools/convert.py +2 -2
  34. langchain_core/tools/simple.py +1 -5
  35. langchain_core/tools/structured.py +0 -10
  36. langchain_core/tracers/event_stream.py +13 -15
  37. langchain_core/utils/aiter.py +1 -1
  38. langchain_core/utils/function_calling.py +13 -8
  39. langchain_core/utils/iter.py +1 -1
  40. langchain_core/utils/json.py +7 -1
  41. langchain_core/utils/json_schema.py +145 -39
  42. langchain_core/utils/pydantic.py +6 -5
  43. langchain_core/utils/utils.py +1 -1
  44. langchain_core/vectorstores/in_memory.py +5 -5
  45. langchain_core/version.py +1 -1
  46. {langchain_core-0.3.76.dist-info → langchain_core-0.3.77.dist-info}/METADATA +8 -18
  47. {langchain_core-0.3.76.dist-info → langchain_core-0.3.77.dist-info}/RECORD +49 -49
  48. {langchain_core-0.3.76.dist-info → langchain_core-0.3.77.dist-info}/WHEEL +0 -0
  49. {langchain_core-0.3.76.dist-info → langchain_core-0.3.77.dist-info}/entry_points.txt +0 -0
@@ -6,6 +6,7 @@ import asyncio
6
6
  import base64
7
7
  import random
8
8
  import re
9
+ import string
9
10
  import time
10
11
  from dataclasses import asdict
11
12
  from pathlib import Path
@@ -148,7 +149,7 @@ def draw_mermaid(
148
149
  + "</em></small>"
149
150
  )
150
151
  node_label = format_dict.get(key, format_dict[default_class_label]).format(
151
- _escape_node_label(key), label
152
+ _to_safe_id(key), label
152
153
  )
153
154
  return f"{indent}{node_label}\n"
154
155
 
@@ -211,8 +212,7 @@ def draw_mermaid(
211
212
  edge_label = " -.-> " if edge.conditional else " --> "
212
213
 
213
214
  mermaid_graph += (
214
- f"\t{_escape_node_label(source)}{edge_label}"
215
- f"{_escape_node_label(target)};\n"
215
+ f"\t{_to_safe_id(source)}{edge_label}{_to_safe_id(target)};\n"
216
216
  )
217
217
 
218
218
  # Recursively add nested subgraphs
@@ -256,9 +256,18 @@ def draw_mermaid(
256
256
  return mermaid_graph
257
257
 
258
258
 
259
- def _escape_node_label(node_label: str) -> str:
260
- """Escapes the node label for Mermaid syntax."""
261
- return re.sub(r"[^a-zA-Z-_0-9]", "_", node_label)
259
+ def _to_safe_id(label: str) -> str:
260
+ """Convert a string into a Mermaid-compatible node id.
261
+
262
+ Keep [a-zA-Z0-9_-] characters unchanged.
263
+ Map every other character -> backslash + lowercase hex codepoint.
264
+
265
+ Result is guaranteed to be unique and Mermaid-compatible,
266
+ so nodes with special characters always render correctly.
267
+ """
268
+ allowed = string.ascii_letters + string.digits + "_-"
269
+ out = [ch if ch in allowed else "\\" + format(ord(ch), "x") for ch in label]
270
+ return "".join(out)
262
271
 
263
272
 
264
273
  def _generate_mermaid_graph_styles(node_colors: NodeStyles) -> str:
@@ -277,6 +286,7 @@ def draw_mermaid_png(
277
286
  padding: int = 10,
278
287
  max_retries: int = 1,
279
288
  retry_delay: float = 1.0,
289
+ base_url: Optional[str] = None,
280
290
  ) -> bytes:
281
291
  """Draws a Mermaid graph as PNG using provided syntax.
282
292
 
@@ -293,6 +303,8 @@ def draw_mermaid_png(
293
303
  Defaults to 1.
294
304
  retry_delay (float, optional): Delay between retries (MermaidDrawMethod.API).
295
305
  Defaults to 1.0.
306
+ base_url (str, optional): Base URL for the Mermaid.ink API.
307
+ Defaults to None.
296
308
 
297
309
  Returns:
298
310
  bytes: PNG image bytes.
@@ -313,6 +325,7 @@ def draw_mermaid_png(
313
325
  background_color=background_color,
314
326
  max_retries=max_retries,
315
327
  retry_delay=retry_delay,
328
+ base_url=base_url,
316
329
  )
317
330
  else:
318
331
  supported_methods = ", ".join([m.value for m in MermaidDrawMethod])
@@ -404,8 +417,12 @@ def _render_mermaid_using_api(
404
417
  file_type: Optional[Literal["jpeg", "png", "webp"]] = "png",
405
418
  max_retries: int = 1,
406
419
  retry_delay: float = 1.0,
420
+ base_url: Optional[str] = None,
407
421
  ) -> bytes:
408
422
  """Renders Mermaid graph using the Mermaid.INK API."""
423
+ # Defaults to using the public mermaid.ink server.
424
+ base_url = base_url if base_url is not None else "https://mermaid.ink"
425
+
409
426
  if not _HAS_REQUESTS:
410
427
  msg = (
411
428
  "Install the `requests` module to use the Mermaid.INK API: "
@@ -425,7 +442,7 @@ def _render_mermaid_using_api(
425
442
  background_color = f"!{background_color}"
426
443
 
427
444
  image_url = (
428
- f"https://mermaid.ink/img/{mermaid_syntax_encoded}"
445
+ f"{base_url}/img/{mermaid_syntax_encoded}"
429
446
  f"?type={file_type}&bgColor={background_color}"
430
447
  )
431
448
 
@@ -457,7 +474,7 @@ def _render_mermaid_using_api(
457
474
 
458
475
  # For other status codes, fail immediately
459
476
  msg = (
460
- "Failed to reach https://mermaid.ink/ API while trying to render "
477
+ f"Failed to reach {base_url} API while trying to render "
461
478
  f"your graph. Status code: {response.status_code}.\n\n"
462
479
  ) + error_msg_suffix
463
480
  raise ValueError(msg)
@@ -469,14 +486,14 @@ def _render_mermaid_using_api(
469
486
  time.sleep(sleep_time)
470
487
  else:
471
488
  msg = (
472
- "Failed to reach https://mermaid.ink/ API while trying to render "
489
+ f"Failed to reach {base_url} API while trying to render "
473
490
  f"your graph after {max_retries} retries. "
474
491
  ) + error_msg_suffix
475
492
  raise ValueError(msg) from e
476
493
 
477
494
  # This should not be reached, but just in case
478
495
  msg = (
479
- "Failed to reach https://mermaid.ink/ API while trying to render "
496
+ f"Failed to reach {base_url} API while trying to render "
480
497
  f"your graph after {max_retries} retries. "
481
498
  ) + error_msg_suffix
482
499
  raise ValueError(msg)
@@ -238,31 +238,40 @@ class RunnableRetry(RunnableBindingBase[Input, Output]): # type: ignore[no-rede
238
238
  ) -> list[Union[Output, Exception]]:
239
239
  results_map: dict[int, Output] = {}
240
240
 
241
- def pending(iterable: list[U]) -> list[U]:
242
- return [item for idx, item in enumerate(iterable) if idx not in results_map]
243
-
244
241
  not_set: list[Output] = []
245
242
  result = not_set
246
243
  try:
247
244
  for attempt in self._sync_retrying():
248
245
  with attempt:
249
- # Get the results of the inputs that have not succeeded yet.
246
+ # Retry for inputs that have not yet succeeded
247
+ # Determine which original indices remain.
248
+ remaining_indices = [
249
+ i for i in range(len(inputs)) if i not in results_map
250
+ ]
251
+ if not remaining_indices:
252
+ break
253
+ pending_inputs = [inputs[i] for i in remaining_indices]
254
+ pending_configs = [config[i] for i in remaining_indices]
255
+ pending_run_managers = [run_manager[i] for i in remaining_indices]
256
+ # Invoke underlying batch only on remaining elements.
250
257
  result = super().batch(
251
- pending(inputs),
258
+ pending_inputs,
252
259
  self._patch_config_list(
253
- pending(config), pending(run_manager), attempt.retry_state
260
+ pending_configs, pending_run_managers, attempt.retry_state
254
261
  ),
255
262
  return_exceptions=True,
256
263
  **kwargs,
257
264
  )
258
- # Register the results of the inputs that have succeeded.
265
+ # Register the results of the inputs that have succeeded, mapping
266
+ # back to their original indices.
259
267
  first_exception = None
260
- for i, r in enumerate(result):
268
+ for offset, r in enumerate(result):
261
269
  if isinstance(r, Exception):
262
270
  if not first_exception:
263
271
  first_exception = r
264
272
  continue
265
- results_map[i] = r
273
+ orig_idx = remaining_indices[offset]
274
+ results_map[orig_idx] = r
266
275
  # If any exception occurred, raise it, to retry the failed ones
267
276
  if first_exception:
268
277
  raise first_exception
@@ -305,31 +314,39 @@ class RunnableRetry(RunnableBindingBase[Input, Output]): # type: ignore[no-rede
305
314
  ) -> list[Union[Output, Exception]]:
306
315
  results_map: dict[int, Output] = {}
307
316
 
308
- def pending(iterable: list[U]) -> list[U]:
309
- return [item for idx, item in enumerate(iterable) if idx not in results_map]
310
-
311
317
  not_set: list[Output] = []
312
318
  result = not_set
313
319
  try:
314
320
  async for attempt in self._async_retrying():
315
321
  with attempt:
316
- # Get the results of the inputs that have not succeeded yet.
322
+ # Retry for inputs that have not yet succeeded
323
+ # Determine which original indices remain.
324
+ remaining_indices = [
325
+ i for i in range(len(inputs)) if i not in results_map
326
+ ]
327
+ if not remaining_indices:
328
+ break
329
+ pending_inputs = [inputs[i] for i in remaining_indices]
330
+ pending_configs = [config[i] for i in remaining_indices]
331
+ pending_run_managers = [run_manager[i] for i in remaining_indices]
317
332
  result = await super().abatch(
318
- pending(inputs),
333
+ pending_inputs,
319
334
  self._patch_config_list(
320
- pending(config), pending(run_manager), attempt.retry_state
335
+ pending_configs, pending_run_managers, attempt.retry_state
321
336
  ),
322
337
  return_exceptions=True,
323
338
  **kwargs,
324
339
  )
325
- # Register the results of the inputs that have succeeded.
340
+ # Register the results of the inputs that have succeeded, mapping
341
+ # back to their original indices.
326
342
  first_exception = None
327
- for i, r in enumerate(result):
343
+ for offset, r in enumerate(result):
328
344
  if isinstance(r, Exception):
329
345
  if not first_exception:
330
346
  first_exception = r
331
347
  continue
332
- results_map[i] = r
348
+ orig_idx = remaining_indices[offset]
349
+ results_map[orig_idx] = r
333
350
  # If any exception occurred, raise it, to retry the failed ones
334
351
  if first_exception:
335
352
  raise first_exception
langchain_core/stores.py CHANGED
@@ -56,22 +56,22 @@ class BaseStore(ABC, Generic[K, V]):
56
56
 
57
57
 
58
58
  class MyInMemoryStore(BaseStore[str, int]):
59
- def __init__(self):
60
- self.store = {}
59
+ def __init__(self) -> None:
60
+ self.store: dict[str, int] = {}
61
61
 
62
- def mget(self, keys):
62
+ def mget(self, keys: Sequence[str]) -> list[int | None]:
63
63
  return [self.store.get(key) for key in keys]
64
64
 
65
- def mset(self, key_value_pairs):
65
+ def mset(self, key_value_pairs: Sequence[tuple[str, int]]) -> None:
66
66
  for key, value in key_value_pairs:
67
67
  self.store[key] = value
68
68
 
69
- def mdelete(self, keys):
69
+ def mdelete(self, keys: Sequence[str]) -> None:
70
70
  for key in keys:
71
71
  if key in self.store:
72
72
  del self.store[key]
73
73
 
74
- def yield_keys(self, prefix=None):
74
+ def yield_keys(self, prefix: str | None = None) -> Iterator[str]:
75
75
  if prefix is None:
76
76
  yield from self.store.keys()
77
77
  else:
@@ -82,6 +82,7 @@ TOOL_MESSAGE_BLOCK_TYPES = (
82
82
  "search_result",
83
83
  "custom_tool_call_output",
84
84
  "document",
85
+ "file",
85
86
  )
86
87
 
87
88
 
@@ -546,6 +547,8 @@ class ChildTool(BaseTool):
546
547
  """
547
548
  if isinstance(self.args_schema, dict):
548
549
  json_schema = self.args_schema
550
+ elif self.args_schema and issubclass(self.args_schema, BaseModelV1):
551
+ json_schema = self.args_schema.schema()
549
552
  else:
550
553
  input_schema = self.get_input_schema()
551
554
  json_schema = input_schema.model_json_schema()
@@ -1322,8 +1325,7 @@ def get_all_basemodel_annotations(
1322
1325
  """
1323
1326
  # cls has no subscript: cls = FooBar
1324
1327
  if isinstance(cls, type):
1325
- # Gather pydantic field objects (v2: model_fields / v1: __fields__)
1326
- fields = getattr(cls, "model_fields", {}) or getattr(cls, "__fields__", {})
1328
+ fields = get_fields(cls)
1327
1329
  alias_map = {field.alias: name for name, field in fields.items() if field.alias}
1328
1330
 
1329
1331
  annotations: dict[str, Union[type, TypeVar]] = {}
@@ -1362,8 +1364,8 @@ def get_all_basemodel_annotations(
1362
1364
  continue
1363
1365
 
1364
1366
  # if class = FooBar inherits from Baz[str]:
1365
- # parent = Baz[str],
1366
- # parent_origin = Baz,
1367
+ # parent = class Baz[str],
1368
+ # parent_origin = class Baz,
1367
1369
  # generic_type_vars = (type vars in Baz)
1368
1370
  # generic_map = {type var in Baz: str}
1369
1371
  generic_type_vars: tuple = getattr(parent_origin, "__parameters__", ())
@@ -1400,7 +1402,7 @@ def _replace_type_vars(
1400
1402
  if type_ in generic_map:
1401
1403
  return generic_map[type_]
1402
1404
  if default_to_bound:
1403
- return type_.__bound__ or Any
1405
+ return type_.__bound__ if type_.__bound__ is not None else Any
1404
1406
  return type_
1405
1407
  if (origin := get_origin(type_)) and (args := get_args(type_)):
1406
1408
  new_args = tuple(
@@ -227,7 +227,7 @@ def tool(
227
227
  \"\"\"
228
228
  return bar
229
229
 
230
- """ # noqa: D214, D410, D411
230
+ """ # noqa: D214, D410, D411 # We're intentionally showing bad formatting in examples
231
231
 
232
232
  def _create_tool_factory(
233
233
  tool_name: str,
@@ -315,7 +315,7 @@ def tool(
315
315
 
316
316
  if runnable is not None:
317
317
  # tool is used as a function
318
- # tool_from_runnable = tool("name", runnable)
318
+ # for instance tool_from_runnable = tool("name", runnable)
319
319
  if not name_or_callable:
320
320
  msg = "Runnable without name for tool constructor"
321
321
  raise ValueError(msg)
@@ -64,11 +64,7 @@ class Tool(BaseTool):
64
64
  The input arguments for the tool.
65
65
  """
66
66
  if self.args_schema is not None:
67
- if isinstance(self.args_schema, dict):
68
- json_schema = self.args_schema
69
- else:
70
- json_schema = self.args_schema.model_json_schema()
71
- return json_schema["properties"]
67
+ return super().args
72
68
  # For backwards compatibility, if the function signature is ambiguous,
73
69
  # assume it takes a single string input.
74
70
  return {"tool_input": {"type": "string"}}
@@ -67,16 +67,6 @@ class StructuredTool(BaseTool):
67
67
 
68
68
  # --- Tool ---
69
69
 
70
- @property
71
- def args(self) -> dict:
72
- """The tool's input arguments."""
73
- if isinstance(self.args_schema, dict):
74
- json_schema = self.args_schema
75
- else:
76
- input_schema = self.get_input_schema()
77
- json_schema = input_schema.model_json_schema()
78
- return json_schema["properties"]
79
-
80
70
  def _run(
81
71
  self,
82
72
  *args: Any,
@@ -224,7 +224,7 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
224
224
  yield chunk
225
225
 
226
226
  def tap_output_iter(self, run_id: UUID, output: Iterator[T]) -> Iterator[T]:
227
- """Tap the output aiter.
227
+ """Tap the output iter.
228
228
 
229
229
  Args:
230
230
  run_id: The ID of the run.
@@ -315,7 +315,7 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
315
315
  name: Optional[str] = None,
316
316
  **kwargs: Any,
317
317
  ) -> None:
318
- """Start a trace for an LLM run."""
318
+ """Start a trace for a chat model run."""
319
319
  name_ = _assign_name(name, serialized)
320
320
  run_type = "chat_model"
321
321
 
@@ -357,7 +357,7 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
357
357
  name: Optional[str] = None,
358
358
  **kwargs: Any,
359
359
  ) -> None:
360
- """Start a trace for an LLM run."""
360
+ """Start a trace for a (non-chat model) LLM run."""
361
361
  name_ = _assign_name(name, serialized)
362
362
  run_type = "llm"
363
363
 
@@ -421,6 +421,10 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
421
421
  parent_run_id: Optional[UUID] = None,
422
422
  **kwargs: Any,
423
423
  ) -> None:
424
+ """Run on new output token. Only available when streaming is enabled.
425
+
426
+ For both chat models and non-chat models (legacy LLMs).
427
+ """
424
428
  run_info = self.run_map.get(run_id)
425
429
  chunk_: Union[GenerationChunk, BaseMessageChunk]
426
430
 
@@ -466,17 +470,15 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
466
470
  async def on_llm_end(
467
471
  self, response: LLMResult, *, run_id: UUID, **kwargs: Any
468
472
  ) -> None:
469
- """End a trace for an LLM run.
473
+ """End a trace for a model run.
470
474
 
471
- Args:
472
- response (LLMResult): The response which was generated.
473
- run_id (UUID): The run ID. This is the ID of the current run.
475
+ For both chat models and non-chat models (legacy LLMs).
474
476
 
475
477
  Raises:
476
478
  ValueError: If the run type is not ``'llm'`` or ``'chat_model'``.
477
479
  """
478
480
  run_info = self.run_map.pop(run_id)
479
- inputs_ = run_info["inputs"]
481
+ inputs_ = run_info.get("inputs")
480
482
 
481
483
  generations: Union[list[list[GenerationChunk]], list[list[ChatGenerationChunk]]]
482
484
  output: Union[dict, BaseMessage] = {}
@@ -654,10 +656,6 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
654
656
  async def on_tool_end(self, output: Any, *, run_id: UUID, **kwargs: Any) -> None:
655
657
  """End a trace for a tool run.
656
658
 
657
- Args:
658
- output: The output of the tool.
659
- run_id: The run ID. This is the ID of the current run.
660
-
661
659
  Raises:
662
660
  AssertionError: If the run ID is a tool call and does not have inputs
663
661
  """
@@ -742,7 +740,7 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
742
740
  "event": "on_retriever_end",
743
741
  "data": {
744
742
  "output": documents,
745
- "input": run_info["inputs"],
743
+ "input": run_info.get("inputs"),
746
744
  },
747
745
  "run_id": str(run_id),
748
746
  "name": run_info["name"],
@@ -854,12 +852,12 @@ async def _astream_events_implementation_v1(
854
852
  # Usually they will NOT be available for components that operate
855
853
  # on streams, since those components stream the input and
856
854
  # don't know its final value until the end of the stream.
857
- inputs = log_entry["inputs"]
855
+ inputs = log_entry.get("inputs")
858
856
  if inputs is not None:
859
857
  data["input"] = inputs
860
858
 
861
859
  if event_type == "end":
862
- inputs = log_entry["inputs"]
860
+ inputs = log_entry.get("inputs")
863
861
  if inputs is not None:
864
862
  data["input"] = inputs
865
863
 
@@ -165,7 +165,7 @@ class Tee(Generic[T]):
165
165
  A ``tee`` works lazily and can handle an infinite ``iterable``, provided
166
166
  that all iterators advance.
167
167
 
168
- .. code-block:: python3
168
+ .. code-block:: python
169
169
 
170
170
  async def derivative(sensor_data):
171
171
  previous, current = a.tee(sensor_data, n=2)
@@ -667,14 +667,13 @@ def tool_example_to_messages(
667
667
  The ``ToolMessage`` is required because some chat models are hyper-optimized for
668
668
  agents rather than for an extraction use case.
669
669
 
670
- Arguments:
671
- input: string, the user input
672
- tool_calls: list[BaseModel], a list of tool calls represented as Pydantic
673
- BaseModels
674
- tool_outputs: Optional[list[str]], a list of tool call outputs.
670
+ Args:
671
+ input: The user input
672
+ tool_calls: Tool calls represented as Pydantic BaseModels
673
+ tool_outputs: Tool call outputs.
675
674
  Does not need to be provided. If not provided, a placeholder value
676
675
  will be inserted. Defaults to None.
677
- ai_response: Optional[str], if provided, content for a final ``AIMessage``.
676
+ ai_response: If provided, content for a final ``AIMessage``.
678
677
 
679
678
  Returns:
680
679
  A list of messages
@@ -833,8 +832,14 @@ def _recursive_set_additional_properties_false(
833
832
  if isinstance(schema, dict):
834
833
  # Check if 'required' is a key at the current level or if the schema is empty,
835
834
  # in which case additionalProperties still needs to be specified.
836
- if "required" in schema or (
837
- "properties" in schema and not schema["properties"]
835
+ if (
836
+ "required" in schema
837
+ or ("properties" in schema and not schema["properties"])
838
+ # Since Pydantic 2.11, it will always add `additionalProperties: True`
839
+ # for arbitrary dictionary schemas
840
+ # See: https://pydantic.dev/articles/pydantic-v2-11-release#changes
841
+ # If it is already set to True, we need override it to False
842
+ or "additionalProperties" in schema
838
843
  ):
839
844
  schema["additionalProperties"] = False
840
845
 
@@ -102,7 +102,7 @@ class Tee(Generic[T]):
102
102
  A ``tee`` works lazily and can handle an infinite ``iterable``, provided
103
103
  that all iterators advance.
104
104
 
105
- .. code-block:: python3
105
+ .. code-block:: python
106
106
 
107
107
  async def derivative(sensor_data):
108
108
  previous, current = a.tee(sensor_data, n=2)
@@ -101,7 +101,7 @@ def parse_partial_json(s: str, *, strict: bool = False) -> Any:
101
101
  # If we're still inside a string at the end of processing,
102
102
  # we need to close the string.
103
103
  if is_inside_string:
104
- if escaped: # Remoe unterminated escape character
104
+ if escaped: # Remove unterminated escape character
105
105
  new_chars.pop()
106
106
  new_chars.append('"')
107
107
 
@@ -190,6 +190,12 @@ def parse_and_check_json_markdown(text: str, expected_keys: list[str]) -> dict:
190
190
  except json.JSONDecodeError as e:
191
191
  msg = f"Got invalid JSON object. Error: {e}"
192
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
+
193
199
  for key in expected_keys:
194
200
  if key not in json_obj:
195
201
  msg = (