deepagents 0.1.4__py3-none-any.whl → 0.1.5rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ """Memory backends for pluggable file storage."""
2
+
3
+ from deepagents.backends.composite import CompositeBackend, build_composite_state_backend
4
+ from deepagents.backends.filesystem import FilesystemBackend
5
+ from deepagents.backends.state import StateBackend
6
+ from deepagents.backends.store import StoreBackend
7
+ from deepagents.backends.protocol import BackendProtocol
8
+
9
+ __all__ = [
10
+ "BackendProtocol",
11
+ "CompositeBackend",
12
+ "build_composite_state_backend",
13
+ "FilesystemBackend",
14
+ "StateBackend",
15
+ "StoreBackend",
16
+ ]
@@ -0,0 +1,235 @@
1
+ """CompositeBackend: Route operations to different backends based on path prefix."""
2
+
3
+ from typing import Any, Literal, Optional, TYPE_CHECKING
4
+
5
+ from langchain.tools import ToolRuntime
6
+
7
+ from deepagents.backends.protocol import BackendProtocol, BackendFactory, WriteResult, EditResult
8
+ from deepagents.backends.state import StateBackend
9
+ from deepagents.backends.utils import FileInfo, GrepMatch
10
+ from deepagents.backends.protocol import BackendFactory
11
+
12
+
13
+ class CompositeBackend:
14
+
15
+ def __init__(
16
+ self,
17
+ default: BackendProtocol | StateBackend,
18
+ routes: dict[str, BackendProtocol],
19
+ ) -> None:
20
+ # Default backend
21
+ self.default = default
22
+
23
+ # Virtual routes
24
+ self.routes = routes
25
+
26
+ # Sort routes by length (longest first) for correct prefix matching
27
+ self.sorted_routes = sorted(routes.items(), key=lambda x: len(x[0]), reverse=True)
28
+
29
+ def _get_backend_and_key(self, key: str) -> tuple[BackendProtocol, str]:
30
+ """Determine which backend handles this key and strip prefix.
31
+
32
+ Args:
33
+ key: Original file path
34
+
35
+ Returns:
36
+ Tuple of (backend, stripped_key) where stripped_key has the route
37
+ prefix removed (but keeps leading slash).
38
+ """
39
+ # Check routes in order of length (longest first)
40
+ for prefix, backend in self.sorted_routes:
41
+ if key.startswith(prefix):
42
+ # Strip full prefix and ensure a leading slash remains
43
+ # e.g., "/memories/notes.txt" → "/notes.txt"; "/memories/" → "/"
44
+ suffix = key[len(prefix):]
45
+ stripped_key = f"/{suffix}" if suffix else "/"
46
+ return backend, stripped_key
47
+
48
+ return self.default, key
49
+
50
+ def ls_info(self, path: str) -> list[FileInfo]:
51
+ """List files from backends, with appropriate prefixes.
52
+
53
+ Args:
54
+ path: Absolute path to directory.
55
+
56
+ Returns:
57
+ List of FileInfo-like dicts with route prefixes added.
58
+ """
59
+ # Check if path matches a specific route
60
+ for route_prefix, backend in self.sorted_routes:
61
+ if path.startswith(route_prefix.rstrip("/")):
62
+ # Query only the matching routed backend
63
+ suffix = path[len(route_prefix):]
64
+ search_path = f"/{suffix}" if suffix else "/"
65
+ infos = backend.ls_info(search_path)
66
+ prefixed: list[FileInfo] = []
67
+ for fi in infos:
68
+ fi = dict(fi)
69
+ fi["path"] = f"{route_prefix[:-1]}{fi['path']}"
70
+ prefixed.append(fi)
71
+ return prefixed
72
+
73
+ # At root, aggregate default and all routed backends
74
+ if path == "/":
75
+ results: list[FileInfo] = []
76
+ results.extend(self.default.ls_info(path))
77
+ for route_prefix, backend in self.sorted_routes:
78
+ infos = backend.ls_info("/")
79
+ for fi in infos:
80
+ fi = dict(fi)
81
+ fi["path"] = f"{route_prefix[:-1]}{fi['path']}"
82
+ results.append(fi)
83
+ results.sort(key=lambda x: x.get("path", ""))
84
+ return results
85
+
86
+ # Path doesn't match a route: query only default backend
87
+ return self.default.ls_info(path)
88
+
89
+
90
+ def read(
91
+ self,
92
+ file_path: str,
93
+ offset: int = 0,
94
+ limit: int = 2000,
95
+ ) -> str:
96
+ """Read file content, routing to appropriate backend.
97
+
98
+ Args:
99
+ file_path: Absolute file path
100
+ offset: Line offset to start reading from (0-indexed)
101
+ limit: Maximum number of lines to readReturns:
102
+ Formatted file content with line numbers, or error message.
103
+ """
104
+ backend, stripped_key = self._get_backend_and_key(file_path)
105
+ return backend.read(stripped_key, offset=offset, limit=limit)
106
+
107
+
108
+ def grep_raw(
109
+ self,
110
+ pattern: str,
111
+ path: Optional[str] = None,
112
+ glob: Optional[str] = None,
113
+ ) -> list[GrepMatch] | str:
114
+ # If path targets a specific route, search only that backend
115
+ for route_prefix, backend in self.sorted_routes:
116
+ if path is not None and path.startswith(route_prefix.rstrip("/")):
117
+ search_path = path[len(route_prefix) - 1:]
118
+ raw = backend.grep_raw(pattern, search_path if search_path else "/", glob)
119
+ if isinstance(raw, str):
120
+ return raw
121
+ return [{**m, "path": f"{route_prefix[:-1]}{m['path']}"} for m in raw]
122
+
123
+ # Otherwise, search default and all routed backends and merge
124
+ all_matches: list[GrepMatch] = []
125
+ raw_default = self.default.grep_raw(pattern, path, glob) # type: ignore[attr-defined]
126
+ if isinstance(raw_default, str):
127
+ # This happens if error occurs
128
+ return raw_default
129
+ all_matches.extend(raw_default)
130
+
131
+ for route_prefix, backend in self.routes.items():
132
+ raw = backend.grep_raw(pattern, "/", glob)
133
+ if isinstance(raw, str):
134
+ # This happens if error occurs
135
+ return raw
136
+ all_matches.extend({**m, "path": f"{route_prefix[:-1]}{m['path']}"} for m in raw)
137
+
138
+ return all_matches
139
+
140
+ def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
141
+ results: list[FileInfo] = []
142
+
143
+ # Route based on path, not pattern
144
+ for route_prefix, backend in self.sorted_routes:
145
+ if path.startswith(route_prefix.rstrip("/")):
146
+ search_path = path[len(route_prefix) - 1:]
147
+ infos = backend.glob_info(pattern, search_path if search_path else "/")
148
+ return [
149
+ {**fi, "path": f"{route_prefix[:-1]}{fi['path']}"}
150
+ for fi in infos
151
+ ]
152
+
153
+ # Path doesn't match any specific route - search default backend AND all routed backends
154
+ results.extend(self.default.glob_info(pattern, path))
155
+
156
+ for route_prefix, backend in self.routes.items():
157
+ infos = backend.glob_info(pattern, "/")
158
+ results.extend({**fi, "path": f"{route_prefix[:-1]}{fi['path']}"} for fi in infos)
159
+
160
+ # Deterministic ordering
161
+ results.sort(key=lambda x: x.get("path", ""))
162
+ return results
163
+
164
+
165
+ def write(
166
+ self,
167
+ file_path: str,
168
+ content: str,
169
+ ) -> WriteResult:
170
+ """Create a new file, routing to appropriate backend.
171
+
172
+ Args:
173
+ file_path: Absolute file path
174
+ content: File content as a stringReturns:
175
+ Success message or Command object, or error if file already exists.
176
+ """
177
+ backend, stripped_key = self._get_backend_and_key(file_path)
178
+ res = backend.write(stripped_key, content)
179
+ # If this is a state-backed update and default has state, merge so listings reflect changes
180
+ if res.files_update:
181
+ try:
182
+ runtime = getattr(self.default, "runtime", None)
183
+ if runtime is not None:
184
+ state = runtime.state
185
+ files = state.get("files", {})
186
+ files.update(res.files_update)
187
+ state["files"] = files
188
+ except Exception:
189
+ pass
190
+ return res
191
+
192
+ def edit(
193
+ self,
194
+ file_path: str,
195
+ old_string: str,
196
+ new_string: str,
197
+ replace_all: bool = False,
198
+ ) -> EditResult:
199
+ """Edit a file, routing to appropriate backend.
200
+
201
+ Args:
202
+ file_path: Absolute file path
203
+ old_string: String to find and replace
204
+ new_string: Replacement string
205
+ replace_all: If True, replace all occurrencesReturns:
206
+ Success message or Command object, or error message on failure.
207
+ """
208
+ backend, stripped_key = self._get_backend_and_key(file_path)
209
+ res = backend.edit(stripped_key, old_string, new_string, replace_all=replace_all)
210
+ if res.files_update:
211
+ try:
212
+ runtime = getattr(self.default, "runtime", None)
213
+ if runtime is not None:
214
+ state = runtime.state
215
+ files = state.get("files", {})
216
+ files.update(res.files_update)
217
+ state["files"] = files
218
+ except Exception:
219
+ pass
220
+ return res
221
+
222
+
223
+ def build_composite_state_backend(
224
+ runtime: ToolRuntime,
225
+ *,
226
+ routes: dict[str, BackendProtocol | BackendFactory],
227
+ ) -> BackendProtocol:
228
+ built_routes: dict[str, BackendProtocol] = {}
229
+ for k, v in routes.items():
230
+ if isinstance(v, BackendProtocol):
231
+ built_routes[k] = v
232
+ else:
233
+ built_routes[k] = v(runtime)
234
+ default_state = StateBackend(runtime)
235
+ return CompositeBackend(default=default_state, routes=built_routes)