deepanalysts 0.2.0__tar.gz → 0.2.1__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.
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/PKG-INFO +1 -1
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/backends/basement.py +25 -33
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/backends/sandbox.py +5 -19
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/middleware/_utils.py +1 -3
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/middleware/memory.py +5 -15
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/middleware/patch_tool_calls.py +1 -5
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/middleware/skills.py +11 -34
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/middleware/summarization.py +9 -31
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts.egg-info/PKG-INFO +1 -1
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/pyproject.toml +1 -1
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/tests/test_basement.py +42 -9
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/tests/test_filesystem_middleware.py +1 -3
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/tests/test_sandbox_backend.py +1 -4
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/tests/test_skills_middleware.py +9 -28
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/tests/test_summarization_middleware.py +6 -13
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/README.md +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/__init__.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/backends/__init__.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/backends/composite.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/backends/filesystem.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/backends/protocol.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/backends/state.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/backends/store.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/backends/utils.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/clients/__init__.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/clients/basement.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/middleware/__init__.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/middleware/filesystem.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/middleware/subagents.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/middleware/tool_errors.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/utils/__init__.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts/utils/retry.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts.egg-info/SOURCES.txt +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts.egg-info/dependency_links.txt +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts.egg-info/requires.txt +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/deepanalysts.egg-info/top_level.txt +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/setup.cfg +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/tests/test_composite_backend.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.1}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepanalysts
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: LangChain/LangGraph middleware for building AI agents with memory, skills, and filesystem support
|
|
5
5
|
Author-email: Ganchuluun Narantsatsralt <tsatsralt@swifttech.cloud>
|
|
6
6
|
License: MIT
|
|
@@ -130,14 +130,10 @@ class BasementSkillsLoader:
|
|
|
130
130
|
)
|
|
131
131
|
|
|
132
132
|
# For orchestrator (all skills without specific targeting)
|
|
133
|
-
orchestrator_skills = await loader.load_skills(
|
|
134
|
-
user_id="user-123", agent_name="orchestrator"
|
|
135
|
-
)
|
|
133
|
+
orchestrator_skills = await loader.load_skills(user_id="user-123", agent_name="orchestrator")
|
|
136
134
|
|
|
137
135
|
# For technical analyst (only skills targeting technical_analyst)
|
|
138
|
-
ta_skills = await loader.load_skills(
|
|
139
|
-
user_id="user-123", agent_name="technical_analyst"
|
|
140
|
-
)
|
|
136
|
+
ta_skills = await loader.load_skills(user_id="user-123", agent_name="technical_analyst")
|
|
141
137
|
```
|
|
142
138
|
"""
|
|
143
139
|
|
|
@@ -147,6 +143,7 @@ class BasementSkillsLoader:
|
|
|
147
143
|
client: BasementClient | None = None,
|
|
148
144
|
token_provider: Callable[[], str | None] | None = None,
|
|
149
145
|
store: BaseStore | None = None,
|
|
146
|
+
store_namespace: str = "filesystem",
|
|
150
147
|
supabase_url: str | None = None,
|
|
151
148
|
built_in_dirs: list[str] | None = None,
|
|
152
149
|
) -> None:
|
|
@@ -157,6 +154,9 @@ class BasementSkillsLoader:
|
|
|
157
154
|
token_provider: Callable that returns JWT token.
|
|
158
155
|
store: LangGraph Store instance for writing skill content.
|
|
159
156
|
If None, skills won't be available via read_file.
|
|
157
|
+
store_namespace: Second element of the store namespace tuple.
|
|
158
|
+
Skills are written to ``(user_id, store_namespace)``.
|
|
159
|
+
Defaults to ``"filesystem"`` for backward compatibility.
|
|
160
160
|
supabase_url: Supabase URL for asset downloads. Required if store is provided.
|
|
161
161
|
built_in_dirs: Directories containing built-in skills (SKILL.md files).
|
|
162
162
|
These are loaded from the filesystem and merged with API skills.
|
|
@@ -165,6 +165,7 @@ class BasementSkillsLoader:
|
|
|
165
165
|
self._token_provider = token_provider
|
|
166
166
|
self._cache: dict[str, list[dict[str, Any]]] = {}
|
|
167
167
|
self._store = store
|
|
168
|
+
self._store_namespace = store_namespace
|
|
168
169
|
self._supabase_url = supabase_url
|
|
169
170
|
self._written_skills: set[str] = set() # Track which skills already written
|
|
170
171
|
self._built_in_dirs = built_in_dirs or []
|
|
@@ -234,15 +235,17 @@ class BasementSkillsLoader:
|
|
|
234
235
|
if not name or not description:
|
|
235
236
|
continue
|
|
236
237
|
|
|
237
|
-
skills.append(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
238
|
+
skills.append(
|
|
239
|
+
{
|
|
240
|
+
"name": name,
|
|
241
|
+
"description": description,
|
|
242
|
+
"content": content,
|
|
243
|
+
"path": f"/skills/{name}",
|
|
244
|
+
"target_agents": [], # Built-in skills available to all agents
|
|
245
|
+
"metadata": frontmatter.get("metadata", {}),
|
|
246
|
+
"assets": [],
|
|
247
|
+
}
|
|
248
|
+
)
|
|
246
249
|
|
|
247
250
|
self._built_in_cache = skills
|
|
248
251
|
logger.debug(f"Loaded {len(skills)} built-in skills from filesystem")
|
|
@@ -289,8 +292,7 @@ class BasementSkillsLoader:
|
|
|
289
292
|
# Write skills to store for read_file access (only on first load per user)
|
|
290
293
|
if self._store and user_id:
|
|
291
294
|
needs_write = any(
|
|
292
|
-
f"{user_id}:{self._get_store_path(skill)}/SKILL.md"
|
|
293
|
-
not in self._written_skills
|
|
295
|
+
f"{user_id}:{self._get_store_path(skill)}/SKILL.md" not in self._written_skills
|
|
294
296
|
for skill in all_skills
|
|
295
297
|
if skill.get("path")
|
|
296
298
|
)
|
|
@@ -305,9 +307,7 @@ class BasementSkillsLoader:
|
|
|
305
307
|
if self._skill_matches_agent(skill, agent_name):
|
|
306
308
|
filtered.append(self._to_skill_metadata(skill))
|
|
307
309
|
|
|
308
|
-
logger.debug(
|
|
309
|
-
f"Filtered {len(filtered)}/{len(all_skills)} skills for agent '{agent_name}'"
|
|
310
|
-
)
|
|
310
|
+
logger.debug(f"Filtered {len(filtered)}/{len(all_skills)} skills for agent '{agent_name}'")
|
|
311
311
|
return filtered
|
|
312
312
|
|
|
313
313
|
async def _write_skills_to_store(self, skills: list[dict[str, Any]], user_id: str) -> None:
|
|
@@ -323,7 +323,7 @@ class BasementSkillsLoader:
|
|
|
323
323
|
|
|
324
324
|
from deepanalysts.backends.utils import create_file_data
|
|
325
325
|
|
|
326
|
-
namespace = (user_id,
|
|
326
|
+
namespace = (user_id, self._store_namespace)
|
|
327
327
|
|
|
328
328
|
# Collect all put operations for batch execution
|
|
329
329
|
put_ops: list[PutOp] = []
|
|
@@ -354,9 +354,7 @@ class BasementSkillsLoader:
|
|
|
354
354
|
"created_at": file_data["created_at"],
|
|
355
355
|
"modified_at": file_data["modified_at"],
|
|
356
356
|
}
|
|
357
|
-
put_ops.append(
|
|
358
|
-
PutOp(namespace=namespace, key=file_path, value=store_value)
|
|
359
|
-
)
|
|
357
|
+
put_ops.append(PutOp(namespace=namespace, key=file_path, value=store_value))
|
|
360
358
|
cache_keys_to_add.append(cache_key)
|
|
361
359
|
|
|
362
360
|
# Process assets
|
|
@@ -377,9 +375,7 @@ class BasementSkillsLoader:
|
|
|
377
375
|
|
|
378
376
|
# Queue text asset downloads for parallel execution
|
|
379
377
|
if asset_type in ("script", "markdown"):
|
|
380
|
-
asset_download_tasks.append(
|
|
381
|
-
(full_path, cache_key, storage_path, asset_type)
|
|
382
|
-
)
|
|
378
|
+
asset_download_tasks.append((full_path, cache_key, storage_path, asset_type))
|
|
383
379
|
# For images, create reference file immediately (no download needed)
|
|
384
380
|
elif asset_type == "image" and self._supabase_url:
|
|
385
381
|
supabase_url = self._supabase_url.rstrip("/")
|
|
@@ -420,9 +416,7 @@ class BasementSkillsLoader:
|
|
|
420
416
|
"created_at": file_data["created_at"],
|
|
421
417
|
"modified_at": file_data["modified_at"],
|
|
422
418
|
}
|
|
423
|
-
put_ops.append(
|
|
424
|
-
PutOp(namespace=namespace, key=full_path, value=store_value)
|
|
425
|
-
)
|
|
419
|
+
put_ops.append(PutOp(namespace=namespace, key=full_path, value=store_value))
|
|
426
420
|
cache_keys_to_add.append(cache_key)
|
|
427
421
|
|
|
428
422
|
# Execute all writes in a single batch
|
|
@@ -430,9 +424,7 @@ class BasementSkillsLoader:
|
|
|
430
424
|
await self._store.abatch(put_ops)
|
|
431
425
|
# Update cache after successful batch write
|
|
432
426
|
self._written_skills.update(cache_keys_to_add)
|
|
433
|
-
logger.debug(
|
|
434
|
-
f"Wrote {len(put_ops)} skill files to store for user {user_id}"
|
|
435
|
-
)
|
|
427
|
+
logger.debug(f"Wrote {len(put_ops)} skill files to store for user {user_id}")
|
|
436
428
|
|
|
437
429
|
async def _download_asset(self, storage_path: str) -> str | None:
|
|
438
430
|
"""Download text asset from Supabase storage.
|
|
@@ -230,9 +230,7 @@ except PermissionError:
|
|
|
230
230
|
|
|
231
231
|
def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> str:
|
|
232
232
|
"""Read file content with line numbers."""
|
|
233
|
-
cmd = _READ_COMMAND_TEMPLATE.format(
|
|
234
|
-
file_path=file_path, offset=offset, limit=limit
|
|
235
|
-
)
|
|
233
|
+
cmd = _READ_COMMAND_TEMPLATE.format(file_path=file_path, offset=offset, limit=limit)
|
|
236
234
|
result = self.execute(cmd)
|
|
237
235
|
|
|
238
236
|
output = result.output.rstrip()
|
|
@@ -324,9 +322,7 @@ except PermissionError:
|
|
|
324
322
|
for line in output.split("\n"):
|
|
325
323
|
parts = line.split(":", 2)
|
|
326
324
|
if len(parts) >= 3:
|
|
327
|
-
matches.append(
|
|
328
|
-
{"path": parts[0], "line": int(parts[1]), "text": parts[2]}
|
|
329
|
-
)
|
|
325
|
+
matches.append({"path": parts[0], "line": int(parts[1]), "text": parts[2]})
|
|
330
326
|
|
|
331
327
|
return matches
|
|
332
328
|
|
|
@@ -483,23 +479,13 @@ class RestrictedSubprocessBackend(BaseSandbox):
|
|
|
483
479
|
try:
|
|
484
480
|
file_path = self._temp_dir / path.lstrip("/")
|
|
485
481
|
if not file_path.exists():
|
|
486
|
-
responses.append(
|
|
487
|
-
FileDownloadResponse(
|
|
488
|
-
path=path, content=None, error="file_not_found"
|
|
489
|
-
)
|
|
490
|
-
)
|
|
482
|
+
responses.append(FileDownloadResponse(path=path, content=None, error="file_not_found"))
|
|
491
483
|
continue
|
|
492
484
|
|
|
493
485
|
content = file_path.read_bytes()
|
|
494
|
-
responses.append(
|
|
495
|
-
FileDownloadResponse(path=path, content=content, error=None)
|
|
496
|
-
)
|
|
486
|
+
responses.append(FileDownloadResponse(path=path, content=content, error=None))
|
|
497
487
|
except Exception:
|
|
498
|
-
responses.append(
|
|
499
|
-
FileDownloadResponse(
|
|
500
|
-
path=path, content=None, error="permission_denied"
|
|
501
|
-
)
|
|
502
|
-
)
|
|
488
|
+
responses.append(FileDownloadResponse(path=path, content=None, error="permission_denied"))
|
|
503
489
|
|
|
504
490
|
return responses
|
|
505
491
|
|
|
@@ -19,9 +19,7 @@ def append_to_system_message(
|
|
|
19
19
|
Returns:
|
|
20
20
|
New SystemMessage with the text appended.
|
|
21
21
|
"""
|
|
22
|
-
new_content: list[str | dict[str, str]] = (
|
|
23
|
-
list(system_message.content_blocks) if system_message else []
|
|
24
|
-
)
|
|
22
|
+
new_content: list[str | dict[str, str]] = list(system_message.content_blocks) if system_message else []
|
|
25
23
|
if new_content:
|
|
26
24
|
text = f"\n\n{text}"
|
|
27
25
|
new_content.append({"type": "text", "text": text})
|
|
@@ -152,9 +152,7 @@ class MemoryMiddleware(AgentMiddleware):
|
|
|
152
152
|
self.loader = loader
|
|
153
153
|
self.system_prompt_template = MEMORY_SYSTEM_PROMPT
|
|
154
154
|
|
|
155
|
-
def _get_backend(
|
|
156
|
-
self, state: MemoryState, runtime: Runtime, config: RunnableConfig
|
|
157
|
-
) -> BackendProtocol:
|
|
155
|
+
def _get_backend(self, state: MemoryState, runtime: Runtime, config: RunnableConfig) -> BackendProtocol:
|
|
158
156
|
"""Resolve backend from instance or factory.
|
|
159
157
|
|
|
160
158
|
Args:
|
|
@@ -246,9 +244,7 @@ class MemoryMiddleware(AgentMiddleware):
|
|
|
246
244
|
results = await backend.adownload_files([path])
|
|
247
245
|
# Should get exactly one response for one path
|
|
248
246
|
if len(results) != 1:
|
|
249
|
-
raise AssertionError(
|
|
250
|
-
f"Expected 1 response for path {path}, got {len(results)}"
|
|
251
|
-
)
|
|
247
|
+
raise AssertionError(f"Expected 1 response for path {path}, got {len(results)}")
|
|
252
248
|
response = results[0]
|
|
253
249
|
|
|
254
250
|
if response.error is not None:
|
|
@@ -276,9 +272,7 @@ class MemoryMiddleware(AgentMiddleware):
|
|
|
276
272
|
results = backend.download_files([path])
|
|
277
273
|
# Should get exactly one response for one path
|
|
278
274
|
if len(results) != 1:
|
|
279
|
-
raise AssertionError(
|
|
280
|
-
f"Expected 1 response for path {path}, got {len(results)}"
|
|
281
|
-
)
|
|
275
|
+
raise AssertionError(f"Expected 1 response for path {path}, got {len(results)}")
|
|
282
276
|
response = results[0]
|
|
283
277
|
|
|
284
278
|
if response.error is not None:
|
|
@@ -289,9 +283,7 @@ class MemoryMiddleware(AgentMiddleware):
|
|
|
289
283
|
|
|
290
284
|
return None
|
|
291
285
|
|
|
292
|
-
def before_agent(
|
|
293
|
-
self, state: MemoryState, runtime: Runtime, config: RunnableConfig
|
|
294
|
-
) -> MemoryStateUpdate | None:
|
|
286
|
+
def before_agent(self, state: MemoryState, runtime: Runtime, config: RunnableConfig) -> MemoryStateUpdate | None:
|
|
295
287
|
"""Load memory content before agent execution (synchronous).
|
|
296
288
|
|
|
297
289
|
Loads memory from all configured sources and stores in state.
|
|
@@ -384,9 +376,7 @@ class MemoryMiddleware(AgentMiddleware):
|
|
|
384
376
|
memory_contents=memory_contents,
|
|
385
377
|
)
|
|
386
378
|
|
|
387
|
-
system_message = append_to_system_message(
|
|
388
|
-
request.system_message, memory_section
|
|
389
|
-
)
|
|
379
|
+
system_message = append_to_system_message(request.system_message, memory_section)
|
|
390
380
|
return request.override(system_message=system_message)
|
|
391
381
|
|
|
392
382
|
def wrap_model_call(
|
|
@@ -43,11 +43,7 @@ class PatchToolCallsMiddleware(AgentMiddleware):
|
|
|
43
43
|
if msg.type == "ai" and msg.tool_calls:
|
|
44
44
|
for tool_call in msg.tool_calls:
|
|
45
45
|
corresponding_tool_msg = next(
|
|
46
|
-
(
|
|
47
|
-
m
|
|
48
|
-
for m in messages[i:]
|
|
49
|
-
if m.type == "tool" and m.tool_call_id == tool_call["id"]
|
|
50
|
-
),
|
|
46
|
+
(m for m in messages[i:] if m.type == "tool" and m.tool_call_id == tool_call["id"]),
|
|
51
47
|
None,
|
|
52
48
|
)
|
|
53
49
|
if corresponding_tool_msg is None:
|
|
@@ -182,9 +182,7 @@ def _parse_skill_metadata(
|
|
|
182
182
|
SkillMetadata if parsing succeeds, None if parsing fails or validation errors occur
|
|
183
183
|
"""
|
|
184
184
|
if len(content) > MAX_SKILL_FILE_SIZE:
|
|
185
|
-
logger.warning(
|
|
186
|
-
"Skipping %s: content too large (%d bytes)", skill_path, len(content)
|
|
187
|
-
)
|
|
185
|
+
logger.warning("Skipping %s: content too large (%d bytes)", skill_path, len(content))
|
|
188
186
|
return None
|
|
189
187
|
|
|
190
188
|
# Match YAML frontmatter between --- delimiters
|
|
@@ -213,9 +211,7 @@ def _parse_skill_metadata(
|
|
|
213
211
|
description = frontmatter_data.get("description")
|
|
214
212
|
|
|
215
213
|
if not name or not description:
|
|
216
|
-
logger.warning(
|
|
217
|
-
"Skipping %s: missing required 'name' or 'description'", skill_path
|
|
218
|
-
)
|
|
214
|
+
logger.warning("Skipping %s: missing required 'name' or 'description'", skill_path)
|
|
219
215
|
return None
|
|
220
216
|
|
|
221
217
|
# Validate name format per spec (warn but continue loading for backwards compatibility)
|
|
@@ -299,9 +295,7 @@ def _list_skills(backend: BackendProtocol, source_path: str) -> list[SkillMetada
|
|
|
299
295
|
responses = backend.download_files(paths_to_download)
|
|
300
296
|
|
|
301
297
|
# Parse each downloaded SKILL.md
|
|
302
|
-
for (skill_dir_path, skill_md_path), response in zip(
|
|
303
|
-
skill_md_paths, responses, strict=True
|
|
304
|
-
):
|
|
298
|
+
for (skill_dir_path, skill_md_path), response in zip(skill_md_paths, responses, strict=True):
|
|
305
299
|
if response.error:
|
|
306
300
|
# Skill doesn't have a SKILL.md, skip it
|
|
307
301
|
continue
|
|
@@ -331,9 +325,7 @@ def _list_skills(backend: BackendProtocol, source_path: str) -> list[SkillMetada
|
|
|
331
325
|
return skills
|
|
332
326
|
|
|
333
327
|
|
|
334
|
-
async def _alist_skills(
|
|
335
|
-
backend: BackendProtocol, source_path: str
|
|
336
|
-
) -> list[SkillMetadata]:
|
|
328
|
+
async def _alist_skills(backend: BackendProtocol, source_path: str) -> list[SkillMetadata]:
|
|
337
329
|
"""List all skills from a backend source (async version).
|
|
338
330
|
|
|
339
331
|
Scans backend for subdirectories containing SKILL.md files, downloads their content,
|
|
@@ -378,9 +370,7 @@ async def _alist_skills(
|
|
|
378
370
|
responses = await backend.adownload_files(paths_to_download)
|
|
379
371
|
|
|
380
372
|
# Parse each downloaded SKILL.md
|
|
381
|
-
for (skill_dir_path, skill_md_path), response in zip(
|
|
382
|
-
skill_md_paths, responses, strict=True
|
|
383
|
-
):
|
|
373
|
+
for (skill_dir_path, skill_md_path), response in zip(skill_md_paths, responses, strict=True):
|
|
384
374
|
if response.error:
|
|
385
375
|
# Skill doesn't have a SKILL.md, skip it
|
|
386
376
|
continue
|
|
@@ -479,9 +469,7 @@ class SkillsMiddleware(AgentMiddleware):
|
|
|
479
469
|
self.agent_name = agent_name
|
|
480
470
|
self.system_prompt_template = SKILLS_SYSTEM_PROMPT
|
|
481
471
|
|
|
482
|
-
def _get_backend(
|
|
483
|
-
self, state: SkillsState, runtime: Runtime, config: RunnableConfig
|
|
484
|
-
) -> BackendProtocol:
|
|
472
|
+
def _get_backend(self, state: SkillsState, runtime: Runtime, config: RunnableConfig) -> BackendProtocol:
|
|
485
473
|
"""Resolve backend from instance or factory.
|
|
486
474
|
|
|
487
475
|
Args:
|
|
@@ -504,9 +492,7 @@ class SkillsMiddleware(AgentMiddleware):
|
|
|
504
492
|
)
|
|
505
493
|
backend = self._backend(tool_runtime)
|
|
506
494
|
if backend is None:
|
|
507
|
-
raise AssertionError(
|
|
508
|
-
"SkillsMiddleware requires a valid backend instance"
|
|
509
|
-
)
|
|
495
|
+
raise AssertionError("SkillsMiddleware requires a valid backend instance")
|
|
510
496
|
return backend
|
|
511
497
|
|
|
512
498
|
return self._backend
|
|
@@ -560,14 +546,10 @@ class SkillsMiddleware(AgentMiddleware):
|
|
|
560
546
|
skills_list=skills_list,
|
|
561
547
|
)
|
|
562
548
|
|
|
563
|
-
system_message = append_to_system_message(
|
|
564
|
-
request.system_message, skills_section
|
|
565
|
-
)
|
|
549
|
+
system_message = append_to_system_message(request.system_message, skills_section)
|
|
566
550
|
return request.override(system_message=system_message)
|
|
567
551
|
|
|
568
|
-
def before_agent(
|
|
569
|
-
self, state: SkillsState, runtime: Runtime, config: RunnableConfig
|
|
570
|
-
) -> SkillsStateUpdate | None:
|
|
552
|
+
def before_agent(self, state: SkillsState, runtime: Runtime, config: RunnableConfig) -> SkillsStateUpdate | None:
|
|
571
553
|
"""Load skills metadata before agent execution (synchronous).
|
|
572
554
|
|
|
573
555
|
Runs before each agent interaction to discover available skills from all
|
|
@@ -634,14 +616,9 @@ class SkillsMiddleware(AgentMiddleware):
|
|
|
634
616
|
if self.loader:
|
|
635
617
|
# Get user_id for store namespace (multi-tenant isolation)
|
|
636
618
|
user_id = config.get("configurable", {}).get("user_id")
|
|
637
|
-
skills_metadata = await self.loader.load_skills(
|
|
638
|
-
agent_name=self.agent_name, user_id=user_id
|
|
639
|
-
)
|
|
619
|
+
skills_metadata = await self.loader.load_skills(agent_name=self.agent_name, user_id=user_id)
|
|
640
620
|
if skills_metadata:
|
|
641
|
-
logger.debug(
|
|
642
|
-
f"Loaded {len(skills_metadata)} skills from API "
|
|
643
|
-
f"for agent '{self.agent_name}'"
|
|
644
|
-
)
|
|
621
|
+
logger.debug(f"Loaded {len(skills_metadata)} skills from API for agent '{self.agent_name}'")
|
|
645
622
|
|
|
646
623
|
# Also load from backend/sources (for built-in skills and file-based skills)
|
|
647
624
|
if self._backend and self.sources:
|
|
@@ -162,13 +162,9 @@ class SummarizationMiddleware(BaseSummarizationMiddleware):
|
|
|
162
162
|
self._truncation_text = "...(argument truncated)"
|
|
163
163
|
else:
|
|
164
164
|
self._truncate_args_trigger = truncate_args_settings.get("trigger")
|
|
165
|
-
self._truncate_args_keep = truncate_args_settings.get(
|
|
166
|
-
"keep", ("messages", 20)
|
|
167
|
-
)
|
|
165
|
+
self._truncate_args_keep = truncate_args_settings.get("keep", ("messages", 20))
|
|
168
166
|
self._max_arg_length = truncate_args_settings.get("max_length", 2000)
|
|
169
|
-
self._truncation_text = truncate_args_settings.get(
|
|
170
|
-
"truncation_text", "...(argument truncated)"
|
|
171
|
-
)
|
|
167
|
+
self._truncation_text = truncate_args_settings.get("truncation_text", "...(argument truncated)")
|
|
172
168
|
|
|
173
169
|
def _get_backend(
|
|
174
170
|
self,
|
|
@@ -255,9 +251,7 @@ class SummarizationMiddleware(BaseSummarizationMiddleware):
|
|
|
255
251
|
"""
|
|
256
252
|
return [msg for msg in messages if not self._is_summary_message(msg)]
|
|
257
253
|
|
|
258
|
-
def _build_new_messages_with_path(
|
|
259
|
-
self, summary: str, file_path: str | None
|
|
260
|
-
) -> list[AnyMessage]:
|
|
254
|
+
def _build_new_messages_with_path(self, summary: str, file_path: str | None) -> list[AnyMessage]:
|
|
261
255
|
"""Build the summary message with optional file path reference.
|
|
262
256
|
|
|
263
257
|
Args:
|
|
@@ -290,9 +284,7 @@ class SummarizationMiddleware(BaseSummarizationMiddleware):
|
|
|
290
284
|
)
|
|
291
285
|
]
|
|
292
286
|
|
|
293
|
-
def _should_truncate_args(
|
|
294
|
-
self, messages: list[AnyMessage], total_tokens: int
|
|
295
|
-
) -> bool:
|
|
287
|
+
def _should_truncate_args(self, messages: list[AnyMessage], total_tokens: int) -> bool:
|
|
296
288
|
"""Check if argument truncation should be triggered.
|
|
297
289
|
|
|
298
290
|
Args:
|
|
@@ -399,9 +391,7 @@ class SummarizationMiddleware(BaseSummarizationMiddleware):
|
|
|
399
391
|
}
|
|
400
392
|
return tool_call
|
|
401
393
|
|
|
402
|
-
def _truncate_args(
|
|
403
|
-
self, messages: list[AnyMessage]
|
|
404
|
-
) -> tuple[list[AnyMessage], bool]:
|
|
394
|
+
def _truncate_args(self, messages: list[AnyMessage]) -> tuple[list[AnyMessage], bool]:
|
|
405
395
|
"""Truncate large tool call arguments in old messages.
|
|
406
396
|
|
|
407
397
|
Only truncates arguments for write_file and edit_file tool calls,
|
|
@@ -485,11 +475,7 @@ class SummarizationMiddleware(BaseSummarizationMiddleware):
|
|
|
485
475
|
existing_content = ""
|
|
486
476
|
try:
|
|
487
477
|
responses = backend.download_files([path])
|
|
488
|
-
if
|
|
489
|
-
responses
|
|
490
|
-
and responses[0].content is not None
|
|
491
|
-
and responses[0].error is None
|
|
492
|
-
):
|
|
478
|
+
if responses and responses[0].content is not None and responses[0].error is None:
|
|
493
479
|
existing_content = responses[0].content.decode("utf-8")
|
|
494
480
|
except Exception as e:
|
|
495
481
|
logger.debug(
|
|
@@ -560,11 +546,7 @@ class SummarizationMiddleware(BaseSummarizationMiddleware):
|
|
|
560
546
|
existing_content = ""
|
|
561
547
|
try:
|
|
562
548
|
responses = await backend.adownload_files([path])
|
|
563
|
-
if
|
|
564
|
-
responses
|
|
565
|
-
and responses[0].content is not None
|
|
566
|
-
and responses[0].error is None
|
|
567
|
-
):
|
|
549
|
+
if responses and responses[0].content is not None and responses[0].error is None:
|
|
568
550
|
existing_content = responses[0].content.decode("utf-8")
|
|
569
551
|
except Exception as e:
|
|
570
552
|
logger.debug(
|
|
@@ -660,9 +642,7 @@ class SummarizationMiddleware(BaseSummarizationMiddleware):
|
|
|
660
642
|
}
|
|
661
643
|
return None
|
|
662
644
|
|
|
663
|
-
messages_to_summarize, preserved_messages = self._partition_messages(
|
|
664
|
-
truncated_messages, cutoff_index
|
|
665
|
-
)
|
|
645
|
+
messages_to_summarize, preserved_messages = self._partition_messages(truncated_messages, cutoff_index)
|
|
666
646
|
|
|
667
647
|
# Offload to backend first - warn if this fails but continue with summarization
|
|
668
648
|
backend = self._get_backend(state, runtime)
|
|
@@ -743,9 +723,7 @@ class SummarizationMiddleware(BaseSummarizationMiddleware):
|
|
|
743
723
|
}
|
|
744
724
|
return None
|
|
745
725
|
|
|
746
|
-
messages_to_summarize, preserved_messages = self._partition_messages(
|
|
747
|
-
truncated_messages, cutoff_index
|
|
748
|
-
)
|
|
726
|
+
messages_to_summarize, preserved_messages = self._partition_messages(truncated_messages, cutoff_index)
|
|
749
727
|
|
|
750
728
|
# Offload to backend first - warn if this fails but continue with summarization
|
|
751
729
|
backend = self._get_backend(state, runtime)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepanalysts
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: LangChain/LangGraph middleware for building AI agents with memory, skills, and filesystem support
|
|
5
5
|
Author-email: Ganchuluun Narantsatsralt <tsatsralt@swifttech.cloud>
|
|
6
6
|
License: MIT
|
|
@@ -120,9 +120,7 @@ async def test_basement_skills_loader_assets_to_store():
|
|
|
120
120
|
"deepanalysts.backends.basement.basement_client.get_active_skills",
|
|
121
121
|
new_callable=AsyncMock,
|
|
122
122
|
) as mock_get_skills,
|
|
123
|
-
patch.object(
|
|
124
|
-
BasementSkillsLoader, "_download_asset", new_callable=AsyncMock
|
|
125
|
-
) as mock_download,
|
|
123
|
+
patch.object(BasementSkillsLoader, "_download_asset", new_callable=AsyncMock) as mock_download,
|
|
126
124
|
):
|
|
127
125
|
mock_get_skills.return_value = mock_skills
|
|
128
126
|
mock_download.return_value = "print('hello')"
|
|
@@ -191,9 +189,7 @@ async def test_basement_skills_loader_filters_by_agent():
|
|
|
191
189
|
mock_get_skills.return_value = mock_skills
|
|
192
190
|
|
|
193
191
|
# Load for technical_analyst
|
|
194
|
-
skills = await loader.load_skills(
|
|
195
|
-
agent_name="technical_analyst", user_id=user_id
|
|
196
|
-
)
|
|
192
|
+
skills = await loader.load_skills(agent_name="technical_analyst", user_id=user_id)
|
|
197
193
|
|
|
198
194
|
# Should get shared-skill and ta-only-skill, but NOT other-skill
|
|
199
195
|
skill_names = [s["name"] for s in skills]
|
|
@@ -230,9 +226,7 @@ async def test_basement_skills_loader_wildcard_target():
|
|
|
230
226
|
mock_get_skills.return_value = mock_skills
|
|
231
227
|
|
|
232
228
|
# Should match any agent due to wildcard
|
|
233
|
-
skills = await loader.load_skills(
|
|
234
|
-
agent_name="any_agent", user_id="test-user-123"
|
|
235
|
-
)
|
|
229
|
+
skills = await loader.load_skills(agent_name="any_agent", user_id="test-user-123")
|
|
236
230
|
|
|
237
231
|
assert len(skills) == 1
|
|
238
232
|
assert skills[0]["name"] == "wildcard-skill"
|
|
@@ -290,3 +284,42 @@ async def test_basement_skills_loader_caching():
|
|
|
290
284
|
loader.clear_cache()
|
|
291
285
|
await loader.load_skills(agent_name="ta", user_id="user1")
|
|
292
286
|
assert mock_get_skills.call_count == 2
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@pytest.mark.anyio
|
|
290
|
+
async def test_basement_skills_loader_custom_store_namespace():
|
|
291
|
+
"""Test BasementSkillsLoader writes to custom namespace when store_namespace is set."""
|
|
292
|
+
mock_skills = [
|
|
293
|
+
{
|
|
294
|
+
"name": "ns-skill",
|
|
295
|
+
"path": "/skills/ns-skill",
|
|
296
|
+
"content": "# Namespaced Skill",
|
|
297
|
+
"target_agents": [],
|
|
298
|
+
"assets": [],
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
store = InMemoryStore()
|
|
303
|
+
jwt_token = "jwt"
|
|
304
|
+
|
|
305
|
+
loader = BasementSkillsLoader(
|
|
306
|
+
store=store,
|
|
307
|
+
store_namespace="skills",
|
|
308
|
+
token_provider=lambda: jwt_token,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
with patch(
|
|
312
|
+
"deepanalysts.backends.basement.basement_client.get_active_skills",
|
|
313
|
+
new_callable=AsyncMock,
|
|
314
|
+
) as mock_get_skills:
|
|
315
|
+
mock_get_skills.return_value = mock_skills
|
|
316
|
+
await loader.load_skills(agent_name="orchestrator", user_id="user-abc")
|
|
317
|
+
|
|
318
|
+
# Verify data is in the custom namespace, not "filesystem"
|
|
319
|
+
custom_ns = await store.aget(("user-abc", "skills"), "/ns-skill/SKILL.md")
|
|
320
|
+
assert custom_ns is not None
|
|
321
|
+
assert "Namespaced Skill" in "\n".join(custom_ns.value["content"])
|
|
322
|
+
|
|
323
|
+
# Verify data is NOT in the default namespace
|
|
324
|
+
default_ns = await store.aget(("user-abc", "filesystem"), "/ns-skill/SKILL.md")
|
|
325
|
+
assert default_ns is None
|
|
@@ -73,9 +73,7 @@ class TestReadFileTruncation:
|
|
|
73
73
|
result = _invoke_read_file(middleware, file_path)
|
|
74
74
|
|
|
75
75
|
# Result must not exceed the budget
|
|
76
|
-
assert len(result) <= max_chars, (
|
|
77
|
-
f"Result length {len(result)} exceeds budget {max_chars}"
|
|
78
|
-
)
|
|
76
|
+
assert len(result) <= max_chars, f"Result length {len(result)} exceeds budget {max_chars}"
|
|
79
77
|
|
|
80
78
|
# Truncation message should be present
|
|
81
79
|
assert "[Output was truncated due to size limits" in result
|
|
@@ -4,7 +4,6 @@ Tests the sandbox backend's execute() method with timeout handling,
|
|
|
4
4
|
math calculations, and error capture.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
from deepanalysts.backends.sandbox import RestrictedSubprocessBackend
|
|
9
8
|
|
|
10
9
|
|
|
@@ -41,9 +40,7 @@ class TestRestrictedSubprocessBackend:
|
|
|
41
40
|
def test_execute_captures_stderr(self):
|
|
42
41
|
"""Test stderr is captured in output."""
|
|
43
42
|
backend = RestrictedSubprocessBackend()
|
|
44
|
-
result = backend.execute(
|
|
45
|
-
"python3 -c \"import sys; print('error', file=sys.stderr)\""
|
|
46
|
-
)
|
|
43
|
+
result = backend.execute("python3 -c \"import sys; print('error', file=sys.stderr)\"")
|
|
47
44
|
assert "error" in result.output
|
|
48
45
|
|
|
49
46
|
def test_execute_fibonacci_calculation(self):
|
|
@@ -4,7 +4,6 @@ from typing import Any
|
|
|
4
4
|
from unittest.mock import AsyncMock, MagicMock
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
|
-
|
|
8
7
|
from deepanalysts.middleware.skills import SkillMetadata, SkillsMiddleware
|
|
9
8
|
|
|
10
9
|
|
|
@@ -42,9 +41,7 @@ class TestSkillsMiddlewareDualLoading:
|
|
|
42
41
|
"""Tests for the dual loader + backend/sources merging behavior."""
|
|
43
42
|
|
|
44
43
|
@pytest.mark.anyio
|
|
45
|
-
async def test_loader_only(
|
|
46
|
-
self, mock_runtime: MagicMock, mock_config: dict
|
|
47
|
-
) -> None:
|
|
44
|
+
async def test_loader_only(self, mock_runtime: MagicMock, mock_config: dict) -> None:
|
|
48
45
|
"""Test that loader-only mode works (no backend/sources)."""
|
|
49
46
|
api_skill = _make_skill("api-skill", "From API", "/skills/api-skill/SKILL.md")
|
|
50
47
|
loader = AsyncMock()
|
|
@@ -60,9 +57,7 @@ class TestSkillsMiddlewareDualLoading:
|
|
|
60
57
|
assert result["skills_metadata"] == []
|
|
61
58
|
|
|
62
59
|
@pytest.mark.anyio
|
|
63
|
-
async def test_async_loader_only(
|
|
64
|
-
self, mock_runtime: MagicMock, mock_config: dict
|
|
65
|
-
) -> None:
|
|
60
|
+
async def test_async_loader_only(self, mock_runtime: MagicMock, mock_config: dict) -> None:
|
|
66
61
|
"""Test that async loader-only mode works (no backend/sources)."""
|
|
67
62
|
api_skill = _make_skill("api-skill", "From API", "/skills/api-skill/SKILL.md")
|
|
68
63
|
loader = AsyncMock()
|
|
@@ -79,23 +74,17 @@ class TestSkillsMiddlewareDualLoading:
|
|
|
79
74
|
assert skills[0]["name"] == "api-skill"
|
|
80
75
|
|
|
81
76
|
@pytest.mark.anyio
|
|
82
|
-
async def test_backend_only(
|
|
83
|
-
self, mock_runtime: MagicMock, mock_config: dict, tmp_path: Any
|
|
84
|
-
) -> None:
|
|
77
|
+
async def test_backend_only(self, mock_runtime: MagicMock, mock_config: dict, tmp_path: Any) -> None:
|
|
85
78
|
"""Test that backend-only mode works (no loader)."""
|
|
86
79
|
# Create a skill directory structure on disk
|
|
87
80
|
skill_dir = tmp_path / "skills" / "disk-skill"
|
|
88
81
|
skill_dir.mkdir(parents=True)
|
|
89
|
-
(skill_dir / "SKILL.md").write_text(
|
|
90
|
-
"---\nname: disk-skill\ndescription: From disk\n---\nContent"
|
|
91
|
-
)
|
|
82
|
+
(skill_dir / "SKILL.md").write_text("---\nname: disk-skill\ndescription: From disk\n---\nContent")
|
|
92
83
|
|
|
93
84
|
from deepanalysts.backends import FilesystemBackend
|
|
94
85
|
|
|
95
86
|
backend = FilesystemBackend()
|
|
96
|
-
middleware = SkillsMiddleware(
|
|
97
|
-
backend=backend, sources=[str(tmp_path / "skills")]
|
|
98
|
-
)
|
|
87
|
+
middleware = SkillsMiddleware(backend=backend, sources=[str(tmp_path / "skills")])
|
|
99
88
|
|
|
100
89
|
state: dict[str, Any] = {"messages": []}
|
|
101
90
|
result = await middleware.abefore_agent(state, mock_runtime, mock_config)
|
|
@@ -118,9 +107,7 @@ class TestSkillsMiddlewareDualLoading:
|
|
|
118
107
|
# Disk skill via backend
|
|
119
108
|
skill_dir = tmp_path / "skills" / "disk-skill"
|
|
120
109
|
skill_dir.mkdir(parents=True)
|
|
121
|
-
(skill_dir / "SKILL.md").write_text(
|
|
122
|
-
"---\nname: disk-skill\ndescription: From disk\n---\nContent"
|
|
123
|
-
)
|
|
110
|
+
(skill_dir / "SKILL.md").write_text("---\nname: disk-skill\ndescription: From disk\n---\nContent")
|
|
124
111
|
|
|
125
112
|
from deepanalysts.backends import FilesystemBackend
|
|
126
113
|
|
|
@@ -148,18 +135,14 @@ class TestSkillsMiddlewareDualLoading:
|
|
|
148
135
|
) -> None:
|
|
149
136
|
"""Test that API skills override backend skills with same name."""
|
|
150
137
|
# API skill with name "shared-skill"
|
|
151
|
-
api_skill = _make_skill(
|
|
152
|
-
"shared-skill", "API version", "/api/shared-skill/SKILL.md"
|
|
153
|
-
)
|
|
138
|
+
api_skill = _make_skill("shared-skill", "API version", "/api/shared-skill/SKILL.md")
|
|
154
139
|
loader = AsyncMock()
|
|
155
140
|
loader.load_skills = AsyncMock(return_value=[api_skill])
|
|
156
141
|
|
|
157
142
|
# Disk skill with same name
|
|
158
143
|
skill_dir = tmp_path / "skills" / "shared-skill"
|
|
159
144
|
skill_dir.mkdir(parents=True)
|
|
160
|
-
(skill_dir / "SKILL.md").write_text(
|
|
161
|
-
"---\nname: shared-skill\ndescription: Disk version\n---\nContent"
|
|
162
|
-
)
|
|
145
|
+
(skill_dir / "SKILL.md").write_text("---\nname: shared-skill\ndescription: Disk version\n---\nContent")
|
|
163
146
|
|
|
164
147
|
from deepanalysts.backends import FilesystemBackend
|
|
165
148
|
|
|
@@ -183,9 +166,7 @@ class TestSkillsMiddlewareDualLoading:
|
|
|
183
166
|
assert shared[0]["description"] == "API version"
|
|
184
167
|
|
|
185
168
|
@pytest.mark.anyio
|
|
186
|
-
async def test_skips_if_already_in_state(
|
|
187
|
-
self, mock_runtime: MagicMock, mock_config: dict
|
|
188
|
-
) -> None:
|
|
169
|
+
async def test_skips_if_already_in_state(self, mock_runtime: MagicMock, mock_config: dict) -> None:
|
|
189
170
|
"""Test that loading is skipped if skills_metadata already in state."""
|
|
190
171
|
loader = AsyncMock()
|
|
191
172
|
loader.load_skills = AsyncMock(return_value=[])
|
|
@@ -47,9 +47,7 @@ def make_conversation_messages(
|
|
|
47
47
|
AIMessage(
|
|
48
48
|
content=f"AI response {i}",
|
|
49
49
|
id=f"ai-{i}",
|
|
50
|
-
tool_calls=[
|
|
51
|
-
{"id": f"tool-call-{i}", "name": "test_tool", "args": {}}
|
|
52
|
-
],
|
|
50
|
+
tool_calls=[{"id": f"tool-call-{i}", "name": "test_tool", "args": {}}],
|
|
53
51
|
)
|
|
54
52
|
)
|
|
55
53
|
else:
|
|
@@ -63,9 +61,7 @@ def make_conversation_messages(
|
|
|
63
61
|
|
|
64
62
|
for i in range(num_recent):
|
|
65
63
|
idx = num_old + i
|
|
66
|
-
messages.append(
|
|
67
|
-
HumanMessage(content=f"Recent message {idx}", id=f"recent-{idx}")
|
|
68
|
-
)
|
|
64
|
+
messages.append(HumanMessage(content=f"Recent message {idx}", id=f"recent-{idx}"))
|
|
69
65
|
|
|
70
66
|
return messages
|
|
71
67
|
|
|
@@ -129,17 +125,13 @@ class MockBackend(BackendProtocol):
|
|
|
129
125
|
async def awrite(self, path: str, content: str) -> WriteResult:
|
|
130
126
|
return self.write(path, content)
|
|
131
127
|
|
|
132
|
-
def edit(
|
|
133
|
-
self, path: str, old_string: str, new_string: str, replace_all: bool = False
|
|
134
|
-
) -> EditResult:
|
|
128
|
+
def edit(self, path: str, old_string: str, new_string: str, replace_all: bool = False) -> EditResult:
|
|
135
129
|
self.edit_calls.append((path, old_string, new_string))
|
|
136
130
|
if self.should_fail:
|
|
137
131
|
return EditResult(error=self.error_message or "Mock edit failure")
|
|
138
132
|
return EditResult(path=path, occurrences=1)
|
|
139
133
|
|
|
140
|
-
async def aedit(
|
|
141
|
-
self, path: str, old_string: str, new_string: str, replace_all: bool = False
|
|
142
|
-
) -> EditResult:
|
|
134
|
+
async def aedit(self, path: str, old_string: str, new_string: str, replace_all: bool = False) -> EditResult:
|
|
143
135
|
return self.edit(path, old_string, new_string, replace_all)
|
|
144
136
|
|
|
145
137
|
|
|
@@ -255,7 +247,8 @@ class TestOffloadingBasic:
|
|
|
255
247
|
assert result is not None
|
|
256
248
|
# Summary message should not include file path (since offload failed)
|
|
257
249
|
summary_msgs = [
|
|
258
|
-
m
|
|
250
|
+
m
|
|
251
|
+
for m in result["messages"]
|
|
259
252
|
if hasattr(m, "additional_kwargs") and m.additional_kwargs.get("lc_source") == "summarization"
|
|
260
253
|
]
|
|
261
254
|
assert len(summary_msgs) == 1
|
|
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
|
|
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
|