Skip to content

Commit 3a21c22

Browse files
committed
feat: add local tools
1 parent 6836719 commit 3a21c22

File tree

7 files changed

+825
-14
lines changed

7 files changed

+825
-14
lines changed

src/deepagents/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
from deepagents.graph import create_deep_agent
2+
from deepagents.interrupt import ToolInterruptConfig
23
from deepagents.state import DeepAgentState
34
from deepagents.sub_agent import SubAgent
5+
from deepagents.model import get_default_model

src/deepagents/graph.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@
22
from deepagents.model import get_default_model
33
from deepagents.tools import write_todos, write_file, read_file, ls, edit_file
44
from deepagents.state import DeepAgentState
5-
from typing import Sequence, Union, Callable, Any, TypeVar, Type, Optional
5+
from typing import Sequence, Union, Callable, Any, TypeVar, Type, Optional, Dict
66
from langchain_core.tools import BaseTool
77
from langchain_core.language_models import LanguageModelLike
88

9+
from deepagents.local_tools import (
10+
write_file as local_write_file,
11+
read_file as local_read_file,
12+
ls as local_ls,
13+
glob as local_glob,
14+
grep as local_grep,
15+
str_replace_based_edit_tool,
16+
)
17+
from deepagents.interrupt import create_interrupt_hook, ToolInterruptConfig
18+
from langgraph.types import Checkpointer
919
from langgraph.prebuilt import create_react_agent
1020

1121
StateSchema = TypeVar("StateSchema", bound=DeepAgentState)
@@ -30,11 +40,16 @@ def create_deep_agent(
3040
model: Optional[Union[str, LanguageModelLike]] = None,
3141
subagents: list[SubAgent] = None,
3242
state_schema: Optional[StateSchemaType] = None,
43+
interrupt_config: Optional[ToolInterruptConfig] = None,
44+
config_schema: Optional[Type[Any]] = None,
45+
checkpointer: Optional[Checkpointer] = None,
46+
post_model_hook: Optional[Callable] = None,
47+
local_filesystem: bool = False,
3348
):
3449
"""Create a deep agent.
3550
3651
This agent will by default have access to a tool to write todos (write_todos),
37-
and then four file editing tools: write_file, ls, read_file, edit_file.
52+
and then four file editing tools: write_file, ls, read_file, str_replace_based_edit_tool.
3853
3954
Args:
4055
tools: The additional tools the agent should have access to.
@@ -48,9 +63,18 @@ def create_deep_agent(
4863
- `prompt` (used as the system prompt in the subagent)
4964
- (optional) `tools`
5065
state_schema: The schema of the deep agent. Should subclass from DeepAgentState
66+
interrupt_config: Optional Dict[str, HumanInterruptConfig] mapping tool names to interrupt configs.
67+
68+
config_schema: The schema of the deep agent.
69+
checkpointer: Optional checkpointer for persisting agent state between runs.
5170
"""
71+
5272
prompt = instructions + base_prompt
53-
built_in_tools = [write_todos, write_file, read_file, ls, edit_file]
73+
if local_filesystem:
74+
built_in_tools = [write_todos, local_write_file, local_read_file, local_ls, local_glob, local_grep, str_replace_based_edit_tool]
75+
else:
76+
built_in_tools = [write_todos, write_file, read_file, ls, edit_file]
77+
5478
if model is None:
5579
model = get_default_model()
5680
state_schema = state_schema or DeepAgentState
@@ -62,9 +86,26 @@ def create_deep_agent(
6286
state_schema
6387
)
6488
all_tools = built_in_tools + list(tools) + [task_tool]
89+
90+
# Should never be the case that both are specified
91+
if post_model_hook and interrupt_config:
92+
raise ValueError(
93+
"Cannot specify both post_model_hook and interrupt_config together. "
94+
"Use either interrupt_config for tool interrupts or post_model_hook for custom post-processing."
95+
)
96+
elif post_model_hook is not None:
97+
selected_post_model_hook = post_model_hook
98+
elif interrupt_config is not None:
99+
selected_post_model_hook = create_interrupt_hook(interrupt_config)
100+
else:
101+
selected_post_model_hook = None
102+
65103
return create_react_agent(
66104
model,
67105
prompt=prompt,
68106
tools=all_tools,
69107
state_schema=state_schema,
108+
post_model_hook=selected_post_model_hook,
109+
config_schema=config_schema,
110+
checkpointer=checkpointer,
70111
)

src/deepagents/interrupt.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Interrupt configuration functionality for deep agents using LangGraph prebuilts."""
2+
3+
from typing import Dict, Any, List, Optional, Union
4+
from langgraph.types import interrupt
5+
from langgraph.prebuilt.interrupt import (
6+
HumanInterruptConfig,
7+
ActionRequest,
8+
HumanInterrupt,
9+
HumanResponse,
10+
)
11+
12+
ToolInterruptConfig = Dict[str, HumanInterruptConfig]
13+
14+
def create_interrupt_hook(
15+
tool_configs: ToolInterruptConfig,
16+
message_prefix: str = "Tool execution requires approval",
17+
) -> callable:
18+
"""Create a post model hook that handles interrupts using native LangGraph schemas.
19+
20+
Args:
21+
tool_configs: Dict mapping tool names to HumanInterruptConfig objects
22+
message_prefix: Optional message prefix for interrupt descriptions
23+
"""
24+
25+
def interrupt_hook(state: Dict[str, Any]) -> Dict[str, Any]:
26+
"""Post model hook that checks for tool calls and triggers interrupts if needed."""
27+
messages = state.get("messages", [])
28+
if not messages:
29+
return
30+
31+
last_message = messages[-1]
32+
33+
if not hasattr(last_message, "tool_calls") or not last_message.tool_calls:
34+
return
35+
36+
# Separate tool calls that need interrupts from those that don't
37+
interrupt_tool_calls = []
38+
auto_approved_tool_calls = []
39+
40+
for tool_call in last_message.tool_calls:
41+
tool_name = tool_call["name"]
42+
if tool_name in tool_configs:
43+
interrupt_tool_calls.append(tool_call)
44+
else:
45+
auto_approved_tool_calls.append(tool_call)
46+
47+
# If no interrupts needed, return early
48+
if not interrupt_tool_calls:
49+
return
50+
51+
approved_tool_calls = auto_approved_tool_calls.copy()
52+
53+
# Process all tool calls that need interrupts in parallel
54+
requests = []
55+
56+
for tool_call in interrupt_tool_calls:
57+
tool_name = tool_call["name"]
58+
tool_args = tool_call["args"]
59+
description = f"{message_prefix}\n\nTool: {tool_name}\nArgs: {tool_args}"
60+
tool_config = tool_configs[tool_name]
61+
62+
request: HumanInterrupt = {
63+
"action_request": ActionRequest(
64+
action=tool_name,
65+
args=tool_args,
66+
),
67+
"config": tool_config,
68+
"description": description,
69+
}
70+
requests.append(request)
71+
72+
responses: List[HumanResponse] = interrupt(requests)
73+
74+
for i, response in enumerate(responses):
75+
tool_call = interrupt_tool_calls[i]
76+
77+
if response["type"] == "accept":
78+
approved_tool_calls.append(tool_call)
79+
elif response["type"] == "edit":
80+
edited: ActionRequest = response["args"]
81+
new_tool_call = {
82+
"name": tool_call["name"],
83+
"args": edited["args"],
84+
"id": tool_call["id"],
85+
}
86+
approved_tool_calls.append(new_tool_call)
87+
else:
88+
raise ValueError(f"Unknown response type: {response['type']}")
89+
90+
last_message.tool_calls = approved_tool_calls
91+
92+
return {"messages": [last_message]}
93+
94+
return interrupt_hook
95+
96+

0 commit comments

Comments
 (0)