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.
- langchain/__init__.py +1 -1
- langchain/agents/__init__.py +1 -7
- langchain/agents/factory.py +100 -41
- langchain/agents/middleware/__init__.py +5 -7
- langchain/agents/middleware/_execution.py +21 -20
- langchain/agents/middleware/_redaction.py +27 -12
- langchain/agents/middleware/_retry.py +123 -0
- langchain/agents/middleware/context_editing.py +26 -22
- langchain/agents/middleware/file_search.py +18 -13
- langchain/agents/middleware/human_in_the_loop.py +60 -54
- langchain/agents/middleware/model_call_limit.py +63 -17
- langchain/agents/middleware/model_fallback.py +7 -9
- langchain/agents/middleware/model_retry.py +300 -0
- langchain/agents/middleware/pii.py +80 -27
- langchain/agents/middleware/shell_tool.py +230 -103
- langchain/agents/middleware/summarization.py +439 -90
- langchain/agents/middleware/todo.py +111 -27
- langchain/agents/middleware/tool_call_limit.py +105 -71
- langchain/agents/middleware/tool_emulator.py +42 -33
- langchain/agents/middleware/tool_retry.py +171 -159
- langchain/agents/middleware/tool_selection.py +37 -27
- langchain/agents/middleware/types.py +754 -392
- langchain/agents/structured_output.py +22 -12
- langchain/chat_models/__init__.py +1 -7
- langchain/chat_models/base.py +234 -185
- langchain/embeddings/__init__.py +0 -5
- langchain/embeddings/base.py +80 -66
- langchain/messages/__init__.py +0 -5
- langchain/tools/__init__.py +1 -7
- {langchain-1.0.4.dist-info → langchain-1.2.3.dist-info}/METADATA +3 -5
- langchain-1.2.3.dist-info/RECORD +36 -0
- {langchain-1.0.4.dist-info → langchain-1.2.3.dist-info}/WHEEL +1 -1
- langchain-1.0.4.dist-info/RECORD +0 -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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",
|