deepagents 0.0.6rc2__py3-none-any.whl → 0.0.7__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.
- deepagents/__init__.py +2 -6
- deepagents/graph.py +74 -147
- deepagents/middleware.py +198 -0
- deepagents/prompts.py +14 -13
- deepagents/state.py +9 -1
- deepagents/tools.py +7 -8
- deepagents/types.py +21 -0
- {deepagents-0.0.6rc2.dist-info → deepagents-0.0.7.dist-info}/METADATA +30 -57
- deepagents-0.0.7.dist-info/RECORD +17 -0
- {deepagents-0.0.6rc2.dist-info → deepagents-0.0.7.dist-info}/top_level.txt +1 -0
- tests/test_deepagents.py +136 -0
- tests/test_hitl.py +51 -0
- tests/test_middleware.py +57 -0
- tests/utils.py +81 -0
- deepagents/builder.py +0 -84
- deepagents/interrupt.py +0 -122
- deepagents/sub_agent.py +0 -169
- deepagents-0.0.6rc2.dist-info/RECORD +0 -14
- {deepagents-0.0.6rc2.dist-info → deepagents-0.0.7.dist-info}/WHEEL +0 -0
- {deepagents-0.0.6rc2.dist-info → deepagents-0.0.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagents
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
4
4
|
Summary: General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: <4.0,>=3.11
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
|
-
Requires-Dist: langgraph>=0.
|
|
9
|
+
Requires-Dist: langgraph>=1.0.0a3
|
|
10
10
|
Requires-Dist: langchain-anthropic>=0.1.23
|
|
11
|
-
Requires-Dist: langchain>=0.
|
|
11
|
+
Requires-Dist: langchain>=1.0.0a8
|
|
12
|
+
Requires-Dist: langgraph-prebuilt>=0.7.0a2
|
|
12
13
|
Dynamic: license-file
|
|
13
14
|
|
|
14
15
|
# 🧠🤖Deep Agents
|
|
@@ -37,7 +38,6 @@ pip install deepagents
|
|
|
37
38
|
```python
|
|
38
39
|
import os
|
|
39
40
|
from typing import Literal
|
|
40
|
-
|
|
41
41
|
from tavily import TavilyClient
|
|
42
42
|
from deepagents import create_deep_agent
|
|
43
43
|
|
|
@@ -86,7 +86,7 @@ in the same way you would any LangGraph agent.
|
|
|
86
86
|
|
|
87
87
|
## Creating a custom deep agent
|
|
88
88
|
|
|
89
|
-
There are
|
|
89
|
+
There are several parameters you can pass to `create_deep_agent` to create your own custom deep agent.
|
|
90
90
|
|
|
91
91
|
### `tools` (Required)
|
|
92
92
|
|
|
@@ -98,7 +98,7 @@ The agent (and any subagents) will have access to these tools.
|
|
|
98
98
|
|
|
99
99
|
The second argument to `create_deep_agent` is `instructions`.
|
|
100
100
|
This will serve as part of the prompt of the deep agent.
|
|
101
|
-
Note that
|
|
101
|
+
Note that our deep agent middleware appends further instructions to the deep agent regarding to-do list, filesystem, and subagent usage, so this is not the *entire* prompt the agent will see.
|
|
102
102
|
|
|
103
103
|
### `subagents` (Optional)
|
|
104
104
|
|
|
@@ -114,7 +114,8 @@ class SubAgent(TypedDict):
|
|
|
114
114
|
description: str
|
|
115
115
|
prompt: str
|
|
116
116
|
tools: NotRequired[list[str]]
|
|
117
|
-
|
|
117
|
+
model: NotRequired[Union[LanguageModelLike, dict[str, Any]]]
|
|
118
|
+
middleware: NotRequired[list[AgentMiddleware]]
|
|
118
119
|
|
|
119
120
|
class CustomSubAgent(TypedDict):
|
|
120
121
|
name: str
|
|
@@ -127,7 +128,8 @@ class CustomSubAgent(TypedDict):
|
|
|
127
128
|
- **description**: This is the description of the subagent that is shown to the main agent
|
|
128
129
|
- **prompt**: This is the prompt used for the subagent
|
|
129
130
|
- **tools**: This is the list of tools that the subagent has access to. By default will have access to all tools passed in, as well as all built-in tools.
|
|
130
|
-
- **
|
|
131
|
+
- **model**: Optional model instance OR dictionary for per-subagent model configuration (inherits the main model when omitted).
|
|
132
|
+
- **middleware** Additional middleware to attach to the subagent. See [here](https://docs.langchain.com/oss/python/langchain/middleware) for an introduction into middleware and how it works with create_agent.
|
|
131
133
|
|
|
132
134
|
**CustomSubAgent fields:**
|
|
133
135
|
- **name**: This is the name of the subagent, and how the main agent will call the subagent
|
|
@@ -141,6 +143,7 @@ research_subagent = {
|
|
|
141
143
|
"name": "research-agent",
|
|
142
144
|
"description": "Used to research more in depth questions",
|
|
143
145
|
"prompt": sub_research_prompt,
|
|
146
|
+
"tools": [internet_search]
|
|
144
147
|
}
|
|
145
148
|
subagents = [research_subagent]
|
|
146
149
|
agent = create_deep_agent(
|
|
@@ -183,18 +186,6 @@ agent = create_deep_agent(
|
|
|
183
186
|
|
|
184
187
|
By default, `deepagents` uses `"claude-sonnet-4-20250514"`. You can customize this by passing any [LangChain model object](https://python.langchain.com/docs/integrations/chat/).
|
|
185
188
|
|
|
186
|
-
### `builtin_tools` (Optional)
|
|
187
|
-
|
|
188
|
-
By default, a deep agent will have access to a number of [built-in tools](#builtintools--optional-).
|
|
189
|
-
You can change this by specifying the tools (by name) that the agent should have access to with this parameter.
|
|
190
|
-
|
|
191
|
-
Example:
|
|
192
|
-
```python
|
|
193
|
-
# Only give agent access to todo tool, none of the filesystem tools
|
|
194
|
-
builtin_tools = ["write_todos"]
|
|
195
|
-
agent = create_deep_agent(..., builtin_tools=builtin_tools, ...)
|
|
196
|
-
```
|
|
197
|
-
|
|
198
189
|
#### Example: Using a Custom Model
|
|
199
190
|
|
|
200
191
|
Here's how to use a custom model (like OpenAI's `gpt-oss` model via Ollama):
|
|
@@ -243,6 +234,15 @@ agent = create_deep_agent(
|
|
|
243
234
|
)
|
|
244
235
|
```
|
|
245
236
|
|
|
237
|
+
|
|
238
|
+
### `middleware` (Optional)
|
|
239
|
+
Both the main agent and sub-agents can take additional custom AgentMiddleware. Middleware is the best supported approach for extending the state_schema, adding additional tools, and adding pre / post model hooks. See this [doc](https://docs.langchain.com/oss/python/langchain/middleware) to learn more about Middleware and how you can use it!
|
|
240
|
+
|
|
241
|
+
### `tool_configs` (Optional)
|
|
242
|
+
Tool configs are used to specify how to handle Human In The Loop interactions on certain tools that require additional human oversight.
|
|
243
|
+
|
|
244
|
+
These tool_configs are passed to our prebuilt [HITL middleware](https://docs.langchain.com/oss/python/langchain/middleware#human-in-the-loop) so that the agent pauses execution and waits for feedback from the user before executing configured tools.
|
|
245
|
+
|
|
246
246
|
## Deep Agent Details
|
|
247
247
|
|
|
248
248
|
The below components are built into `deepagents` and helps make it work for deep tasks off-the-shelf.
|
|
@@ -304,25 +304,20 @@ By default, deep agents come with five built-in tools:
|
|
|
304
304
|
- `ls`: Tool for listing files in the virtual filesystem
|
|
305
305
|
- `edit_file`: Tool for editing a file in the virtual filesystem
|
|
306
306
|
|
|
307
|
-
|
|
307
|
+
If you want to omit some deepagents functionality, use specific middleware components directly!
|
|
308
308
|
|
|
309
309
|
### Human-in-the-Loop
|
|
310
310
|
|
|
311
|
-
`deepagents` supports human-in-the-loop approval for tool execution. You can configure specific tools to require human approval before execution using the `
|
|
311
|
+
`deepagents` supports human-in-the-loop approval for tool execution. You can configure specific tools to require human approval before execution using the `tool_configs` parameter, which maps tool names to a `HumanInTheLoopConfig`.
|
|
312
312
|
|
|
313
|
-
`
|
|
313
|
+
`HumanInTheLoopConfig` is how you specify what type of human in the loop patterns are supported.
|
|
314
314
|
It is a dictionary with four specific keys:
|
|
315
315
|
|
|
316
|
-
- `
|
|
317
|
-
- `allow_respond`: Whether the
|
|
318
|
-
- `allow_edit`: Whether the
|
|
319
|
-
- `allow_accept`: Whether the user can accept the tool call
|
|
320
|
-
|
|
321
|
-
Currently, `deepagents` does NOT support `allow_ignore`
|
|
316
|
+
- `allow_accept`: Whether the human can approve the current action without changes
|
|
317
|
+
- `allow_respond`: Whether the human can reject the current action with feedback
|
|
318
|
+
- `allow_edit`: Whether the human can approve the current action with edited content
|
|
322
319
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
Instead of specifying a `HumanInterruptConfig` for a tool, you can also just set `True`. This will set `allow_ignore`, `allow_respond`, `allow_edit`, and `allow_accept` to be `True`.
|
|
320
|
+
Instead of specifying a `HumanInTheLoopConfig` for a tool, you can also just set `True`. This will set `allow_ignore`, `allow_respond`, `allow_edit`, and `allow_accept` to be `True`.
|
|
326
321
|
|
|
327
322
|
In order to use human in the loop, you need to have a checkpointer attached.
|
|
328
323
|
Note: if you are using LangGraph Platform, this is automatically attached.
|
|
@@ -337,10 +332,9 @@ from langgraph.checkpoint.memory import InMemorySaver
|
|
|
337
332
|
agent = create_deep_agent(
|
|
338
333
|
tools=[your_tools],
|
|
339
334
|
instructions="Your instructions here",
|
|
340
|
-
|
|
335
|
+
tool_configs={
|
|
341
336
|
# You can specify a dictionary for fine grained control over what interrupt options exist
|
|
342
337
|
"tool_1": {
|
|
343
|
-
"allow_ignore": False,
|
|
344
338
|
"allow_respond": True,
|
|
345
339
|
"allow_edit": True,
|
|
346
340
|
"allow_accept":True,
|
|
@@ -413,12 +407,12 @@ for s in agent.stream(Command(resume=[{"type": "response", "args": "..."}]), con
|
|
|
413
407
|
```
|
|
414
408
|
## Async
|
|
415
409
|
|
|
416
|
-
If you are passing async tools to your agent, you will want to `from deepagents import async_create_deep_agent`
|
|
410
|
+
If you are passing async tools to your agent, you will want to use `from deepagents import async_create_deep_agent`
|
|
417
411
|
## MCP
|
|
418
412
|
|
|
419
413
|
The `deepagents` library can be ran with MCP tools. This can be achieved by using the [Langchain MCP Adapter library](https://github.com/langchain-ai/langchain-mcp-adapters).
|
|
420
414
|
|
|
421
|
-
**NOTE:** will want to use `from deepagents import async_create_deep_agent` to use the async version of `deepagents`, since MCP tools are async
|
|
415
|
+
**NOTE:** You will want to use `from deepagents import async_create_deep_agent` to use the async version of `deepagents`, since MCP tools are async
|
|
422
416
|
|
|
423
417
|
(To run the example below, will need to `pip install langchain-mcp-adapters`)
|
|
424
418
|
|
|
@@ -446,27 +440,6 @@ async def main():
|
|
|
446
440
|
asyncio.run(main())
|
|
447
441
|
```
|
|
448
442
|
|
|
449
|
-
## Configurable Agent
|
|
450
|
-
|
|
451
|
-
Configurable agents allow you to control the agent via a config passed in.
|
|
452
|
-
|
|
453
|
-
```python
|
|
454
|
-
from deepagents import create_configurable_agent
|
|
455
|
-
|
|
456
|
-
agent_config = {"instructions": "foo", "subagents": []}
|
|
457
|
-
|
|
458
|
-
build_agent = create_configurable_agent(
|
|
459
|
-
agent_config['instructions'],
|
|
460
|
-
agent_config['subagents'],
|
|
461
|
-
[],
|
|
462
|
-
agent_config={"recursion_limit": 1000}
|
|
463
|
-
)
|
|
464
|
-
```
|
|
465
|
-
You can now use `build_agent` in your `langgraph.json` and deploy it with `langgraph dev`
|
|
466
|
-
|
|
467
|
-
For async tools, you can use `from deepagents import async_create_configurable_agent`
|
|
468
|
-
|
|
469
|
-
|
|
470
443
|
## Roadmap
|
|
471
444
|
- [ ] Allow users to customize full system prompt
|
|
472
445
|
- [ ] Code cleanliness (type hinting, docstrings, formating)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
deepagents/__init__.py,sha256=fA_91ByxPb3e8aPfci43zOXrWz8ylh_CFQALo7EUKi8,312
|
|
2
|
+
deepagents/graph.py,sha256=LciBn8Q2wbi-vlEbqPS0udQvXDnMTP0WKQHNcOyrQsc,6419
|
|
3
|
+
deepagents/middleware.py,sha256=WyK1bf3u1QmMxVcM5KD2tsYMoIKBFQivi6bWYa_cFfA,7242
|
|
4
|
+
deepagents/model.py,sha256=VyRIkdeXJH8HqLrudTKucHpBTtrwMFTQGRlXBj0kUyo,155
|
|
5
|
+
deepagents/prompts.py,sha256=mCnNGTRljfDUMXEfVRiBNGvhoc_wQjt0epiGMJkktgo,25150
|
|
6
|
+
deepagents/state.py,sha256=8so3MgL-zRPYP8Ci_OuVg4wHrs5uAXCErKF1AjjCSt8,726
|
|
7
|
+
deepagents/tools.py,sha256=NpjO2ZEAnL8ZKsYBfxK36AYaayOxC7hWYOvEBCL_5tA,4777
|
|
8
|
+
deepagents/types.py,sha256=5KBSUPlWOnv9It3SnJCMHrOtp9Y4_NQGtGCp69JsEjE,694
|
|
9
|
+
deepagents-0.0.7.dist-info/licenses/LICENSE,sha256=c__BaxUCK69leo2yEKynf8lWndu8iwYwge1CbyqAe-E,1071
|
|
10
|
+
tests/test_deepagents.py,sha256=SwtOiJF4c1O3r_Q3AiM7XZu6tVq4uMIcZlnsfRjx8Ig,7648
|
|
11
|
+
tests/test_hitl.py,sha256=B16ZFiyaVSOcDLz7mh1RTaQZ93EMTKOPUY-IEslkcfM,2460
|
|
12
|
+
tests/test_middleware.py,sha256=3HYmTx0Jw4XTNJjqLYeyGS_QZzcqkFuKfShtajIDhF4,2146
|
|
13
|
+
tests/utils.py,sha256=Ln_DYaMkwAVBo4XQ-QKwlCWP8zZYMenWTcFhsneoL0g,2913
|
|
14
|
+
deepagents-0.0.7.dist-info/METADATA,sha256=0r2OP4f1o-M1Cu_BRRqmYDNCy8koVSnEwnDmWNqEmq0,17130
|
|
15
|
+
deepagents-0.0.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
+
deepagents-0.0.7.dist-info/top_level.txt,sha256=_w9VMQtG4YDNg5A5eAeUre7dF7x7hk9zRpT9zsFaukY,17
|
|
17
|
+
deepagents-0.0.7.dist-info/RECORD,,
|
tests/test_deepagents.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from deepagents.graph import create_deep_agent
|
|
2
|
+
from langchain.agents import create_agent
|
|
3
|
+
from tests.utils import assert_all_deepagent_qualities, SAMPLE_MODEL, sample_tool, get_weather, get_soccer_scores, SampleMiddlewareWithTools, SampleMiddlewareWithToolsAndState, WeatherToolMiddleware, ResearchMiddleware, ResearchMiddlewareWithTools, TOY_BASKETBALL_RESEARCH
|
|
4
|
+
|
|
5
|
+
class TestDeepAgents:
|
|
6
|
+
def test_base_deep_agent(self):
|
|
7
|
+
agent = create_deep_agent()
|
|
8
|
+
assert_all_deepagent_qualities(agent)
|
|
9
|
+
|
|
10
|
+
def test_deep_agent_with_tool(self):
|
|
11
|
+
agent = create_deep_agent(tools=[sample_tool])
|
|
12
|
+
assert_all_deepagent_qualities(agent)
|
|
13
|
+
assert "sample_tool" in agent.nodes["tools"].bound._tools_by_name.keys()
|
|
14
|
+
|
|
15
|
+
def test_deep_agent_with_middleware_with_tool(self):
|
|
16
|
+
agent = create_deep_agent(middleware=[SampleMiddlewareWithTools()])
|
|
17
|
+
assert_all_deepagent_qualities(agent)
|
|
18
|
+
assert "sample_tool" in agent.nodes["tools"].bound._tools_by_name.keys()
|
|
19
|
+
|
|
20
|
+
def test_deep_agent_with_middleware_with_tool_and_state(self):
|
|
21
|
+
agent = create_deep_agent(middleware=[SampleMiddlewareWithToolsAndState()])
|
|
22
|
+
assert_all_deepagent_qualities(agent)
|
|
23
|
+
assert "sample_tool" in agent.nodes["tools"].bound._tools_by_name.keys()
|
|
24
|
+
assert "sample_input" in agent.stream_channels
|
|
25
|
+
|
|
26
|
+
def test_deep_agent_with_subagents(self):
|
|
27
|
+
subagents = [
|
|
28
|
+
{
|
|
29
|
+
"name": "weather_agent",
|
|
30
|
+
"description": "Use this agent to get the weather",
|
|
31
|
+
"prompt": "You are a weather agent.",
|
|
32
|
+
"tools": [get_weather],
|
|
33
|
+
"model": SAMPLE_MODEL,
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
agent = create_deep_agent(tools=[sample_tool], subagents=subagents)
|
|
37
|
+
assert_all_deepagent_qualities(agent)
|
|
38
|
+
result = agent.invoke({"messages": [{"role": "user", "content": "What is the weather in Tokyo?"}]})
|
|
39
|
+
agent_messages = [msg for msg in result.get("messages", []) if msg.type == "ai"]
|
|
40
|
+
tool_calls = [tool_call for msg in agent_messages for tool_call in msg.tool_calls]
|
|
41
|
+
assert any([tool_call["name"] == "task" and tool_call["args"].get("subagent_type") == "weather_agent" for tool_call in tool_calls])
|
|
42
|
+
|
|
43
|
+
def test_deep_agent_with_subagents_gen_purpose(self):
|
|
44
|
+
subagents = [
|
|
45
|
+
{
|
|
46
|
+
"name": "weather_agent",
|
|
47
|
+
"description": "Use this agent to get the weather",
|
|
48
|
+
"prompt": "You are a weather agent.",
|
|
49
|
+
"tools": [get_weather],
|
|
50
|
+
"model": SAMPLE_MODEL,
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
agent = create_deep_agent(tools=[sample_tool], subagents=subagents)
|
|
54
|
+
assert_all_deepagent_qualities(agent)
|
|
55
|
+
result = agent.invoke({"messages": [{"role": "user", "content": "Use the general purpose subagent to call the sample tool"}]})
|
|
56
|
+
agent_messages = [msg for msg in result.get("messages", []) if msg.type == "ai"]
|
|
57
|
+
tool_calls = [tool_call for msg in agent_messages for tool_call in msg.tool_calls]
|
|
58
|
+
assert any([tool_call["name"] == "task" and tool_call["args"].get("subagent_type") == "general-purpose" for tool_call in tool_calls])
|
|
59
|
+
|
|
60
|
+
def test_deep_agent_with_subagents_with_middleware(self):
|
|
61
|
+
subagents = [
|
|
62
|
+
{
|
|
63
|
+
"name": "weather_agent",
|
|
64
|
+
"description": "Use this agent to get the weather",
|
|
65
|
+
"prompt": "You are a weather agent.",
|
|
66
|
+
"tools": [],
|
|
67
|
+
"model": SAMPLE_MODEL,
|
|
68
|
+
"middleware": [WeatherToolMiddleware()],
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
agent = create_deep_agent(tools=[sample_tool], subagents=subagents)
|
|
72
|
+
assert_all_deepagent_qualities(agent)
|
|
73
|
+
result = agent.invoke({"messages": [{"role": "user", "content": "What is the weather in Tokyo?"}]})
|
|
74
|
+
agent_messages = [msg for msg in result.get("messages", []) if msg.type == "ai"]
|
|
75
|
+
tool_calls = [tool_call for msg in agent_messages for tool_call in msg.tool_calls]
|
|
76
|
+
assert any([tool_call["name"] == "task" and tool_call["args"].get("subagent_type") == "weather_agent" for tool_call in tool_calls])
|
|
77
|
+
|
|
78
|
+
def test_deep_agent_with_custom_subagents(self):
|
|
79
|
+
subagents = [
|
|
80
|
+
{
|
|
81
|
+
"name": "weather_agent",
|
|
82
|
+
"description": "Use this agent to get the weather",
|
|
83
|
+
"prompt": "You are a weather agent.",
|
|
84
|
+
"tools": [get_weather],
|
|
85
|
+
"model": SAMPLE_MODEL,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "soccer_agent",
|
|
89
|
+
"description": "Use this agent to get the latest soccer scores",
|
|
90
|
+
"graph": create_agent(
|
|
91
|
+
model=SAMPLE_MODEL,
|
|
92
|
+
tools=[get_soccer_scores],
|
|
93
|
+
prompt="You are a soccer agent.",
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
agent = create_deep_agent(tools=[sample_tool], subagents=subagents)
|
|
98
|
+
assert_all_deepagent_qualities(agent)
|
|
99
|
+
result = agent.invoke({"messages": [{"role": "user", "content": "Look up the weather in Tokyo, and the latest scores for Manchester City!"}]})
|
|
100
|
+
agent_messages = [msg for msg in result.get("messages", []) if msg.type == "ai"]
|
|
101
|
+
tool_calls = [tool_call for msg in agent_messages for tool_call in msg.tool_calls]
|
|
102
|
+
assert any([tool_call["name"] == "task" and tool_call["args"].get("subagent_type") == "weather_agent" for tool_call in tool_calls])
|
|
103
|
+
assert any([tool_call["name"] == "task" and tool_call["args"].get("subagent_type") == "soccer_agent" for tool_call in tool_calls])
|
|
104
|
+
|
|
105
|
+
def test_deep_agent_with_extended_state_and_subagents(self):
|
|
106
|
+
subagents = [
|
|
107
|
+
{
|
|
108
|
+
"name": "basketball_info_agent",
|
|
109
|
+
"description": "Use this agent to get surface level info on any basketball topic",
|
|
110
|
+
"prompt": "You are a basketball info agent.",
|
|
111
|
+
"middleware": [ResearchMiddlewareWithTools()],
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
agent = create_deep_agent(tools=[sample_tool], subagents=subagents, middleware=[ResearchMiddleware()])
|
|
115
|
+
assert_all_deepagent_qualities(agent)
|
|
116
|
+
assert "research" in agent.stream_channels
|
|
117
|
+
result = agent.invoke({"messages": [{"role": "user", "content": "Get surface level info on lebron james"}]}, config={"recursion_limit": 100})
|
|
118
|
+
agent_messages = [msg for msg in result.get("messages", []) if msg.type == "ai"]
|
|
119
|
+
tool_calls = [tool_call for msg in agent_messages for tool_call in msg.tool_calls]
|
|
120
|
+
assert any([tool_call["name"] == "task" and tool_call["args"].get("subagent_type") == "basketball_info_agent" for tool_call in tool_calls])
|
|
121
|
+
assert TOY_BASKETBALL_RESEARCH in result["research"]
|
|
122
|
+
|
|
123
|
+
def test_deep_agent_with_subagents_no_tools(self):
|
|
124
|
+
subagents = [
|
|
125
|
+
{
|
|
126
|
+
"name": "basketball_info_agent",
|
|
127
|
+
"description": "Use this agent to get surface level info on any basketball topic",
|
|
128
|
+
"prompt": "You are a basketball info agent.",
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
agent = create_deep_agent(tools=[sample_tool], subagents=subagents)
|
|
132
|
+
assert_all_deepagent_qualities(agent)
|
|
133
|
+
result = agent.invoke({"messages": [{"role": "user", "content": "Use the basketball info subagent to call the sample tool"}]}, config={"recursion_limit": 100})
|
|
134
|
+
agent_messages = [msg for msg in result.get("messages", []) if msg.type == "ai"]
|
|
135
|
+
tool_calls = [tool_call for msg in agent_messages for tool_call in msg.tool_calls]
|
|
136
|
+
assert any([tool_call["name"] == "task" and tool_call["args"].get("subagent_type") == "basketball_info_agent" for tool_call in tool_calls])
|
tests/test_hitl.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from deepagents.graph import create_deep_agent
|
|
2
|
+
from tests.utils import assert_all_deepagent_qualities, get_weather, sample_tool, get_soccer_scores
|
|
3
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
4
|
+
from langgraph.types import Command
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
SAMPLE_TOOL_CONFIG = {
|
|
8
|
+
"sample_tool": True,
|
|
9
|
+
"get_weather": False,
|
|
10
|
+
"get_soccer_scores": {
|
|
11
|
+
"allow_accept": True,
|
|
12
|
+
"allow_reject": True,
|
|
13
|
+
"allow_respond": False,
|
|
14
|
+
"description": "Ohohohooooo"
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class TestHITL:
|
|
19
|
+
def test_hitl_agent(self):
|
|
20
|
+
checkpointer = MemorySaver()
|
|
21
|
+
agent = create_deep_agent(tools=[sample_tool, get_weather, get_soccer_scores], tool_configs=SAMPLE_TOOL_CONFIG, checkpointer=checkpointer)
|
|
22
|
+
config = {
|
|
23
|
+
"configurable": {
|
|
24
|
+
"thread_id": uuid.uuid4()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
assert_all_deepagent_qualities(agent)
|
|
28
|
+
result = agent.invoke({"messages": [{"role": "user", "content": "Call the sample tool, get the weather in New York and get scores for the latest soccer games in parallel"}]}, config=config)
|
|
29
|
+
agent_messages = [msg for msg in result.get("messages", []) if msg.type == "ai"]
|
|
30
|
+
tool_calls = [tool_call for msg in agent_messages for tool_call in msg.tool_calls]
|
|
31
|
+
assert any([tool_call["name"] == "sample_tool" for tool_call in tool_calls])
|
|
32
|
+
assert any([tool_call["name"] == "get_weather" for tool_call in tool_calls])
|
|
33
|
+
assert any([tool_call["name"] == "get_soccer_scores" for tool_call in tool_calls])
|
|
34
|
+
|
|
35
|
+
assert result["__interrupt__"] is not None
|
|
36
|
+
interrupts = result["__interrupt__"][0].value
|
|
37
|
+
assert len(interrupts) == 2
|
|
38
|
+
assert any([interrupt["action_request"]["action"] == "sample_tool" for interrupt in interrupts])
|
|
39
|
+
assert any([interrupt["action_request"]["action"] == "get_soccer_scores" for interrupt in interrupts])
|
|
40
|
+
|
|
41
|
+
result2 = agent.invoke(
|
|
42
|
+
Command(
|
|
43
|
+
resume=[{"type": "accept"}, {"type": "accept"}]
|
|
44
|
+
),
|
|
45
|
+
config=config
|
|
46
|
+
)
|
|
47
|
+
tool_results = [msg for msg in result2.get("messages", []) if msg.type == "tool"]
|
|
48
|
+
assert any([tool_result.name == "sample_tool" for tool_result in tool_results])
|
|
49
|
+
assert any([tool_result.name == "get_weather" for tool_result in tool_results])
|
|
50
|
+
assert any([tool_result.name == "get_soccer_scores" for tool_result in tool_results])
|
|
51
|
+
assert "__interrupt__" not in result2
|
tests/test_middleware.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from langchain.agents import create_agent
|
|
2
|
+
from deepagents.middleware import (
|
|
3
|
+
PlanningMiddleware,
|
|
4
|
+
FilesystemMiddleware,
|
|
5
|
+
SubAgentMiddleware,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
SAMPLE_MODEL = "claude-3-5-sonnet-20240620"
|
|
9
|
+
|
|
10
|
+
class TestAddMiddleware:
|
|
11
|
+
def test_planning_middleware(self):
|
|
12
|
+
middleware = [PlanningMiddleware()]
|
|
13
|
+
agent = create_agent(model=SAMPLE_MODEL, middleware=middleware, tools=[])
|
|
14
|
+
assert "todos" in agent.stream_channels
|
|
15
|
+
assert "write_todos" in agent.nodes["tools"].bound._tools_by_name.keys()
|
|
16
|
+
|
|
17
|
+
def test_filesystem_middleware(self):
|
|
18
|
+
middleware = [FilesystemMiddleware()]
|
|
19
|
+
agent = create_agent(model=SAMPLE_MODEL, middleware=middleware, tools=[])
|
|
20
|
+
assert "files" in agent.stream_channels
|
|
21
|
+
agent_tools = agent.nodes["tools"].bound._tools_by_name.keys()
|
|
22
|
+
assert "ls" in agent_tools
|
|
23
|
+
assert "read_file" in agent_tools
|
|
24
|
+
assert "write_file" in agent_tools
|
|
25
|
+
assert "edit_file" in agent_tools
|
|
26
|
+
|
|
27
|
+
def test_subagent_middleware(self):
|
|
28
|
+
middleware = [
|
|
29
|
+
SubAgentMiddleware(
|
|
30
|
+
default_subagent_tools=[],
|
|
31
|
+
subagents=[],
|
|
32
|
+
model=SAMPLE_MODEL
|
|
33
|
+
)
|
|
34
|
+
]
|
|
35
|
+
agent = create_agent(model=SAMPLE_MODEL, middleware=middleware, tools=[])
|
|
36
|
+
assert "task" in agent.nodes["tools"].bound._tools_by_name.keys()
|
|
37
|
+
|
|
38
|
+
def test_multiple_middleware(self):
|
|
39
|
+
middleware = [
|
|
40
|
+
PlanningMiddleware(),
|
|
41
|
+
FilesystemMiddleware(),
|
|
42
|
+
SubAgentMiddleware(
|
|
43
|
+
default_subagent_tools=[],
|
|
44
|
+
subagents=[],
|
|
45
|
+
model=SAMPLE_MODEL
|
|
46
|
+
)
|
|
47
|
+
]
|
|
48
|
+
agent = create_agent(model=SAMPLE_MODEL, middleware=middleware, tools=[])
|
|
49
|
+
assert "todos" in agent.stream_channels
|
|
50
|
+
assert "files" in agent.stream_channels
|
|
51
|
+
agent_tools = agent.nodes["tools"].bound._tools_by_name.keys()
|
|
52
|
+
assert "write_todos" in agent_tools
|
|
53
|
+
assert "ls" in agent_tools
|
|
54
|
+
assert "read_file" in agent_tools
|
|
55
|
+
assert "write_file" in agent_tools
|
|
56
|
+
assert "edit_file" in agent_tools
|
|
57
|
+
assert "task" in agent_tools
|
tests/utils.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from langchain_core.tools import tool, InjectedToolCallId
|
|
2
|
+
from langchain.agents.middleware import AgentMiddleware
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
from langchain.agents.tool_node import InjectedState
|
|
5
|
+
from langchain.agents.middleware import AgentMiddleware, AgentState
|
|
6
|
+
from langgraph.types import Command
|
|
7
|
+
from langchain_core.messages import ToolMessage
|
|
8
|
+
|
|
9
|
+
def assert_all_deepagent_qualities(agent):
|
|
10
|
+
assert "todos" in agent.stream_channels
|
|
11
|
+
assert "files" in agent.stream_channels
|
|
12
|
+
assert "write_todos" in agent.nodes["tools"].bound._tools_by_name.keys()
|
|
13
|
+
assert "ls" in agent.nodes["tools"].bound._tools_by_name.keys()
|
|
14
|
+
assert "read_file" in agent.nodes["tools"].bound._tools_by_name.keys()
|
|
15
|
+
assert "write_file" in agent.nodes["tools"].bound._tools_by_name.keys()
|
|
16
|
+
assert "edit_file" in agent.nodes["tools"].bound._tools_by_name.keys()
|
|
17
|
+
assert "task" in agent.nodes["tools"].bound._tools_by_name.keys()
|
|
18
|
+
|
|
19
|
+
###########################
|
|
20
|
+
# Mock tools and middleware
|
|
21
|
+
###########################
|
|
22
|
+
|
|
23
|
+
SAMPLE_MODEL = "claude-3-5-sonnet-20240620"
|
|
24
|
+
|
|
25
|
+
@tool(description="Use this tool to get the weather")
|
|
26
|
+
def get_weather(location: str):
|
|
27
|
+
return f"The weather in {location} is sunny."
|
|
28
|
+
|
|
29
|
+
@tool(description="Use this tool to get the latest soccer scores")
|
|
30
|
+
def get_soccer_scores(team: str):
|
|
31
|
+
return f"The latest soccer scores for {team} are 2-1."
|
|
32
|
+
|
|
33
|
+
@tool(description="Sample tool")
|
|
34
|
+
def sample_tool(sample_input: str):
|
|
35
|
+
return sample_input
|
|
36
|
+
|
|
37
|
+
@tool(description="Sample tool with injected state")
|
|
38
|
+
def sample_tool_with_injected_state(sample_input: str, state: Annotated[dict, InjectedState]):
|
|
39
|
+
return sample_input + state["sample_input"]
|
|
40
|
+
|
|
41
|
+
TOY_BASKETBALL_RESEARCH = "Lebron James is the best basketball player of all time with over 40k points and 21 seasons in the NBA."
|
|
42
|
+
|
|
43
|
+
@tool(description="Use this tool to conduct research into basketball and save it to state")
|
|
44
|
+
def research_basketball(
|
|
45
|
+
topic: str,
|
|
46
|
+
state: Annotated[dict, InjectedState],
|
|
47
|
+
tool_call_id: Annotated[str, InjectedToolCallId]
|
|
48
|
+
):
|
|
49
|
+
current_research = state.get("research", "")
|
|
50
|
+
research = f"{current_research}\n\nResearching on {topic}... Done! {TOY_BASKETBALL_RESEARCH}"
|
|
51
|
+
return Command(
|
|
52
|
+
update={
|
|
53
|
+
"research": research,
|
|
54
|
+
"messages": [
|
|
55
|
+
ToolMessage(research, tool_call_id=tool_call_id)
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
class ResearchState(AgentState):
|
|
61
|
+
research: str
|
|
62
|
+
|
|
63
|
+
class ResearchMiddlewareWithTools(AgentMiddleware):
|
|
64
|
+
state_schema = ResearchState
|
|
65
|
+
tools = [research_basketball]
|
|
66
|
+
|
|
67
|
+
class ResearchMiddleware(AgentMiddleware):
|
|
68
|
+
state_schema = ResearchState
|
|
69
|
+
|
|
70
|
+
class SampleMiddlewareWithTools(AgentMiddleware):
|
|
71
|
+
tools = [sample_tool]
|
|
72
|
+
|
|
73
|
+
class SampleState(AgentState):
|
|
74
|
+
sample_input: str
|
|
75
|
+
|
|
76
|
+
class SampleMiddlewareWithToolsAndState(AgentMiddleware):
|
|
77
|
+
state_schema = SampleState
|
|
78
|
+
tools = [sample_tool]
|
|
79
|
+
|
|
80
|
+
class WeatherToolMiddleware(AgentMiddleware):
|
|
81
|
+
tools = [get_weather]
|
deepagents/builder.py
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
from deepagents import create_deep_agent, async_create_deep_agent, SubAgent
|
|
2
|
-
from langchain_core.tools import BaseTool, tool
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
from typing import Any, Optional
|
|
5
|
-
from typing_extensions import TypedDict, NotRequired
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class SerializableSubAgent(TypedDict):
|
|
9
|
-
name: str
|
|
10
|
-
description: str
|
|
11
|
-
prompt: str
|
|
12
|
-
tools: NotRequired[list[str]]
|
|
13
|
-
# Optional per-subagent model: can be either a model instance OR dict settings
|
|
14
|
-
model: NotRequired[dict[str, Any]]
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def create_configurable_agent(
|
|
18
|
-
default_instructions: str,
|
|
19
|
-
default_sub_agents: list[SerializableSubAgent],
|
|
20
|
-
tools,
|
|
21
|
-
agent_config: Optional[dict] = None,
|
|
22
|
-
**kwargs,
|
|
23
|
-
):
|
|
24
|
-
tools = [t if isinstance(t, BaseTool) else tool(t) for t in tools]
|
|
25
|
-
tool_names = [t.name for t in tools]
|
|
26
|
-
|
|
27
|
-
class AgentConfig(BaseModel):
|
|
28
|
-
instructions: str = default_instructions
|
|
29
|
-
subagents: list[SerializableSubAgent] = default_sub_agents
|
|
30
|
-
tools: list[str] = tool_names
|
|
31
|
-
|
|
32
|
-
def build_agent(config: Optional[dict] = None):
|
|
33
|
-
if config is not None:
|
|
34
|
-
config = config.get("configurable", {})
|
|
35
|
-
else:
|
|
36
|
-
config = {}
|
|
37
|
-
config_fields = {
|
|
38
|
-
k: v for k, v in config.items() if k in ["instructions", "subagents"]
|
|
39
|
-
}
|
|
40
|
-
config = AgentConfig(**config_fields)
|
|
41
|
-
return create_deep_agent(
|
|
42
|
-
instructions=config.instructions,
|
|
43
|
-
tools=[t for t in tools if t.name in config.tools],
|
|
44
|
-
subagents=config.subagents,
|
|
45
|
-
config_schema=AgentConfig,
|
|
46
|
-
**kwargs,
|
|
47
|
-
).with_config(agent_config or {})
|
|
48
|
-
|
|
49
|
-
return build_agent
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def async_create_configurable_agent(
|
|
53
|
-
default_instructions: str,
|
|
54
|
-
default_sub_agents: list[SerializableSubAgent],
|
|
55
|
-
tools,
|
|
56
|
-
agent_config: Optional[dict] = None,
|
|
57
|
-
**kwargs,
|
|
58
|
-
):
|
|
59
|
-
tools = [t if isinstance(t, BaseTool) else tool(t) for t in tools]
|
|
60
|
-
tool_names = [t.name for t in tools]
|
|
61
|
-
|
|
62
|
-
class AgentConfig(BaseModel):
|
|
63
|
-
instructions: str = default_instructions
|
|
64
|
-
subagents: list[SerializableSubAgent] = default_sub_agents
|
|
65
|
-
tools: list[str] = tool_names
|
|
66
|
-
|
|
67
|
-
def build_agent(config: Optional[dict] = None):
|
|
68
|
-
if config is not None:
|
|
69
|
-
config = config.get("configurable", {})
|
|
70
|
-
else:
|
|
71
|
-
config = {}
|
|
72
|
-
config_fields = {
|
|
73
|
-
k: v for k, v in config.items() if k in ["instructions", "subagents"]
|
|
74
|
-
}
|
|
75
|
-
config = AgentConfig(**config_fields)
|
|
76
|
-
return async_create_deep_agent(
|
|
77
|
-
instructions=config.instructions,
|
|
78
|
-
tools=[t for t in tools if t.name in config.tools],
|
|
79
|
-
subagents=config.subagents,
|
|
80
|
-
config_schema=AgentConfig,
|
|
81
|
-
**kwargs,
|
|
82
|
-
).with_config(agent_config or {"recursion_limit": 1000})
|
|
83
|
-
|
|
84
|
-
return build_agent
|