langchain 1.0.0a4__py3-none-any.whl → 1.0.0a6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of langchain might be problematic. Click here for more details.

langchain/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Any
4
4
 
5
- __version__ = "1.0.0a3"
5
+ __version__ = "1.0.0a4"
6
6
 
7
7
 
8
8
  def __getattr__(name: str) -> Any: # noqa: ANN401
@@ -1,13 +1,12 @@
1
1
  """Lazy import utilities."""
2
2
 
3
3
  from importlib import import_module
4
- from typing import Union
5
4
 
6
5
 
7
6
  def import_attr(
8
7
  attr_name: str,
9
- module_name: Union[str, None],
10
- package: Union[str, None],
8
+ module_name: str | None,
9
+ package: str | None,
11
10
  ) -> object:
12
11
  """Import an attribute from a module located in a package.
13
12
 
@@ -12,7 +12,7 @@ particularly for summarization chains and other document processing workflows.
12
12
  from __future__ import annotations
13
13
 
14
14
  import inspect
15
- from typing import TYPE_CHECKING, Union
15
+ from typing import TYPE_CHECKING
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from collections.abc import Awaitable, Callable
@@ -24,11 +24,7 @@ if TYPE_CHECKING:
24
24
 
25
25
 
26
26
  def resolve_prompt(
27
- prompt: Union[
28
- str,
29
- None,
30
- Callable[[StateT, Runtime[ContextT]], list[MessageLikeRepresentation]],
31
- ],
27
+ prompt: str | None | Callable[[StateT, Runtime[ContextT]], list[MessageLikeRepresentation]],
32
28
  state: StateT,
33
29
  runtime: Runtime[ContextT],
34
30
  default_user_content: str,
@@ -61,6 +57,7 @@ def resolve_prompt(
61
57
  def custom_prompt(state, runtime):
62
58
  return [{"role": "system", "content": "Custom"}]
63
59
 
60
+
64
61
  messages = resolve_prompt(custom_prompt, state, runtime, "content", "default")
65
62
  messages = resolve_prompt("Custom system", state, runtime, "content", "default")
66
63
  messages = resolve_prompt(None, state, runtime, "content", "Default")
@@ -88,12 +85,10 @@ def resolve_prompt(
88
85
 
89
86
 
90
87
  async def aresolve_prompt(
91
- prompt: Union[
92
- str,
93
- None,
94
- Callable[[StateT, Runtime[ContextT]], list[MessageLikeRepresentation]],
95
- Callable[[StateT, Runtime[ContextT]], Awaitable[list[MessageLikeRepresentation]]],
96
- ],
88
+ prompt: str
89
+ | None
90
+ | Callable[[StateT, Runtime[ContextT]], list[MessageLikeRepresentation]]
91
+ | Callable[[StateT, Runtime[ContextT]], Awaitable[list[MessageLikeRepresentation]]],
97
92
  state: StateT,
98
93
  runtime: Runtime[ContextT],
99
94
  default_user_content: str,
@@ -128,15 +123,13 @@ async def aresolve_prompt(
128
123
  async def async_prompt(state, runtime):
129
124
  return [{"role": "system", "content": "Async"}]
130
125
 
126
+
131
127
  def sync_prompt(state, runtime):
132
128
  return [{"role": "system", "content": "Sync"}]
133
129
 
134
- messages = await aresolve_prompt(
135
- async_prompt, state, runtime, "content", "default"
136
- )
137
- messages = await aresolve_prompt(
138
- sync_prompt, state, runtime, "content", "default"
139
- )
130
+
131
+ messages = await aresolve_prompt(async_prompt, state, runtime, "content", "default")
132
+ messages = await aresolve_prompt(sync_prompt, state, runtime, "content", "default")
140
133
  messages = await aresolve_prompt("Custom", state, runtime, "content", "default")
141
134
  ```
142
135
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeAlias, TypeVar, Union
5
+ from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeAlias, TypeVar
6
6
 
7
7
  from langgraph.graph._node import StateNode
8
8
  from pydantic import BaseModel
@@ -44,7 +44,7 @@ class DataclassLike(Protocol):
44
44
  __dataclass_fields__: ClassVar[dict[str, Field[Any]]]
45
45
 
46
46
 
47
- StateLike: TypeAlias = Union[TypedDictLikeV1, TypedDictLikeV2, DataclassLike, BaseModel]
47
+ StateLike: TypeAlias = TypedDictLikeV1 | TypedDictLikeV2 | DataclassLike | BaseModel
48
48
  """Type alias for state-like types.
49
49
 
50
50
  It can either be a ``TypedDict``, ``dataclass``, or Pydantic ``BaseModel``.
@@ -58,7 +58,7 @@ It can either be a ``TypedDict``, ``dataclass``, or Pydantic ``BaseModel``.
58
58
  StateT = TypeVar("StateT", bound=StateLike)
59
59
  """Type variable used to represent the state in a graph."""
60
60
 
61
- ContextT = TypeVar("ContextT", bound=Union[StateLike, None])
61
+ ContextT = TypeVar("ContextT", bound=StateLike | None)
62
62
  """Type variable for context types."""
63
63
 
64
64
 
@@ -3,11 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Awaitable, Callable
6
- from typing import TypeVar, Union
6
+ from typing import TypeVar
7
7
 
8
8
  from typing_extensions import ParamSpec
9
9
 
10
10
  P = ParamSpec("P")
11
11
  R = TypeVar("R")
12
12
 
13
- SyncOrAsync = Callable[P, Union[R, Awaitable[R]]]
13
+ SyncOrAsync = Callable[P, R | Awaitable[R]]
@@ -1,5 +1,6 @@
1
1
  """Middleware plugins for agents."""
2
2
 
3
+ from .dynamic_system_prompt import DynamicSystemPromptMiddleware
3
4
  from .human_in_the_loop import HumanInTheLoopMiddleware
4
5
  from .prompt_caching import AnthropicPromptCachingMiddleware
5
6
  from .summarization import SummarizationMiddleware
@@ -8,7 +9,9 @@ from .types import AgentMiddleware, AgentState, ModelRequest
8
9
  __all__ = [
9
10
  "AgentMiddleware",
10
11
  "AgentState",
12
+ # should move to langchain-anthropic if we decide to keep it
11
13
  "AnthropicPromptCachingMiddleware",
14
+ "DynamicSystemPromptMiddleware",
12
15
  "HumanInTheLoopMiddleware",
13
16
  "ModelRequest",
14
17
  "SummarizationMiddleware",
@@ -0,0 +1,105 @@
1
+ """Dynamic System Prompt Middleware.
2
+
3
+ Allows setting the system prompt dynamically right before each model invocation.
4
+ Useful when the prompt depends on the current agent state or per-invocation context.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from inspect import signature
10
+ from typing import TYPE_CHECKING, Protocol, TypeAlias, cast
11
+
12
+ from langgraph.typing import ContextT
13
+
14
+ from langchain.agents.middleware.types import (
15
+ AgentMiddleware,
16
+ AgentState,
17
+ ModelRequest,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from langgraph.runtime import Runtime
22
+
23
+
24
+ class DynamicSystemPromptWithoutRuntime(Protocol):
25
+ """Dynamic system prompt without runtime in call signature."""
26
+
27
+ def __call__(self, state: AgentState) -> str:
28
+ """Return the system prompt for the next model call."""
29
+ ...
30
+
31
+
32
+ class DynamicSystemPromptWithRuntime(Protocol[ContextT]):
33
+ """Dynamic system prompt with runtime in call signature."""
34
+
35
+ def __call__(self, state: AgentState, runtime: Runtime[ContextT]) -> str:
36
+ """Return the system prompt for the next model call."""
37
+ ...
38
+
39
+
40
+ DynamicSystemPrompt: TypeAlias = (
41
+ DynamicSystemPromptWithoutRuntime | DynamicSystemPromptWithRuntime[ContextT]
42
+ )
43
+
44
+
45
+ class DynamicSystemPromptMiddleware(AgentMiddleware):
46
+ """Dynamic System Prompt Middleware.
47
+
48
+ Allows setting the system prompt dynamically right before each model invocation.
49
+ Useful when the prompt depends on the current agent state or per-invocation context.
50
+
51
+ Example:
52
+ ```python
53
+ from langchain.agents.middleware import DynamicSystemPromptMiddleware
54
+
55
+
56
+ class Context(TypedDict):
57
+ user_name: str
58
+
59
+
60
+ def system_prompt(state: AgentState, runtime: Runtime[Context]) -> str:
61
+ user_name = runtime.context.get("user_name", "n/a")
62
+ return (
63
+ f"You are a helpful assistant. Always address the user by their name: {user_name}"
64
+ )
65
+
66
+
67
+ middleware = DynamicSystemPromptMiddleware(system_prompt)
68
+ ```
69
+ """
70
+
71
+ _accepts_runtime: bool
72
+
73
+ def __init__(
74
+ self,
75
+ dynamic_system_prompt: DynamicSystemPrompt[ContextT],
76
+ ) -> None:
77
+ """Initialize the dynamic system prompt middleware.
78
+
79
+ Args:
80
+ dynamic_system_prompt: Function that receives the current agent state
81
+ and optionally runtime with context, and returns the system prompt for
82
+ the next model call. Returns a string.
83
+ """
84
+ super().__init__()
85
+ self.dynamic_system_prompt = dynamic_system_prompt
86
+ self._accepts_runtime = "runtime" in signature(dynamic_system_prompt).parameters
87
+
88
+ def modify_model_request(
89
+ self,
90
+ request: ModelRequest,
91
+ state: AgentState,
92
+ runtime: Runtime[ContextT],
93
+ ) -> ModelRequest:
94
+ """Modify the model request to include the dynamic system prompt."""
95
+ if self._accepts_runtime:
96
+ system_prompt = cast(
97
+ "DynamicSystemPromptWithRuntime[ContextT]", self.dynamic_system_prompt
98
+ )(state, runtime)
99
+ else:
100
+ system_prompt = cast("DynamicSystemPromptWithoutRuntime", self.dynamic_system_prompt)(
101
+ state
102
+ )
103
+
104
+ request.system_prompt = system_prompt
105
+ return request
@@ -1,19 +1,110 @@
1
1
  """Human in the loop middleware."""
2
2
 
3
- from typing import Any
4
-
5
- from langgraph.prebuilt.interrupt import (
6
- ActionRequest,
7
- HumanInterrupt,
8
- HumanInterruptConfig,
9
- HumanResponse,
10
- )
3
+ from typing import Any, Literal
4
+
5
+ from langchain_core.messages import AIMessage, ToolCall, ToolMessage
11
6
  from langgraph.types import interrupt
7
+ from typing_extensions import NotRequired, TypedDict
12
8
 
13
- from langchain.agents.middleware._utils import _generate_correction_tool_messages
14
9
  from langchain.agents.middleware.types import AgentMiddleware, AgentState
15
10
 
16
- ToolInterruptConfig = dict[str, HumanInterruptConfig]
11
+
12
+ class HumanInTheLoopConfig(TypedDict):
13
+ """Configuration that defines what actions are allowed for a human interrupt.
14
+
15
+ This controls the available interaction options when the graph is paused for human input.
16
+ """
17
+
18
+ allow_accept: NotRequired[bool]
19
+ """Whether the human can approve the current action without changes."""
20
+ allow_edit: NotRequired[bool]
21
+ """Whether the human can approve the current action with edited content."""
22
+ allow_respond: NotRequired[bool]
23
+ """Whether the human can reject the current action with feedback."""
24
+
25
+
26
+ class ActionRequest(TypedDict):
27
+ """Represents a request with a name and arguments."""
28
+
29
+ action: str
30
+ """The type or name of action being requested (e.g., "add_numbers")."""
31
+ args: dict
32
+ """Key-value pairs of arguments needed for the action (e.g., {"a": 1, "b": 2})."""
33
+
34
+
35
+ class HumanInTheLoopRequest(TypedDict):
36
+ """Represents an interrupt triggered by the graph that requires human intervention.
37
+
38
+ Example:
39
+ ```python
40
+ # Extract a tool call from the state and create an interrupt request
41
+ request = HumanInterrupt(
42
+ action_request=ActionRequest(
43
+ action="run_command", # The action being requested
44
+ args={"command": "ls", "args": ["-l"]}, # Arguments for the action
45
+ ),
46
+ config=HumanInTheLoopConfig(
47
+ allow_accept=True, # Allow approval
48
+ allow_respond=True, # Allow rejection with feedback
49
+ allow_edit=False, # Don't allow approval with edits
50
+ ),
51
+ description="Please review the command before execution",
52
+ )
53
+ # Send the interrupt request and get the response
54
+ response = interrupt([request])[0]
55
+ ```
56
+ """
57
+
58
+ action_request: ActionRequest
59
+ """The specific action being requested from the human."""
60
+ config: HumanInTheLoopConfig
61
+ """Configuration defining what response types are allowed."""
62
+ description: str | None
63
+ """Optional detailed description of what input is needed."""
64
+
65
+
66
+ class AcceptPayload(TypedDict):
67
+ """Response when a human approves the action."""
68
+
69
+ type: Literal["accept"]
70
+ """The type of response when a human approves the action."""
71
+
72
+
73
+ class ResponsePayload(TypedDict):
74
+ """Response when a human rejects the action."""
75
+
76
+ type: Literal["response"]
77
+ """The type of response when a human rejects the action."""
78
+
79
+ args: NotRequired[str]
80
+ """The message to be sent to the model explaining why the action was rejected."""
81
+
82
+
83
+ class EditPayload(TypedDict):
84
+ """Response when a human edits the action."""
85
+
86
+ type: Literal["edit"]
87
+ """The type of response when a human edits the action."""
88
+
89
+ args: ActionRequest
90
+ """The action request with the edited content."""
91
+
92
+
93
+ HumanInTheLoopResponse = AcceptPayload | ResponsePayload | EditPayload
94
+ """Aggregated response type for all possible human in the loop responses."""
95
+
96
+
97
+ class ToolConfig(TypedDict):
98
+ """Configuration for a tool requiring human in the loop."""
99
+
100
+ allow_accept: NotRequired[bool]
101
+ """Whether the human can approve the current action without changes."""
102
+ allow_edit: NotRequired[bool]
103
+ """Whether the human can approve the current action with edited content."""
104
+ allow_respond: NotRequired[bool]
105
+ """Whether the human can reject the current action with feedback."""
106
+ description: NotRequired[str]
107
+ """The description attached to the request for human input."""
17
108
 
18
109
 
19
110
  class HumanInTheLoopMiddleware(AgentMiddleware):
@@ -21,108 +112,142 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
21
112
 
22
113
  def __init__(
23
114
  self,
24
- tool_configs: ToolInterruptConfig,
25
- message_prefix: str = "Tool execution requires approval",
115
+ tool_configs: dict[str, bool | ToolConfig],
116
+ *,
117
+ description_prefix: str = "Tool execution requires approval",
26
118
  ) -> None:
27
119
  """Initialize the human in the loop middleware.
28
120
 
29
121
  Args:
30
- tool_configs: The tool interrupt configs to use for the middleware.
31
- message_prefix: The message prefix to use when constructing interrupt content.
122
+ tool_configs: Mapping of tool name to allowed actions.
123
+ If a tool doesn't have an entry, it's auto-approved by default.
124
+ * `True` indicates all actions are allowed: accept, edit, and respond.
125
+ * `False` indicates that the tool is auto-approved.
126
+ * ToolConfig indicates the specific actions allowed for this tool.
127
+ description_prefix: The prefix to use when constructing action requests.
128
+ This is used to provide context about the tool call and the action being requested.
129
+ Not used if a tool has a description in its ToolConfig.
32
130
  """
33
131
  super().__init__()
34
- self.tool_configs = tool_configs
35
- self.message_prefix = message_prefix
132
+ resolved_tool_configs: dict[str, ToolConfig] = {}
133
+ for tool_name, tool_config in tool_configs.items():
134
+ if isinstance(tool_config, bool):
135
+ if tool_config is True:
136
+ resolved_tool_configs[tool_name] = ToolConfig(
137
+ allow_accept=True,
138
+ allow_edit=True,
139
+ allow_respond=True,
140
+ )
141
+ else:
142
+ resolved_tool_configs[tool_name] = tool_config
143
+ self.tool_configs = resolved_tool_configs
144
+ self.description_prefix = description_prefix
36
145
 
37
- def after_model(self, state: AgentState) -> dict[str, Any] | None:
146
+ def after_model(self, state: AgentState) -> dict[str, Any] | None: # type: ignore[override]
38
147
  """Trigger HITL flows for relevant tool calls after an AIMessage."""
39
148
  messages = state["messages"]
40
149
  if not messages:
41
150
  return None
42
151
 
43
- last_message = messages[-1]
44
-
45
- if not hasattr(last_message, "tool_calls") or not last_message.tool_calls:
152
+ last_ai_msg = next((msg for msg in messages if isinstance(msg, AIMessage)), None)
153
+ if not last_ai_msg or not last_ai_msg.tool_calls:
46
154
  return None
47
155
 
48
156
  # Separate tool calls that need interrupts from those that don't
49
- interrupt_tool_calls = []
157
+ hitl_tool_calls: list[ToolCall] = []
50
158
  auto_approved_tool_calls = []
51
159
 
52
- for tool_call in last_message.tool_calls:
53
- tool_name = tool_call["name"]
54
- if tool_name in self.tool_configs:
55
- interrupt_tool_calls.append(tool_call)
56
- else:
57
- auto_approved_tool_calls.append(tool_call)
160
+ for tool_call in last_ai_msg.tool_calls:
161
+ hitl_tool_calls.append(tool_call) if tool_call[
162
+ "name"
163
+ ] in self.tool_configs else auto_approved_tool_calls.append(tool_call)
58
164
 
59
165
  # If no interrupts needed, return early
60
- if not interrupt_tool_calls:
166
+ if not hitl_tool_calls:
61
167
  return None
62
168
 
63
- approved_tool_calls = auto_approved_tool_calls.copy()
169
+ # Process all tool calls that require interrupts
170
+ approved_tool_calls: list[ToolCall] = auto_approved_tool_calls.copy()
171
+ artificial_tool_messages: list[ToolMessage] = []
64
172
 
65
- # Right now, we do not support multiple tool calls with interrupts
66
- if len(interrupt_tool_calls) > 1:
67
- tool_names = [t["name"] for t in interrupt_tool_calls]
68
- msg = f"Called the following tools which require interrupts: {tool_names}\n\nYou may only call ONE tool that requires an interrupt at a time"
69
- return {
70
- "messages": _generate_correction_tool_messages(msg, last_message.tool_calls),
71
- "jump_to": "model",
173
+ # Create interrupt requests for all tools that need approval
174
+ hitl_requests: list[HumanInTheLoopRequest] = []
175
+ for tool_call in hitl_tool_calls:
176
+ tool_name = tool_call["name"]
177
+ tool_args = tool_call["args"]
178
+ config = self.tool_configs[tool_name]
179
+ description = (
180
+ config.get("description")
181
+ or f"{self.description_prefix}\n\nTool: {tool_name}\nArgs: {tool_args}"
182
+ )
183
+
184
+ request: HumanInTheLoopRequest = {
185
+ "action_request": ActionRequest(
186
+ action=tool_name,
187
+ args=tool_args,
188
+ ),
189
+ "config": config,
190
+ "description": description,
72
191
  }
192
+ hitl_requests.append(request)
73
193
 
74
- # Right now, we do not support interrupting a tool call if other tool calls exist
75
- if auto_approved_tool_calls:
76
- tool_names = [t["name"] for t in interrupt_tool_calls]
77
- msg = f"Called the following tools which require interrupts: {tool_names}. You also called other tools that do not require interrupts. If you call a tool that requires and interrupt, you may ONLY call that tool."
78
- return {
79
- "messages": _generate_correction_tool_messages(msg, last_message.tool_calls),
80
- "jump_to": "model",
81
- }
194
+ responses: list[HumanInTheLoopResponse] = interrupt(hitl_requests)
82
195
 
83
- # Only one tool call will need interrupts
84
- tool_call = interrupt_tool_calls[0]
85
- tool_name = tool_call["name"]
86
- tool_args = tool_call["args"]
87
- description = f"{self.message_prefix}\n\nTool: {tool_name}\nArgs: {tool_args}"
88
- tool_config = self.tool_configs[tool_name]
89
-
90
- request: HumanInterrupt = {
91
- "action_request": ActionRequest(
92
- action=tool_name,
93
- args=tool_args,
94
- ),
95
- "config": tool_config,
96
- "description": description,
97
- }
98
-
99
- responses: list[HumanResponse] = interrupt([request])
100
- response = responses[0]
101
-
102
- if response["type"] == "accept":
103
- approved_tool_calls.append(tool_call)
104
- elif response["type"] == "edit":
105
- edited: ActionRequest = response["args"] # type: ignore[assignment]
106
- new_tool_call = {
107
- "type": "tool_call",
108
- "name": tool_call["name"],
109
- "args": edited["args"],
110
- "id": tool_call["id"],
111
- }
112
- approved_tool_calls.append(new_tool_call)
113
- elif response["type"] == "ignore":
114
- return {"jump_to": "__end__"}
115
- elif response["type"] == "response":
116
- tool_message = {
117
- "role": "tool",
118
- "tool_call_id": tool_call["id"],
119
- "content": response["args"],
120
- }
121
- return {"messages": [tool_message], "jump_to": "model"}
122
- else:
123
- msg = f"Unknown response type: {response['type']}"
196
+ # Validate that the number of responses matches the number of interrupt tool calls
197
+ if (responses_len := len(responses)) != (hitl_tool_calls_len := len(hitl_tool_calls)):
198
+ msg = (
199
+ f"Number of human responses ({responses_len}) does not match "
200
+ f"number of hanging tool calls ({hitl_tool_calls_len})."
201
+ )
124
202
  raise ValueError(msg)
125
203
 
126
- last_message.tool_calls = approved_tool_calls
127
-
128
- return {"messages": [last_message]}
204
+ for i, response in enumerate(responses):
205
+ tool_call = hitl_tool_calls[i]
206
+ config = self.tool_configs[tool_call["name"]]
207
+
208
+ if response["type"] == "accept" and config.get("allow_accept"):
209
+ approved_tool_calls.append(tool_call)
210
+ elif response["type"] == "edit" and config.get("allow_edit"):
211
+ edited_action = response["args"]
212
+ approved_tool_calls.append(
213
+ ToolCall(
214
+ type="tool_call",
215
+ name=edited_action["action"],
216
+ args=edited_action["args"],
217
+ id=tool_call["id"],
218
+ )
219
+ )
220
+ elif response["type"] == "response" and config.get("allow_respond"):
221
+ # Create a tool message with the human's text response
222
+ content = response.get("args") or (
223
+ f"User rejected the tool call for `{tool_call['name']}` "
224
+ f"with id {tool_call['id']}"
225
+ )
226
+ tool_message = ToolMessage(
227
+ content=content,
228
+ name=tool_call["name"],
229
+ tool_call_id=tool_call["id"],
230
+ status="error",
231
+ )
232
+ artificial_tool_messages.append(tool_message)
233
+ else:
234
+ allowed_actions = [
235
+ action
236
+ for action in ["accept", "edit", "response"]
237
+ if config.get(f"allow_{'respond' if action == 'response' else action}")
238
+ ]
239
+ msg = (
240
+ f"Unexpected human response: {response}. "
241
+ f"Response action '{response.get('type')}' "
242
+ f"is not allowed for tool '{tool_call['name']}'. "
243
+ f"Expected one of {allowed_actions} based on the tool's configuration."
244
+ )
245
+ raise ValueError(msg)
246
+
247
+ # Update the AI message to only include approved tool calls
248
+ last_ai_msg.tool_calls = approved_tool_calls
249
+
250
+ if len(approved_tool_calls) > 0:
251
+ return {"messages": [last_ai_msg, *artificial_tool_messages]}
252
+
253
+ return {"jump_to": "model", "messages": artificial_tool_messages}
@@ -2,13 +2,16 @@
2
2
 
3
3
  from typing import Literal
4
4
 
5
- from langchain.agents.middleware.types import AgentMiddleware, AgentState, ModelRequest
5
+ from langchain.agents.middleware.types import AgentMiddleware, ModelRequest
6
6
 
7
7
 
8
8
  class AnthropicPromptCachingMiddleware(AgentMiddleware):
9
- """Prompt Caching Middleware - Optimizes API usage by caching conversation prefixes for Anthropic models.
9
+ """Prompt Caching Middleware.
10
10
 
11
- Learn more about anthropic prompt caching [here](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching).
11
+ Optimizes API usage by caching conversation prefixes for Anthropic models.
12
+
13
+ Learn more about Anthropic prompt caching
14
+ `here <https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching>`__.
12
15
  """
13
16
 
14
17
  def __init__(
@@ -22,27 +25,32 @@ class AnthropicPromptCachingMiddleware(AgentMiddleware):
22
25
  Args:
23
26
  type: The type of cache to use, only "ephemeral" is supported.
24
27
  ttl: The time to live for the cache, only "5m" and "1h" are supported.
25
- min_messages_to_cache: The minimum number of messages until the cache is used, default is 0.
28
+ min_messages_to_cache: The minimum number of messages until the cache is used,
29
+ default is 0.
26
30
  """
27
31
  self.type = type
28
32
  self.ttl = ttl
29
33
  self.min_messages_to_cache = min_messages_to_cache
30
34
 
31
- def modify_model_request(self, request: ModelRequest, state: AgentState) -> ModelRequest: # noqa: ARG002
35
+ def modify_model_request( # type: ignore[override]
36
+ self,
37
+ request: ModelRequest,
38
+ ) -> ModelRequest:
32
39
  """Modify the model request to add cache control blocks."""
33
40
  try:
34
41
  from langchain_anthropic import ChatAnthropic
35
42
  except ImportError:
36
43
  msg = (
37
- "AnthropicPromptCachingMiddleware caching middleware only supports Anthropic models."
44
+ "AnthropicPromptCachingMiddleware caching middleware only supports "
45
+ "Anthropic models."
38
46
  "Please install langchain-anthropic."
39
47
  )
40
48
  raise ValueError(msg)
41
49
 
42
50
  if not isinstance(request.model, ChatAnthropic):
43
51
  msg = (
44
- "AnthropicPromptCachingMiddleware caching middleware only supports Anthropic models, "
45
- f"not instances of {type(request.model)}"
52
+ "AnthropicPromptCachingMiddleware caching middleware only supports "
53
+ f"Anthropic models, not instances of {type(request.model)}"
46
54
  )
47
55
  raise ValueError(msg)
48
56