langchain 1.1.0__tar.gz → 1.1.3__tar.gz

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.
Files changed (134) hide show
  1. {langchain-1.1.0 → langchain-1.1.3}/PKG-INFO +2 -2
  2. {langchain-1.1.0 → langchain-1.1.3}/langchain/__init__.py +1 -1
  3. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/factory.py +4 -0
  4. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/context_editing.py +1 -1
  5. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/human_in_the_loop.py +9 -7
  6. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/summarization.py +132 -78
  7. {langchain-1.1.0 → langchain-1.1.3}/langchain/chat_models/base.py +10 -0
  8. {langchain-1.1.0 → langchain-1.1.3}/pyproject.toml +2 -2
  9. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_summarization.py +124 -132
  10. langchain-1.1.3/tests/unit_tests/agents/test_agent_name.py +99 -0
  11. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/chat_models/test_chat_models.py +1 -0
  12. {langchain-1.1.0 → langchain-1.1.3}/uv.lock +37 -6
  13. {langchain-1.1.0 → langchain-1.1.3}/.gitignore +0 -0
  14. {langchain-1.1.0 → langchain-1.1.3}/LICENSE +0 -0
  15. {langchain-1.1.0 → langchain-1.1.3}/Makefile +0 -0
  16. {langchain-1.1.0 → langchain-1.1.3}/README.md +0 -0
  17. {langchain-1.1.0 → langchain-1.1.3}/extended_testing_deps.txt +0 -0
  18. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/__init__.py +0 -0
  19. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/__init__.py +0 -0
  20. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/_execution.py +0 -0
  21. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/_redaction.py +0 -0
  22. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/_retry.py +0 -0
  23. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/file_search.py +0 -0
  24. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/model_call_limit.py +0 -0
  25. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/model_fallback.py +0 -0
  26. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/model_retry.py +0 -0
  27. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/pii.py +0 -0
  28. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/shell_tool.py +0 -0
  29. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/todo.py +0 -0
  30. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/tool_call_limit.py +0 -0
  31. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/tool_emulator.py +0 -0
  32. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/tool_retry.py +0 -0
  33. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/tool_selection.py +0 -0
  34. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/middleware/types.py +0 -0
  35. {langchain-1.1.0 → langchain-1.1.3}/langchain/agents/structured_output.py +0 -0
  36. {langchain-1.1.0 → langchain-1.1.3}/langchain/chat_models/__init__.py +0 -0
  37. {langchain-1.1.0 → langchain-1.1.3}/langchain/embeddings/__init__.py +0 -0
  38. {langchain-1.1.0 → langchain-1.1.3}/langchain/embeddings/base.py +0 -0
  39. {langchain-1.1.0 → langchain-1.1.3}/langchain/messages/__init__.py +0 -0
  40. {langchain-1.1.0 → langchain-1.1.3}/langchain/py.typed +0 -0
  41. {langchain-1.1.0 → langchain-1.1.3}/langchain/rate_limiters/__init__.py +0 -0
  42. {langchain-1.1.0 → langchain-1.1.3}/langchain/tools/__init__.py +0 -0
  43. {langchain-1.1.0 → langchain-1.1.3}/langchain/tools/tool_node.py +0 -0
  44. {langchain-1.1.0 → langchain-1.1.3}/scripts/check_imports.py +0 -0
  45. {langchain-1.1.0 → langchain-1.1.3}/tests/__init__.py +0 -0
  46. {langchain-1.1.0 → langchain-1.1.3}/tests/cassettes/test_inference_to_native_output[False].yaml.gz +0 -0
  47. {langchain-1.1.0 → langchain-1.1.3}/tests/cassettes/test_inference_to_native_output[True].yaml.gz +0 -0
  48. {langchain-1.1.0 → langchain-1.1.3}/tests/cassettes/test_inference_to_tool_output[False].yaml.gz +0 -0
  49. {langchain-1.1.0 → langchain-1.1.3}/tests/cassettes/test_inference_to_tool_output[True].yaml.gz +0 -0
  50. {langchain-1.1.0 → langchain-1.1.3}/tests/integration_tests/__init__.py +0 -0
  51. {langchain-1.1.0 → langchain-1.1.3}/tests/integration_tests/agents/__init__.py +0 -0
  52. {langchain-1.1.0 → langchain-1.1.3}/tests/integration_tests/agents/middleware/__init__.py +0 -0
  53. {langchain-1.1.0 → langchain-1.1.3}/tests/integration_tests/agents/middleware/test_shell_tool_integration.py +0 -0
  54. {langchain-1.1.0 → langchain-1.1.3}/tests/integration_tests/cache/__init__.py +0 -0
  55. {langchain-1.1.0 → langchain-1.1.3}/tests/integration_tests/cache/fake_embeddings.py +0 -0
  56. {langchain-1.1.0 → langchain-1.1.3}/tests/integration_tests/chat_models/__init__.py +0 -0
  57. {langchain-1.1.0 → langchain-1.1.3}/tests/integration_tests/chat_models/test_base.py +0 -0
  58. {langchain-1.1.0 → langchain-1.1.3}/tests/integration_tests/conftest.py +0 -0
  59. {langchain-1.1.0 → langchain-1.1.3}/tests/integration_tests/embeddings/__init__.py +0 -0
  60. {langchain-1.1.0 → langchain-1.1.3}/tests/integration_tests/embeddings/test_base.py +0 -0
  61. {langchain-1.1.0 → langchain-1.1.3}/tests/integration_tests/test_compile.py +0 -0
  62. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/__init__.py +0 -0
  63. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/__init__.py +0 -0
  64. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/__snapshots__/test_middleware_agent.ambr +0 -0
  65. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/__snapshots__/test_middleware_decorators.ambr +0 -0
  66. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/__snapshots__/test_middleware_framework.ambr +0 -0
  67. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/__snapshots__/test_return_direct_graph.ambr +0 -0
  68. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/any_str.py +0 -0
  69. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/compose-postgres.yml +0 -0
  70. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/compose-redis.yml +0 -0
  71. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/conftest.py +0 -0
  72. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/conftest_checkpointer.py +0 -0
  73. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/conftest_store.py +0 -0
  74. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/memory_assert.py +0 -0
  75. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/messages.py +0 -0
  76. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/__init__.py +0 -0
  77. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/__snapshots__/test_middleware_decorators.ambr +0 -0
  78. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/__snapshots__/test_middleware_diagram.ambr +0 -0
  79. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/__snapshots__/test_middleware_framework.ambr +0 -0
  80. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/__init__.py +0 -0
  81. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/__snapshots__/test_decorators.ambr +0 -0
  82. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/__snapshots__/test_diagram.ambr +0 -0
  83. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/__snapshots__/test_framework.ambr +0 -0
  84. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/test_composition.py +0 -0
  85. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/test_decorators.py +0 -0
  86. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/test_diagram.py +0 -0
  87. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/test_framework.py +0 -0
  88. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/test_overrides.py +0 -0
  89. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/test_sync_async_wrappers.py +0 -0
  90. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/test_tools.py +0 -0
  91. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/test_wrap_model_call.py +0 -0
  92. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/core/test_wrap_tool_call.py +0 -0
  93. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/__init__.py +0 -0
  94. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_context_editing.py +0 -0
  95. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_file_search.py +0 -0
  96. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_human_in_the_loop.py +0 -0
  97. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_model_call_limit.py +0 -0
  98. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_model_fallback.py +0 -0
  99. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_model_retry.py +0 -0
  100. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_pii.py +0 -0
  101. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_shell_execution_policies.py +0 -0
  102. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_shell_tool.py +0 -0
  103. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_structured_output_retry.py +0 -0
  104. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_todo.py +0 -0
  105. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_tool_call_limit.py +0 -0
  106. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_tool_emulator.py +0 -0
  107. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_tool_retry.py +0 -0
  108. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/middleware/implementations/test_tool_selection.py +0 -0
  109. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/model.py +0 -0
  110. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/specifications/responses.json +0 -0
  111. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/specifications/return_direct.json +0 -0
  112. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/test_create_agent_tool_validation.py +0 -0
  113. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/test_injected_runtime_create_agent.py +0 -0
  114. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/test_react_agent.py +0 -0
  115. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/test_response_format.py +0 -0
  116. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/test_response_format_integration.py +0 -0
  117. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/test_responses.py +0 -0
  118. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/test_responses_spec.py +0 -0
  119. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/test_return_direct_graph.py +0 -0
  120. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/test_return_direct_spec.py +0 -0
  121. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/test_state_schema.py +0 -0
  122. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/test_system_message.py +0 -0
  123. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/agents/utils.py +0 -0
  124. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/chat_models/__init__.py +0 -0
  125. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/conftest.py +0 -0
  126. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/embeddings/__init__.py +0 -0
  127. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/embeddings/test_base.py +0 -0
  128. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/embeddings/test_imports.py +0 -0
  129. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/stubs.py +0 -0
  130. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/test_dependencies.py +0 -0
  131. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/test_imports.py +0 -0
  132. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/test_pytest_config.py +0 -0
  133. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/tools/__init__.py +0 -0
  134. {langchain-1.1.0 → langchain-1.1.3}/tests/unit_tests/tools/test_imports.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain
3
- Version: 1.1.0
3
+ Version: 1.1.3
4
4
  Summary: Building applications with LLMs through composability
5
5
  Project-URL: Homepage, https://docs.langchain.com/
6
6
  Project-URL: Documentation, https://reference.langchain.com/python/langchain/langchain/
@@ -12,7 +12,7 @@ Project-URL: Reddit, https://www.reddit.com/r/LangChain/
12
12
  License: MIT
13
13
  License-File: LICENSE
14
14
  Requires-Python: <4.0.0,>=3.10.0
15
- Requires-Dist: langchain-core<2.0.0,>=1.1.0
15
+ Requires-Dist: langchain-core<2.0.0,>=1.1.2
16
16
  Requires-Dist: langgraph<1.1.0,>=1.0.2
17
17
  Requires-Dist: pydantic<3.0.0,>=2.7.4
18
18
  Provides-Extra: anthropic
@@ -1,3 +1,3 @@
1
1
  """Main entrypoint into LangChain."""
2
2
 
3
- __version__ = "1.1.0"
3
+ __version__ = "1.1.3"
@@ -1100,6 +1100,8 @@ def create_agent( # noqa: PLR0915
1100
1100
  messages = [request.system_message, *messages]
1101
1101
 
1102
1102
  output = model_.invoke(messages)
1103
+ if name:
1104
+ output.name = name
1103
1105
 
1104
1106
  # Handle model output to get messages and structured_response
1105
1107
  handled_output = _handle_model_output(output, effective_response_format)
@@ -1153,6 +1155,8 @@ def create_agent( # noqa: PLR0915
1153
1155
  messages = [request.system_message, *messages]
1154
1156
 
1155
1157
  output = await model_.ainvoke(messages)
1158
+ if name:
1159
+ output.name = name
1156
1160
 
1157
1161
  # Handle model output to get messages and structured_response
1158
1162
  handled_output = _handle_model_output(output, effective_response_format)
@@ -189,7 +189,7 @@ class ContextEditingMiddleware(AgentMiddleware):
189
189
  configured thresholds.
190
190
 
191
191
  Currently the `ClearToolUsesEdit` strategy is supported, aligning with Anthropic's
192
- `clear_tool_uses_20250919` behavior [(read more)](https://docs.claude.com/en/docs/agents-and-tools/tool-use/memory-tool).
192
+ `clear_tool_uses_20250919` behavior [(read more)](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool).
193
193
  """
194
194
 
195
195
  edits: list[ContextEdit]
@@ -7,7 +7,7 @@ from langgraph.runtime import Runtime
7
7
  from langgraph.types import interrupt
8
8
  from typing_extensions import NotRequired, TypedDict
9
9
 
10
- from langchain.agents.middleware.types import AgentMiddleware, AgentState
10
+ from langchain.agents.middleware.types import AgentMiddleware, AgentState, ContextT, StateT
11
11
 
12
12
 
13
13
  class Action(TypedDict):
@@ -102,7 +102,7 @@ class HITLResponse(TypedDict):
102
102
  class _DescriptionFactory(Protocol):
103
103
  """Callable that generates a description for a tool call."""
104
104
 
105
- def __call__(self, tool_call: ToolCall, state: AgentState, runtime: Runtime) -> str:
105
+ def __call__(self, tool_call: ToolCall, state: AgentState, runtime: Runtime[ContextT]) -> str:
106
106
  """Generate a description for a tool call."""
107
107
  ...
108
108
 
@@ -138,7 +138,7 @@ class InterruptOnConfig(TypedDict):
138
138
  def format_tool_description(
139
139
  tool_call: ToolCall,
140
140
  state: AgentState,
141
- runtime: Runtime
141
+ runtime: Runtime[ContextT]
142
142
  ) -> str:
143
143
  import json
144
144
  return (
@@ -156,7 +156,7 @@ class InterruptOnConfig(TypedDict):
156
156
  """JSON schema for the args associated with the action, if edits are allowed."""
157
157
 
158
158
 
159
- class HumanInTheLoopMiddleware(AgentMiddleware):
159
+ class HumanInTheLoopMiddleware(AgentMiddleware[StateT, ContextT]):
160
160
  """Human in the loop middleware."""
161
161
 
162
162
  def __init__(
@@ -204,7 +204,7 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
204
204
  tool_call: ToolCall,
205
205
  config: InterruptOnConfig,
206
206
  state: AgentState,
207
- runtime: Runtime,
207
+ runtime: Runtime[ContextT],
208
208
  ) -> tuple[ActionRequest, ReviewConfig]:
209
209
  """Create an ActionRequest and ReviewConfig for a tool call."""
210
210
  tool_name = tool_call["name"]
@@ -277,7 +277,7 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
277
277
  )
278
278
  raise ValueError(msg)
279
279
 
280
- def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
280
+ def after_model(self, state: AgentState, runtime: Runtime[ContextT]) -> dict[str, Any] | None:
281
281
  """Trigger interrupt flows for relevant tool calls after an `AIMessage`."""
282
282
  messages = state["messages"]
283
283
  if not messages:
@@ -350,6 +350,8 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
350
350
 
351
351
  return {"messages": [last_ai_msg, *artificial_tool_messages]}
352
352
 
353
- async def aafter_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
353
+ async def aafter_model(
354
+ self, state: AgentState, runtime: Runtime[ContextT]
355
+ ) -> dict[str, Any] | None:
354
356
  """Async trigger interrupt flows for relevant tool calls after an `AIMessage`."""
355
357
  return self.after_model(state, runtime)
@@ -3,10 +3,10 @@
3
3
  import uuid
4
4
  import warnings
5
5
  from collections.abc import Callable, Iterable, Mapping
6
+ from functools import partial
6
7
  from typing import Any, Literal, cast
7
8
 
8
9
  from langchain_core.messages import (
9
- AIMessage,
10
10
  AnyMessage,
11
11
  MessageLikeRepresentation,
12
12
  RemoveMessage,
@@ -55,13 +55,76 @@ Messages to summarize:
55
55
  _DEFAULT_MESSAGES_TO_KEEP = 20
56
56
  _DEFAULT_TRIM_TOKEN_LIMIT = 4000
57
57
  _DEFAULT_FALLBACK_MESSAGE_COUNT = 15
58
- _SEARCH_RANGE_FOR_TOOL_PAIRS = 5
59
58
 
60
59
  ContextFraction = tuple[Literal["fraction"], float]
60
+ """Fraction of model's maximum input tokens.
61
+
62
+ Example:
63
+ To specify 50% of the model's max input tokens:
64
+
65
+ ```python
66
+ ("fraction", 0.5)
67
+ ```
68
+ """
69
+
61
70
  ContextTokens = tuple[Literal["tokens"], int]
71
+ """Absolute number of tokens.
72
+
73
+ Example:
74
+ To specify 3000 tokens:
75
+
76
+ ```python
77
+ ("tokens", 3000)
78
+ ```
79
+ """
80
+
62
81
  ContextMessages = tuple[Literal["messages"], int]
82
+ """Absolute number of messages.
83
+
84
+ Example:
85
+ To specify 50 messages:
86
+
87
+ ```python
88
+ ("messages", 50)
89
+ ```
90
+ """
63
91
 
64
92
  ContextSize = ContextFraction | ContextTokens | ContextMessages
93
+ """Union type for context size specifications.
94
+
95
+ Can be either:
96
+
97
+ - [`ContextFraction`][langchain.agents.middleware.summarization.ContextFraction]: A
98
+ fraction of the model's maximum input tokens.
99
+ - [`ContextTokens`][langchain.agents.middleware.summarization.ContextTokens]: An absolute
100
+ number of tokens.
101
+ - [`ContextMessages`][langchain.agents.middleware.summarization.ContextMessages]: An
102
+ absolute number of messages.
103
+
104
+ Depending on use with `trigger` or `keep` parameters, this type indicates either
105
+ when to trigger summarization or how much context to retain.
106
+
107
+ Example:
108
+ ```python
109
+ # ContextFraction
110
+ context_size: ContextSize = ("fraction", 0.5)
111
+
112
+ # ContextTokens
113
+ context_size: ContextSize = ("tokens", 3000)
114
+
115
+ # ContextMessages
116
+ context_size: ContextSize = ("messages", 50)
117
+ ```
118
+ """
119
+
120
+
121
+ def _get_approximate_token_counter(model: BaseChatModel) -> TokenCounter:
122
+ """Tune parameters of approximate token counter based on model type."""
123
+ if model._llm_type == "anthropic-chat":
124
+ # 3.3 was estimated in an offline experiment, comparing with Claude's token-counting
125
+ # API: https://platform.claude.com/docs/en/build-with-claude/token-counting
126
+ return partial(count_tokens_approximately, chars_per_token=3.3)
127
+ return count_tokens_approximately
65
128
 
66
129
 
67
130
  class SummarizationMiddleware(AgentMiddleware):
@@ -89,19 +152,48 @@ class SummarizationMiddleware(AgentMiddleware):
89
152
  model: The language model to use for generating summaries.
90
153
  trigger: One or more thresholds that trigger summarization.
91
154
 
92
- Provide a single `ContextSize` tuple or a list of tuples, in which case
93
- summarization runs when any threshold is breached.
155
+ Provide a single
156
+ [`ContextSize`][langchain.agents.middleware.summarization.ContextSize]
157
+ tuple or a list of tuples, in which case summarization runs when any
158
+ threshold is met.
159
+
160
+ !!! example
161
+
162
+ ```python
163
+ # Trigger summarization when 50 messages is reached
164
+ ("messages", 50)
165
+
166
+ # Trigger summarization when 3000 tokens is reached
167
+ ("tokens", 3000)
168
+
169
+ # Trigger summarization either when 80% of model's max input tokens
170
+ # is reached or when 100 messages is reached (whichever comes first)
171
+ [("fraction", 0.8), ("messages", 100)]
172
+ ```
94
173
 
95
- Examples: `("messages", 50)`, `("tokens", 3000)`, `[("fraction", 0.8),
96
- ("messages", 100)]`.
174
+ See [`ContextSize`][langchain.agents.middleware.summarization.ContextSize]
175
+ for more details.
97
176
  keep: Context retention policy applied after summarization.
98
177
 
99
- Provide a `ContextSize` tuple to specify how much history to preserve.
178
+ Provide a [`ContextSize`][langchain.agents.middleware.summarization.ContextSize]
179
+ tuple to specify how much history to preserve.
100
180
 
101
- Defaults to keeping the most recent 20 messages.
181
+ Defaults to keeping the most recent `20` messages.
102
182
 
103
- Examples: `("messages", 20)`, `("tokens", 3000)`, or
104
- `("fraction", 0.3)`.
183
+ Does not support multiple values like `trigger`.
184
+
185
+ !!! example
186
+
187
+ ```python
188
+ # Keep the most recent 20 messages
189
+ ("messages", 20)
190
+
191
+ # Keep the most recent 3000 tokens
192
+ ("tokens", 3000)
193
+
194
+ # Keep the most recent 30% of the model's max input tokens
195
+ ("fraction", 0.3)
196
+ ```
105
197
  token_counter: Function to count tokens in messages.
106
198
  summary_prompt: Prompt template for generating summaries.
107
199
  trim_tokens_to_summarize: Maximum tokens to keep when preparing messages for
@@ -150,7 +242,10 @@ class SummarizationMiddleware(AgentMiddleware):
150
242
  self._trigger_conditions = trigger_conditions
151
243
 
152
244
  self.keep = self._validate_context_size(keep, "keep")
153
- self.token_counter = token_counter
245
+ if token_counter is count_tokens_approximately:
246
+ self.token_counter = _get_approximate_token_counter(self.model)
247
+ else:
248
+ self.token_counter = token_counter
154
249
  self.summary_prompt = summary_prompt
155
250
  self.trim_tokens_to_summarize = trim_tokens_to_summarize
156
251
 
@@ -300,11 +395,8 @@ class SummarizationMiddleware(AgentMiddleware):
300
395
  return 0
301
396
  cutoff_candidate = len(messages) - 1
302
397
 
303
- for i in range(cutoff_candidate, -1, -1):
304
- if self._is_safe_cutoff_point(messages, i):
305
- return i
306
-
307
- return 0
398
+ # Advance past any ToolMessages to avoid splitting AI/Tool pairs
399
+ return self._find_safe_cutoff_point(messages, cutoff_candidate)
308
400
 
309
401
  def _get_profile_limits(self) -> int | None:
310
402
  """Retrieve max input token limit from the model profile."""
@@ -366,67 +458,26 @@ class SummarizationMiddleware(AgentMiddleware):
366
458
 
367
459
  Returns the index where messages can be safely cut without separating
368
460
  related AI and Tool messages. Returns `0` if no safe cutoff is found.
461
+
462
+ This is aggressive with summarization - if the target cutoff lands in the
463
+ middle of tool messages, we advance past all of them (summarizing more).
369
464
  """
370
465
  if len(messages) <= messages_to_keep:
371
466
  return 0
372
467
 
373
468
  target_cutoff = len(messages) - messages_to_keep
469
+ return self._find_safe_cutoff_point(messages, target_cutoff)
374
470
 
375
- for i in range(target_cutoff, -1, -1):
376
- if self._is_safe_cutoff_point(messages, i):
377
- return i
378
-
379
- return 0
380
-
381
- def _is_safe_cutoff_point(self, messages: list[AnyMessage], cutoff_index: int) -> bool:
382
- """Check if cutting at index would separate AI/Tool message pairs."""
383
- if cutoff_index >= len(messages):
384
- return True
385
-
386
- search_start = max(0, cutoff_index - _SEARCH_RANGE_FOR_TOOL_PAIRS)
387
- search_end = min(len(messages), cutoff_index + _SEARCH_RANGE_FOR_TOOL_PAIRS)
471
+ def _find_safe_cutoff_point(self, messages: list[AnyMessage], cutoff_index: int) -> int:
472
+ """Find a safe cutoff point that doesn't split AI/Tool message pairs.
388
473
 
389
- for i in range(search_start, search_end):
390
- if not self._has_tool_calls(messages[i]):
391
- continue
392
-
393
- tool_call_ids = self._extract_tool_call_ids(cast("AIMessage", messages[i]))
394
- if self._cutoff_separates_tool_pair(messages, i, cutoff_index, tool_call_ids):
395
- return False
396
-
397
- return True
398
-
399
- def _has_tool_calls(self, message: AnyMessage) -> bool:
400
- """Check if message is an AI message with tool calls."""
401
- return (
402
- isinstance(message, AIMessage) and hasattr(message, "tool_calls") and message.tool_calls # type: ignore[return-value]
403
- )
404
-
405
- def _extract_tool_call_ids(self, ai_message: AIMessage) -> set[str]:
406
- """Extract tool call IDs from an AI message."""
407
- tool_call_ids = set()
408
- for tc in ai_message.tool_calls:
409
- call_id = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", None)
410
- if call_id is not None:
411
- tool_call_ids.add(call_id)
412
- return tool_call_ids
413
-
414
- def _cutoff_separates_tool_pair(
415
- self,
416
- messages: list[AnyMessage],
417
- ai_message_index: int,
418
- cutoff_index: int,
419
- tool_call_ids: set[str],
420
- ) -> bool:
421
- """Check if cutoff separates an AI message from its corresponding tool messages."""
422
- for j in range(ai_message_index + 1, len(messages)):
423
- message = messages[j]
424
- if isinstance(message, ToolMessage) and message.tool_call_id in tool_call_ids:
425
- ai_before_cutoff = ai_message_index < cutoff_index
426
- tool_before_cutoff = j < cutoff_index
427
- if ai_before_cutoff != tool_before_cutoff:
428
- return True
429
- return False
474
+ If the message at cutoff_index is a ToolMessage, advance until we find
475
+ a non-ToolMessage. This ensures we never cut in the middle of parallel
476
+ tool call responses.
477
+ """
478
+ while cutoff_index < len(messages) and isinstance(messages[cutoff_index], ToolMessage):
479
+ cutoff_index += 1
480
+ return cutoff_index
430
481
 
431
482
  def _create_summary(self, messages_to_summarize: list[AnyMessage]) -> str:
432
483
  """Generate summary for the given messages."""
@@ -465,14 +516,17 @@ class SummarizationMiddleware(AgentMiddleware):
465
516
  try:
466
517
  if self.trim_tokens_to_summarize is None:
467
518
  return messages
468
- return trim_messages(
469
- messages,
470
- max_tokens=self.trim_tokens_to_summarize,
471
- token_counter=self.token_counter,
472
- start_on="human",
473
- strategy="last",
474
- allow_partial=True,
475
- include_system=True,
519
+ return cast(
520
+ "list[AnyMessage]",
521
+ trim_messages(
522
+ messages,
523
+ max_tokens=self.trim_tokens_to_summarize,
524
+ token_counter=self.token_counter,
525
+ start_on="human",
526
+ strategy="last",
527
+ allow_partial=True,
528
+ include_system=True,
529
+ ),
476
530
  )
477
531
  except Exception: # noqa: BLE001
478
532
  return messages[-_DEFAULT_FALLBACK_MESSAGE_COUNT:]
@@ -102,6 +102,7 @@ def init_chat_model(
102
102
  - `deepseek...` -> `deepseek`
103
103
  - `grok...` -> `xai`
104
104
  - `sonar...` -> `perplexity`
105
+ - `solar...` -> `upstage`
105
106
  model_provider: The model provider if not specified as part of the model arg
106
107
  (see above).
107
108
 
@@ -129,6 +130,7 @@ def init_chat_model(
129
130
  - `nvidia` -> [`langchain-nvidia-ai-endpoints`](https://docs.langchain.com/oss/python/integrations/providers/nvidia)
130
131
  - `xai` -> [`langchain-xai`](https://docs.langchain.com/oss/python/integrations/providers/xai)
131
132
  - `perplexity` -> [`langchain-perplexity`](https://docs.langchain.com/oss/python/integrations/providers/perplexity)
133
+ - `upstage` -> [`langchain-upstage`](https://docs.langchain.com/oss/python/integrations/providers/upstage)
132
134
 
133
135
  configurable_fields: Which model parameters are configurable at runtime:
134
136
 
@@ -449,6 +451,11 @@ def _init_chat_model_helper(
449
451
  from langchain_perplexity import ChatPerplexity
450
452
 
451
453
  return ChatPerplexity(model=model, **kwargs)
454
+ if model_provider == "upstage":
455
+ _check_pkg("langchain_upstage")
456
+ from langchain_upstage import ChatUpstage
457
+
458
+ return ChatUpstage(model=model, **kwargs)
452
459
  supported = ", ".join(_SUPPORTED_PROVIDERS)
453
460
  msg = f"Unsupported {model_provider=}.\n\nSupported model providers are: {supported}"
454
461
  raise ValueError(msg)
@@ -475,6 +482,7 @@ _SUPPORTED_PROVIDERS = {
475
482
  "ibm",
476
483
  "xai",
477
484
  "perplexity",
485
+ "upstage",
478
486
  }
479
487
 
480
488
 
@@ -499,6 +507,8 @@ def _attempt_infer_model_provider(model_name: str) -> str | None:
499
507
  return "xai"
500
508
  if model_name.startswith("sonar"):
501
509
  return "perplexity"
510
+ if model_name.startswith("solar"):
511
+ return "upstage"
502
512
  return None
503
513
 
504
514
 
@@ -9,10 +9,10 @@ license = { text = "MIT" }
9
9
  readme = "README.md"
10
10
  authors = []
11
11
 
12
- version = "1.1.0"
12
+ version = "1.1.3"
13
13
  requires-python = ">=3.10.0,<4.0.0"
14
14
  dependencies = [
15
- "langchain-core>=1.1.0,<2.0.0",
15
+ "langchain-core>=1.1.2,<2.0.0",
16
16
  "langgraph>=1.0.2,<1.1.0",
17
17
  "pydantic>=2.7.4,<3.0.0",
18
18
  ]