deepagents 0.0.11rc1__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.
Files changed (24) hide show
  1. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/PKG-INFO +3 -3
  2. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/deepagents.egg-info/PKG-INFO +3 -3
  3. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/deepagents.egg-info/SOURCES.txt +1 -0
  4. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/deepagents.egg-info/requires.txt +2 -2
  5. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/pyproject.toml +3 -3
  6. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/src/deepagents/graph.py +20 -2
  7. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/src/deepagents/middleware.py +29 -11
  8. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/src/deepagents/prompts.py +30 -24
  9. deepagents-0.0.12rc1/src/deepagents/tools.py +313 -0
  10. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/tests/test_deepagents.py +1 -1
  11. deepagents-0.0.12rc1/tests/test_filesystem.py +196 -0
  12. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/tests/utils.py +1 -1
  13. deepagents-0.0.11rc1/src/deepagents/tools.py +0 -201
  14. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/LICENSE +0 -0
  15. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/README.md +0 -0
  16. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/deepagents.egg-info/dependency_links.txt +0 -0
  17. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/deepagents.egg-info/top_level.txt +0 -0
  18. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/setup.cfg +0 -0
  19. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/src/deepagents/__init__.py +0 -0
  20. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/src/deepagents/model.py +0 -0
  21. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/src/deepagents/state.py +0 -0
  22. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/src/deepagents/types.py +0 -0
  23. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/tests/test_hitl.py +0 -0
  24. {deepagents-0.0.11rc1 → deepagents-0.0.12rc1}/tests/test_middleware.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagents
3
- Version: 0.0.11rc1
3
+ Version: 0.0.12rc1
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
@@ -8,9 +8,9 @@ Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: langgraph>=1.0.0a3
10
10
  Requires-Dist: langchain-anthropic>=0.1.23
11
- Requires-Dist: langchain>=1.0.0a10
11
+ Requires-Dist: langchain>=1.0.0a12
12
12
  Requires-Dist: langgraph-prebuilt>=0.7.0a2
13
- Requires-Dist: ai-filesystem>=0.1.4
13
+ Requires-Dist: langchain-core>=1.0.0a6
14
14
  Provides-Extra: dev
15
15
  Requires-Dist: pytest; extra == "dev"
16
16
  Requires-Dist: pytest-cov; extra == "dev"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagents
3
- Version: 0.0.11rc1
3
+ Version: 0.0.12rc1
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
@@ -8,9 +8,9 @@ Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: langgraph>=1.0.0a3
10
10
  Requires-Dist: langchain-anthropic>=0.1.23
11
- Requires-Dist: langchain>=1.0.0a10
11
+ Requires-Dist: langchain>=1.0.0a12
12
12
  Requires-Dist: langgraph-prebuilt>=0.7.0a2
13
- Requires-Dist: ai-filesystem>=0.1.4
13
+ Requires-Dist: langchain-core>=1.0.0a6
14
14
  Provides-Extra: dev
15
15
  Requires-Dist: pytest; extra == "dev"
16
16
  Requires-Dist: pytest-cov; extra == "dev"
@@ -15,6 +15,7 @@ src/deepagents/state.py
15
15
  src/deepagents/tools.py
16
16
  src/deepagents/types.py
17
17
  tests/test_deepagents.py
18
+ tests/test_filesystem.py
18
19
  tests/test_hitl.py
19
20
  tests/test_middleware.py
20
21
  tests/utils.py
@@ -1,8 +1,8 @@
1
1
  langgraph>=1.0.0a3
2
2
  langchain-anthropic>=0.1.23
3
- langchain>=1.0.0a10
3
+ langchain>=1.0.0a12
4
4
  langgraph-prebuilt>=0.7.0a2
5
- ai-filesystem>=0.1.4
5
+ langchain-core>=1.0.0a6
6
6
 
7
7
  [dev]
8
8
  pytest
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepagents"
3
- version = "0.0.11rc1"
3
+ version = "0.0.12rc1"
4
4
  description = "General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph."
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -8,9 +8,9 @@ requires-python = ">=3.11,<4.0"
8
8
  dependencies = [
9
9
  "langgraph>=1.0.0a3",
10
10
  "langchain-anthropic>=0.1.23",
11
- "langchain>=1.0.0a10",
11
+ "langchain>=1.0.0a12",
12
12
  "langgraph-prebuilt>=0.7.0a2",
13
- "ai-filesystem>=0.1.4",
13
+ "langchain-core>=1.0.0a6"
14
14
  ]
15
15
 
16
16
  [project.optional-dependencies]
@@ -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 [],
@@ -50,11 +55,12 @@ def agent_builder(
50
55
 
51
56
  return create_agent(
52
57
  model,
53
- prompt=instructions + "\n\n" + BASE_AGENT_PROMPT,
58
+ system_prompt=instructions + "\n\n" + BASE_AGENT_PROMPT,
54
59
  tools=tools,
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
  )
@@ -7,11 +7,12 @@ from langchain_core.tools import BaseTool, tool, InjectedToolCallId
7
7
  from langchain_core.messages import ToolMessage
8
8
  from langchain.chat_models import init_chat_model
9
9
  from langgraph.types import Command
10
+ from langgraph.runtime import Runtime
10
11
  from langchain.tools.tool_node import InjectedState
11
12
  from typing import Annotated
12
13
  from deepagents.state import PlanningState, FilesystemState
13
- from deepagents.tools import write_todos, ls, read_file, write_file, edit_file
14
- 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
15
16
  from deepagents.types import SubAgent, CustomSubAgent
16
17
 
17
18
  ###########################
@@ -22,8 +23,11 @@ class PlanningMiddleware(AgentMiddleware):
22
23
  state_schema = PlanningState
23
24
  tools = [write_todos]
24
25
 
25
- def modify_model_request(self, request: ModelRequest, agent_state: PlanningState) -> ModelRequest:
26
- request.system_prompt = request.system_prompt + "\n\n" + WRITE_TODOS_SYSTEM_PROMPT
26
+ def modify_model_request(self, request: ModelRequest, agent_state: PlanningState, runtime: Runtime) -> ModelRequest:
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
27
31
  return request
28
32
 
29
33
  ###########################
@@ -32,10 +36,21 @@ class PlanningMiddleware(AgentMiddleware):
32
36
 
33
37
  class FilesystemMiddleware(AgentMiddleware):
34
38
  state_schema = FilesystemState
35
- tools = [ls, read_file, write_file, edit_file]
36
39
 
37
- def modify_model_request(self, request: ModelRequest, agent_state: FilesystemState) -> ModelRequest:
38
- request.system_prompt = request.system_prompt + "\n\n" + FILESYSTEM_SYSTEM_PROMPT
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)
48
+
49
+ def modify_model_request(self, request: ModelRequest, agent_state: FilesystemState, runtime: Runtime) -> ModelRequest:
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
39
54
  return request
40
55
 
41
56
  ###########################
@@ -59,8 +74,11 @@ class SubAgentMiddleware(AgentMiddleware):
59
74
  )
60
75
  self.tools = [task_tool]
61
76
 
62
- def modify_model_request(self, request: ModelRequest, agent_state: AgentState) -> ModelRequest:
63
- request.system_prompt = request.system_prompt + "\n\n" + TASK_SYSTEM_PROMPT
77
+ def modify_model_request(self, request: ModelRequest, agent_state: AgentState, runtime: Runtime) -> ModelRequest:
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
64
82
  return request
65
83
 
66
84
  def _get_agents(
@@ -82,7 +100,7 @@ def _get_agents(
82
100
  agents = {
83
101
  "general-purpose": create_agent(
84
102
  model,
85
- prompt=BASE_AGENT_PROMPT,
103
+ system_prompt=BASE_AGENT_PROMPT,
86
104
  tools=default_subagent_tools,
87
105
  checkpointer=False,
88
106
  middleware=default_subagent_middleware
@@ -114,7 +132,7 @@ def _get_agents(
114
132
  _middleware = default_subagent_middleware
115
133
  agents[_agent["name"]] = create_agent(
116
134
  sub_model,
117
- prompt=_agent["prompt"],
135
+ system_prompt=_agent["prompt"],
118
136
  tools=_tools,
119
137
  middleware=_middleware,
120
138
  checkpointer=False,
@@ -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 local filesystem.
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 local filesystem.
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 local filesystem. You can access any file directly by using this tool.
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 local filesystem.
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,21 +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
-
410
- 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.
411
- In order to interact with the longterm filesystem, you can use those same tools, but filenames must be prefixed with the memories/ path.
412
-
413
- - ls: list all files in the filesystem (local and longterm)
414
- - read_file: read a file from the filesystem (local and longterm)
415
- - write_file: write to a file in the filesystem (local and longterm)
416
- - edit_file: edit a file in the filesystem (local and longterm)
417
-
418
- Remember, to interact with the longterm filesystem, you must prefix the filename with the memories/ path.
419
- """
420
-
421
427
  BASE_AGENT_PROMPT = """
422
428
  In order to complete the objective that the user asks of you, you have access to a number of standard tools.
423
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
@@ -90,7 +90,7 @@ class TestDeepAgents:
90
90
  "graph": create_agent(
91
91
  model=SAMPLE_MODEL,
92
92
  tools=[get_soccer_scores],
93
- prompt="You are a soccer agent.",
93
+ system_prompt="You are a soccer agent.",
94
94
  )
95
95
  }
96
96
  ]
@@ -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,7 +1,7 @@
1
1
  from langchain_core.tools import tool, InjectedToolCallId
2
2
  from langchain.agents.middleware import AgentMiddleware
3
3
  from typing import Annotated
4
- from langchain.agents.tool_node import InjectedState
4
+ from langchain.tools.tool_node import InjectedState
5
5
  from langchain.agents.middleware import AgentMiddleware, AgentState
6
6
  from langgraph.types import Command
7
7
  from langchain_core.messages import ToolMessage
@@ -1,201 +0,0 @@
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 langchain.tools.tool_node import InjectedState
6
- from typing import Annotated, Union
7
- from deepagents.state import Todo, FilesystemState
8
- from deepagents.prompts import (
9
- WRITE_TODOS_TOOL_DESCRIPTION,
10
- LIST_FILES_TOOL_DESCRIPTION,
11
- READ_FILE_TOOL_DESCRIPTION,
12
- WRITE_FILE_TOOL_DESCRIPTION,
13
- EDIT_FILE_TOOL_DESCRIPTION,
14
- )
15
- from ai_filesystem import FilesystemClient
16
- import os
17
-
18
- def has_memories_prefix(file_path: str) -> bool:
19
- return file_path.startswith("memories/")
20
-
21
- def append_memories_prefix(file_path: str) -> str:
22
- return f"memories/{file_path}"
23
-
24
- def strip_memories_prefix(file_path: str) -> str:
25
- return file_path.replace("memories/", "")
26
-
27
- @tool(description=WRITE_TODOS_TOOL_DESCRIPTION)
28
- def write_todos(
29
- todos: list[Todo], tool_call_id: Annotated[str, InjectedToolCallId]
30
- ) -> Command:
31
- return Command(
32
- update={
33
- "todos": todos,
34
- "messages": [
35
- ToolMessage(f"Updated todo list to {todos}", tool_call_id=tool_call_id)
36
- ],
37
- }
38
- )
39
-
40
-
41
- @tool(description=LIST_FILES_TOOL_DESCRIPTION)
42
- def ls(state: Annotated[FilesystemState, InjectedState]) -> list[str]:
43
- """List all files"""
44
- files = []
45
- files.extend(list(state.get("files", {}).keys()))
46
- # Special handling for longterm filesystem
47
- if os.getenv("LONGTERM_FILESYSTEM_NAME") and os.getenv("AGENT_FS_API_KEY"):
48
- filesystem_client = FilesystemClient(
49
- filesystem=os.getenv("LONGTERM_FILESYSTEM_NAME")
50
- )
51
- file_data_list = filesystem_client._list_files()
52
- memories_files = [f"memories/{f.path}" for f in file_data_list]
53
- files.extend(memories_files)
54
- return files
55
-
56
-
57
- @tool(description=READ_FILE_TOOL_DESCRIPTION)
58
- def read_file(
59
- file_path: str,
60
- state: Annotated[FilesystemState, InjectedState],
61
- offset: int = 0,
62
- limit: int = 2000,
63
- ) -> str:
64
- # Special handling for longterm filesystem
65
- if os.getenv("LONGTERM_FILESYSTEM_NAME") and os.getenv("AGENT_FS_API_KEY") and has_memories_prefix(file_path):
66
- filesystem_client = FilesystemClient(
67
- filesystem=os.getenv("LONGTERM_FILESYSTEM_NAME")
68
- )
69
- file_path = strip_memories_prefix(file_path)
70
- content = filesystem_client.read_file(file_path)
71
- return content
72
-
73
- mock_filesystem = state.get("files", {})
74
- if file_path not in mock_filesystem:
75
- return f"Error: File '{file_path}' not found"
76
-
77
- # Get file content
78
- content = mock_filesystem[file_path]
79
-
80
- # Handle empty file
81
- if not content or content.strip() == "":
82
- return "System reminder: File exists but has empty contents"
83
-
84
- # Split content into lines
85
- lines = content.splitlines()
86
-
87
- # Apply line offset and limit
88
- start_idx = offset
89
- end_idx = min(start_idx + limit, len(lines))
90
-
91
- # Handle case where offset is beyond file length
92
- if start_idx >= len(lines):
93
- return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
94
-
95
- # Format output with line numbers (cat -n format)
96
- result_lines = []
97
- for i in range(start_idx, end_idx):
98
- line_content = lines[i]
99
-
100
- # Truncate lines longer than 2000 characters
101
- if len(line_content) > 2000:
102
- line_content = line_content[:2000]
103
-
104
- # Line numbers start at 1, so add 1 to the index
105
- line_number = i + 1
106
- result_lines.append(f"{line_number:6d}\t{line_content}")
107
-
108
- return "\n".join(result_lines)
109
-
110
-
111
- @tool(description=WRITE_FILE_TOOL_DESCRIPTION)
112
- def write_file(
113
- file_path: str,
114
- content: str,
115
- state: Annotated[FilesystemState, InjectedState],
116
- tool_call_id: Annotated[str, InjectedToolCallId],
117
- ) -> Command:
118
- # Special handling for longterm filesystem
119
- if os.getenv("LONGTERM_FILESYSTEM_NAME") and os.getenv("AGENT_FS_API_KEY") and has_memories_prefix(file_path):
120
- filesystem_client = FilesystemClient(
121
- filesystem=os.getenv("LONGTERM_FILESYSTEM_NAME")
122
- )
123
- short_file_path = strip_memories_prefix(file_path)
124
- filesystem_client.create_file(short_file_path, content)
125
- return Command(
126
- update={
127
- "messages": [ToolMessage(f"Updated longterm memories file {file_path}", tool_call_id=tool_call_id)]
128
- }
129
- )
130
-
131
- files = state.get("files", {})
132
- return Command(
133
- update={
134
- "files": files,
135
- "messages": [ToolMessage(f"Updated file {file_path}", tool_call_id=tool_call_id)]
136
- }
137
- )
138
-
139
-
140
- @tool(description=EDIT_FILE_TOOL_DESCRIPTION)
141
- def edit_file(
142
- file_path: str,
143
- old_string: str,
144
- new_string: str,
145
- state: Annotated[FilesystemState, InjectedState],
146
- tool_call_id: Annotated[str, InjectedToolCallId],
147
- replace_all: bool = False,
148
- ) -> Union[Command, str]:
149
- """Write to a file."""
150
- # Special handling for longterm filesystem
151
- if os.getenv("LONGTERM_FILESYSTEM_NAME") and os.getenv("AGENT_FS_API_KEY") and has_memories_prefix(file_path):
152
- filesystem_client = FilesystemClient(
153
- filesystem=os.getenv("LONGTERM_FILESYSTEM_NAME")
154
- )
155
- short_file_path = strip_memories_prefix(file_path)
156
- filesystem_client.edit_file(short_file_path, old_string, new_string, replace_all)
157
- return Command(
158
- update={
159
- "messages": [ToolMessage(f"Successfully edited longterm memories file {file_path}", tool_call_id=tool_call_id)]
160
- }
161
- )
162
-
163
- mock_filesystem = state.get("files", {})
164
- # Check if file exists in mock filesystem
165
- if file_path not in mock_filesystem:
166
- return f"Error: File '{file_path}' not found"
167
-
168
- # Get current file content
169
- content = mock_filesystem[file_path]
170
-
171
- # Check if old_string exists in the file
172
- if old_string not in content:
173
- return f"Error: String not found in file: '{old_string}'"
174
-
175
- # If not replace_all, check for uniqueness
176
- if not replace_all:
177
- occurrences = content.count(old_string)
178
- if occurrences > 1:
179
- 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."
180
- elif occurrences == 0:
181
- return f"Error: String not found in file: '{old_string}'"
182
-
183
- # Perform the replacement
184
- if replace_all:
185
- new_content = content.replace(old_string, new_string)
186
- replacement_count = content.count(old_string)
187
- result_msg = f"Successfully replaced {replacement_count} instance(s) of the string in '{file_path}'"
188
- else:
189
- new_content = content.replace(
190
- old_string, new_string, 1
191
- ) # Replace only first occurrence
192
- result_msg = f"Successfully replaced string in '{file_path}'"
193
-
194
- # Update the mock filesystem
195
- mock_filesystem[file_path] = new_content
196
- return Command(
197
- update={
198
- "files": mock_filesystem,
199
- "messages": [ToolMessage(result_msg, tool_call_id=tool_call_id)],
200
- }
201
- )
File without changes
File without changes
File without changes