Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-langchain-core for 
openSUSE:Factory checked in at 2026-08-15 22:41:12
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langchain-core (Old)
 and      /work/SRC/openSUSE:Factory/.python-langchain-core.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-langchain-core"

Sat Aug 15 22:41:12 2026 rev:8 rq:1371288 version:1.5.5

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-langchain-core/python-langchain-core.changes  
    2026-08-12 16:13:23.420709417 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-langchain-core.new.1258/python-langchain-core.changes
    2026-08-15 22:41:38.069716038 +0200
@@ -1,0 +2,26 @@
+Sat Aug 15 05:46:56 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to version 1.5.5:
+  * Guard against malformed Anthropic content blocks
+  * Respect pydantic field aliases when validating tool inputs
+  * Fix chunk merging: merge_dicts now raises TypeError for
+    differing boolean values instead of silently summing them to
+    an int, AddableDict addition raises TypeError on
+    type-incompatible keys instead of dropping data, and
+    merge_lists no longer misidentifies non-dict elements as
+    index-keyed
+  * Handle v1 base model validation in the async path
+  * Handle tool descriptions when infer_schema=False
+  * Clear the usage metadata callback on exceptions in the
+    context manager
+  * Handle falsy LLM and chat model caches
+  * Preserve non-str/non-dict items in DictPromptTemplate list
+    values
+  * Raise ValueError when the explicit tool_outputs length does
+    not match tool_calls in tool_example_to_messages
+  * Make abatch_iterate consistent with batch_iterate for a None
+    or zero batch size
+- Add python-httpx >= 0.23.0 dependency: upstream now declares
+  httpx as an explicit runtime requirement
+
+-------------------------------------------------------------------

Old:
----
  langchain_core-1.5.4.tar.gz

New:
----
  langchain_core-1.5.5.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-langchain-core.spec ++++++
--- /var/tmp/diff_new_pack.OVG4lq/_old  2026-08-15 22:41:38.761740398 +0200
+++ /var/tmp/diff_new_pack.OVG4lq/_new  2026-08-15 22:41:38.763740468 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-langchain-core
-Version:        1.5.4
+Version:        1.5.5
 Release:        0
 Summary:        Building applications with LLMs through composability
 License:        MIT
@@ -30,6 +30,7 @@
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 Requires:       python-PyYAML >= 5.3
+Requires:       python-httpx >= 0.23.0
 Requires:       python-jsonpatch >= 1.33
 Requires:       python-langchain-protocol >= 0.0.17
 Requires:       python-langsmith >= 0.3.45
@@ -43,6 +44,7 @@
 BuildRequires:  %{python_module PyYAML >= 5.3}
 BuildRequires:  %{python_module blockbuster}
 BuildRequires:  %{python_module freezegun}
+BuildRequires:  %{python_module httpx >= 0.23.0}
 BuildRequires:  %{python_module jsonpatch >= 1.33}
 BuildRequires:  %{python_module langchain-protocol >= 0.0.17}
 BuildRequires:  %{python_module langsmith >= 0.3.45}

++++++ langchain_core-1.5.4.tar.gz -> langchain_core-1.5.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.5.4/PKG-INFO 
new/langchain_core-1.5.5/PKG-INFO
--- old/langchain_core-1.5.4/PKG-INFO   2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/PKG-INFO   2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.5
 Name: langchain-core
-Version: 1.5.4
+Version: 1.5.5
 Summary: Building applications with LLMs through composability
 Project-URL: Homepage, https://docs.langchain.com/
 Project-URL: Documentation, 
https://reference.langchain.com/python/langchain_core/
@@ -24,6 +24,7 @@
 Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Requires-Python: <4.0.0,>=3.10.0
+Requires-Dist: httpx<1.0.0,>=0.23.0
 Requires-Dist: jsonpatch<2.0.0,>=1.33.0
 Requires-Dist: langchain-protocol>=0.0.17
 Requires-Dist: langsmith<1.0.0,>=0.3.45
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/langchain_core/callbacks/usage.py 
new/langchain_core-1.5.5/langchain_core/callbacks/usage.py
--- old/langchain_core-1.5.4/langchain_core/callbacks/usage.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/langchain_core/callbacks/usage.py  2020-02-02 
01:00:00.000000000 +0100
@@ -114,6 +114,11 @@
     )
     register_configure_hook(usage_metadata_callback_var, inheritable=True)
     cb = UsageMetadataCallbackHandler()
-    usage_metadata_callback_var.set(cb)
-    yield cb
-    usage_metadata_callback_var.set(None)
+    token = usage_metadata_callback_var.set(cb)
+    try:
+        yield cb
+    finally:
+        # Always clear the context var, including when the with-block raises.
+        # Without finally, post-block model calls keep accumulating into cb
+        # (see #38989).
+        usage_metadata_callback_var.reset(token)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/langchain_core/language_models/chat_models.py 
new/langchain_core-1.5.5/langchain_core/language_models/chat_models.py
--- old/langchain_core-1.5.4/langchain_core/language_models/chat_models.py      
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/langchain_core/language_models/chat_models.py      
2020-02-02 01:00:00.000000000 +0100
@@ -1888,9 +1888,9 @@
         # We should check the cache unless it's explicitly set to False
         # A None cache means we should use the default global cache
         # if it's configured.
-        check_cache = self.cache or self.cache is None
+        check_cache = self.cache is not False
         if check_cache:
-            if llm_cache:
+            if llm_cache is not None:
                 llm_string = self._get_llm_string(stop=stop, **kwargs)
                 normalized_messages = [
                     (
@@ -2032,7 +2032,7 @@
                 **result.llm_output,
                 **result.generations[0].message.response_metadata,
             }
-        if check_cache and llm_cache:
+        if check_cache and llm_cache is not None:
             llm_cache.update(prompt, llm_string, result.generations)
         return result
 
@@ -2047,9 +2047,9 @@
         # We should check the cache unless it's explicitly set to False
         # A None cache means we should use the default global cache
         # if it's configured.
-        check_cache = self.cache or self.cache is None
+        check_cache = self.cache is not False
         if check_cache:
-            if llm_cache:
+            if llm_cache is not None:
                 llm_string = self._get_llm_string(stop=stop, **kwargs)
                 normalized_messages = [
                     (
@@ -2189,7 +2189,7 @@
                 **result.llm_output,
                 **result.generations[0].message.response_metadata,
             }
-        if check_cache and llm_cache:
+        if check_cache and llm_cache is not None:
             await llm_cache.aupdate(prompt, llm_string, result.generations)
         return result
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/langchain_core/language_models/llms.py 
new/langchain_core-1.5.5/langchain_core/language_models/llms.py
--- old/langchain_core-1.5.4/langchain_core/language_models/llms.py     
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/langchain_core/language_models/llms.py     
2020-02-02 01:00:00.000000000 +0100
@@ -182,13 +182,13 @@
 
     llm_cache = _resolve_cache(cache=cache)
     for i, prompt in enumerate(prompts):
-        if llm_cache:
+        if llm_cache is not None:
             cache_val = llm_cache.lookup(prompt, llm_string)
             if isinstance(cache_val, list):
                 existing_prompts[i] = cache_val
-            else:
-                missing_prompts.append(prompt)
-                missing_prompt_idxs.append(i)
+                continue
+        missing_prompts.append(prompt)
+        missing_prompt_idxs.append(i)
     return existing_prompts, llm_string, missing_prompt_idxs, missing_prompts
 
 
@@ -217,13 +217,13 @@
     existing_prompts = {}
     llm_cache = _resolve_cache(cache=cache)
     for i, prompt in enumerate(prompts):
-        if llm_cache:
+        if llm_cache is not None:
             cache_val = await llm_cache.alookup(prompt, llm_string)
             if isinstance(cache_val, list):
                 existing_prompts[i] = cache_val
-            else:
-                missing_prompts.append(prompt)
-                missing_prompt_idxs.append(i)
+                continue
+        missing_prompts.append(prompt)
+        missing_prompt_idxs.append(i)
     return existing_prompts, llm_string, missing_prompt_idxs, missing_prompts
 
 
@@ -288,7 +288,7 @@
     for i, result in enumerate(new_results.generations):
         existing_prompts[missing_prompt_idxs[i]] = result
         prompt = prompts[missing_prompt_idxs[i]]
-        if llm_cache:
+        if llm_cache is not None:
             await llm_cache.aupdate(prompt, llm_string, result)
     return new_results.llm_output
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/langchain_core/messages/block_translators/anthropic.py 
new/langchain_core-1.5.5/langchain_core/messages/block_translators/anthropic.py
--- 
old/langchain_core-1.5.4/langchain_core/messages/block_translators/anthropic.py 
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_core-1.5.5/langchain_core/messages/block_translators/anthropic.py 
    2020-02-02 01:00:00.000000000 +0100
@@ -26,6 +26,10 @@
     return standard_block
 
 
+def _non_standard_block(block: dict[str, Any]) -> 
types.NonStandardContentBlock:
+    return {"type": "non_standard", "value": block}
+
+
 def _convert_to_v1_from_anthropic_input(
     content: list[types.ContentBlock],
 ) -> list[types.ContentBlock]:
@@ -54,87 +58,107 @@
         ]
         for block in blocks:
             block_type = block.get("type")
+            source = block.get("source")
 
             if (
                 block_type == "document"
-                and "source" in block
-                and "type" in block["source"]
+                and isinstance(source, dict)
+                and "type" in source
             ):
-                if block["source"]["type"] == "base64":
+                if source["type"] == "base64":
+                    if "data" not in source or "media_type" not in source:
+                        yield _non_standard_block(block)
+                        continue
                     file_block: types.FileContentBlock = {
                         "type": "file",
-                        "base64": block["source"]["data"],
-                        "mime_type": block["source"]["media_type"],
+                        "base64": source["data"],
+                        "mime_type": source["media_type"],
                     }
                     _populate_extras(file_block, block, {"type", "source"})
                     yield file_block
 
-                elif block["source"]["type"] == "url":
+                elif source["type"] == "url":
+                    if "url" not in source:
+                        yield _non_standard_block(block)
+                        continue
                     file_block = {
                         "type": "file",
-                        "url": block["source"]["url"],
+                        "url": source["url"],
                     }
                     _populate_extras(file_block, block, {"type", "source"})
                     yield file_block
 
-                elif block["source"]["type"] == "file":
+                elif source["type"] == "file":
+                    if "file_id" not in source:
+                        yield _non_standard_block(block)
+                        continue
                     file_block = {
                         "type": "file",
-                        "id": block["source"]["file_id"],
+                        "id": source["file_id"],
                     }
                     _populate_extras(file_block, block, {"type", "source"})
                     yield file_block
 
-                elif block["source"]["type"] == "text":
+                elif source["type"] == "text":
+                    if "data" not in source:
+                        yield _non_standard_block(block)
+                        continue
                     plain_text_block: types.PlainTextContentBlock = {
                         "type": "text-plain",
-                        "text": block["source"]["data"],
+                        "text": source["data"],
                         "mime_type": block.get("media_type", "text/plain"),
                     }
                     _populate_extras(plain_text_block, block, {"type", 
"source"})
                     yield plain_text_block
 
                 else:
-                    yield {"type": "non_standard", "value": block}
+                    yield _non_standard_block(block)
 
             elif (
-                block_type == "image"
-                and "source" in block
-                and "type" in block["source"]
+                block_type == "image" and isinstance(source, dict) and "type" 
in source
             ):
-                if block["source"]["type"] == "base64":
+                if source["type"] == "base64":
+                    if "data" not in source or "media_type" not in source:
+                        yield _non_standard_block(block)
+                        continue
                     image_block: types.ImageContentBlock = {
                         "type": "image",
-                        "base64": block["source"]["data"],
-                        "mime_type": block["source"]["media_type"],
+                        "base64": source["data"],
+                        "mime_type": source["media_type"],
                     }
                     _populate_extras(image_block, block, {"type", "source"})
                     yield image_block
 
-                elif block["source"]["type"] == "url":
+                elif source["type"] == "url":
+                    if "url" not in source:
+                        yield _non_standard_block(block)
+                        continue
                     image_block = {
                         "type": "image",
-                        "url": block["source"]["url"],
+                        "url": source["url"],
                     }
                     _populate_extras(image_block, block, {"type", "source"})
                     yield image_block
 
-                elif block["source"]["type"] == "file":
+                elif source["type"] == "file":
+                    if "file_id" not in source:
+                        yield _non_standard_block(block)
+                        continue
                     image_block = {
                         "type": "image",
-                        "id": block["source"]["file_id"],
+                        "id": source["file_id"],
                     }
                     _populate_extras(image_block, block, {"type", "source"})
                     yield image_block
 
                 else:
-                    yield {"type": "non_standard", "value": block}
+                    yield _non_standard_block(block)
 
             elif block_type in types.KNOWN_BLOCK_TYPES:
                 yield cast("types.ContentBlock", block)
 
             else:
-                yield {"type": "non_standard", "value": block}
+                yield _non_standard_block(block)
 
     return list(_iter_blocks())
 
@@ -143,6 +167,11 @@
     citation_type = citation.get("type")
 
     if citation_type == "web_search_result_location":
+        if "cited_text" not in citation or "url" not in citation:
+            return {
+                "type": "non_standard_annotation",
+                "value": citation,
+            }
         url_citation: types.Citation = {
             "type": "citation",
             "cited_text": citation["cited_text"],
@@ -165,6 +194,11 @@
         "page_location",
         "search_result_location",
     }:
+        if "cited_text" not in citation:
+            return {
+                "type": "non_standard_annotation",
+                "value": citation,
+            }
         document_citation: types.Citation = {
             "type": "citation",
             "cited_text": citation["cited_text"],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.5.4/langchain_core/prompts/dict.py 
new/langchain_core-1.5.5/langchain_core/prompts/dict.py
--- old/langchain_core-1.5.4/langchain_core/prompts/dict.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/langchain_core/prompts/dict.py     2020-02-02 
01:00:00.000000000 +0100
@@ -161,7 +161,7 @@
                 warnings.warn(msg, stacklevel=2)
             formatted[k] = _insert_input_variables(v, inputs, template_format)
         elif isinstance(v, (list, tuple)):
-            formatted_v: list[str | dict[str, Any]] = []
+            formatted_v: list[Any] = []
             for x in v:
                 if isinstance(x, str):
                     formatted_v.append(formatter(x, **inputs))
@@ -169,6 +169,8 @@
                     formatted_v.append(
                         _insert_input_variables(x, inputs, template_format)
                     )
+                else:
+                    formatted_v.append(x)
             formatted[k] = type(v)(formatted_v)
         else:
             formatted[k] = v
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/langchain_core/runnables/utils.py 
new/langchain_core-1.5.5/langchain_core/runnables/utils.py
--- old/langchain_core-1.5.4/langchain_core/runnables/utils.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/langchain_core/runnables/utils.py  2020-02-02 
01:00:00.000000000 +0100
@@ -477,6 +477,9 @@
 
         Returns:
             A dictionary that is the result of adding the two dictionaries.
+
+        Raises:
+            TypeError: If a shared key holds values of incompatible types.
         """
         chunk = AddableDict(self)
         for key in other:
@@ -485,8 +488,13 @@
             elif other[key] is not None:
                 try:
                     added = chunk[key] + other[key]
-                except TypeError:
-                    added = other[key]
+                except TypeError as exc:
+                    msg = (
+                        f"Cannot add incompatible types for key {key!r}: "
+                        f"{type(chunk[key]).__name__!r} and "
+                        f"{type(other[key]).__name__!r}."
+                    )
+                    raise TypeError(msg) from exc
                 chunk[key] = added
         return chunk
 
@@ -498,6 +506,9 @@
 
         Returns:
             A dictionary that is the result of adding the two dictionaries.
+
+        Raises:
+            TypeError: If a shared key holds values of incompatible types.
         """
         chunk = AddableDict(other)
         for key in self:
@@ -506,8 +517,13 @@
             elif self[key] is not None:
                 try:
                     added = chunk[key] + self[key]
-                except TypeError:
-                    added = self[key]
+                except TypeError as exc:
+                    msg = (
+                        f"Cannot add incompatible types for key {key!r}: "
+                        f"{type(chunk[key]).__name__!r} and "
+                        f"{type(self[key]).__name__!r}."
+                    )
+                    raise TypeError(msg) from exc
                 chunk[key] = added
         return chunk
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.5.4/langchain_core/tools/base.py 
new/langchain_core-1.5.5/langchain_core/tools/base.py
--- old/langchain_core-1.5.4/langchain_core/tools/base.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/langchain_core/tools/base.py       2020-02-02 
01:00:00.000000000 +0100
@@ -835,6 +835,7 @@
                         tool_input[k] = tool_call_id
                 result_v2 = input_args.model_validate(tool_input)
                 result_dict = result_v2.model_dump()
+                provided_fields = result_v2.model_fields_set
                 result = result_v2
             elif issubclass(input_args, BaseModelV1):
                 # Check args_schema for InjectedToolCallId
@@ -852,6 +853,7 @@
                         tool_input[k] = tool_call_id
                 result_v1 = input_args.parse_obj(tool_input)
                 result_dict = result_v1.dict()
+                provided_fields = result_v1.__fields_set__
                 result = result_v1
             else:
                 msg = (  # type: ignore[unreachable]
@@ -859,15 +861,19 @@
                 )
                 raise NotImplementedError(msg)
 
-            # Include fields from tool_input, plus fields with explicit 
defaults.
-            # This applies Pydantic defaults (like Field(default=1)) while 
excluding
-            # synthetic "args"/"kwargs" fields that Pydantic creates for 
*args/**kwargs.
+            # Include fields from tool_input, fields provided through Pydantic 
aliases,
+            # plus fields with explicit defaults. This applies Pydantic 
defaults (like
+            # Field(default=1)) while excluding synthetic "args"/"kwargs" 
fields that
+            # Pydantic creates for *args/**kwargs.
             field_info = get_fields(input_args)
             validated_input = {}
             for k in result_dict:
                 if k in tool_input:
                     # Field was provided in input - include it (validated)
                     validated_input[k] = getattr(result, k)
+                elif k in provided_fields:
+                    # Field was provided through a Pydantic alias - include it.
+                    validated_input[k] = getattr(result, k)
                 elif k in field_info and k not in {"args", "kwargs"}:
                     # Check if field has an explicit default defined in the 
schema.
                     # Exclude "args"/"kwargs" as these are synthetic fields 
for variadic
@@ -1238,7 +1244,7 @@
                         error_to_raise = ValueError(msg)
             else:
                 content = response
-        except ValidationError as e:
+        except (ValidationError, ValidationErrorV1) as e:
             if not self.handle_validation_error:
                 error_to_raise = e
             else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.5.4/langchain_core/tools/convert.py 
new/langchain_core-1.5.5/langchain_core/tools/convert.py
--- old/langchain_core-1.5.4/langchain_core/tools/convert.py    2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/langchain_core/tools/convert.py    2020-02-02 
01:00:00.000000000 +0100
@@ -329,16 +329,17 @@
                 )
             # If someone doesn't want a schema applied, we must treat it as
             # a simple string->string function
-            if dec_func.__doc__ is None:
+            tool_description = tool_description or dec_func.__doc__
+            if tool_description is None:
                 msg = (
-                    "Function must have a docstring if "
-                    "description not provided and infer_schema is False."
+                    "Function must have either a docstring or description "
+                    "when infer_schema is False."
                 )
                 raise ValueError(msg)
             return Tool(
                 name=tool_name,
                 func=func,
-                description=f"{tool_name} tool",
+                description=tool_description,
                 return_direct=return_direct,
                 coroutine=coroutine,
                 response_format=response_format,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.5.4/langchain_core/utils/_merge.py 
new/langchain_core-1.5.5/langchain_core/utils/_merge.py
--- old/langchain_core-1.5.4/langchain_core/utils/_merge.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/langchain_core/utils/_merge.py     2020-02-02 
01:00:00.000000000 +0100
@@ -68,6 +68,15 @@
                 merged[right_k] = merge_lists(merged[right_k], right_v)
             elif merged[right_k] == right_v:
                 continue
+            elif isinstance(merged[right_k], bool):
+                # `bool` is a subclass of `int`, so without this check 
differing
+                # booleans would fall into the int branch below and get summed,
+                # silently turning e.g. `True + False` into the int `1`.
+                msg = (
+                    f"Additional kwargs key {right_k} already exists in left 
dict and "
+                    f"value has unsupported type {type(merged[right_k])}."
+                )
+                raise TypeError(msg)
             elif isinstance(merged[right_k], int):
                 # Preserve identification and temporal fields using last-wins 
strategy
                 # instead of summing:
@@ -118,7 +127,8 @@
                         i
                         for i, e_left in enumerate(merged)
                         if (
-                            "index" in e_left
+                            isinstance(e_left, dict)
+                            and "index" in e_left
                             and e_left["index"] == e["index"]  # index matches
                             and (  # IDs not inconsistent
                                 e_left.get("id") in {None, ""}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.5.4/langchain_core/utils/aiter.py 
new/langchain_core-1.5.5/langchain_core/utils/aiter.py
--- old/langchain_core-1.5.4/langchain_core/utils/aiter.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/langchain_core/utils/aiter.py      2020-02-02 
01:00:00.000000000 +0100
@@ -323,25 +323,35 @@
 
 
 async def abatch_iterate(
-    size: int, iterable: AsyncIterable[T]
+    size: int | None, iterable: AsyncIterable[T]
 ) -> AsyncIterator[list[T]]:
     """Utility batching function for async iterables.
 
     Args:
         size: The size of the batch.
+
+            If `None`, returns a single batch.
         iterable: The async iterable to batch.
 
     Yields:
         The batches.
+
+    Raises:
+        ValueError: If `size` is not `None` and is not a positive integer.
     """
+    if size is None:
+        single_batch = [el async for el in iterable]
+        if single_batch:
+            yield single_batch
+        return
+    if size <= 0:
+        msg = f"Batch size must be a positive integer, got {size}."
+        raise ValueError(msg)
     batch: list[T] = []
     async for element in iterable:
-        if len(batch) < size:
-            batch.append(element)
-
+        batch.append(element)
         if len(batch) >= size:
             yield batch
             batch = []
-
     if batch:
         yield batch
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/langchain_core/utils/function_calling.py 
new/langchain_core-1.5.5/langchain_core/utils/function_calling.py
--- old/langchain_core-1.5.4/langchain_core/utils/function_calling.py   
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/langchain_core/utils/function_calling.py   
2020-02-02 01:00:00.000000000 +0100
@@ -713,6 +713,13 @@
     messages.append(
         AIMessage(content="", additional_kwargs={"tool_calls": 
openai_tool_calls})
     )
+    if tool_outputs is not None and len(tool_outputs) != 
len(openai_tool_calls):
+        msg = (
+            f"The number of tool_outputs ({len(tool_outputs)}) must match the 
number "
+            f"of tool_calls ({len(openai_tool_calls)}). Got 
{len(tool_outputs)} "
+            f"output(s) for {len(openai_tool_calls)} tool call(s)."
+        )
+        raise ValueError(msg)
     tool_outputs = tool_outputs or ["You have correctly called this tool."] * 
len(
         openai_tool_calls
     )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.5.4/langchain_core/utils/iter.py 
new/langchain_core-1.5.5/langchain_core/utils/iter.py
--- old/langchain_core-1.5.4/langchain_core/utils/iter.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/langchain_core/utils/iter.py       2020-02-02 
01:00:00.000000000 +0100
@@ -214,7 +214,13 @@
 
     Yields:
         The batches of the iterable.
+
+    Raises:
+        ValueError: If `size` is not `None` and is not a positive integer.
     """
+    if size is not None and size <= 0:
+        msg = f"Batch size must be a positive integer, got {size}."
+        raise ValueError(msg)
     it = iter(iterable)
     while True:
         chunk = list(islice(it, size))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.5.4/langchain_core/version.py 
new/langchain_core-1.5.5/langchain_core/version.py
--- old/langchain_core-1.5.4/langchain_core/version.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/langchain_core/version.py  2020-02-02 
01:00:00.000000000 +0100
@@ -1,3 +1,3 @@
 """Version information for `langchain-core`."""
 
-VERSION = "1.5.4"
+VERSION = "1.5.5"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.5.4/pyproject.toml 
new/langchain_core-1.5.5/pyproject.toml
--- old/langchain_core-1.5.4/pyproject.toml     2020-02-02 01:00:00.000000000 
+0100
+++ new/langchain_core-1.5.5/pyproject.toml     2020-02-02 01:00:00.000000000 
+0100
@@ -21,10 +21,11 @@
     "Topic :: Software Development :: Libraries :: Python Modules",
 ]
 
-version = "1.5.4"
+version = "1.5.5"
 requires-python = ">=3.10.0,<4.0.0"
 dependencies = [
     "langsmith>=0.3.45,<1.0.0",
+    "httpx>=0.23.0,<1.0.0",
     "tenacity!=8.4.0,>=8.1.0,<10.0.0",
     "jsonpatch>=1.33.0,<2.0.0",
     "PyYAML>=5.3.0,<7.0.0",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/callbacks/test_usage_callback.py 
new/langchain_core-1.5.5/tests/unit_tests/callbacks/test_usage_callback.py
--- old/langchain_core-1.5.4/tests/unit_tests/callbacks/test_usage_callback.py  
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/tests/unit_tests/callbacks/test_usage_callback.py  
2020-02-02 01:00:00.000000000 +0100
@@ -1,3 +1,4 @@
+import contextlib
 from typing import Any
 
 from langchain_core.callbacks import (
@@ -120,3 +121,15 @@
     callback = UsageMetadataCallbackHandler()
     _ = await llm.abatch(["Message 1", "Message 2"], config={"callbacks": 
[callback]})
     assert callback.usage_metadata == {"test_model": total_1_2}
+
+
+def test_usage_callback_clears_on_exception() -> None:
+    """Callback must stop tracking after with-block exits via exception 
(#38989)."""
+    llm = FakeChatModelWithResponseMetadata(messages=iter(messages), 
model_name="fake")
+    with contextlib.suppress(RuntimeError), get_usage_metadata_callback() as 
cb:
+        _ = llm.invoke("in block")
+        raise RuntimeError
+
+    # Calls after the block must not accumulate into the previous callback.
+    _ = llm.invoke("outside block")
+    assert cb.usage_metadata == {"fake": usage1}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/language_models/chat_models/test_cache.py
 
new/langchain_core-1.5.5/tests/unit_tests/language_models/chat_models/test_cache.py
--- 
old/langchain_core-1.5.4/tests/unit_tests/language_models/chat_models/test_cache.py
 2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_core-1.5.5/tests/unit_tests/language_models/chat_models/test_cache.py
 2020-02-02 01:00:00.000000000 +0100
@@ -38,6 +38,10 @@
         """Clear cache."""
         self._cache = {}
 
+    def __len__(self) -> int:
+        """Return the number of cached entries."""
+        return len(self._cache)
+
 
 def test_local_cache_sync() -> None:
     """Test that the local cache is being populated but not the global one."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/language_models/llms/test_cache.py 
new/langchain_core-1.5.5/tests/unit_tests/language_models/llms/test_cache.py
--- 
old/langchain_core-1.5.4/tests/unit_tests/language_models/llms/test_cache.py    
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_core-1.5.5/tests/unit_tests/language_models/llms/test_cache.py    
    2020-02-02 01:00:00.000000000 +0100
@@ -27,6 +27,10 @@
         """Clear cache."""
         self._cache = {}
 
+    def __len__(self) -> int:
+        """Return the number of cached entries."""
+        return len(self._cache)
+
 
 async def test_local_cache_generate_async() -> None:
     global_cache = InMemoryCache()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/messages/block_translators/test_anthropic.py
 
new/langchain_core-1.5.5/tests/unit_tests/messages/block_translators/test_anthropic.py
--- 
old/langchain_core-1.5.4/tests/unit_tests/messages/block_translators/test_anthropic.py
      2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_core-1.5.5/tests/unit_tests/messages/block_translators/test_anthropic.py
      2020-02-02 01:00:00.000000000 +0100
@@ -1,3 +1,5 @@
+from typing import Any
+
 from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
 from langchain_core.messages import content as types
 
@@ -507,3 +509,66 @@
     ]
 
     assert message.content_blocks == expected
+
+
+def test_convert_to_v1_from_anthropic_input_malformed_sources() -> None:
+    content: list[str | dict[Any, Any]] = [
+        {"type": "document", "source": {"type": "base64", "media_type": 
"app/pdf"}},
+        {"type": "document", "source": {"type": "url"}},
+        {"type": "document", "source": {"type": "file"}},
+        {"type": "document", "source": {"type": "text"}},
+        {"type": "image", "source": {"type": "base64", "media_type": 
"image/jpeg"}},
+        {"type": "image", "source": {"type": "url"}},
+        {"type": "image", "source": {"type": "file"}},
+    ]
+    message = HumanMessage(content)
+
+    assert message.content_blocks == [
+        *[{"type": "non_standard", "value": block} for block in content[:4]],
+        *content[4:],
+    ]
+
+
+def test_convert_to_v1_from_anthropic_malformed_citations() -> None:
+    message = AIMessage(
+        [
+            {
+                "type": "text",
+                "text": "Source-backed answer.",
+                "citations": [
+                    {
+                        "type": "web_search_result_location",
+                        "cited_text": "Source text",
+                    },
+                    {
+                        "type": "search_result_location",
+                        "title": "Document Title",
+                    },
+                ],
+            },
+        ],
+        response_metadata={"model_provider": "anthropic"},
+    )
+
+    assert message.content_blocks == [
+        {
+            "type": "text",
+            "text": "Source-backed answer.",
+            "annotations": [
+                {
+                    "type": "non_standard_annotation",
+                    "value": {
+                        "type": "web_search_result_location",
+                        "cited_text": "Source text",
+                    },
+                },
+                {
+                    "type": "non_standard_annotation",
+                    "value": {
+                        "type": "search_result_location",
+                        "title": "Document Title",
+                    },
+                },
+            ],
+        },
+    ]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/prompts/test_dict.py 
new/langchain_core-1.5.5/tests/unit_tests/prompts/test_dict.py
--- old/langchain_core-1.5.4/tests/unit_tests/prompts/test_dict.py      
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/tests/unit_tests/prompts/test_dict.py      
2020-02-02 01:00:00.000000000 +0100
@@ -116,3 +116,46 @@
         ValueError, match="Variable names cannot contain attribute access"
     ):
         PromptTemplate.from_template("{name.__class__}", 
template_format="f-string")
+
+
+def test_dict_prompt_template_preserves_non_str_items_in_list() -> None:
+    """Non-str/non-dict items inside lists must be preserved, not silently 
dropped.
+
+    Regression test for #39152: int, float, bool, None and other scalars nested
+    inside a list value were silently discarded by _insert_input_variables.
+    """
+    template = {
+        "type": "tool_use",
+        "id": "call_1",
+        "name": "search",
+        "input": {
+            "query": "{q}",
+            "top_k_scores": [1, 2, 3],
+            "flags": [True, None],
+        },
+    }
+    prompt = DictPromptTemplate(template=template, template_format="f-string")
+    result = prompt.format(q="cats")
+
+    assert result["input"]["query"] == "cats"
+    assert result["input"]["top_k_scores"] == [1, 2, 3]
+    assert result["input"]["flags"] == [True, None]
+
+
+def test_dict_prompt_template_preserves_mixed_list() -> None:
+    """Mixed-type lists must survive formatting unchanged (except str 
interpolation)."""
+    template = {
+        "type": "x",
+        "mixed": ["a {v}", 1, 2.5, {"k": "val"}, None, True],
+    }
+    prompt = DictPromptTemplate(template=template, template_format="f-string")
+    result = prompt.format(v="b")
+
+    assert result["mixed"] == ["a b", 1, 2.5, {"k": "val"}, None, True]
+
+
+def test_dict_prompt_template_preserves_list_with_no_variables() -> None:
+    """Items must survive even when the template has no variables at all."""
+    template = {"nums": [1, 2]}
+    prompt = DictPromptTemplate(template=template, template_format="mustache")
+    assert prompt.format() == {"nums": [1, 2]}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/runnables/__snapshots__/test_fallbacks.ambr
 
new/langchain_core-1.5.5/tests/unit_tests/runnables/__snapshots__/test_fallbacks.ambr
--- 
old/langchain_core-1.5.4/tests/unit_tests/runnables/__snapshots__/test_fallbacks.ambr
       2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_core-1.5.5/tests/unit_tests/runnables/__snapshots__/test_fallbacks.ambr
       2020-02-02 01:00:00.000000000 +0100
@@ -84,7 +84,7 @@
                   "fake",
                   "FakeListLLM"
                 ],
-                "repr": "FakeListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['foo'], i=1)",
+                "repr": "FakeListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['foo'], i=1)",
                 "name": "FakeListLLM"
               }
             },
@@ -128,7 +128,7 @@
                     "fake",
                     "FakeListLLM"
                   ],
-                  "repr": "FakeListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['bar'])",
+                  "repr": "FakeListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['bar'])",
                   "name": "FakeListLLM"
                 }
               },
@@ -268,7 +268,7 @@
           "fake",
           "FakeListLLM"
         ],
-        "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.4'}}, responses=['foo'], i=1)",
+        "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.5'}}, responses=['foo'], i=1)",
         "name": "FakeListLLM"
       },
       "fallbacks": [
@@ -281,7 +281,7 @@
             "fake",
             "FakeListLLM"
           ],
-          "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.4'}}, responses=['bar'])",
+          "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.5'}}, responses=['bar'])",
           "name": "FakeListLLM"
         }
       ],
@@ -322,7 +322,7 @@
           "fake",
           "FakeListLLM"
         ],
-        "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.4'}}, responses=['foo'], i=1)",
+        "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.5'}}, responses=['foo'], i=1)",
         "name": "FakeListLLM"
       },
       "fallbacks": [
@@ -335,7 +335,7 @@
             "fake",
             "FakeListLLM"
           ],
-          "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.4'}}, responses=['baz'], i=1)",
+          "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.5'}}, responses=['baz'], i=1)",
           "name": "FakeListLLM"
         },
         {
@@ -347,7 +347,7 @@
             "fake",
             "FakeListLLM"
           ],
-          "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.4'}}, responses=['bar'])",
+          "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.5'}}, responses=['bar'])",
           "name": "FakeListLLM"
         }
       ],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/runnables/__snapshots__/test_runnable.ambr
 
new/langchain_core-1.5.5/tests/unit_tests/runnables/__snapshots__/test_runnable.ambr
--- 
old/langchain_core-1.5.4/tests/unit_tests/runnables/__snapshots__/test_runnable.ambr
        2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_core-1.5.5/tests/unit_tests/runnables/__snapshots__/test_runnable.ambr
        2020-02-02 01:00:00.000000000 +0100
@@ -97,7 +97,7 @@
             "fake_chat_models",
             "FakeListChatModel"
           ],
-          "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['foo, bar'])",
+          "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['foo, bar'])",
           "name": "FakeListChatModel"
         }
       ],
@@ -227,7 +227,7 @@
             "fake_chat_models",
             "FakeListChatModel"
           ],
-          "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['baz, qux'])",
+          "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['baz, qux'])",
           "name": "FakeListChatModel"
         }
       ],
@@ -346,7 +346,7 @@
             "fake_chat_models",
             "FakeListChatModel"
           ],
-          "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['foo, bar'])",
+          "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['foo, bar'])",
           "name": "FakeListChatModel"
         },
         {
@@ -457,7 +457,7 @@
             "fake_chat_models",
             "FakeListChatModel"
           ],
-          "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['baz, qux'])",
+          "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['baz, qux'])",
           "name": "FakeListChatModel"
         }
       ],
@@ -848,7 +848,7 @@
             "fake",
             "FakeStreamingListLLM"
           ],
-          "repr": "FakeStreamingListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['first item, second item, third 
item'])",
+          "repr": "FakeStreamingListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['first item, second item, third 
item'])",
           "name": "FakeStreamingListLLM"
         },
         {
@@ -884,7 +884,7 @@
               "fake",
               "FakeStreamingListLLM"
             ],
-            "repr": "FakeStreamingListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['this', 'is', 'a', 'test'])",
+            "repr": "FakeStreamingListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['this', 'is', 'a', 'test'])",
             "name": "FakeStreamingListLLM"
           }
         },
@@ -1009,7 +1009,7 @@
 # name: test_prompt_with_chat_model
   '''
   ChatPromptTemplate(input_variables=['question'], input_types={}, 
partial_variables={}, 
messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], 
input_types={}, partial_variables={}, template='You are a nice assistant.'), 
additional_kwargs={}), 
HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['question'], 
input_types={}, partial_variables={}, template='{question}'), 
additional_kwargs={})])
-  | FakeListChatModel(metadata={'lc_versions': {'langchain-core': '1.5.4'}}, 
responses=['foo'])
+  | FakeListChatModel(metadata={'lc_versions': {'langchain-core': '1.5.5'}}, 
responses=['foo'])
   '''
 # ---
 # name: test_prompt_with_chat_model.1
@@ -1109,7 +1109,7 @@
           "fake_chat_models",
           "FakeListChatModel"
         ],
-        "repr": "FakeListChatModel(metadata={'lc_versions': {'langchain-core': 
'1.5.4'}}, responses=['foo'])",
+        "repr": "FakeListChatModel(metadata={'lc_versions': {'langchain-core': 
'1.5.5'}}, responses=['foo'])",
         "name": "FakeListChatModel"
       }
     },
@@ -1220,7 +1220,7 @@
             "fake_chat_models",
             "FakeListChatModel"
           ],
-          "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['foo, bar'])",
+          "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['foo, bar'])",
           "name": "FakeListChatModel"
         }
       ],
@@ -1249,7 +1249,7 @@
 # name: test_prompt_with_chat_model_async
   '''
   ChatPromptTemplate(input_variables=['question'], input_types={}, 
partial_variables={}, 
messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], 
input_types={}, partial_variables={}, template='You are a nice assistant.'), 
additional_kwargs={}), 
HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['question'], 
input_types={}, partial_variables={}, template='{question}'), 
additional_kwargs={})])
-  | FakeListChatModel(metadata={'lc_versions': {'langchain-core': '1.5.4'}}, 
responses=['foo'])
+  | FakeListChatModel(metadata={'lc_versions': {'langchain-core': '1.5.5'}}, 
responses=['foo'])
   '''
 # ---
 # name: test_prompt_with_chat_model_async.1
@@ -1349,7 +1349,7 @@
           "fake_chat_models",
           "FakeListChatModel"
         ],
-        "repr": "FakeListChatModel(metadata={'lc_versions': {'langchain-core': 
'1.5.4'}}, responses=['foo'])",
+        "repr": "FakeListChatModel(metadata={'lc_versions': {'langchain-core': 
'1.5.5'}}, responses=['foo'])",
         "name": "FakeListChatModel"
       }
     },
@@ -1459,7 +1459,7 @@
           "fake",
           "FakeListLLM"
         ],
-        "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.4'}}, responses=['foo', 'bar'])",
+        "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.5'}}, responses=['foo', 'bar'])",
         "name": "FakeListLLM"
       }
     },
@@ -1576,7 +1576,7 @@
             "fake",
             "FakeListLLM"
           ],
-          "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.4'}}, responses=['foo', 'bar'])",
+          "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.5'}}, responses=['foo', 'bar'])",
           "name": "FakeListLLM"
         }
       ],
@@ -1699,7 +1699,7 @@
             "fake",
             "FakeStreamingListLLM"
           ],
-          "repr": "FakeStreamingListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['bear, dog, cat', 'tomato, lettuce, 
onion'])",
+          "repr": "FakeStreamingListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['bear, dog, cat', 'tomato, lettuce, 
onion'])",
           "name": "FakeStreamingListLLM"
         }
       ],
@@ -1867,7 +1867,7 @@
                     "fake",
                     "FakeListLLM"
                   ],
-                  "repr": "FakeListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['4'])",
+                  "repr": "FakeListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['4'])",
                   "name": "FakeListLLM"
                 }
               },
@@ -1940,7 +1940,7 @@
                     "fake",
                     "FakeListLLM"
                   ],
-                  "repr": "FakeListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['2'])",
+                  "repr": "FakeListLLM(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['2'])",
                   "name": "FakeListLLM"
                 }
               },
@@ -13407,7 +13407,7 @@
     just_to_test_lambda: RunnableLambda(...)
   }
   | ChatPromptTemplate(input_variables=['documents', 'question'], 
input_types={}, partial_variables={}, 
messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], 
input_types={}, partial_variables={}, template='You are a nice assistant.'), 
additional_kwargs={}), 
HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['documents', 
'question'], input_types={}, partial_variables={}, 
template='Context:\n{documents}\n\nQuestion:\n{question}'), 
additional_kwargs={})])
-  | FakeListChatModel(metadata={'lc_versions': {'langchain-core': '1.5.4'}}, 
responses=['foo, bar'])
+  | FakeListChatModel(metadata={'lc_versions': {'langchain-core': '1.5.5'}}, 
responses=['foo, bar'])
   | CommaSeparatedListOutputParser()
   '''
 # ---
@@ -13610,7 +13610,7 @@
             "fake_chat_models",
             "FakeListChatModel"
           ],
-          "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=['foo, bar'])",
+          "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=['foo, bar'])",
           "name": "FakeListChatModel"
         }
       ],
@@ -13636,8 +13636,8 @@
   ChatPromptTemplate(input_variables=['question'], input_types={}, 
partial_variables={}, 
messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], 
input_types={}, partial_variables={}, template='You are a nice assistant.'), 
additional_kwargs={}), 
HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['question'], 
input_types={}, partial_variables={}, template='{question}'), 
additional_kwargs={})])
   | RunnableLambda(...)
   | {
-      chat: FakeListChatModel(metadata={'lc_versions': {'langchain-core': 
'1.5.4'}}, responses=["i'm a chatbot"]),
-      llm: FakeListLLM(metadata={'lc_versions': {'langchain-core': '1.5.4'}}, 
responses=["i'm a textbot"])
+      chat: FakeListChatModel(metadata={'lc_versions': {'langchain-core': 
'1.5.5'}}, responses=["i'm a chatbot"]),
+      llm: FakeListLLM(metadata={'lc_versions': {'langchain-core': '1.5.5'}}, 
responses=["i'm a textbot"])
     }
   '''
 # ---
@@ -13762,7 +13762,7 @@
                 "fake_chat_models",
                 "FakeListChatModel"
               ],
-              "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=[\"i'm a chatbot\"])",
+              "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=[\"i'm a chatbot\"])",
               "name": "FakeListChatModel"
             },
             "llm": {
@@ -13774,7 +13774,7 @@
                 "fake",
                 "FakeListLLM"
               ],
-              "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.4'}}, responses=[\"i'm a textbot\"])",
+              "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.5'}}, responses=[\"i'm a textbot\"])",
               "name": "FakeListLLM"
             }
           }
@@ -13917,7 +13917,7 @@
                     "fake_chat_models",
                     "FakeListChatModel"
                   ],
-                  "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.4'}}, responses=[\"i'm a chatbot\"])",
+                  "repr": "FakeListChatModel(metadata={'lc_versions': 
{'langchain-core': '1.5.5'}}, responses=[\"i'm a chatbot\"])",
                   "name": "FakeListChatModel"
                 },
                 "kwargs": {
@@ -13938,7 +13938,7 @@
                 "fake",
                 "FakeListLLM"
               ],
-              "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.4'}}, responses=[\"i'm a textbot\"])",
+              "repr": "FakeListLLM(metadata={'lc_versions': {'langchain-core': 
'1.5.5'}}, responses=[\"i'm a textbot\"])",
               "name": "FakeListLLM"
             },
             "passthrough": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/runnables/test_utils.py 
new/langchain_core-1.5.5/tests/unit_tests/runnables/test_utils.py
--- old/langchain_core-1.5.4/tests/unit_tests/runnables/test_utils.py   
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/tests/unit_tests/runnables/test_utils.py   
2020-02-02 01:00:00.000000000 +0100
@@ -5,6 +5,7 @@
 
 from langchain_core.runnables.base import RunnableLambda
 from langchain_core.runnables.utils import (
+    AddableDict,
     get_function_nonlocals,
     get_lambda_source,
     indent_lines_after_first,
@@ -73,3 +74,29 @@
     assert RunnableLambda(my_func3).deps == [agent]
     assert RunnableLambda(my_func4).deps == [global_agent]
     assert RunnableLambda(func).deps == [nl]
+
+
+def test_addable_dict_add_incompatible_types_raises() -> None:
+    left = AddableDict({"count": 1})
+    right = AddableDict({"count": "some_string"})
+    with pytest.raises(
+        TypeError,
+        match=r"Cannot add incompatible types for key 'count': 'int' and 
'str'\.",
+    ):
+        left + right
+
+
+def test_addable_dict_radd_incompatible_types_raises() -> None:
+    left = AddableDict({"count": 1})
+    right = AddableDict({"count": "some_string"})
+    with pytest.raises(
+        TypeError,
+        match=r"Cannot add incompatible types for key 'count': 'int' and 
'str'\.",
+    ):
+        right.__radd__(left)
+
+
+def test_addable_dict_add_none_seeded_key_is_unaffected() -> None:
+    left = AddableDict({"data": None})
+    right = AddableDict({"data": {"a": 1}})
+    assert (left + right) == AddableDict({"data": {"a": 1}})
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/test_messages.py 
new/langchain_core-1.5.5/tests/unit_tests/test_messages.py
--- old/langchain_core-1.5.4/tests/unit_tests/test_messages.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/tests/unit_tests/test_messages.py  2020-02-02 
01:00:00.000000000 +0100
@@ -225,6 +225,14 @@
     assert (default_id_chunk + provider_chunk).id == meaningful_id
 
 
+def test_message_chunks_bool_additional_kwargs_raises() -> None:
+    """Differing booleans (e.g. `refusal`) must not silently coerce to 
`int`."""
+    a = AIMessageChunk(content="", additional_kwargs={"refusal": True})
+    b = AIMessageChunk(content="", additional_kwargs={"refusal": False})
+    with pytest.raises(TypeError, match="unsupported type"):
+        a + b
+
+
 def test_chat_message_chunks() -> None:
     assert ChatMessageChunk(role="User", content="I am", id="ai4") + 
ChatMessageChunk(
         role="User", content=" indeed."
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.5.4/tests/unit_tests/test_tools.py 
new/langchain_core-1.5.5/tests/unit_tests/test_tools.py
--- old/langchain_core-1.5.4/tests/unit_tests/test_tools.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/tests/unit_tests/test_tools.py     2020-02-02 
01:00:00.000000000 +0100
@@ -23,7 +23,14 @@
 )
 
 import pytest
-from pydantic import BaseModel, ConfigDict, Field, RootModel, ValidationError
+from pydantic import (
+    AliasChoices,
+    BaseModel,
+    ConfigDict,
+    Field,
+    RootModel,
+    ValidationError,
+)
 from pydantic.v1 import BaseModel as BaseModelV1
 from pydantic.v1 import ValidationError as ValidationErrorV1
 from typing_extensions import TypedDict, override
@@ -365,9 +372,34 @@
 
     assert isinstance(unstructured_tool_input, BaseTool)
     assert unstructured_tool_input.args_schema is None
+    assert unstructured_tool_input.description == "Return the arguments 
directly."
     assert unstructured_tool_input.run("foo") == "foo"
 
 
+def test_simple_tool_decorator_no_infer_schema_uses_explicit_description() -> 
None:
+    """Test that a simple tool preserves an explicit description."""
+
+    @tool(infer_schema=False, description="Echo the supplied input.")
+    def echo(tool_input: str) -> str:
+        return tool_input
+
+    assert echo.description == "Echo the supplied input."
+
+
+def 
test_simple_tool_decorator_no_infer_schema_requires_description_or_docstring() 
-> (
+    None
+):
+    """Test that a simple tool requires an authored description."""
+    with pytest.raises(
+        ValueError,
+        match="Function must have either a docstring or description",
+    ):
+
+        @tool(infer_schema=False)
+        def echo(tool_input: str) -> str:
+            return tool_input
+
+
 def test_structured_tool_types_parsed() -> None:
     """Test the non-primitive types are correctly passed to structured 
tools."""
 
@@ -1123,6 +1155,30 @@
     assert expected == actual
 
 
[email protected](
+    sys.version_info >= (3, 14),
+    reason="pydantic.v1 namespace not supported with Python 3.14+",
+)
+async def test_async_validation_error_handling_pydantic_v1_schema() -> None:
+    """Test async validation error handling for Pydantic V1 schemas."""
+
+    class Args(BaseModelV1):
+        x: int
+
+    def foo(x: int) -> str:
+        """Return x as text."""
+        return str(x)
+
+    tool_ = StructuredTool.from_function(
+        foo,
+        args_schema=cast("ArgsSchema", Args),
+        handle_validation_error=True,
+    )
+
+    assert tool_.run({"x": "not-an-integer"}) == "Tool input validation error"
+    assert await tool_.arun({"x": "not-an-integer"}) == "Tool input validation 
error"
+
+
 @pytest.mark.parametrize(
     "handler",
     [
@@ -3975,6 +4031,22 @@
     assert handler.captured_tool_call_ids[0] == "run_method_tool_call_id"
 
 
+def test_tool_args_schema_required_field_validation_alias() -> None:
+    """Test required args provided through validation aliases reach the 
tool."""
+
+    class Args(BaseModel):
+        """Tool arguments."""
+
+        canonical: str = Field(validation_alias=AliasChoices("canonical", 
"alias"))
+
+    @tool(args_schema=Args)
+    def aliased_tool(canonical: str) -> str:
+        """Return the canonical argument."""
+        return canonical
+
+    assert aliased_tool.invoke({"alias": "value"}) == "value"
+
+
 def test_tool_args_schema_default_values() -> None:
     """Test that Pydantic default values from `args_schema` are applied.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/utils/test_aiter.py 
new/langchain_core-1.5.5/tests/unit_tests/utils/test_aiter.py
--- old/langchain_core-1.5.4/tests/unit_tests/utils/test_aiter.py       
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/tests/unit_tests/utils/test_aiter.py       
2020-02-02 01:00:00.000000000 +0100
@@ -12,10 +12,14 @@
         (3, [10, 20, 30, 40, 50], [[10, 20, 30], [40, 50]]),
         (1, [100, 200, 300], [[100], [200], [300]]),
         (4, [], []),
+        (None, [1, 2, 3], [[1, 2, 3]]),
+        (None, [], []),
     ],
 )
 async def test_abatch_iterate(
-    input_size: int, input_iterable: list[str], expected_output: 
list[list[str]]
+    input_size: int | None,
+    input_iterable: list[str],
+    expected_output: list[list[str]],
 ) -> None:
     """Test batching function."""
 
@@ -29,3 +33,15 @@
 
     output = [el async for el in iterator_]
     assert output == expected_output
+
+
[email protected]("input_size", [0, -1])
+async def test_abatch_iterate_invalid_size(input_size: int) -> None:
+    """Non-positive sizes should raise instead of silently discarding data."""
+
+    async def _to_async_iterable(iterable: list[int]) -> AsyncIterator[int]:
+        for item in iterable:
+            yield item
+
+    with pytest.raises(ValueError, match="positive integer"):
+        _ = [el async for el in abatch_iterate(input_size, 
_to_async_iterable([1, 2]))]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/utils/test_function_calling.py 
new/langchain_core-1.5.5/tests/unit_tests/utils/test_function_calling.py
--- old/langchain_core-1.5.4/tests/unit_tests/utils/test_function_calling.py    
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/tests/unit_tests/utils/test_function_calling.py    
2020-02-02 01:00:00.000000000 +0100
@@ -795,6 +795,24 @@
     assert not response.tool_calls
 
 
+def test_tool_outputs_fewer_than_tool_calls_raises() -> None:
+    with pytest.raises(ValueError, match="must match"):
+        tool_example_to_messages(
+            input="Extract both values",
+            tool_calls=[FakeCall(data="a"), FakeCall(data="b")],
+            tool_outputs=["only one output"],
+        )
+
+
+def test_tool_outputs_more_than_tool_calls_raises() -> None:
+    with pytest.raises(ValueError, match="must match"):
+        tool_example_to_messages(
+            input="Extract one value",
+            tool_calls=[FakeCall(data="a")],
+            tool_outputs=["output1", "extra output"],
+        )
+
+
 @pytest.mark.parametrize(
     "typed_dict",
     [ExtensionsTypedDict, TypingTypedDict],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/utils/test_iter.py 
new/langchain_core-1.5.5/tests/unit_tests/utils/test_iter.py
--- old/langchain_core-1.5.4/tests/unit_tests/utils/test_iter.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/tests/unit_tests/utils/test_iter.py        
2020-02-02 01:00:00.000000000 +0100
@@ -10,10 +10,21 @@
         (3, [10, 20, 30, 40, 50], [[10, 20, 30], [40, 50]]),
         (1, [100, 200, 300], [[100], [200], [300]]),
         (4, [], []),
+        (None, [1, 2, 3], [[1, 2, 3]]),
+        (None, [], []),
     ],
 )
 def test_batch_iterate(
-    input_size: int, input_iterable: list[str], expected_output: 
list[list[str]]
+    input_size: int | None,
+    input_iterable: list[str],
+    expected_output: list[list[str]],
 ) -> None:
     """Test batching function."""
     assert list(batch_iterate(input_size, input_iterable)) == expected_output
+
+
[email protected]("input_size", [0, -1])
+def test_batch_iterate_invalid_size(input_size: int) -> None:
+    """Non-positive sizes should raise instead of silently discarding data."""
+    with pytest.raises(ValueError, match="positive integer"):
+        list(batch_iterate(input_size, [1, 2, 3]))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_core-1.5.4/tests/unit_tests/utils/test_utils.py 
new/langchain_core-1.5.5/tests/unit_tests/utils/test_utils.py
--- old/langchain_core-1.5.4/tests/unit_tests/utils/test_utils.py       
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/tests/unit_tests/utils/test_utils.py       
2020-02-02 01:00:00.000000000 +0100
@@ -129,6 +129,18 @@
         # Other integer fields should still be summed (e.g., token counts)
         ({"tokens": 10}, {"tokens": 5}, {"tokens": 15}),
         ({"count": 1}, {"count": 2}, {"count": 3}),
+        # Differing booleans must not silently coerce to `int` (e.g. `True + 
False`).
+        (
+            {"a": True},
+            {"a": False},
+            pytest.raises(
+                TypeError,
+                match=(
+                    "Additional kwargs key a already exists in left dict and 
value "
+                    r"has unsupported type .+bool.+."
+                ),
+            ),
+        ),
     ],
 )
 def test_merge_dicts(
@@ -437,6 +449,16 @@
             [{"no_index": "b"}],
             [{"no_index": "a"}, {"no_index": "b"}],
         ),
+        # A string element whose text happens to contain the literal substring
+        # "index" must not be treated as index-keyed (it isn't a dict).
+        (
+            ["the index is here", {"index": 0, "reference_ids": ["a"]}],
+            [{"index": 0, "reference_ids": ["b"]}],
+            [
+                "the index is here",
+                {"index": 0, "reference_ids": ["a", "b"]},
+            ],
+        ),
     ],
 )
 def test_merge_lists(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_core-1.5.4/uv.lock 
new/langchain_core-1.5.5/uv.lock
--- old/langchain_core-1.5.4/uv.lock    2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_core-1.5.5/uv.lock    2020-02-02 01:00:00.000000000 +0100
@@ -1040,9 +1040,10 @@
 
 [[package]]
 name = "langchain-core"
-version = "1.5.4"
+version = "1.5.5"
 source = { editable = "." }
 dependencies = [
+    { name = "httpx" },
     { name = "jsonpatch" },
     { name = "langchain-protocol" },
     { name = "langsmith" },
@@ -1090,6 +1091,7 @@
 
 [package.metadata]
 requires-dist = [
+    { name = "httpx", specifier = ">=0.23.0,<1.0.0" },
     { name = "jsonpatch", specifier = ">=1.33.0,<2.0.0" },
     { name = "langchain-protocol", specifier = ">=0.0.17" },
     { name = "langsmith", specifier = ">=0.3.45,<1.0.0" },

Reply via email to