deepagents 0.0.11__tar.gz → 0.0.12rc1__tar.gz
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-0.0.11 → deepagents-0.0.12rc1}/PKG-INFO +1 -1
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/deepagents.egg-info/PKG-INFO +1 -1
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/deepagents.egg-info/SOURCES.txt +1 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/pyproject.toml +1 -1
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/src/deepagents/graph.py +19 -1
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/src/deepagents/middleware.py +23 -6
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/src/deepagents/prompts.py +30 -17
- deepagents-0.0.12rc1/src/deepagents/tools.py +313 -0
- deepagents-0.0.12rc1/tests/test_filesystem.py +196 -0
- deepagents-0.0.11/src/deepagents/tools.py +0 -148
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/LICENSE +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/README.md +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/deepagents.egg-info/dependency_links.txt +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/deepagents.egg-info/requires.txt +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/deepagents.egg-info/top_level.txt +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/setup.cfg +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/src/deepagents/__init__.py +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/src/deepagents/model.py +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/src/deepagents/state.py +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/src/deepagents/types.py +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/tests/test_deepagents.py +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/tests/test_hitl.py +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/tests/test_middleware.py +0 -0
- {deepagents-0.0.11 → deepagents-0.0.12rc1}/tests/utils.py +0 -0
|
@@ -2,6 +2,7 @@ from typing import Sequence, Union, Callable, Any, Type, Optional
|
|
|
2
2
|
from langchain_core.tools import BaseTool
|
|
3
3
|
from langchain_core.language_models import LanguageModelLike
|
|
4
4
|
from langgraph.types import Checkpointer
|
|
5
|
+
from langgraph.store.base import BaseStore
|
|
5
6
|
from langchain.agents import create_agent
|
|
6
7
|
from langchain.agents.middleware import AgentMiddleware, SummarizationMiddleware, HumanInTheLoopMiddleware
|
|
7
8
|
from langchain.agents.middleware.human_in_the_loop import ToolConfig
|
|
@@ -20,6 +21,8 @@ def agent_builder(
|
|
|
20
21
|
subagents: Optional[list[SubAgent | CustomSubAgent]] = None,
|
|
21
22
|
context_schema: Optional[Type[Any]] = None,
|
|
22
23
|
checkpointer: Optional[Checkpointer] = None,
|
|
24
|
+
store: Optional[BaseStore] = None,
|
|
25
|
+
use_longterm_memory: bool = False,
|
|
23
26
|
is_async: bool = False,
|
|
24
27
|
):
|
|
25
28
|
if model is None:
|
|
@@ -27,7 +30,9 @@ def agent_builder(
|
|
|
27
30
|
|
|
28
31
|
deepagent_middleware = [
|
|
29
32
|
PlanningMiddleware(),
|
|
30
|
-
FilesystemMiddleware(
|
|
33
|
+
FilesystemMiddleware(
|
|
34
|
+
use_longterm_memory=use_longterm_memory,
|
|
35
|
+
),
|
|
31
36
|
SubAgentMiddleware(
|
|
32
37
|
default_subagent_tools=tools, # NOTE: These tools are piped to the general-purpose subagent.
|
|
33
38
|
subagents=subagents if subagents is not None else [],
|
|
@@ -55,6 +60,7 @@ def agent_builder(
|
|
|
55
60
|
middleware=deepagent_middleware,
|
|
56
61
|
context_schema=context_schema,
|
|
57
62
|
checkpointer=checkpointer,
|
|
63
|
+
store=store,
|
|
58
64
|
)
|
|
59
65
|
|
|
60
66
|
def create_deep_agent(
|
|
@@ -65,6 +71,8 @@ def create_deep_agent(
|
|
|
65
71
|
subagents: Optional[list[SubAgent | CustomSubAgent]] = None,
|
|
66
72
|
context_schema: Optional[Type[Any]] = None,
|
|
67
73
|
checkpointer: Optional[Checkpointer] = None,
|
|
74
|
+
store: Optional[BaseStore] = None,
|
|
75
|
+
use_longterm_memory: bool = False,
|
|
68
76
|
tool_configs: Optional[dict[str, bool | ToolConfig]] = None,
|
|
69
77
|
):
|
|
70
78
|
"""Create a deep agent.
|
|
@@ -85,6 +93,8 @@ def create_deep_agent(
|
|
|
85
93
|
- (optional) `middleware` (list of AgentMiddleware)
|
|
86
94
|
context_schema: The schema of the deep agent.
|
|
87
95
|
checkpointer: Optional checkpointer for persisting agent state between runs.
|
|
96
|
+
store: Optional store for persisting longterm memories.
|
|
97
|
+
use_longterm_memory: Whether to use longterm memory - you must provide a store in order to use longterm memory.
|
|
88
98
|
tool_configs: Optional Dict[str, HumanInTheLoopConfig] mapping tool names to interrupt configs.
|
|
89
99
|
"""
|
|
90
100
|
return agent_builder(
|
|
@@ -95,6 +105,8 @@ def create_deep_agent(
|
|
|
95
105
|
subagents=subagents,
|
|
96
106
|
context_schema=context_schema,
|
|
97
107
|
checkpointer=checkpointer,
|
|
108
|
+
store=store,
|
|
109
|
+
use_longterm_memory=use_longterm_memory,
|
|
98
110
|
tool_configs=tool_configs,
|
|
99
111
|
is_async=False,
|
|
100
112
|
)
|
|
@@ -107,6 +119,8 @@ def async_create_deep_agent(
|
|
|
107
119
|
subagents: Optional[list[SubAgent | CustomSubAgent]] = None,
|
|
108
120
|
context_schema: Optional[Type[Any]] = None,
|
|
109
121
|
checkpointer: Optional[Checkpointer] = None,
|
|
122
|
+
store: Optional[BaseStore] = None,
|
|
123
|
+
use_longterm_memory: bool = False,
|
|
110
124
|
tool_configs: Optional[dict[str, bool | ToolConfig]] = None,
|
|
111
125
|
):
|
|
112
126
|
"""Create a deep agent.
|
|
@@ -127,6 +141,8 @@ def async_create_deep_agent(
|
|
|
127
141
|
- (optional) `middleware` (list of AgentMiddleware)
|
|
128
142
|
context_schema: The schema of the deep agent.
|
|
129
143
|
checkpointer: Optional checkpointer for persisting agent state between runs.
|
|
144
|
+
use_longterm_memory: Whether to use longterm memory - you must provide a store in order to use longterm memory.
|
|
145
|
+
store: Optional store for persisting longterm memories.
|
|
130
146
|
tool_configs: Optional Dict[str, HumanInTheLoopConfig] mapping tool names to interrupt configs.
|
|
131
147
|
"""
|
|
132
148
|
return agent_builder(
|
|
@@ -137,6 +153,8 @@ def async_create_deep_agent(
|
|
|
137
153
|
subagents=subagents,
|
|
138
154
|
context_schema=context_schema,
|
|
139
155
|
checkpointer=checkpointer,
|
|
156
|
+
store=store,
|
|
157
|
+
use_longterm_memory=use_longterm_memory,
|
|
140
158
|
tool_configs=tool_configs,
|
|
141
159
|
is_async=True,
|
|
142
160
|
)
|
|
@@ -11,8 +11,8 @@ from langgraph.runtime import Runtime
|
|
|
11
11
|
from langchain.tools.tool_node import InjectedState
|
|
12
12
|
from typing import Annotated
|
|
13
13
|
from deepagents.state import PlanningState, FilesystemState
|
|
14
|
-
from deepagents.tools import write_todos,
|
|
15
|
-
from deepagents.prompts import WRITE_TODOS_SYSTEM_PROMPT, TASK_SYSTEM_PROMPT, FILESYSTEM_SYSTEM_PROMPT, TASK_TOOL_DESCRIPTION, BASE_AGENT_PROMPT
|
|
14
|
+
from deepagents.tools import write_todos, get_filesystem_tools
|
|
15
|
+
from deepagents.prompts import WRITE_TODOS_SYSTEM_PROMPT, TASK_SYSTEM_PROMPT, FILESYSTEM_SYSTEM_PROMPT, FILESYSTEM_SYSTEM_PROMPT_LONGTERM_SUPPLEMENT, TASK_TOOL_DESCRIPTION, BASE_AGENT_PROMPT
|
|
16
16
|
from deepagents.types import SubAgent, CustomSubAgent
|
|
17
17
|
|
|
18
18
|
###########################
|
|
@@ -24,7 +24,10 @@ class PlanningMiddleware(AgentMiddleware):
|
|
|
24
24
|
tools = [write_todos]
|
|
25
25
|
|
|
26
26
|
def modify_model_request(self, request: ModelRequest, agent_state: PlanningState, runtime: Runtime) -> ModelRequest:
|
|
27
|
-
|
|
27
|
+
if request.system_prompt is None:
|
|
28
|
+
request.system_prompt = WRITE_TODOS_SYSTEM_PROMPT
|
|
29
|
+
else:
|
|
30
|
+
request.system_prompt = request.system_prompt + "\n\n" + WRITE_TODOS_SYSTEM_PROMPT
|
|
28
31
|
return request
|
|
29
32
|
|
|
30
33
|
###########################
|
|
@@ -33,10 +36,21 @@ class PlanningMiddleware(AgentMiddleware):
|
|
|
33
36
|
|
|
34
37
|
class FilesystemMiddleware(AgentMiddleware):
|
|
35
38
|
state_schema = FilesystemState
|
|
36
|
-
|
|
39
|
+
|
|
40
|
+
def __init__(self, *, use_longterm_memory: bool = False, system_prompt: str = None, custom_tool_descriptions: dict[str, str] = {}) -> None:
|
|
41
|
+
self.system_prompt = FILESYSTEM_SYSTEM_PROMPT
|
|
42
|
+
if system_prompt is not None:
|
|
43
|
+
self.system_prompt = system_prompt
|
|
44
|
+
elif use_longterm_memory:
|
|
45
|
+
self.system_prompt += FILESYSTEM_SYSTEM_PROMPT_LONGTERM_SUPPLEMENT
|
|
46
|
+
|
|
47
|
+
self.tools = get_filesystem_tools(use_longterm_memory, custom_tool_descriptions)
|
|
37
48
|
|
|
38
49
|
def modify_model_request(self, request: ModelRequest, agent_state: FilesystemState, runtime: Runtime) -> ModelRequest:
|
|
39
|
-
|
|
50
|
+
if request.system_prompt is None:
|
|
51
|
+
request.system_prompt = self.system_prompt
|
|
52
|
+
else:
|
|
53
|
+
request.system_prompt = request.system_prompt + "\n\n" + self.system_prompt
|
|
40
54
|
return request
|
|
41
55
|
|
|
42
56
|
###########################
|
|
@@ -61,7 +75,10 @@ class SubAgentMiddleware(AgentMiddleware):
|
|
|
61
75
|
self.tools = [task_tool]
|
|
62
76
|
|
|
63
77
|
def modify_model_request(self, request: ModelRequest, agent_state: AgentState, runtime: Runtime) -> ModelRequest:
|
|
64
|
-
|
|
78
|
+
if request.system_prompt is None:
|
|
79
|
+
request.system_prompt = TASK_SYSTEM_PROMPT
|
|
80
|
+
else:
|
|
81
|
+
request.system_prompt = request.system_prompt + "\n\n" + TASK_SYSTEM_PROMPT
|
|
65
82
|
return request
|
|
66
83
|
|
|
67
84
|
def _get_agents(
|
|
@@ -323,14 +323,15 @@ Since the user is greeting, use the greeting-responder agent to respond with a f
|
|
|
323
323
|
assistant: "I'm going to use the Task tool to launch with the greeting-responder agent"
|
|
324
324
|
</example>"""
|
|
325
325
|
|
|
326
|
-
LIST_FILES_TOOL_DESCRIPTION = """Lists all files in the
|
|
326
|
+
LIST_FILES_TOOL_DESCRIPTION = """Lists all files in the filesystem.
|
|
327
327
|
|
|
328
328
|
Usage:
|
|
329
|
-
- The list_files tool will return a list of all files in the
|
|
329
|
+
- The list_files tool will return a list of all files in the filesystem.
|
|
330
330
|
- This is very useful for exploring the file system and finding the right file to read or edit.
|
|
331
331
|
- You should almost ALWAYS use this tool before using the Read or Edit tools."""
|
|
332
|
+
LIST_FILES_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = "\n- Files from the longterm filesystem will be prefixed with the memories/ path."
|
|
332
333
|
|
|
333
|
-
READ_FILE_TOOL_DESCRIPTION = """Reads a file from the
|
|
334
|
+
READ_FILE_TOOL_DESCRIPTION = """Reads a file from the filesystem. You can access any file directly by using this tool.
|
|
334
335
|
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
|
335
336
|
|
|
336
337
|
Usage:
|
|
@@ -339,27 +340,47 @@ Usage:
|
|
|
339
340
|
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
|
340
341
|
- Any lines longer than 2000 characters will be truncated
|
|
341
342
|
- Results are returned using cat -n format, with line numbers starting at 1
|
|
342
|
-
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
|
|
343
|
+
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
|
|
343
344
|
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
|
|
344
345
|
- You should ALWAYS make sure a file has been read before editing it."""
|
|
346
|
+
READ_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = "\n- file_paths prefixed with the memories/ path will be read from the longterm filesystem."
|
|
345
347
|
|
|
346
|
-
EDIT_FILE_TOOL_DESCRIPTION = """Performs exact string replacements in files.
|
|
348
|
+
EDIT_FILE_TOOL_DESCRIPTION = """Performs exact string replacements in files.
|
|
347
349
|
|
|
348
350
|
Usage:
|
|
349
|
-
- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
|
|
351
|
+
- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
|
|
350
352
|
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
|
|
351
353
|
- ALWAYS prefer editing existing files. NEVER write new files unless explicitly required.
|
|
352
354
|
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
|
|
353
|
-
- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.
|
|
355
|
+
- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.
|
|
354
356
|
- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance."""
|
|
357
|
+
EDIT_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = "\n- You can edit files in the longterm filesystem by prefixing the filename with the memories/ path."
|
|
355
358
|
|
|
356
|
-
WRITE_FILE_TOOL_DESCRIPTION = """Writes to a file in the
|
|
359
|
+
WRITE_FILE_TOOL_DESCRIPTION = """Writes to a new file in the filesystem.
|
|
357
360
|
|
|
358
361
|
Usage:
|
|
359
362
|
- The file_path parameter must be an absolute path, not a relative path
|
|
360
363
|
- The content parameter must be a string
|
|
361
364
|
- The write_file tool will create the a new file.
|
|
362
|
-
- Prefer to edit existing files over creating new ones when possible.
|
|
365
|
+
- Prefer to edit existing files over creating new ones when possible.
|
|
366
|
+
- file_paths prefixed with the memories/ path will be written to the longterm filesystem."""
|
|
367
|
+
WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = "\n- file_paths prefixed with the memories/ path will be written to the longterm filesystem."
|
|
368
|
+
|
|
369
|
+
FILESYSTEM_SYSTEM_PROMPT = """## Filesystem Tools `ls`, `read_file`, `write_file`, `edit_file`
|
|
370
|
+
|
|
371
|
+
You have access to a filesystem which you can interact with using these tools.
|
|
372
|
+
Do not prepend a / to file_paths.
|
|
373
|
+
|
|
374
|
+
- ls: list all files in the filesystem
|
|
375
|
+
- read_file: read a file from the filesystem
|
|
376
|
+
- write_file: write to a file in the filesystem
|
|
377
|
+
- edit_file: edit a file in the filesystem"""
|
|
378
|
+
FILESYSTEM_SYSTEM_PROMPT_LONGTERM_SUPPLEMENT = """
|
|
379
|
+
|
|
380
|
+
You also have access to a longterm filesystem in which you can store files that you want to keep around for longer than the current conversation.
|
|
381
|
+
In order to interact with the longterm filesystem, you can use those same tools, but filenames must be prefixed with the memories/ path.
|
|
382
|
+
Remember, to interact with the longterm filesystem, you must prefix the filename with the memories/ path."""
|
|
383
|
+
|
|
363
384
|
|
|
364
385
|
WRITE_TODOS_SYSTEM_PROMPT = """## `write_todos`
|
|
365
386
|
|
|
@@ -403,14 +424,6 @@ When NOT to use the task tool:
|
|
|
403
424
|
- Remember to use the `task` tool to silo independent tasks within a multi-part objective.
|
|
404
425
|
- You should use the `task` tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient."""
|
|
405
426
|
|
|
406
|
-
FILESYSTEM_SYSTEM_PROMPT = """## Filesystem Tools `ls`, `read_file`, `write_file`, `edit_file`
|
|
407
|
-
|
|
408
|
-
You have access to a local, private filesystem which you can interact with using these tools.
|
|
409
|
-
- ls: list all files in the local filesystem
|
|
410
|
-
- read_file: read a file from the local filesystem
|
|
411
|
-
- write_file: write to a file in the local filesystem
|
|
412
|
-
- edit_file: edit a file in the local filesystem"""
|
|
413
|
-
|
|
414
427
|
BASE_AGENT_PROMPT = """
|
|
415
428
|
In order to complete the objective that the user asks of you, you have access to a number of standard tools.
|
|
416
429
|
"""
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
from re import L
|
|
2
|
+
from langchain_core.tools import tool, InjectedToolCallId
|
|
3
|
+
from langchain_core.messages import ToolMessage
|
|
4
|
+
from langgraph.types import Command
|
|
5
|
+
from langgraph.runtime import get_runtime, Runtime
|
|
6
|
+
from langchain.tools.tool_node import InjectedState
|
|
7
|
+
from typing import Annotated, Any
|
|
8
|
+
from deepagents.state import Todo, FilesystemState
|
|
9
|
+
from deepagents.prompts import (
|
|
10
|
+
WRITE_TODOS_TOOL_DESCRIPTION,
|
|
11
|
+
LIST_FILES_TOOL_DESCRIPTION,
|
|
12
|
+
READ_FILE_TOOL_DESCRIPTION,
|
|
13
|
+
WRITE_FILE_TOOL_DESCRIPTION,
|
|
14
|
+
EDIT_FILE_TOOL_DESCRIPTION,
|
|
15
|
+
LIST_FILES_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT,
|
|
16
|
+
READ_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT,
|
|
17
|
+
WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT,
|
|
18
|
+
EDIT_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def has_memories_prefix(file_path: str) -> bool:
|
|
22
|
+
return file_path.startswith("memories/")
|
|
23
|
+
|
|
24
|
+
def append_memories_prefix(file_path: str) -> str:
|
|
25
|
+
return f"memories/{file_path}"
|
|
26
|
+
|
|
27
|
+
def strip_memories_prefix(file_path: str) -> str:
|
|
28
|
+
return file_path.replace("memories/", "")
|
|
29
|
+
|
|
30
|
+
def get_namespace(runtime: Runtime[Any]) -> tuple[str, str]:
|
|
31
|
+
namespace = ("filesystem")
|
|
32
|
+
if runtime.context is None:
|
|
33
|
+
return namespace
|
|
34
|
+
assistant_id = runtime.context.get("assistant_id")
|
|
35
|
+
if assistant_id is None:
|
|
36
|
+
return namespace
|
|
37
|
+
return (assistant_id, "filesystem")
|
|
38
|
+
|
|
39
|
+
@tool(description=WRITE_TODOS_TOOL_DESCRIPTION)
|
|
40
|
+
def write_todos(
|
|
41
|
+
todos: list[Todo], tool_call_id: Annotated[str, InjectedToolCallId]
|
|
42
|
+
) -> Command:
|
|
43
|
+
return Command(
|
|
44
|
+
update={
|
|
45
|
+
"todos": todos,
|
|
46
|
+
"messages": [
|
|
47
|
+
ToolMessage(f"Updated todo list to {todos}", tool_call_id=tool_call_id)
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def ls_tool_generator(has_longterm_memory: bool, custom_description: str = None) -> tool:
|
|
53
|
+
tool_description = LIST_FILES_TOOL_DESCRIPTION
|
|
54
|
+
if custom_description:
|
|
55
|
+
tool_description = custom_description
|
|
56
|
+
elif has_longterm_memory:
|
|
57
|
+
tool_description += LIST_FILES_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
|
|
58
|
+
|
|
59
|
+
if has_longterm_memory:
|
|
60
|
+
# Tool with Long-term memory
|
|
61
|
+
@tool(description=tool_description)
|
|
62
|
+
def ls(state: Annotated[FilesystemState, InjectedState]) -> list[str]:
|
|
63
|
+
files = []
|
|
64
|
+
files.extend(list(state.get("files", {}).keys()))
|
|
65
|
+
|
|
66
|
+
runtime = get_runtime()
|
|
67
|
+
store = runtime.store
|
|
68
|
+
if store is None:
|
|
69
|
+
raise ValueError("Longterm memory is enabled, but no store is available")
|
|
70
|
+
namespace = get_namespace(runtime)
|
|
71
|
+
file_data_list = store.search(namespace)
|
|
72
|
+
memories_files = [append_memories_prefix(f.key) for f in file_data_list]
|
|
73
|
+
files.extend(memories_files)
|
|
74
|
+
return files
|
|
75
|
+
else:
|
|
76
|
+
# Tool without long-term memory
|
|
77
|
+
@tool(description=tool_description)
|
|
78
|
+
def ls(state: Annotated[FilesystemState, InjectedState]) -> list[str]:
|
|
79
|
+
files = list(state.get("files", {}).keys())
|
|
80
|
+
return files
|
|
81
|
+
return ls
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def read_file_tool_generator(has_longterm_memory: bool, custom_description: str = None) -> tool:
|
|
85
|
+
tool_description = READ_FILE_TOOL_DESCRIPTION
|
|
86
|
+
if custom_description:
|
|
87
|
+
tool_description = custom_description
|
|
88
|
+
elif has_longterm_memory:
|
|
89
|
+
tool_description += READ_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
|
|
90
|
+
|
|
91
|
+
if has_longterm_memory:
|
|
92
|
+
# Tool with Long-term memory
|
|
93
|
+
@tool(description=tool_description)
|
|
94
|
+
def read_file(
|
|
95
|
+
file_path: str,
|
|
96
|
+
state: Annotated[FilesystemState, InjectedState],
|
|
97
|
+
offset: int = 0,
|
|
98
|
+
limit: int = 2000,
|
|
99
|
+
) -> str:
|
|
100
|
+
if has_memories_prefix(file_path):
|
|
101
|
+
stripped_file_path = strip_memories_prefix(file_path)
|
|
102
|
+
runtime = get_runtime()
|
|
103
|
+
store = runtime.store
|
|
104
|
+
if store is None:
|
|
105
|
+
raise ValueError("Longterm memory is enabled, but no store is available")
|
|
106
|
+
namespace = get_namespace(runtime)
|
|
107
|
+
item = store.get(namespace, stripped_file_path)
|
|
108
|
+
if item is None:
|
|
109
|
+
return f"Error: File '{file_path}' not found"
|
|
110
|
+
content = item.value
|
|
111
|
+
else:
|
|
112
|
+
mock_filesystem = state.get("files", {})
|
|
113
|
+
if file_path not in mock_filesystem:
|
|
114
|
+
return f"Error: File '{file_path}' not found"
|
|
115
|
+
content = mock_filesystem[file_path]
|
|
116
|
+
if not content or content.strip() == "":
|
|
117
|
+
return "System reminder: File exists but has empty contents"
|
|
118
|
+
lines = content.splitlines()
|
|
119
|
+
start_idx = offset
|
|
120
|
+
end_idx = min(start_idx + limit, len(lines))
|
|
121
|
+
if start_idx >= len(lines):
|
|
122
|
+
return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
|
|
123
|
+
result_lines = []
|
|
124
|
+
for i in range(start_idx, end_idx):
|
|
125
|
+
line_content = lines[i]
|
|
126
|
+
if len(line_content) > 2000:
|
|
127
|
+
line_content = line_content[:2000]
|
|
128
|
+
line_number = i + 1
|
|
129
|
+
result_lines.append(f"{line_number:6d}\t{line_content}")
|
|
130
|
+
|
|
131
|
+
return "\n".join(result_lines)
|
|
132
|
+
else:
|
|
133
|
+
# Tool without long-term memory
|
|
134
|
+
@tool(description=tool_description)
|
|
135
|
+
def read_file(
|
|
136
|
+
file_path: str,
|
|
137
|
+
state: Annotated[FilesystemState, InjectedState],
|
|
138
|
+
offset: int = 0,
|
|
139
|
+
limit: int = 2000,
|
|
140
|
+
) -> str:
|
|
141
|
+
mock_filesystem = state.get("files", {})
|
|
142
|
+
if file_path not in mock_filesystem:
|
|
143
|
+
return f"Error: File '{file_path}' not found"
|
|
144
|
+
content = mock_filesystem[file_path]
|
|
145
|
+
if not content or content.strip() == "":
|
|
146
|
+
return "System reminder: File exists but has empty contents"
|
|
147
|
+
lines = content.splitlines()
|
|
148
|
+
start_idx = offset
|
|
149
|
+
end_idx = min(start_idx + limit, len(lines))
|
|
150
|
+
if start_idx >= len(lines):
|
|
151
|
+
return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
|
|
152
|
+
result_lines = []
|
|
153
|
+
for i in range(start_idx, end_idx):
|
|
154
|
+
line_content = lines[i]
|
|
155
|
+
if len(line_content) > 2000:
|
|
156
|
+
line_content = line_content[:2000]
|
|
157
|
+
line_number = i + 1
|
|
158
|
+
result_lines.append(f"{line_number:6d}\t{line_content}")
|
|
159
|
+
|
|
160
|
+
return "\n".join(result_lines)
|
|
161
|
+
return read_file
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def write_file_tool_generator(has_longterm_memory: bool, custom_description: str = None) -> tool:
|
|
165
|
+
tool_description = WRITE_FILE_TOOL_DESCRIPTION
|
|
166
|
+
if custom_description:
|
|
167
|
+
tool_description = custom_description
|
|
168
|
+
elif has_longterm_memory:
|
|
169
|
+
tool_description += WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
|
|
170
|
+
|
|
171
|
+
if has_longterm_memory:
|
|
172
|
+
# Tool with Long-term memory
|
|
173
|
+
@tool(description=tool_description)
|
|
174
|
+
def write_file(file_path: str, content: str, state: Annotated[FilesystemState, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId]) -> Command:
|
|
175
|
+
if has_memories_prefix(file_path):
|
|
176
|
+
stripped_file_path = strip_memories_prefix(file_path)
|
|
177
|
+
runtime = get_runtime()
|
|
178
|
+
store = runtime.store
|
|
179
|
+
if store is None:
|
|
180
|
+
raise ValueError("Longterm memory is enabled, but no store is available")
|
|
181
|
+
namespace = get_namespace(runtime)
|
|
182
|
+
store.put(namespace, stripped_file_path, content)
|
|
183
|
+
return Command(
|
|
184
|
+
update={
|
|
185
|
+
"messages": [ToolMessage(f"Updated longterm memories file {file_path}", tool_call_id=tool_call_id)]
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
mock_filesystem = state.get("files", {})
|
|
190
|
+
mock_filesystem[file_path] = content
|
|
191
|
+
return Command(
|
|
192
|
+
update={
|
|
193
|
+
"files": mock_filesystem,
|
|
194
|
+
"messages": [ToolMessage(f"Updated file {file_path}", tool_call_id=tool_call_id)]
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
# Tool without long-term memory
|
|
199
|
+
@tool(description=tool_description)
|
|
200
|
+
def write_file(file_path: str, content: str, state: Annotated[FilesystemState, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId]) -> Command:
|
|
201
|
+
mock_filesystem = state.get("files", {})
|
|
202
|
+
mock_filesystem[file_path] = content
|
|
203
|
+
return Command(
|
|
204
|
+
update={
|
|
205
|
+
"files": mock_filesystem,
|
|
206
|
+
"messages": [ToolMessage(f"Updated file {file_path}", tool_call_id=tool_call_id)]
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
return write_file
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def edit_file_tool_generator(has_longterm_memory: bool, custom_description: str = None) -> tool:
|
|
213
|
+
tool_description = EDIT_FILE_TOOL_DESCRIPTION
|
|
214
|
+
if custom_description:
|
|
215
|
+
tool_description = custom_description
|
|
216
|
+
elif has_longterm_memory:
|
|
217
|
+
tool_description += EDIT_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
|
|
218
|
+
|
|
219
|
+
if has_longterm_memory:
|
|
220
|
+
# Tool with Long-term memory
|
|
221
|
+
@tool(description=tool_description)
|
|
222
|
+
def edit_file(file_path: str, old_string: str, new_string: str, state: Annotated[FilesystemState, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId], replace_all: bool = False) -> Command:
|
|
223
|
+
if has_memories_prefix(file_path):
|
|
224
|
+
stripped_file_path = strip_memories_prefix(file_path)
|
|
225
|
+
runtime = get_runtime()
|
|
226
|
+
store = runtime.store
|
|
227
|
+
if store is None:
|
|
228
|
+
raise ValueError("Longterm memory is enabled, but no store is available")
|
|
229
|
+
namespace = get_namespace(runtime)
|
|
230
|
+
item = store.get(namespace, stripped_file_path)
|
|
231
|
+
if item is None:
|
|
232
|
+
return f"Error: File '{file_path}' not found"
|
|
233
|
+
content = item.value
|
|
234
|
+
if old_string not in content:
|
|
235
|
+
return f"Error: String not found in file: '{old_string}'"
|
|
236
|
+
if not replace_all:
|
|
237
|
+
occurrences = content.count(old_string)
|
|
238
|
+
if occurrences > 1:
|
|
239
|
+
return f"Error: String '{old_string}' appears {occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context."
|
|
240
|
+
elif occurrences == 0:
|
|
241
|
+
return f"Error: String not found in file: '{old_string}'"
|
|
242
|
+
new_content = content.replace(old_string, new_string)
|
|
243
|
+
replacement_count = content.count(old_string)
|
|
244
|
+
store.put(namespace, stripped_file_path, new_content)
|
|
245
|
+
return Command(
|
|
246
|
+
update={
|
|
247
|
+
"messages": [ToolMessage(f"Successfully replaced {replacement_count} instance(s) of the string in '{file_path}'", tool_call_id=tool_call_id)]
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
mock_filesystem = state.get("files", {})
|
|
252
|
+
if file_path not in mock_filesystem:
|
|
253
|
+
return f"Error: File '{file_path}' not found"
|
|
254
|
+
content = mock_filesystem[file_path]
|
|
255
|
+
if old_string not in content:
|
|
256
|
+
return f"Error: String not found in file: '{old_string}'"
|
|
257
|
+
if not replace_all:
|
|
258
|
+
occurrences = content.count(old_string)
|
|
259
|
+
if occurrences > 1:
|
|
260
|
+
return f"Error: String '{old_string}' appears {occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context."
|
|
261
|
+
elif occurrences == 0:
|
|
262
|
+
return f"Error: String not found in file: '{old_string}'"
|
|
263
|
+
new_content = content.replace(old_string, new_string)
|
|
264
|
+
replacement_count = content.count(old_string)
|
|
265
|
+
result_msg = f"Successfully replaced {replacement_count} instance(s) of the string in '{file_path}'"
|
|
266
|
+
mock_filesystem[file_path] = new_content
|
|
267
|
+
return Command(
|
|
268
|
+
update={
|
|
269
|
+
"files": mock_filesystem,
|
|
270
|
+
"messages": [ToolMessage(result_msg, tool_call_id=tool_call_id)],
|
|
271
|
+
}
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
# Tool without long-term memory
|
|
275
|
+
@tool(description=tool_description)
|
|
276
|
+
def edit_file(file_path: str, old_string: str, new_string: str, state: Annotated[FilesystemState, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId], replace_all: bool = False) -> Command:
|
|
277
|
+
mock_filesystem = state.get("files", {})
|
|
278
|
+
if file_path not in mock_filesystem:
|
|
279
|
+
return f"Error: File '{file_path}' not found"
|
|
280
|
+
content = mock_filesystem[file_path]
|
|
281
|
+
if old_string not in content:
|
|
282
|
+
return f"Error: String not found in file: '{old_string}'"
|
|
283
|
+
if not replace_all:
|
|
284
|
+
occurrences = content.count(old_string)
|
|
285
|
+
if occurrences > 1:
|
|
286
|
+
return f"Error: String '{old_string}' appears {occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context."
|
|
287
|
+
elif occurrences == 0:
|
|
288
|
+
return f"Error: String not found in file: '{old_string}'"
|
|
289
|
+
new_content = content.replace(old_string, new_string)
|
|
290
|
+
replacement_count = content.count(old_string)
|
|
291
|
+
result_msg = f"Successfully replaced {replacement_count} instance(s) of the string in '{file_path}'"
|
|
292
|
+
mock_filesystem[file_path] = new_content
|
|
293
|
+
return Command(
|
|
294
|
+
update={
|
|
295
|
+
"files": mock_filesystem,
|
|
296
|
+
"messages": [ToolMessage(result_msg, tool_call_id=tool_call_id)],
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
return edit_file
|
|
300
|
+
|
|
301
|
+
TOOL_GENERATORS = {
|
|
302
|
+
"ls": ls_tool_generator,
|
|
303
|
+
"read_file": read_file_tool_generator,
|
|
304
|
+
"write_file": write_file_tool_generator,
|
|
305
|
+
"edit_file": edit_file_tool_generator,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
def get_filesystem_tools(has_longterm_memory: bool, custom_tool_descriptions: dict[str, str] = {}) -> list[tool]:
|
|
309
|
+
tools = []
|
|
310
|
+
for tool_name, tool_generator in TOOL_GENERATORS.items():
|
|
311
|
+
tool = tool_generator(has_longterm_memory, custom_tool_descriptions.get(tool_name, None))
|
|
312
|
+
tools.append(tool)
|
|
313
|
+
return tools
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from deepagents.graph import create_deep_agent
|
|
2
|
+
from deepagents.middleware import FilesystemMiddleware
|
|
3
|
+
from deepagents.prompts import WRITE_FILE_TOOL_DESCRIPTION, WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
|
|
4
|
+
from langchain.agents import create_agent
|
|
5
|
+
import pytest
|
|
6
|
+
from langchain_core.messages import HumanMessage
|
|
7
|
+
from langchain_anthropic import ChatAnthropic
|
|
8
|
+
from langgraph.store.memory import InMemoryStore
|
|
9
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
class TestFilesystem:
|
|
13
|
+
def test_create_deepagent_without_store_and_with_longterm_memory_should_fail(self):
|
|
14
|
+
with pytest.raises(ValueError):
|
|
15
|
+
deepagent = create_deep_agent(tools=[], use_longterm_memory=True)
|
|
16
|
+
deepagent.invoke({"messages": [HumanMessage(content="List all of the files in your filesystem?")]})
|
|
17
|
+
|
|
18
|
+
def test_filesystem_system_prompt_override(self):
|
|
19
|
+
agent = create_agent(
|
|
20
|
+
model=ChatAnthropic(model="claude-3-5-sonnet-20240620"),
|
|
21
|
+
middleware=[
|
|
22
|
+
FilesystemMiddleware(
|
|
23
|
+
use_longterm_memory=False,
|
|
24
|
+
system_prompt="In every single response, you must say the word 'pokemon'! You love it!"
|
|
25
|
+
)
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
response = agent.invoke({"messages": [HumanMessage(content="What do you like?")]})
|
|
29
|
+
assert "pokemon" in response["messages"][1].text.lower()
|
|
30
|
+
|
|
31
|
+
def test_filesystem_system_prompt_override_with_longterm_memory(self):
|
|
32
|
+
agent = create_agent(
|
|
33
|
+
model=ChatAnthropic(model="claude-3-5-sonnet-20240620"),
|
|
34
|
+
middleware=[
|
|
35
|
+
FilesystemMiddleware(
|
|
36
|
+
use_longterm_memory=True,
|
|
37
|
+
system_prompt="In every single response, you must say the word 'pokemon'! You love it!"
|
|
38
|
+
)
|
|
39
|
+
],
|
|
40
|
+
store=InMemoryStore()
|
|
41
|
+
)
|
|
42
|
+
response = agent.invoke({"messages": [HumanMessage(content="What do you like?")]})
|
|
43
|
+
assert "pokemon" in response["messages"][1].text.lower()
|
|
44
|
+
|
|
45
|
+
def test_filesystem_tool_prompt_override(self):
|
|
46
|
+
agent = create_agent(
|
|
47
|
+
model=ChatAnthropic(model="claude-3-5-sonnet-20240620"),
|
|
48
|
+
middleware=[
|
|
49
|
+
FilesystemMiddleware(
|
|
50
|
+
use_longterm_memory=False,
|
|
51
|
+
custom_tool_descriptions={
|
|
52
|
+
"ls": "Charmander",
|
|
53
|
+
"read_file": "Bulbasaur",
|
|
54
|
+
"edit_file": "Squirtle"
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
tools = agent.nodes["tools"].bound._tools_by_name
|
|
60
|
+
assert "ls" in tools
|
|
61
|
+
assert tools["ls"].description == "Charmander"
|
|
62
|
+
assert "read_file" in tools
|
|
63
|
+
assert tools["read_file"].description == "Bulbasaur"
|
|
64
|
+
assert "write_file" in tools
|
|
65
|
+
assert tools["write_file"].description == WRITE_FILE_TOOL_DESCRIPTION
|
|
66
|
+
assert "edit_file" in tools
|
|
67
|
+
assert tools["edit_file"].description == "Squirtle"
|
|
68
|
+
|
|
69
|
+
def test_filesystem_tool_prompt_override_with_longterm_memory(self):
|
|
70
|
+
agent = create_agent(
|
|
71
|
+
model=ChatAnthropic(model="claude-3-5-sonnet-20240620"),
|
|
72
|
+
middleware=[
|
|
73
|
+
FilesystemMiddleware(
|
|
74
|
+
use_longterm_memory=True,
|
|
75
|
+
custom_tool_descriptions={
|
|
76
|
+
"ls": "Charmander",
|
|
77
|
+
"read_file": "Bulbasaur",
|
|
78
|
+
"edit_file": "Squirtle"
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
],
|
|
82
|
+
store=InMemoryStore()
|
|
83
|
+
)
|
|
84
|
+
tools = agent.nodes["tools"].bound._tools_by_name
|
|
85
|
+
assert "ls" in tools
|
|
86
|
+
assert tools["ls"].description == "Charmander"
|
|
87
|
+
assert "read_file" in tools
|
|
88
|
+
assert tools["read_file"].description == "Bulbasaur"
|
|
89
|
+
assert "write_file" in tools
|
|
90
|
+
assert tools["write_file"].description == WRITE_FILE_TOOL_DESCRIPTION + WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
|
|
91
|
+
assert "edit_file" in tools
|
|
92
|
+
assert tools["edit_file"].description == "Squirtle"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_longterm_memory_tools(self):
|
|
96
|
+
checkpointer = MemorySaver()
|
|
97
|
+
store = InMemoryStore()
|
|
98
|
+
agent = create_agent(
|
|
99
|
+
model=ChatAnthropic(model="claude-3-5-sonnet-20240620"),
|
|
100
|
+
middleware=[
|
|
101
|
+
FilesystemMiddleware(
|
|
102
|
+
use_longterm_memory=True,
|
|
103
|
+
)
|
|
104
|
+
],
|
|
105
|
+
checkpointer=checkpointer,
|
|
106
|
+
store=store
|
|
107
|
+
)
|
|
108
|
+
assert_longterm_mem_tools(agent, store)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_longterm_memory_tools_deepagent(self):
|
|
112
|
+
checkpointer = MemorySaver()
|
|
113
|
+
store = InMemoryStore()
|
|
114
|
+
agent = create_deep_agent(
|
|
115
|
+
use_longterm_memory=True,
|
|
116
|
+
checkpointer=checkpointer,
|
|
117
|
+
store=store
|
|
118
|
+
)
|
|
119
|
+
assert_longterm_mem_tools(agent, store)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_shortterm_memory_tools_deepagent(self):
|
|
123
|
+
checkpointer = MemorySaver()
|
|
124
|
+
store = InMemoryStore()
|
|
125
|
+
agent = create_deep_agent(
|
|
126
|
+
use_longterm_memory=False,
|
|
127
|
+
checkpointer=checkpointer,
|
|
128
|
+
store=store
|
|
129
|
+
)
|
|
130
|
+
assert_shortterm_mem_tools(agent)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def assert_longterm_mem_tools(agent, store):
|
|
134
|
+
config = {"configurable": {"thread_id": uuid.uuid4()}}
|
|
135
|
+
agent.invoke({"messages": [HumanMessage(content="Write a haiku about Charmander to longterm memory in charmander.txt, use the word 'fiery'")]}, config=config)
|
|
136
|
+
|
|
137
|
+
namespaces = store.list_namespaces()
|
|
138
|
+
assert len(namespaces) == 1
|
|
139
|
+
assert namespaces[0] == "filesystem"
|
|
140
|
+
file_item = store.get(("filesystem"), "charmander.txt")
|
|
141
|
+
assert file_item is not None
|
|
142
|
+
assert file_item.key == "charmander.txt"
|
|
143
|
+
|
|
144
|
+
config2 = {"configurable": {"thread_id": uuid.uuid4()}}
|
|
145
|
+
response = agent.invoke({"messages": [HumanMessage(content="Read the haiku about Charmander from longterm memory at charmander.txt")]}, config=config2)
|
|
146
|
+
|
|
147
|
+
messages = response["messages"]
|
|
148
|
+
read_file_message = next(message for message in messages if message.type == "tool" and message.name == "read_file")
|
|
149
|
+
assert "fiery" in read_file_message.content or "Fiery" in read_file_message.content
|
|
150
|
+
|
|
151
|
+
config3 = {"configurable": {"thread_id": uuid.uuid4()}}
|
|
152
|
+
response = agent.invoke({"messages": [HumanMessage(content="List all of the files in longterm memory")]}, config=config3)
|
|
153
|
+
messages = response["messages"]
|
|
154
|
+
ls_message = next(message for message in messages if message.type == "tool" and message.name == "ls")
|
|
155
|
+
assert "memories/charmander.txt" in ls_message.content
|
|
156
|
+
|
|
157
|
+
config4 = {"configurable": {"thread_id": uuid.uuid4()}}
|
|
158
|
+
response = agent.invoke({"messages": [HumanMessage(content="Edit the haiku about Charmander in longterm memory to use the word 'ember'")]}, config=config4)
|
|
159
|
+
file_item = store.get(("filesystem"), "charmander.txt")
|
|
160
|
+
assert file_item is not None
|
|
161
|
+
assert file_item.key == "charmander.txt"
|
|
162
|
+
assert "ember" in file_item.value or "Ember" in file_item.value
|
|
163
|
+
|
|
164
|
+
config5 = {"configurable": {"thread_id": uuid.uuid4()}}
|
|
165
|
+
response = agent.invoke({"messages": [HumanMessage(content="Read the haiku about Charmander from longterm memory at charmander.txt")]}, config=config5)
|
|
166
|
+
messages = response["messages"]
|
|
167
|
+
read_file_message = next(message for message in messages if message.type == "tool" and message.name == "read_file")
|
|
168
|
+
assert "ember" in read_file_message.content or "Ember" in read_file_message.content
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def assert_shortterm_mem_tools(agent):
|
|
172
|
+
config = {"configurable": {"thread_id": uuid.uuid4()}}
|
|
173
|
+
response = agent.invoke({"messages": [HumanMessage(content="Write a haiku about Charmander to charmander.txt, use the word 'fiery'")]}, config=config)
|
|
174
|
+
files = response["files"]
|
|
175
|
+
assert "charmander.txt" in files
|
|
176
|
+
|
|
177
|
+
response = agent.invoke({"messages": [HumanMessage(content="Read the haiku about Charmander from charmander.txt")]}, config=config)
|
|
178
|
+
messages = response["messages"]
|
|
179
|
+
read_file_message = next(message for message in reversed(messages) if message.type == "tool" and message.name == "read_file")
|
|
180
|
+
assert "fiery" in read_file_message.content or "Fiery" in read_file_message.content
|
|
181
|
+
|
|
182
|
+
response = agent.invoke({"messages": [HumanMessage(content="List all of the files in memory")]}, config=config)
|
|
183
|
+
messages = response["messages"]
|
|
184
|
+
ls_message = next(message for message in messages if message.type == "tool" and message.name == "ls")
|
|
185
|
+
assert "charmander.txt" in ls_message.content
|
|
186
|
+
|
|
187
|
+
response = agent.invoke({"messages": [HumanMessage(content="Edit the haiku about Charmander to use the word 'ember'")]}, config=config)
|
|
188
|
+
files = response["files"]
|
|
189
|
+
assert "charmander.txt" in files
|
|
190
|
+
assert "ember" in files["charmander.txt"] or "Ember" in files["charmander.txt"]
|
|
191
|
+
|
|
192
|
+
response = agent.invoke({"messages": [HumanMessage(content="Read the haiku about Charmander at charmander.txt")]}, config=config)
|
|
193
|
+
messages = response["messages"]
|
|
194
|
+
read_file_message = next(message for message in reversed(messages) if message.type == "tool" and message.name == "read_file")
|
|
195
|
+
assert "ember" in read_file_message.content or "Ember" in read_file_message.content
|
|
196
|
+
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
from langchain_core.tools import tool, InjectedToolCallId
|
|
2
|
-
from langchain_core.messages import ToolMessage
|
|
3
|
-
from langgraph.types import Command
|
|
4
|
-
from langchain.tools.tool_node import InjectedState
|
|
5
|
-
from typing import Annotated, Union
|
|
6
|
-
from deepagents.state import Todo, FilesystemState
|
|
7
|
-
from deepagents.prompts import (
|
|
8
|
-
WRITE_TODOS_TOOL_DESCRIPTION,
|
|
9
|
-
LIST_FILES_TOOL_DESCRIPTION,
|
|
10
|
-
READ_FILE_TOOL_DESCRIPTION,
|
|
11
|
-
WRITE_FILE_TOOL_DESCRIPTION,
|
|
12
|
-
EDIT_FILE_TOOL_DESCRIPTION,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@tool(description=WRITE_TODOS_TOOL_DESCRIPTION)
|
|
17
|
-
def write_todos(
|
|
18
|
-
todos: list[Todo], tool_call_id: Annotated[str, InjectedToolCallId]
|
|
19
|
-
) -> Command:
|
|
20
|
-
return Command(
|
|
21
|
-
update={
|
|
22
|
-
"todos": todos,
|
|
23
|
-
"messages": [
|
|
24
|
-
ToolMessage(f"Updated todo list to {todos}", tool_call_id=tool_call_id)
|
|
25
|
-
],
|
|
26
|
-
}
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@tool(description=LIST_FILES_TOOL_DESCRIPTION)
|
|
31
|
-
def ls(state: Annotated[FilesystemState, InjectedState]) -> list[str]:
|
|
32
|
-
"""List all files"""
|
|
33
|
-
return list(state.get("files", {}).keys())
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@tool(description=READ_FILE_TOOL_DESCRIPTION)
|
|
37
|
-
def read_file(
|
|
38
|
-
file_path: str,
|
|
39
|
-
state: Annotated[FilesystemState, InjectedState],
|
|
40
|
-
offset: int = 0,
|
|
41
|
-
limit: int = 2000,
|
|
42
|
-
) -> str:
|
|
43
|
-
mock_filesystem = state.get("files", {})
|
|
44
|
-
if file_path not in mock_filesystem:
|
|
45
|
-
return f"Error: File '{file_path}' not found"
|
|
46
|
-
|
|
47
|
-
# Get file content
|
|
48
|
-
content = mock_filesystem[file_path]
|
|
49
|
-
|
|
50
|
-
# Handle empty file
|
|
51
|
-
if not content or content.strip() == "":
|
|
52
|
-
return "System reminder: File exists but has empty contents"
|
|
53
|
-
|
|
54
|
-
# Split content into lines
|
|
55
|
-
lines = content.splitlines()
|
|
56
|
-
|
|
57
|
-
# Apply line offset and limit
|
|
58
|
-
start_idx = offset
|
|
59
|
-
end_idx = min(start_idx + limit, len(lines))
|
|
60
|
-
|
|
61
|
-
# Handle case where offset is beyond file length
|
|
62
|
-
if start_idx >= len(lines):
|
|
63
|
-
return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
|
|
64
|
-
|
|
65
|
-
# Format output with line numbers (cat -n format)
|
|
66
|
-
result_lines = []
|
|
67
|
-
for i in range(start_idx, end_idx):
|
|
68
|
-
line_content = lines[i]
|
|
69
|
-
|
|
70
|
-
# Truncate lines longer than 2000 characters
|
|
71
|
-
if len(line_content) > 2000:
|
|
72
|
-
line_content = line_content[:2000]
|
|
73
|
-
|
|
74
|
-
# Line numbers start at 1, so add 1 to the index
|
|
75
|
-
line_number = i + 1
|
|
76
|
-
result_lines.append(f"{line_number:6d}\t{line_content}")
|
|
77
|
-
|
|
78
|
-
return "\n".join(result_lines)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@tool(description=WRITE_FILE_TOOL_DESCRIPTION)
|
|
82
|
-
def write_file(
|
|
83
|
-
file_path: str,
|
|
84
|
-
content: str,
|
|
85
|
-
state: Annotated[FilesystemState, InjectedState],
|
|
86
|
-
tool_call_id: Annotated[str, InjectedToolCallId],
|
|
87
|
-
) -> Command:
|
|
88
|
-
files = state.get("files", {})
|
|
89
|
-
files[file_path] = content
|
|
90
|
-
return Command(
|
|
91
|
-
update={
|
|
92
|
-
"files": files,
|
|
93
|
-
"messages": [
|
|
94
|
-
ToolMessage(f"Updated file {file_path}", tool_call_id=tool_call_id)
|
|
95
|
-
],
|
|
96
|
-
}
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
@tool(description=EDIT_FILE_TOOL_DESCRIPTION)
|
|
101
|
-
def edit_file(
|
|
102
|
-
file_path: str,
|
|
103
|
-
old_string: str,
|
|
104
|
-
new_string: str,
|
|
105
|
-
state: Annotated[FilesystemState, InjectedState],
|
|
106
|
-
tool_call_id: Annotated[str, InjectedToolCallId],
|
|
107
|
-
replace_all: bool = False,
|
|
108
|
-
) -> Union[Command, str]:
|
|
109
|
-
"""Write to a file."""
|
|
110
|
-
mock_filesystem = state.get("files", {})
|
|
111
|
-
# Check if file exists in mock filesystem
|
|
112
|
-
if file_path not in mock_filesystem:
|
|
113
|
-
return f"Error: File '{file_path}' not found"
|
|
114
|
-
|
|
115
|
-
# Get current file content
|
|
116
|
-
content = mock_filesystem[file_path]
|
|
117
|
-
|
|
118
|
-
# Check if old_string exists in the file
|
|
119
|
-
if old_string not in content:
|
|
120
|
-
return f"Error: String not found in file: '{old_string}'"
|
|
121
|
-
|
|
122
|
-
# If not replace_all, check for uniqueness
|
|
123
|
-
if not replace_all:
|
|
124
|
-
occurrences = content.count(old_string)
|
|
125
|
-
if occurrences > 1:
|
|
126
|
-
return f"Error: String '{old_string}' appears {occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context."
|
|
127
|
-
elif occurrences == 0:
|
|
128
|
-
return f"Error: String not found in file: '{old_string}'"
|
|
129
|
-
|
|
130
|
-
# Perform the replacement
|
|
131
|
-
if replace_all:
|
|
132
|
-
new_content = content.replace(old_string, new_string)
|
|
133
|
-
replacement_count = content.count(old_string)
|
|
134
|
-
result_msg = f"Successfully replaced {replacement_count} instance(s) of the string in '{file_path}'"
|
|
135
|
-
else:
|
|
136
|
-
new_content = content.replace(
|
|
137
|
-
old_string, new_string, 1
|
|
138
|
-
) # Replace only first occurrence
|
|
139
|
-
result_msg = f"Successfully replaced string in '{file_path}'"
|
|
140
|
-
|
|
141
|
-
# Update the mock filesystem
|
|
142
|
-
mock_filesystem[file_path] = new_content
|
|
143
|
-
return Command(
|
|
144
|
-
update={
|
|
145
|
-
"files": mock_filesystem,
|
|
146
|
-
"messages": [ToolMessage(result_msg, tool_call_id=tool_call_id)],
|
|
147
|
-
}
|
|
148
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|