langchain 1.0.0a11__py3-none-any.whl → 1.0.0a13__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 +1 -1
- langchain/agents/factory.py +511 -180
- langchain/agents/middleware/__init__.py +9 -3
- langchain/agents/middleware/context_editing.py +15 -14
- langchain/agents/middleware/human_in_the_loop.py +213 -170
- langchain/agents/middleware/model_call_limit.py +2 -2
- langchain/agents/middleware/model_fallback.py +46 -36
- langchain/agents/middleware/pii.py +19 -19
- langchain/agents/middleware/planning.py +16 -11
- langchain/agents/middleware/prompt_caching.py +14 -11
- langchain/agents/middleware/summarization.py +1 -1
- langchain/agents/middleware/tool_call_limit.py +5 -5
- langchain/agents/middleware/tool_emulator.py +200 -0
- langchain/agents/middleware/tool_selection.py +25 -21
- langchain/agents/middleware/types.py +484 -225
- langchain/chat_models/base.py +85 -90
- langchain/embeddings/base.py +20 -20
- langchain/embeddings/cache.py +21 -21
- langchain/messages/__init__.py +2 -0
- langchain/storage/encoder_backed.py +22 -23
- langchain/tools/tool_node.py +388 -80
- {langchain-1.0.0a11.dist-info → langchain-1.0.0a13.dist-info}/METADATA +8 -5
- langchain-1.0.0a13.dist-info/RECORD +36 -0
- langchain/_internal/__init__.py +0 -0
- langchain/_internal/_documents.py +0 -35
- langchain/_internal/_lazy_import.py +0 -35
- langchain/_internal/_prompts.py +0 -158
- langchain/_internal/_typing.py +0 -70
- langchain/_internal/_utils.py +0 -7
- langchain/agents/_internal/__init__.py +0 -1
- langchain/agents/_internal/_typing.py +0 -13
- langchain-1.0.0a11.dist-info/RECORD +0 -43
- {langchain-1.0.0a11.dist-info → langchain-1.0.0a13.dist-info}/WHEEL +0 -0
- {langchain-1.0.0a11.dist-info → langchain-1.0.0a13.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,7 +4,10 @@ from .context_editing import (
|
|
|
4
4
|
ClearToolUsesEdit,
|
|
5
5
|
ContextEditingMiddleware,
|
|
6
6
|
)
|
|
7
|
-
from .human_in_the_loop import
|
|
7
|
+
from .human_in_the_loop import (
|
|
8
|
+
HumanInTheLoopMiddleware,
|
|
9
|
+
InterruptOnConfig,
|
|
10
|
+
)
|
|
8
11
|
from .model_call_limit import ModelCallLimitMiddleware
|
|
9
12
|
from .model_fallback import ModelFallbackMiddleware
|
|
10
13
|
from .pii import PIIDetectionError, PIIMiddleware
|
|
@@ -12,6 +15,7 @@ from .planning import PlanningMiddleware
|
|
|
12
15
|
from .prompt_caching import AnthropicPromptCachingMiddleware
|
|
13
16
|
from .summarization import SummarizationMiddleware
|
|
14
17
|
from .tool_call_limit import ToolCallLimitMiddleware
|
|
18
|
+
from .tool_emulator import LLMToolEmulator
|
|
15
19
|
from .tool_selection import LLMToolSelectorMiddleware
|
|
16
20
|
from .types import (
|
|
17
21
|
AgentMiddleware,
|
|
@@ -23,7 +27,7 @@ from .types import (
|
|
|
23
27
|
before_model,
|
|
24
28
|
dynamic_prompt,
|
|
25
29
|
hook_config,
|
|
26
|
-
|
|
30
|
+
wrap_model_call,
|
|
27
31
|
)
|
|
28
32
|
|
|
29
33
|
__all__ = [
|
|
@@ -34,6 +38,8 @@ __all__ = [
|
|
|
34
38
|
"ClearToolUsesEdit",
|
|
35
39
|
"ContextEditingMiddleware",
|
|
36
40
|
"HumanInTheLoopMiddleware",
|
|
41
|
+
"InterruptOnConfig",
|
|
42
|
+
"LLMToolEmulator",
|
|
37
43
|
"LLMToolSelectorMiddleware",
|
|
38
44
|
"ModelCallLimitMiddleware",
|
|
39
45
|
"ModelFallbackMiddleware",
|
|
@@ -49,5 +55,5 @@ __all__ = [
|
|
|
49
55
|
"before_model",
|
|
50
56
|
"dynamic_prompt",
|
|
51
57
|
"hook_config",
|
|
52
|
-
"
|
|
58
|
+
"wrap_model_call",
|
|
53
59
|
]
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from collections.abc import Callable, Iterable, Sequence
|
|
12
12
|
from dataclasses import dataclass
|
|
13
|
-
from typing import
|
|
13
|
+
from typing import Literal
|
|
14
14
|
|
|
15
15
|
from langchain_core.messages import (
|
|
16
16
|
AIMessage,
|
|
@@ -22,10 +22,12 @@ from langchain_core.messages import (
|
|
|
22
22
|
from langchain_core.messages.utils import count_tokens_approximately
|
|
23
23
|
from typing_extensions import Protocol
|
|
24
24
|
|
|
25
|
-
from langchain.agents.middleware.types import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
from langchain.agents.middleware.types import (
|
|
26
|
+
AgentMiddleware,
|
|
27
|
+
ModelCallResult,
|
|
28
|
+
ModelRequest,
|
|
29
|
+
ModelResponse,
|
|
30
|
+
)
|
|
29
31
|
|
|
30
32
|
DEFAULT_TOOL_PLACEHOLDER = "[cleared]"
|
|
31
33
|
|
|
@@ -183,8 +185,8 @@ class ContextEditingMiddleware(AgentMiddleware):
|
|
|
183
185
|
"""Middleware that automatically prunes tool results to manage context size.
|
|
184
186
|
|
|
185
187
|
The middleware applies a sequence of edits when the total input token count
|
|
186
|
-
exceeds configured thresholds. Currently the
|
|
187
|
-
supported, aligning with Anthropic's
|
|
188
|
+
exceeds configured thresholds. Currently the `ClearToolUsesEdit` strategy is
|
|
189
|
+
supported, aligning with Anthropic's `clear_tool_uses_20250919` behaviour.
|
|
188
190
|
"""
|
|
189
191
|
|
|
190
192
|
edits: list[ContextEdit]
|
|
@@ -209,15 +211,14 @@ class ContextEditingMiddleware(AgentMiddleware):
|
|
|
209
211
|
self.edits = list(edits or (ClearToolUsesEdit(),))
|
|
210
212
|
self.token_count_method = token_count_method
|
|
211
213
|
|
|
212
|
-
def
|
|
214
|
+
def wrap_model_call(
|
|
213
215
|
self,
|
|
214
216
|
request: ModelRequest,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
"""Modify the model request by applying context edits before invocation."""
|
|
217
|
+
handler: Callable[[ModelRequest], ModelResponse],
|
|
218
|
+
) -> ModelCallResult:
|
|
219
|
+
"""Apply context edits before invoking the model via handler."""
|
|
219
220
|
if not request.messages:
|
|
220
|
-
return request
|
|
221
|
+
return handler(request)
|
|
221
222
|
|
|
222
223
|
if self.token_count_method == "approximate": # noqa: S105
|
|
223
224
|
|
|
@@ -236,7 +237,7 @@ class ContextEditingMiddleware(AgentMiddleware):
|
|
|
236
237
|
for edit in self.edits:
|
|
237
238
|
edit.apply(request.messages, count_tokens=count_tokens)
|
|
238
239
|
|
|
239
|
-
return request
|
|
240
|
+
return handler(request)
|
|
240
241
|
|
|
241
242
|
|
|
242
243
|
__all__ = [
|
|
@@ -10,89 +10,93 @@ from typing_extensions import NotRequired, TypedDict
|
|
|
10
10
|
from langchain.agents.middleware.types import AgentMiddleware, AgentState
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class
|
|
14
|
-
"""
|
|
13
|
+
class Action(TypedDict):
|
|
14
|
+
"""Represents an action with a name and arguments."""
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
"""
|
|
16
|
+
name: str
|
|
17
|
+
"""The type or name of action being requested (e.g., "add_numbers")."""
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
"""
|
|
21
|
-
allow_edit: NotRequired[bool]
|
|
22
|
-
"""Whether the human can approve the current action with edited content."""
|
|
23
|
-
allow_respond: NotRequired[bool]
|
|
24
|
-
"""Whether the human can reject the current action with feedback."""
|
|
19
|
+
arguments: dict[str, Any]
|
|
20
|
+
"""Key-value pairs of arguments needed for the action (e.g., {"a": 1, "b": 2})."""
|
|
25
21
|
|
|
26
22
|
|
|
27
23
|
class ActionRequest(TypedDict):
|
|
28
|
-
"""Represents
|
|
24
|
+
"""Represents an action request with a name, arguments, and description."""
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
"""The
|
|
32
|
-
|
|
26
|
+
name: str
|
|
27
|
+
"""The name of the action being requested."""
|
|
28
|
+
|
|
29
|
+
arguments: dict[str, Any]
|
|
33
30
|
"""Key-value pairs of arguments needed for the action (e.g., {"a": 1, "b": 2})."""
|
|
34
31
|
|
|
32
|
+
description: NotRequired[str]
|
|
33
|
+
"""The description of the action to be reviewed."""
|
|
35
34
|
|
|
36
|
-
class HumanInTheLoopRequest(TypedDict):
|
|
37
|
-
"""Represents an interrupt triggered by the graph that requires human intervention.
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
```python
|
|
41
|
-
# Extract a tool call from the state and create an interrupt request
|
|
42
|
-
request = HumanInterrupt(
|
|
43
|
-
action_request=ActionRequest(
|
|
44
|
-
action="run_command", # The action being requested
|
|
45
|
-
args={"command": "ls", "args": ["-l"]}, # Arguments for the action
|
|
46
|
-
),
|
|
47
|
-
config=HumanInTheLoopConfig(
|
|
48
|
-
allow_accept=True, # Allow approval
|
|
49
|
-
allow_respond=True, # Allow rejection with feedback
|
|
50
|
-
allow_edit=False, # Don't allow approval with edits
|
|
51
|
-
),
|
|
52
|
-
description="Please review the command before execution",
|
|
53
|
-
)
|
|
54
|
-
# Send the interrupt request and get the response
|
|
55
|
-
response = interrupt([request])[0]
|
|
56
|
-
```
|
|
57
|
-
"""
|
|
36
|
+
DecisionType = Literal["approve", "edit", "reject"]
|
|
58
37
|
|
|
59
|
-
action_request: ActionRequest
|
|
60
|
-
"""The specific action being requested from the human."""
|
|
61
|
-
config: HumanInTheLoopConfig
|
|
62
|
-
"""Configuration defining what response types are allowed."""
|
|
63
|
-
description: str | None
|
|
64
|
-
"""Optional detailed description of what input is needed."""
|
|
65
38
|
|
|
39
|
+
class ReviewConfig(TypedDict):
|
|
40
|
+
"""Policy for reviewing a HITL request."""
|
|
66
41
|
|
|
67
|
-
|
|
68
|
-
"""
|
|
42
|
+
action_name: str
|
|
43
|
+
"""Name of the action associated with this review configuration."""
|
|
69
44
|
|
|
70
|
-
|
|
71
|
-
"""The
|
|
45
|
+
allowed_decisions: list[DecisionType]
|
|
46
|
+
"""The decisions that are allowed for this request."""
|
|
72
47
|
|
|
48
|
+
arguments_schema: NotRequired[dict[str, Any]]
|
|
49
|
+
"""JSON schema for the arguments associated with the action, if edits are allowed."""
|
|
73
50
|
|
|
74
|
-
class ResponsePayload(TypedDict):
|
|
75
|
-
"""Response when a human rejects the action."""
|
|
76
51
|
|
|
77
|
-
|
|
78
|
-
"""
|
|
52
|
+
class HITLRequest(TypedDict):
|
|
53
|
+
"""Request for human feedback on a sequence of actions requested by a model."""
|
|
54
|
+
|
|
55
|
+
action_requests: list[ActionRequest]
|
|
56
|
+
"""A list of agent actions for human review."""
|
|
79
57
|
|
|
80
|
-
|
|
81
|
-
"""
|
|
58
|
+
review_configs: list[ReviewConfig]
|
|
59
|
+
"""Review configuration for all possible actions."""
|
|
82
60
|
|
|
83
61
|
|
|
84
|
-
class
|
|
62
|
+
class ApproveDecision(TypedDict):
|
|
63
|
+
"""Response when a human approves the action."""
|
|
64
|
+
|
|
65
|
+
type: Literal["approve"]
|
|
66
|
+
"""The type of response when a human approves the action."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EditDecision(TypedDict):
|
|
85
70
|
"""Response when a human edits the action."""
|
|
86
71
|
|
|
87
72
|
type: Literal["edit"]
|
|
88
73
|
"""The type of response when a human edits the action."""
|
|
89
74
|
|
|
90
|
-
|
|
91
|
-
"""
|
|
75
|
+
edited_action: Action
|
|
76
|
+
"""Edited action for the agent to perform.
|
|
77
|
+
|
|
78
|
+
Ex: for a tool call, a human reviewer can edit the tool name and args.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class RejectDecision(TypedDict):
|
|
83
|
+
"""Response when a human rejects the action."""
|
|
84
|
+
|
|
85
|
+
type: Literal["reject"]
|
|
86
|
+
"""The type of response when a human rejects the action."""
|
|
87
|
+
|
|
88
|
+
message: NotRequired[str]
|
|
89
|
+
"""The message sent to the model explaining why the action was rejected."""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
Decision = ApproveDecision | EditDecision | RejectDecision
|
|
92
93
|
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
"""
|
|
95
|
+
class HITLResponse(TypedDict):
|
|
96
|
+
"""Response payload for a HITLRequest."""
|
|
97
|
+
|
|
98
|
+
decisions: list[Decision]
|
|
99
|
+
"""The decisions made by the human."""
|
|
96
100
|
|
|
97
101
|
|
|
98
102
|
class _DescriptionFactory(Protocol):
|
|
@@ -103,49 +107,51 @@ class _DescriptionFactory(Protocol):
|
|
|
103
107
|
...
|
|
104
108
|
|
|
105
109
|
|
|
106
|
-
class
|
|
107
|
-
"""Configuration for
|
|
110
|
+
class InterruptOnConfig(TypedDict):
|
|
111
|
+
"""Configuration for an action requiring human in the loop.
|
|
112
|
+
|
|
113
|
+
This is the configuration format used in the `HumanInTheLoopMiddleware.__init__` method.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
allowed_decisions: list[DecisionType]
|
|
117
|
+
"""The decisions that are allowed for this action."""
|
|
108
118
|
|
|
109
|
-
allow_accept: NotRequired[bool]
|
|
110
|
-
"""Whether the human can approve the current action without changes."""
|
|
111
|
-
allow_edit: NotRequired[bool]
|
|
112
|
-
"""Whether the human can approve the current action with edited content."""
|
|
113
|
-
allow_respond: NotRequired[bool]
|
|
114
|
-
"""Whether the human can reject the current action with feedback."""
|
|
115
119
|
description: NotRequired[str | _DescriptionFactory]
|
|
116
120
|
"""The description attached to the request for human input.
|
|
117
121
|
|
|
118
122
|
Can be either:
|
|
119
123
|
- A static string describing the approval request
|
|
120
124
|
- A callable that dynamically generates the description based on agent state,
|
|
121
|
-
|
|
125
|
+
runtime, and tool call information
|
|
122
126
|
|
|
123
127
|
Example:
|
|
124
|
-
|
|
128
|
+
```python
|
|
129
|
+
# Static string description
|
|
130
|
+
config = ToolConfig(
|
|
131
|
+
allowed_decisions=["approve", "reject"],
|
|
132
|
+
description="Please review this tool execution"
|
|
133
|
+
)
|
|
125
134
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
135
|
+
# Dynamic callable description
|
|
136
|
+
def format_tool_description(
|
|
137
|
+
tool_call: ToolCall,
|
|
138
|
+
state: AgentState,
|
|
139
|
+
runtime: Runtime
|
|
140
|
+
) -> str:
|
|
141
|
+
import json
|
|
142
|
+
return (
|
|
143
|
+
f"Tool: {tool_call['name']}\\n"
|
|
144
|
+
f"Arguments:\\n{json.dumps(tool_call['args'], indent=2)}"
|
|
130
145
|
)
|
|
131
146
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
) -> str:
|
|
138
|
-
import json
|
|
139
|
-
return (
|
|
140
|
-
f"Tool: {tool_call['name']}\\n"
|
|
141
|
-
f"Arguments:\\n{json.dumps(tool_call['args'], indent=2)}"
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
config = ToolConfig(
|
|
145
|
-
allow_accept=True,
|
|
146
|
-
description=format_tool_description
|
|
147
|
-
)
|
|
147
|
+
config = InterruptOnConfig(
|
|
148
|
+
allowed_decisions=["approve", "edit", "reject"],
|
|
149
|
+
description=format_tool_description
|
|
150
|
+
)
|
|
151
|
+
```
|
|
148
152
|
"""
|
|
153
|
+
arguments_schema: NotRequired[dict[str, Any]]
|
|
154
|
+
"""JSON schema for the arguments associated with the action, if edits are allowed."""
|
|
149
155
|
|
|
150
156
|
|
|
151
157
|
class HumanInTheLoopMiddleware(AgentMiddleware):
|
|
@@ -153,7 +159,7 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
|
|
|
153
159
|
|
|
154
160
|
def __init__(
|
|
155
161
|
self,
|
|
156
|
-
interrupt_on: dict[str, bool |
|
|
162
|
+
interrupt_on: dict[str, bool | InterruptOnConfig],
|
|
157
163
|
*,
|
|
158
164
|
description_prefix: str = "Tool execution requires approval",
|
|
159
165
|
) -> None:
|
|
@@ -163,32 +169,106 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
|
|
|
163
169
|
interrupt_on: Mapping of tool name to allowed actions.
|
|
164
170
|
If a tool doesn't have an entry, it's auto-approved by default.
|
|
165
171
|
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
|
|
170
|
-
|
|
172
|
+
* `True` indicates all decisions are allowed: approve, edit, and reject.
|
|
173
|
+
* `False` indicates that the tool is auto-approved.
|
|
174
|
+
* `InterruptOnConfig` indicates the specific decisions allowed for this tool.
|
|
175
|
+
The InterruptOnConfig can include a `description` field (str or callable) for
|
|
176
|
+
custom formatting of the interrupt description.
|
|
171
177
|
description_prefix: The prefix to use when constructing action requests.
|
|
172
178
|
This is used to provide context about the tool call and the action being requested.
|
|
173
|
-
Not used if a tool has a
|
|
179
|
+
Not used if a tool has a `description` in its InterruptOnConfig.
|
|
174
180
|
"""
|
|
175
181
|
super().__init__()
|
|
176
|
-
|
|
182
|
+
resolved_configs: dict[str, InterruptOnConfig] = {}
|
|
177
183
|
for tool_name, tool_config in interrupt_on.items():
|
|
178
184
|
if isinstance(tool_config, bool):
|
|
179
185
|
if tool_config is True:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
allow_edit=True,
|
|
183
|
-
allow_respond=True,
|
|
186
|
+
resolved_configs[tool_name] = InterruptOnConfig(
|
|
187
|
+
allowed_decisions=["approve", "edit", "reject"]
|
|
184
188
|
)
|
|
185
|
-
elif
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
resolved_tool_configs[tool_name] = tool_config
|
|
189
|
-
self.interrupt_on = resolved_tool_configs
|
|
189
|
+
elif tool_config.get("allowed_decisions"):
|
|
190
|
+
resolved_configs[tool_name] = tool_config
|
|
191
|
+
self.interrupt_on = resolved_configs
|
|
190
192
|
self.description_prefix = description_prefix
|
|
191
193
|
|
|
194
|
+
def _create_action_and_config(
|
|
195
|
+
self,
|
|
196
|
+
tool_call: ToolCall,
|
|
197
|
+
config: InterruptOnConfig,
|
|
198
|
+
state: AgentState,
|
|
199
|
+
runtime: Runtime,
|
|
200
|
+
) -> tuple[ActionRequest, ReviewConfig]:
|
|
201
|
+
"""Create an ActionRequest and ReviewConfig for a tool call."""
|
|
202
|
+
tool_name = tool_call["name"]
|
|
203
|
+
tool_args = tool_call["args"]
|
|
204
|
+
|
|
205
|
+
# Generate description using the description field (str or callable)
|
|
206
|
+
description_value = config.get("description")
|
|
207
|
+
if callable(description_value):
|
|
208
|
+
description = description_value(tool_call, state, runtime)
|
|
209
|
+
elif description_value is not None:
|
|
210
|
+
description = description_value
|
|
211
|
+
else:
|
|
212
|
+
description = f"{self.description_prefix}\n\nTool: {tool_name}\nArgs: {tool_args}"
|
|
213
|
+
|
|
214
|
+
# Create ActionRequest with description
|
|
215
|
+
action_request = ActionRequest(
|
|
216
|
+
name=tool_name,
|
|
217
|
+
arguments=tool_args,
|
|
218
|
+
description=description,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Create ReviewConfig
|
|
222
|
+
# eventually can get tool information and populate arguments_schema from there
|
|
223
|
+
review_config = ReviewConfig(
|
|
224
|
+
action_name=tool_name,
|
|
225
|
+
allowed_decisions=config["allowed_decisions"],
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return action_request, review_config
|
|
229
|
+
|
|
230
|
+
def _process_decision(
|
|
231
|
+
self,
|
|
232
|
+
decision: Decision,
|
|
233
|
+
tool_call: ToolCall,
|
|
234
|
+
config: InterruptOnConfig,
|
|
235
|
+
) -> tuple[ToolCall | None, ToolMessage | None]:
|
|
236
|
+
"""Process a single decision and return the revised tool call and optional tool message."""
|
|
237
|
+
allowed_decisions = config["allowed_decisions"]
|
|
238
|
+
|
|
239
|
+
if decision["type"] == "approve" and "approve" in allowed_decisions:
|
|
240
|
+
return tool_call, None
|
|
241
|
+
if decision["type"] == "edit" and "edit" in allowed_decisions:
|
|
242
|
+
edited_action = decision["edited_action"]
|
|
243
|
+
return (
|
|
244
|
+
ToolCall(
|
|
245
|
+
type="tool_call",
|
|
246
|
+
name=edited_action["name"],
|
|
247
|
+
args=edited_action["arguments"],
|
|
248
|
+
id=tool_call["id"],
|
|
249
|
+
),
|
|
250
|
+
None,
|
|
251
|
+
)
|
|
252
|
+
if decision["type"] == "reject" and "reject" in allowed_decisions:
|
|
253
|
+
# Create a tool message with the human's text response
|
|
254
|
+
content = decision.get("message") or (
|
|
255
|
+
f"User rejected the tool call for `{tool_call['name']}` with id {tool_call['id']}"
|
|
256
|
+
)
|
|
257
|
+
tool_message = ToolMessage(
|
|
258
|
+
content=content,
|
|
259
|
+
name=tool_call["name"],
|
|
260
|
+
tool_call_id=tool_call["id"],
|
|
261
|
+
status="error",
|
|
262
|
+
)
|
|
263
|
+
return tool_call, tool_message
|
|
264
|
+
msg = (
|
|
265
|
+
f"Unexpected human decision: {decision}. "
|
|
266
|
+
f"Decision type '{decision.get('type')}' "
|
|
267
|
+
f"is not allowed for tool '{tool_call['name']}'. "
|
|
268
|
+
f"Expected one of {allowed_decisions} based on the tool's configuration."
|
|
269
|
+
)
|
|
270
|
+
raise ValueError(msg)
|
|
271
|
+
|
|
192
272
|
def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
|
|
193
273
|
"""Trigger interrupt flows for relevant tool calls after an AIMessage."""
|
|
194
274
|
messages = state["messages"]
|
|
@@ -216,87 +296,50 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
|
|
|
216
296
|
revised_tool_calls: list[ToolCall] = auto_approved_tool_calls.copy()
|
|
217
297
|
artificial_tool_messages: list[ToolMessage] = []
|
|
218
298
|
|
|
219
|
-
# Create
|
|
220
|
-
|
|
299
|
+
# Create action requests and review configs for all tools that need approval
|
|
300
|
+
action_requests: list[ActionRequest] = []
|
|
301
|
+
review_configs: list[ReviewConfig] = []
|
|
302
|
+
|
|
221
303
|
for tool_call in interrupt_tool_calls:
|
|
222
|
-
|
|
223
|
-
tool_args = tool_call["args"]
|
|
224
|
-
config = self.interrupt_on[tool_name]
|
|
225
|
-
|
|
226
|
-
# Generate description using the description field (str or callable)
|
|
227
|
-
description_value = config.get("description")
|
|
228
|
-
if callable(description_value):
|
|
229
|
-
description = description_value(tool_call, state, runtime)
|
|
230
|
-
elif description_value is not None:
|
|
231
|
-
description = description_value
|
|
232
|
-
else:
|
|
233
|
-
description = f"{self.description_prefix}\n\nTool: {tool_name}\nArgs: {tool_args}"
|
|
234
|
-
|
|
235
|
-
request: HumanInTheLoopRequest = {
|
|
236
|
-
"action_request": ActionRequest(
|
|
237
|
-
action=tool_name,
|
|
238
|
-
args=tool_args,
|
|
239
|
-
),
|
|
240
|
-
"config": config,
|
|
241
|
-
"description": description,
|
|
242
|
-
}
|
|
243
|
-
interrupt_requests.append(request)
|
|
304
|
+
config = self.interrupt_on[tool_call["name"]]
|
|
244
305
|
|
|
245
|
-
|
|
306
|
+
# Create ActionRequest and ReviewConfig using helper method
|
|
307
|
+
action_request, review_config = self._create_action_and_config(
|
|
308
|
+
tool_call, config, state, runtime
|
|
309
|
+
)
|
|
310
|
+
action_requests.append(action_request)
|
|
311
|
+
review_configs.append(review_config)
|
|
246
312
|
|
|
247
|
-
#
|
|
248
|
-
|
|
313
|
+
# Create single HITLRequest with all actions and configs
|
|
314
|
+
hitl_request = HITLRequest(
|
|
315
|
+
action_requests=action_requests,
|
|
316
|
+
review_configs=review_configs,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Send interrupt and get response
|
|
320
|
+
hitl_response: HITLResponse = interrupt(hitl_request)
|
|
321
|
+
decisions = hitl_response["decisions"]
|
|
322
|
+
|
|
323
|
+
# Validate that the number of decisions matches the number of interrupt tool calls
|
|
324
|
+
if (decisions_len := len(decisions)) != (
|
|
249
325
|
interrupt_tool_calls_len := len(interrupt_tool_calls)
|
|
250
326
|
):
|
|
251
327
|
msg = (
|
|
252
|
-
f"Number of human
|
|
328
|
+
f"Number of human decisions ({decisions_len}) does not match "
|
|
253
329
|
f"number of hanging tool calls ({interrupt_tool_calls_len})."
|
|
254
330
|
)
|
|
255
331
|
raise ValueError(msg)
|
|
256
332
|
|
|
257
|
-
|
|
333
|
+
# Process each decision using helper method
|
|
334
|
+
for i, decision in enumerate(decisions):
|
|
258
335
|
tool_call = interrupt_tool_calls[i]
|
|
259
336
|
config = self.interrupt_on[tool_call["name"]]
|
|
260
337
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
revised_tool_calls.append(
|
|
266
|
-
ToolCall(
|
|
267
|
-
type="tool_call",
|
|
268
|
-
name=edited_action["action"],
|
|
269
|
-
args=edited_action["args"],
|
|
270
|
-
id=tool_call["id"],
|
|
271
|
-
)
|
|
272
|
-
)
|
|
273
|
-
elif response["type"] == "response" and config.get("allow_respond"):
|
|
274
|
-
# Create a tool message with the human's text response
|
|
275
|
-
content = response.get("args") or (
|
|
276
|
-
f"User rejected the tool call for `{tool_call['name']}` "
|
|
277
|
-
f"with id {tool_call['id']}"
|
|
278
|
-
)
|
|
279
|
-
tool_message = ToolMessage(
|
|
280
|
-
content=content,
|
|
281
|
-
name=tool_call["name"],
|
|
282
|
-
tool_call_id=tool_call["id"],
|
|
283
|
-
status="error",
|
|
284
|
-
)
|
|
285
|
-
revised_tool_calls.append(tool_call)
|
|
338
|
+
revised_tool_call, tool_message = self._process_decision(decision, tool_call, config)
|
|
339
|
+
if revised_tool_call:
|
|
340
|
+
revised_tool_calls.append(revised_tool_call)
|
|
341
|
+
if tool_message:
|
|
286
342
|
artificial_tool_messages.append(tool_message)
|
|
287
|
-
else:
|
|
288
|
-
allowed_actions = [
|
|
289
|
-
action
|
|
290
|
-
for action in ["accept", "edit", "response"]
|
|
291
|
-
if config.get(f"allow_{'respond' if action == 'response' else action}")
|
|
292
|
-
]
|
|
293
|
-
msg = (
|
|
294
|
-
f"Unexpected human response: {response}. "
|
|
295
|
-
f"Response action '{response.get('type')}' "
|
|
296
|
-
f"is not allowed for tool '{tool_call['name']}'. "
|
|
297
|
-
f"Expected one of {allowed_actions} based on the tool's configuration."
|
|
298
|
-
)
|
|
299
|
-
raise ValueError(msg)
|
|
300
343
|
|
|
301
344
|
# Update the AI message to only include approved tool calls
|
|
302
345
|
last_ai_msg.tool_calls = revised_tool_calls
|
|
@@ -108,9 +108,9 @@ class ModelCallLimitMiddleware(AgentMiddleware):
|
|
|
108
108
|
|
|
109
109
|
Args:
|
|
110
110
|
thread_limit: Maximum number of model calls allowed per thread.
|
|
111
|
-
None means no limit. Defaults to None
|
|
111
|
+
None means no limit. Defaults to `None`.
|
|
112
112
|
run_limit: Maximum number of model calls allowed per run.
|
|
113
|
-
None means no limit. Defaults to None
|
|
113
|
+
None means no limit. Defaults to `None`.
|
|
114
114
|
exit_behavior: What to do when limits are exceeded.
|
|
115
115
|
- "end": Jump to the end of the agent execution and
|
|
116
116
|
inject an artificial AI message indicating that the limit was exceeded.
|