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 +1 -1
- langchain/_internal/_lazy_import.py +2 -3
- langchain/_internal/_prompts.py +11 -18
- langchain/_internal/_typing.py +3 -3
- langchain/agents/_internal/_typing.py +2 -2
- langchain/agents/middleware/__init__.py +3 -0
- langchain/agents/middleware/dynamic_system_prompt.py +105 -0
- langchain/agents/middleware/human_in_the_loop.py +213 -88
- langchain/agents/middleware/prompt_caching.py +16 -8
- langchain/agents/middleware/summarization.py +2 -2
- langchain/agents/middleware/types.py +52 -11
- langchain/agents/middleware_agent.py +151 -94
- langchain/agents/react_agent.py +86 -61
- langchain/agents/structured_output.py +29 -24
- langchain/agents/tool_node.py +71 -65
- langchain/chat_models/base.py +28 -32
- langchain/embeddings/base.py +4 -10
- langchain/embeddings/cache.py +5 -8
- langchain/storage/encoder_backed.py +7 -4
- {langchain-1.0.0a4.dist-info → langchain-1.0.0a6.dist-info}/METADATA +17 -17
- langchain-1.0.0a6.dist-info/RECORD +39 -0
- langchain/agents/interrupt.py +0 -92
- langchain/agents/middleware/_utils.py +0 -11
- langchain-1.0.0a4.dist-info/RECORD +0 -40
- {langchain-1.0.0a4.dist-info → langchain-1.0.0a6.dist-info}/WHEEL +0 -0
- {langchain-1.0.0a4.dist-info → langchain-1.0.0a6.dist-info}/entry_points.txt +0 -0
- {langchain-1.0.0a4.dist-info → langchain-1.0.0a6.dist-info}/licenses/LICENSE +0 -0
langchain/__init__.py
CHANGED
|
@@ -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:
|
|
10
|
-
package:
|
|
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
|
|
langchain/_internal/_prompts.py
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
|
langchain/_internal/_typing.py
CHANGED
|
@@ -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
|
|
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 =
|
|
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=
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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:
|
|
25
|
-
|
|
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:
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
+
hitl_tool_calls: list[ToolCall] = []
|
|
50
158
|
auto_approved_tool_calls = []
|
|
51
159
|
|
|
52
|
-
for tool_call in
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
166
|
+
if not hitl_tool_calls:
|
|
61
167
|
return None
|
|
62
168
|
|
|
63
|
-
|
|
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
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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,
|
|
5
|
+
from langchain.agents.middleware.types import AgentMiddleware, ModelRequest
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class AnthropicPromptCachingMiddleware(AgentMiddleware):
|
|
9
|
-
"""Prompt Caching Middleware
|
|
9
|
+
"""Prompt Caching Middleware.
|
|
10
10
|
|
|
11
|
-
|
|
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,
|
|
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(
|
|
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
|
|
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
|
|
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
|
|