langchain 1.0.4__py3-none-any.whl → 1.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. langchain/__init__.py +1 -1
  2. langchain/agents/__init__.py +1 -7
  3. langchain/agents/factory.py +100 -41
  4. langchain/agents/middleware/__init__.py +5 -7
  5. langchain/agents/middleware/_execution.py +21 -20
  6. langchain/agents/middleware/_redaction.py +27 -12
  7. langchain/agents/middleware/_retry.py +123 -0
  8. langchain/agents/middleware/context_editing.py +26 -22
  9. langchain/agents/middleware/file_search.py +18 -13
  10. langchain/agents/middleware/human_in_the_loop.py +60 -54
  11. langchain/agents/middleware/model_call_limit.py +63 -17
  12. langchain/agents/middleware/model_fallback.py +7 -9
  13. langchain/agents/middleware/model_retry.py +300 -0
  14. langchain/agents/middleware/pii.py +80 -27
  15. langchain/agents/middleware/shell_tool.py +230 -103
  16. langchain/agents/middleware/summarization.py +439 -90
  17. langchain/agents/middleware/todo.py +111 -27
  18. langchain/agents/middleware/tool_call_limit.py +105 -71
  19. langchain/agents/middleware/tool_emulator.py +42 -33
  20. langchain/agents/middleware/tool_retry.py +171 -159
  21. langchain/agents/middleware/tool_selection.py +37 -27
  22. langchain/agents/middleware/types.py +754 -392
  23. langchain/agents/structured_output.py +22 -12
  24. langchain/chat_models/__init__.py +1 -7
  25. langchain/chat_models/base.py +234 -185
  26. langchain/embeddings/__init__.py +0 -5
  27. langchain/embeddings/base.py +80 -66
  28. langchain/messages/__init__.py +0 -5
  29. langchain/tools/__init__.py +1 -7
  30. {langchain-1.0.4.dist-info → langchain-1.2.3.dist-info}/METADATA +3 -5
  31. langchain-1.2.3.dist-info/RECORD +36 -0
  32. {langchain-1.0.4.dist-info → langchain-1.2.3.dist-info}/WHEEL +1 -1
  33. langchain-1.0.4.dist-info/RECORD +0 -34
  34. {langchain-1.0.4.dist-info → langchain-1.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -22,7 +22,7 @@ class ModelFallbackMiddleware(AgentMiddleware):
22
22
  """Automatic fallback to alternative models on errors.
23
23
 
24
24
  Retries failed model calls with alternative models in sequence until
25
- success or all models exhausted. Primary model specified in create_agent().
25
+ success or all models exhausted. Primary model specified in `create_agent`.
26
26
 
27
27
  Example:
28
28
  ```python
@@ -87,15 +87,14 @@ class ModelFallbackMiddleware(AgentMiddleware):
87
87
  last_exception: Exception
88
88
  try:
89
89
  return handler(request)
90
- except Exception as e: # noqa: BLE001
90
+ except Exception as e:
91
91
  last_exception = e
92
92
 
93
93
  # Try fallback models
94
94
  for fallback_model in self.models:
95
- request.model = fallback_model
96
95
  try:
97
- return handler(request)
98
- except Exception as e: # noqa: BLE001
96
+ return handler(request.override(model=fallback_model))
97
+ except Exception as e:
99
98
  last_exception = e
100
99
  continue
101
100
 
@@ -122,15 +121,14 @@ class ModelFallbackMiddleware(AgentMiddleware):
122
121
  last_exception: Exception
123
122
  try:
124
123
  return await handler(request)
125
- except Exception as e: # noqa: BLE001
124
+ except Exception as e:
126
125
  last_exception = e
127
126
 
128
127
  # Try fallback models
129
128
  for fallback_model in self.models:
130
- request.model = fallback_model
131
129
  try:
132
- return await handler(request)
133
- except Exception as e: # noqa: BLE001
130
+ return await handler(request.override(model=fallback_model))
131
+ except Exception as e:
134
132
  last_exception = e
135
133
  continue
136
134
 
@@ -0,0 +1,300 @@
1
+ """Model retry middleware for agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from typing import TYPE_CHECKING
8
+
9
+ from langchain_core.messages import AIMessage
10
+
11
+ from langchain.agents.middleware._retry import (
12
+ OnFailure,
13
+ RetryOn,
14
+ calculate_delay,
15
+ should_retry_exception,
16
+ validate_retry_params,
17
+ )
18
+ from langchain.agents.middleware.types import AgentMiddleware, ModelResponse
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Awaitable, Callable
22
+
23
+ from langchain.agents.middleware.types import ModelRequest
24
+
25
+
26
+ class ModelRetryMiddleware(AgentMiddleware):
27
+ """Middleware that automatically retries failed model calls with configurable backoff.
28
+
29
+ Supports retrying on specific exceptions and exponential backoff.
30
+
31
+ Examples:
32
+ !!! example "Basic usage with default settings (2 retries, exponential backoff)"
33
+
34
+ ```python
35
+ from langchain.agents import create_agent
36
+ from langchain.agents.middleware import ModelRetryMiddleware
37
+
38
+ agent = create_agent(model, tools=[search_tool], middleware=[ModelRetryMiddleware()])
39
+ ```
40
+
41
+ !!! example "Retry specific exceptions only"
42
+
43
+ ```python
44
+ from anthropic import RateLimitError
45
+ from openai import APITimeoutError
46
+
47
+ retry = ModelRetryMiddleware(
48
+ max_retries=4,
49
+ retry_on=(APITimeoutError, RateLimitError),
50
+ backoff_factor=1.5,
51
+ )
52
+ ```
53
+
54
+ !!! example "Custom exception filtering"
55
+
56
+ ```python
57
+ from anthropic import APIStatusError
58
+
59
+
60
+ def should_retry(exc: Exception) -> bool:
61
+ # Only retry on 5xx errors
62
+ if isinstance(exc, APIStatusError):
63
+ return 500 <= exc.status_code < 600
64
+ return False
65
+
66
+
67
+ retry = ModelRetryMiddleware(
68
+ max_retries=3,
69
+ retry_on=should_retry,
70
+ )
71
+ ```
72
+
73
+ !!! example "Custom error handling"
74
+
75
+ ```python
76
+ def format_error(exc: Exception) -> str:
77
+ return "Model temporarily unavailable. Please try again later."
78
+
79
+
80
+ retry = ModelRetryMiddleware(
81
+ max_retries=4,
82
+ on_failure=format_error,
83
+ )
84
+ ```
85
+
86
+ !!! example "Constant backoff (no exponential growth)"
87
+
88
+ ```python
89
+ retry = ModelRetryMiddleware(
90
+ max_retries=5,
91
+ backoff_factor=0.0, # No exponential growth
92
+ initial_delay=2.0, # Always wait 2 seconds
93
+ )
94
+ ```
95
+
96
+ !!! example "Raise exception on failure"
97
+
98
+ ```python
99
+ retry = ModelRetryMiddleware(
100
+ max_retries=2,
101
+ on_failure="error", # Re-raise exception instead of returning message
102
+ )
103
+ ```
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ *,
109
+ max_retries: int = 2,
110
+ retry_on: RetryOn = (Exception,),
111
+ on_failure: OnFailure = "continue",
112
+ backoff_factor: float = 2.0,
113
+ initial_delay: float = 1.0,
114
+ max_delay: float = 60.0,
115
+ jitter: bool = True,
116
+ ) -> None:
117
+ """Initialize `ModelRetryMiddleware`.
118
+
119
+ Args:
120
+ max_retries: Maximum number of retry attempts after the initial call.
121
+
122
+ Must be `>= 0`.
123
+ retry_on: Either a tuple of exception types to retry on, or a callable
124
+ that takes an exception and returns `True` if it should be retried.
125
+
126
+ Default is to retry on all exceptions.
127
+ on_failure: Behavior when all retries are exhausted.
128
+
129
+ Options:
130
+
131
+ - `'continue'`: Return an `AIMessage` with error details,
132
+ allowing the agent to continue with an error response.
133
+ - `'error'`: Re-raise the exception, stopping agent execution.
134
+ - **Custom callable:** Function that takes the exception and returns a
135
+ string for the `AIMessage` content, allowing custom error
136
+ formatting.
137
+ backoff_factor: Multiplier for exponential backoff.
138
+
139
+ Each retry waits `initial_delay * (backoff_factor ** retry_number)`
140
+ seconds.
141
+
142
+ Set to `0.0` for constant delay.
143
+ initial_delay: Initial delay in seconds before first retry.
144
+ max_delay: Maximum delay in seconds between retries.
145
+
146
+ Caps exponential backoff growth.
147
+ jitter: Whether to add random jitter (`±25%`) to delay to avoid thundering herd.
148
+
149
+ Raises:
150
+ ValueError: If `max_retries < 0` or delays are negative.
151
+ """
152
+ super().__init__()
153
+
154
+ # Validate parameters
155
+ validate_retry_params(max_retries, initial_delay, max_delay, backoff_factor)
156
+
157
+ self.max_retries = max_retries
158
+ self.tools = [] # No additional tools registered by this middleware
159
+ self.retry_on = retry_on
160
+ self.on_failure = on_failure
161
+ self.backoff_factor = backoff_factor
162
+ self.initial_delay = initial_delay
163
+ self.max_delay = max_delay
164
+ self.jitter = jitter
165
+
166
+ def _format_failure_message(self, exc: Exception, attempts_made: int) -> AIMessage:
167
+ """Format the failure message when retries are exhausted.
168
+
169
+ Args:
170
+ exc: The exception that caused the failure.
171
+ attempts_made: Number of attempts actually made.
172
+
173
+ Returns:
174
+ `AIMessage` with formatted error message.
175
+ """
176
+ exc_type = type(exc).__name__
177
+ exc_msg = str(exc)
178
+ attempt_word = "attempt" if attempts_made == 1 else "attempts"
179
+ content = (
180
+ f"Model call failed after {attempts_made} {attempt_word} with {exc_type}: {exc_msg}"
181
+ )
182
+ return AIMessage(content=content)
183
+
184
+ def _handle_failure(self, exc: Exception, attempts_made: int) -> ModelResponse:
185
+ """Handle failure when all retries are exhausted.
186
+
187
+ Args:
188
+ exc: The exception that caused the failure.
189
+ attempts_made: Number of attempts actually made.
190
+
191
+ Returns:
192
+ `ModelResponse` with error details.
193
+
194
+ Raises:
195
+ Exception: If `on_failure` is `'error'`, re-raises the exception.
196
+ """
197
+ if self.on_failure == "error":
198
+ raise exc
199
+
200
+ if callable(self.on_failure):
201
+ content = self.on_failure(exc)
202
+ ai_msg = AIMessage(content=content)
203
+ else:
204
+ ai_msg = self._format_failure_message(exc, attempts_made)
205
+
206
+ return ModelResponse(result=[ai_msg])
207
+
208
+ def wrap_model_call(
209
+ self,
210
+ request: ModelRequest,
211
+ handler: Callable[[ModelRequest], ModelResponse],
212
+ ) -> ModelResponse | AIMessage:
213
+ """Intercept model execution and retry on failure.
214
+
215
+ Args:
216
+ request: Model request with model, messages, state, and runtime.
217
+ handler: Callable to execute the model (can be called multiple times).
218
+
219
+ Returns:
220
+ `ModelResponse` or `AIMessage` (the final result).
221
+ """
222
+ # Initial attempt + retries
223
+ for attempt in range(self.max_retries + 1):
224
+ try:
225
+ return handler(request)
226
+ except Exception as exc:
227
+ attempts_made = attempt + 1 # attempt is 0-indexed
228
+
229
+ # Check if we should retry this exception
230
+ if not should_retry_exception(exc, self.retry_on):
231
+ # Exception is not retryable, handle failure immediately
232
+ return self._handle_failure(exc, attempts_made)
233
+
234
+ # Check if we have more retries left
235
+ if attempt < self.max_retries:
236
+ # Calculate and apply backoff delay
237
+ delay = calculate_delay(
238
+ attempt,
239
+ backoff_factor=self.backoff_factor,
240
+ initial_delay=self.initial_delay,
241
+ max_delay=self.max_delay,
242
+ jitter=self.jitter,
243
+ )
244
+ if delay > 0:
245
+ time.sleep(delay)
246
+ # Continue to next retry
247
+ else:
248
+ # No more retries, handle failure
249
+ return self._handle_failure(exc, attempts_made)
250
+
251
+ # Unreachable: loop always returns via handler success or _handle_failure
252
+ msg = "Unexpected: retry loop completed without returning"
253
+ raise RuntimeError(msg)
254
+
255
+ async def awrap_model_call(
256
+ self,
257
+ request: ModelRequest,
258
+ handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
259
+ ) -> ModelResponse | AIMessage:
260
+ """Intercept and control async model execution with retry logic.
261
+
262
+ Args:
263
+ request: Model request with model, messages, state, and runtime.
264
+ handler: Async callable to execute the model and returns `ModelResponse`.
265
+
266
+ Returns:
267
+ `ModelResponse` or `AIMessage` (the final result).
268
+ """
269
+ # Initial attempt + retries
270
+ for attempt in range(self.max_retries + 1):
271
+ try:
272
+ return await handler(request)
273
+ except Exception as exc:
274
+ attempts_made = attempt + 1 # attempt is 0-indexed
275
+
276
+ # Check if we should retry this exception
277
+ if not should_retry_exception(exc, self.retry_on):
278
+ # Exception is not retryable, handle failure immediately
279
+ return self._handle_failure(exc, attempts_made)
280
+
281
+ # Check if we have more retries left
282
+ if attempt < self.max_retries:
283
+ # Calculate and apply backoff delay
284
+ delay = calculate_delay(
285
+ attempt,
286
+ backoff_factor=self.backoff_factor,
287
+ initial_delay=self.initial_delay,
288
+ max_delay=self.max_delay,
289
+ jitter=self.jitter,
290
+ )
291
+ if delay > 0:
292
+ await asyncio.sleep(delay)
293
+ # Continue to next retry
294
+ else:
295
+ # No more retries, handle failure
296
+ return self._handle_failure(exc, attempts_made)
297
+
298
+ # Unreachable: loop always returns via handler success or _handle_failure
299
+ msg = "Unexpected: retry loop completed without returning"
300
+ raise RuntimeError(msg)
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING, Any, Literal
6
6
 
7
7
  from langchain_core.messages import AIMessage, AnyMessage, HumanMessage, ToolMessage
8
+ from typing_extensions import override
8
9
 
9
10
  from langchain.agents.middleware._redaction import (
10
11
  PIIDetectionError,
@@ -27,24 +28,26 @@ if TYPE_CHECKING:
27
28
 
28
29
 
29
30
  class PIIMiddleware(AgentMiddleware):
30
- """Detect and handle Personally Identifiable Information (PII) in agent conversations.
31
+ """Detect and handle Personally Identifiable Information (PII) in conversations.
31
32
 
32
33
  This middleware detects common PII types and applies configurable strategies
33
- to handle them. It can detect emails, credit cards, IP addresses,
34
- MAC addresses, and URLs in both user input and agent output.
34
+ to handle them. It can detect emails, credit cards, IP addresses, MAC addresses, and
35
+ URLs in both user input and agent output.
35
36
 
36
37
  Built-in PII types:
37
- - `email`: Email addresses
38
- - `credit_card`: Credit card numbers (validated with Luhn algorithm)
39
- - `ip`: IP addresses (validated with stdlib)
40
- - `mac_address`: MAC addresses
41
- - `url`: URLs (both `http`/`https` and bare URLs)
38
+
39
+ - `email`: Email addresses
40
+ - `credit_card`: Credit card numbers (validated with Luhn algorithm)
41
+ - `ip`: IP addresses (validated with stdlib)
42
+ - `mac_address`: MAC addresses
43
+ - `url`: URLs (both `http`/`https` and bare URLs)
42
44
 
43
45
  Strategies:
44
- - `block`: Raise an exception when PII is detected
45
- - `redact`: Replace PII with `[REDACTED_TYPE]` placeholders
46
- - `mask`: Partially mask PII (e.g., `****-****-****-1234` for credit card)
47
- - `hash`: Replace PII with deterministic hash (e.g., `<email_hash:a1b2c3d4>`)
46
+
47
+ - `block`: Raise an exception when PII is detected
48
+ - `redact`: Replace PII with `[REDACTED_TYPE]` placeholders
49
+ - `mask`: Partially mask PII (e.g., `****-****-****-1234` for credit card)
50
+ - `hash`: Replace PII with deterministic hash (e.g., `<email_hash:a1b2c3d4>`)
48
51
 
49
52
  Strategy Selection Guide:
50
53
 
@@ -90,6 +93,8 @@ class PIIMiddleware(AgentMiddleware):
90
93
 
91
94
  def __init__(
92
95
  self,
96
+ # From a typing point of view, the literals are covered by 'str'.
97
+ # Nonetheless, we escape PYI051 to keep hints and autocompletion for the caller.
93
98
  pii_type: Literal["email", "credit_card", "ip", "mac_address", "url"] | str, # noqa: PYI051
94
99
  *,
95
100
  strategy: Literal["block", "redact", "mask", "hash"] = "redact",
@@ -101,12 +106,15 @@ class PIIMiddleware(AgentMiddleware):
101
106
  """Initialize the PII detection middleware.
102
107
 
103
108
  Args:
104
- pii_type: Type of PII to detect. Can be a built-in type
105
- (`email`, `credit_card`, `ip`, `mac_address`, `url`)
106
- or a custom type name.
107
- strategy: How to handle detected PII:
109
+ pii_type: Type of PII to detect.
110
+
111
+ Can be a built-in type (`email`, `credit_card`, `ip`, `mac_address`,
112
+ `url`) or a custom type name.
113
+ strategy: How to handle detected PII.
114
+
115
+ Options:
108
116
 
109
- * `block`: Raise PIIDetectionError when PII is detected
117
+ * `block`: Raise `PIIDetectionError` when PII is detected
110
118
  * `redact`: Replace with `[REDACTED_TYPE]` placeholders
111
119
  * `mask`: Partially mask PII (show last few characters)
112
120
  * `hash`: Replace with deterministic hash (format: `<type_hash:digest>`)
@@ -114,16 +122,15 @@ class PIIMiddleware(AgentMiddleware):
114
122
  detector: Custom detector function or regex pattern.
115
123
 
116
124
  * If `Callable`: Function that takes content string and returns
117
- list of PIIMatch objects
125
+ list of `PIIMatch` objects
118
126
  * If `str`: Regex pattern to match PII
119
- * If `None`: Uses built-in detector for the pii_type
120
-
127
+ * If `None`: Uses built-in detector for the `pii_type`
121
128
  apply_to_input: Whether to check user messages before model call.
122
129
  apply_to_output: Whether to check AI messages after model call.
123
130
  apply_to_tool_results: Whether to check tool result messages after tool execution.
124
131
 
125
132
  Raises:
126
- ValueError: If pii_type is not built-in and no detector is provided.
133
+ ValueError: If `pii_type` is not built-in and no detector is provided.
127
134
  """
128
135
  super().__init__()
129
136
 
@@ -154,10 +161,11 @@ class PIIMiddleware(AgentMiddleware):
154
161
  return sanitized, matches
155
162
 
156
163
  @hook_config(can_jump_to=["end"])
164
+ @override
157
165
  def before_model(
158
166
  self,
159
167
  state: AgentState,
160
- runtime: Runtime, # noqa: ARG002
168
+ runtime: Runtime,
161
169
  ) -> dict[str, Any] | None:
162
170
  """Check user messages and tool results for PII before model invocation.
163
171
 
@@ -166,10 +174,11 @@ class PIIMiddleware(AgentMiddleware):
166
174
  runtime: The langgraph runtime.
167
175
 
168
176
  Returns:
169
- Updated state with PII handled according to strategy, or None if no PII detected.
177
+ Updated state with PII handled according to strategy, or `None` if no PII
178
+ detected.
170
179
 
171
180
  Raises:
172
- PIIDetectionError: If PII is detected and strategy is "block".
181
+ PIIDetectionError: If PII is detected and strategy is `'block'`.
173
182
  """
174
183
  if not self.apply_to_input and not self.apply_to_tool_results:
175
184
  return None
@@ -247,10 +256,32 @@ class PIIMiddleware(AgentMiddleware):
247
256
 
248
257
  return None
249
258
 
259
+ @hook_config(can_jump_to=["end"])
260
+ async def abefore_model(
261
+ self,
262
+ state: AgentState,
263
+ runtime: Runtime,
264
+ ) -> dict[str, Any] | None:
265
+ """Async check user messages and tool results for PII before model invocation.
266
+
267
+ Args:
268
+ state: The current agent state.
269
+ runtime: The langgraph runtime.
270
+
271
+ Returns:
272
+ Updated state with PII handled according to strategy, or `None` if no PII
273
+ detected.
274
+
275
+ Raises:
276
+ PIIDetectionError: If PII is detected and strategy is `'block'`.
277
+ """
278
+ return self.before_model(state, runtime)
279
+
280
+ @override
250
281
  def after_model(
251
282
  self,
252
283
  state: AgentState,
253
- runtime: Runtime, # noqa: ARG002
284
+ runtime: Runtime,
254
285
  ) -> dict[str, Any] | None:
255
286
  """Check AI messages for PII after model invocation.
256
287
 
@@ -259,10 +290,11 @@ class PIIMiddleware(AgentMiddleware):
259
290
  runtime: The langgraph runtime.
260
291
 
261
292
  Returns:
262
- Updated state with PII handled according to strategy, or None if no PII detected.
293
+ Updated state with PII handled according to strategy, or None if no PII
294
+ detected.
263
295
 
264
296
  Raises:
265
- PIIDetectionError: If PII is detected and strategy is "block".
297
+ PIIDetectionError: If PII is detected and strategy is `'block'`.
266
298
  """
267
299
  if not self.apply_to_output:
268
300
  return None
@@ -305,9 +337,30 @@ class PIIMiddleware(AgentMiddleware):
305
337
 
306
338
  return {"messages": new_messages}
307
339
 
340
+ async def aafter_model(
341
+ self,
342
+ state: AgentState,
343
+ runtime: Runtime,
344
+ ) -> dict[str, Any] | None:
345
+ """Async check AI messages for PII after model invocation.
346
+
347
+ Args:
348
+ state: The current agent state.
349
+ runtime: The langgraph runtime.
350
+
351
+ Returns:
352
+ Updated state with PII handled according to strategy, or None if no PII
353
+ detected.
354
+
355
+ Raises:
356
+ PIIDetectionError: If PII is detected and strategy is `'block'`.
357
+ """
358
+ return self.after_model(state, runtime)
359
+
308
360
 
309
361
  __all__ = [
310
362
  "PIIDetectionError",
363
+ "PIIMatch",
311
364
  "PIIMiddleware",
312
365
  "detect_credit_card",
313
366
  "detect_email",