deepanalysts 0.2.0__tar.gz → 0.2.2__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.2}/PKG-INFO +1 -1
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/__init__.py +2 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/basement.py +25 -222
- deepanalysts-0.2.2/deepanalysts/backends/composite.py +422 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/sandbox.py +5 -19
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/store.py +189 -69
- deepanalysts-0.2.2/deepanalysts/backends/supabase_storage.py +507 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/_utils.py +1 -3
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/memory.py +5 -15
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/patch_tool_calls.py +1 -5
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/skills.py +11 -34
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/summarization.py +9 -31
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts.egg-info/PKG-INFO +1 -1
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts.egg-info/SOURCES.txt +3 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/pyproject.toml +1 -1
- deepanalysts-0.2.2/tests/test_basement.py +171 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/tests/test_composite_backend.py +98 -1
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/tests/test_filesystem_middleware.py +1 -3
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/tests/test_sandbox_backend.py +1 -4
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/tests/test_skills_middleware.py +9 -28
- deepanalysts-0.2.2/tests/test_store_backend.py +295 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/tests/test_summarization_middleware.py +6 -13
- deepanalysts-0.2.2/tests/test_supabase_storage_backend.py +580 -0
- deepanalysts-0.2.0/deepanalysts/backends/composite.py +0 -281
- deepanalysts-0.2.0/tests/test_basement.py +0 -292
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/README.md +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/__init__.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/filesystem.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/protocol.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/state.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/utils.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/clients/__init__.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/clients/basement.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/__init__.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/filesystem.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/subagents.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/tool_errors.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/utils/__init__.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/utils/retry.py +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts.egg-info/dependency_links.txt +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts.egg-info/requires.txt +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts.egg-info/top_level.txt +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/setup.cfg +0 -0
- {deepanalysts-0.2.0 → deepanalysts-0.2.2}/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.2
|
|
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
|
|
@@ -28,6 +28,7 @@ from deepanalysts.backends.store import (
|
|
|
28
28
|
StoreBackend,
|
|
29
29
|
_validate_namespace,
|
|
30
30
|
)
|
|
31
|
+
from deepanalysts.backends.supabase_storage import SupabaseStorageBackend
|
|
31
32
|
|
|
32
33
|
__all__ = [
|
|
33
34
|
# Protocols and types
|
|
@@ -60,6 +61,7 @@ __all__ = [
|
|
|
60
61
|
"RestrictedSubprocessBackend",
|
|
61
62
|
"StateBackend",
|
|
62
63
|
"StoreBackend",
|
|
64
|
+
"SupabaseStorageBackend",
|
|
63
65
|
]
|
|
64
66
|
|
|
65
67
|
# Optional imports that require additional dependencies (Basement API loaders)
|
|
@@ -14,14 +14,11 @@ import logging
|
|
|
14
14
|
import re
|
|
15
15
|
from collections.abc import Callable
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import
|
|
17
|
+
from typing import Any, Protocol, runtime_checkable
|
|
18
18
|
|
|
19
19
|
from deepanalysts.clients.basement import BasementClient, basement_client
|
|
20
20
|
from deepanalysts.middleware.skills import SkillMetadata
|
|
21
21
|
|
|
22
|
-
if TYPE_CHECKING:
|
|
23
|
-
from langgraph.store.base import BaseStore
|
|
24
|
-
|
|
25
22
|
logger = logging.getLogger(__name__)
|
|
26
23
|
|
|
27
24
|
|
|
@@ -113,9 +110,10 @@ class BasementSkillsLoader:
|
|
|
113
110
|
|
|
114
111
|
Fetches active skills via GET /api/v1/skills/active and filters them
|
|
115
112
|
based on target_agents to ensure each subagent only sees relevant skills.
|
|
113
|
+
Returns SkillMetadata for system prompt injection via SkillsMiddleware.
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
Agents read skill *content* via ``read_file("/skills/...")`` through
|
|
116
|
+
SupabaseStorageBackend — this loader only provides metadata.
|
|
119
117
|
|
|
120
118
|
Filtering logic:
|
|
121
119
|
- Empty target_agents = load for all agents
|
|
@@ -124,20 +122,13 @@ class BasementSkillsLoader:
|
|
|
124
122
|
|
|
125
123
|
Example:
|
|
126
124
|
```python
|
|
127
|
-
loader = BasementSkillsLoader(
|
|
128
|
-
token_provider=get_jwt_from_context,
|
|
129
|
-
store=app.state.store,
|
|
130
|
-
)
|
|
125
|
+
loader = BasementSkillsLoader(token_provider=get_jwt_from_context)
|
|
131
126
|
|
|
132
127
|
# For orchestrator (all skills without specific targeting)
|
|
133
|
-
orchestrator_skills = await loader.load_skills(
|
|
134
|
-
user_id="user-123", agent_name="orchestrator"
|
|
135
|
-
)
|
|
128
|
+
orchestrator_skills = await loader.load_skills(agent_name="orchestrator")
|
|
136
129
|
|
|
137
130
|
# 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
|
-
)
|
|
131
|
+
ta_skills = await loader.load_skills(agent_name="technical_analyst")
|
|
141
132
|
```
|
|
142
133
|
"""
|
|
143
134
|
|
|
@@ -146,27 +137,19 @@ class BasementSkillsLoader:
|
|
|
146
137
|
*,
|
|
147
138
|
client: BasementClient | None = None,
|
|
148
139
|
token_provider: Callable[[], str | None] | None = None,
|
|
149
|
-
store: BaseStore | None = None,
|
|
150
|
-
supabase_url: str | None = None,
|
|
151
140
|
built_in_dirs: list[str] | None = None,
|
|
152
141
|
) -> None:
|
|
153
|
-
"""Initialize
|
|
142
|
+
"""Initialize the skills loader.
|
|
154
143
|
|
|
155
144
|
Args:
|
|
156
145
|
client: BasementClient instance to use. Defaults to global basement_client.
|
|
157
146
|
token_provider: Callable that returns JWT token.
|
|
158
|
-
store: LangGraph Store instance for writing skill content.
|
|
159
|
-
If None, skills won't be available via read_file.
|
|
160
|
-
supabase_url: Supabase URL for asset downloads. Required if store is provided.
|
|
161
147
|
built_in_dirs: Directories containing built-in skills (SKILL.md files).
|
|
162
148
|
These are loaded from the filesystem and merged with API skills.
|
|
163
149
|
"""
|
|
164
150
|
self._client = client or basement_client
|
|
165
151
|
self._token_provider = token_provider
|
|
166
152
|
self._cache: dict[str, list[dict[str, Any]]] = {}
|
|
167
|
-
self._store = store
|
|
168
|
-
self._supabase_url = supabase_url
|
|
169
|
-
self._written_skills: set[str] = set() # Track which skills already written
|
|
170
153
|
self._built_in_dirs = built_in_dirs or []
|
|
171
154
|
self._built_in_cache: list[dict[str, Any]] | None = None
|
|
172
155
|
|
|
@@ -234,15 +217,17 @@ class BasementSkillsLoader:
|
|
|
234
217
|
if not name or not description:
|
|
235
218
|
continue
|
|
236
219
|
|
|
237
|
-
skills.append(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
220
|
+
skills.append(
|
|
221
|
+
{
|
|
222
|
+
"name": name,
|
|
223
|
+
"description": description,
|
|
224
|
+
"content": content,
|
|
225
|
+
"path": f"/skills/{name}",
|
|
226
|
+
"target_agents": [], # Built-in skills available to all agents
|
|
227
|
+
"metadata": frontmatter.get("metadata", {}),
|
|
228
|
+
"assets": [],
|
|
229
|
+
}
|
|
230
|
+
)
|
|
246
231
|
|
|
247
232
|
self._built_in_cache = skills
|
|
248
233
|
logger.debug(f"Loaded {len(skills)} built-in skills from filesystem")
|
|
@@ -255,11 +240,13 @@ class BasementSkillsLoader:
|
|
|
255
240
|
) -> list[SkillMetadata]:
|
|
256
241
|
"""Load skills filtered for specific agent.
|
|
257
242
|
|
|
258
|
-
|
|
243
|
+
Fetches skill metadata from Basement API for system prompt injection.
|
|
244
|
+
Agents read skill *content* via ``read_file("/skills/...")`` through
|
|
245
|
+
the SupabaseStorageBackend — no store caching needed here.
|
|
259
246
|
|
|
260
247
|
Args:
|
|
261
248
|
agent_name: Agent to filter skills for (e.g., 'technical_analyst').
|
|
262
|
-
user_id: User ID for
|
|
249
|
+
user_id: User ID (kept for API compatibility, not used for storage).
|
|
263
250
|
|
|
264
251
|
Returns:
|
|
265
252
|
List of SkillMetadata dicts filtered by target_agents.
|
|
@@ -286,198 +273,15 @@ class BasementSkillsLoader:
|
|
|
286
273
|
if bi_skill["name"] not in api_names:
|
|
287
274
|
all_skills.append(bi_skill)
|
|
288
275
|
|
|
289
|
-
# Write skills to store for read_file access (only on first load per user)
|
|
290
|
-
if self._store and user_id:
|
|
291
|
-
needs_write = any(
|
|
292
|
-
f"{user_id}:{self._get_store_path(skill)}/SKILL.md"
|
|
293
|
-
not in self._written_skills
|
|
294
|
-
for skill in all_skills
|
|
295
|
-
if skill.get("path")
|
|
296
|
-
)
|
|
297
|
-
if needs_write:
|
|
298
|
-
await self._write_skills_to_store(all_skills, user_id)
|
|
299
|
-
elif not user_id and self._store:
|
|
300
|
-
logger.warning("No user_id provided to load_skills, skipping store write")
|
|
301
|
-
|
|
302
276
|
# Filter by target_agents and convert to SkillMetadata
|
|
303
277
|
filtered: list[SkillMetadata] = []
|
|
304
278
|
for skill in all_skills:
|
|
305
279
|
if self._skill_matches_agent(skill, agent_name):
|
|
306
280
|
filtered.append(self._to_skill_metadata(skill))
|
|
307
281
|
|
|
308
|
-
logger.debug(
|
|
309
|
-
f"Filtered {len(filtered)}/{len(all_skills)} skills for agent '{agent_name}'"
|
|
310
|
-
)
|
|
282
|
+
logger.debug(f"Filtered {len(filtered)}/{len(all_skills)} skills for agent '{agent_name}'")
|
|
311
283
|
return filtered
|
|
312
284
|
|
|
313
|
-
async def _write_skills_to_store(self, skills: list[dict[str, Any]], user_id: str) -> None:
|
|
314
|
-
"""Write skill content and assets to Store for read_file access.
|
|
315
|
-
|
|
316
|
-
Args:
|
|
317
|
-
skills: List of skill dicts from Basement API.
|
|
318
|
-
user_id: User ID for store namespace.
|
|
319
|
-
"""
|
|
320
|
-
import asyncio
|
|
321
|
-
|
|
322
|
-
from langgraph.store.base import PutOp
|
|
323
|
-
|
|
324
|
-
from deepanalysts.backends.utils import create_file_data
|
|
325
|
-
|
|
326
|
-
namespace = (user_id, "filesystem")
|
|
327
|
-
|
|
328
|
-
# Collect all put operations for batch execution
|
|
329
|
-
put_ops: list[PutOp] = []
|
|
330
|
-
cache_keys_to_add: list[str] = []
|
|
331
|
-
|
|
332
|
-
# Collect asset download tasks for parallel execution
|
|
333
|
-
asset_download_tasks: list[tuple[str, str, str, str]] = []
|
|
334
|
-
|
|
335
|
-
for skill in skills:
|
|
336
|
-
skill_path = skill.get("path", "")
|
|
337
|
-
content = skill.get("content", "")
|
|
338
|
-
|
|
339
|
-
if not skill_path:
|
|
340
|
-
continue
|
|
341
|
-
|
|
342
|
-
# Strip /skills/ prefix for store
|
|
343
|
-
store_skill_path = self._get_store_path(skill)
|
|
344
|
-
|
|
345
|
-
# Queue SKILL.md write
|
|
346
|
-
if content:
|
|
347
|
-
file_path = store_skill_path.rstrip("/") + "/SKILL.md"
|
|
348
|
-
cache_key = f"{user_id}:{file_path}"
|
|
349
|
-
|
|
350
|
-
if cache_key not in self._written_skills:
|
|
351
|
-
file_data = create_file_data(content)
|
|
352
|
-
store_value = {
|
|
353
|
-
"content": file_data["content"],
|
|
354
|
-
"created_at": file_data["created_at"],
|
|
355
|
-
"modified_at": file_data["modified_at"],
|
|
356
|
-
}
|
|
357
|
-
put_ops.append(
|
|
358
|
-
PutOp(namespace=namespace, key=file_path, value=store_value)
|
|
359
|
-
)
|
|
360
|
-
cache_keys_to_add.append(cache_key)
|
|
361
|
-
|
|
362
|
-
# Process assets
|
|
363
|
-
assets = skill.get("assets", []) or skill.get("skill_assets", []) or []
|
|
364
|
-
for asset in assets:
|
|
365
|
-
asset_path = asset.get("path", "")
|
|
366
|
-
asset_type = asset.get("type", "")
|
|
367
|
-
storage_path = asset.get("storage_path", "")
|
|
368
|
-
|
|
369
|
-
if not asset_path or not storage_path:
|
|
370
|
-
continue
|
|
371
|
-
|
|
372
|
-
full_path = store_skill_path.rstrip("/") + "/" + asset_path.lstrip("/")
|
|
373
|
-
cache_key = f"{user_id}:{full_path}"
|
|
374
|
-
|
|
375
|
-
if cache_key in self._written_skills:
|
|
376
|
-
continue
|
|
377
|
-
|
|
378
|
-
# Queue text asset downloads for parallel execution
|
|
379
|
-
if asset_type in ("script", "markdown"):
|
|
380
|
-
asset_download_tasks.append(
|
|
381
|
-
(full_path, cache_key, storage_path, asset_type)
|
|
382
|
-
)
|
|
383
|
-
# For images, create reference file immediately (no download needed)
|
|
384
|
-
elif asset_type == "image" and self._supabase_url:
|
|
385
|
-
supabase_url = self._supabase_url.rstrip("/")
|
|
386
|
-
public_url = f"{supabase_url}/storage/v1/object/public/skill-assets/{storage_path}"
|
|
387
|
-
ref_content = f"# Image Asset Reference\n\nURL: {public_url}\nType: {asset.get('mime_type', 'image')}\nSize: {asset.get('size_bytes', 0)} bytes"
|
|
388
|
-
file_data = create_file_data(ref_content)
|
|
389
|
-
store_value = {
|
|
390
|
-
"content": file_data["content"],
|
|
391
|
-
"created_at": file_data["created_at"],
|
|
392
|
-
"modified_at": file_data["modified_at"],
|
|
393
|
-
}
|
|
394
|
-
put_ops.append(
|
|
395
|
-
PutOp(
|
|
396
|
-
namespace=namespace,
|
|
397
|
-
key=full_path + ".ref",
|
|
398
|
-
value=store_value,
|
|
399
|
-
)
|
|
400
|
-
)
|
|
401
|
-
cache_keys_to_add.append(cache_key)
|
|
402
|
-
|
|
403
|
-
# Download text assets in parallel
|
|
404
|
-
if asset_download_tasks:
|
|
405
|
-
download_results = await asyncio.gather(
|
|
406
|
-
*[self._download_asset(task[2]) for task in asset_download_tasks],
|
|
407
|
-
return_exceptions=True,
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
for (full_path, cache_key, storage_path, _), result in zip(
|
|
411
|
-
asset_download_tasks, download_results, strict=False
|
|
412
|
-
):
|
|
413
|
-
if isinstance(result, Exception):
|
|
414
|
-
logger.warning(f"Failed to download asset {storage_path}: {result}")
|
|
415
|
-
continue
|
|
416
|
-
if result:
|
|
417
|
-
file_data = create_file_data(result)
|
|
418
|
-
store_value = {
|
|
419
|
-
"content": file_data["content"],
|
|
420
|
-
"created_at": file_data["created_at"],
|
|
421
|
-
"modified_at": file_data["modified_at"],
|
|
422
|
-
}
|
|
423
|
-
put_ops.append(
|
|
424
|
-
PutOp(namespace=namespace, key=full_path, value=store_value)
|
|
425
|
-
)
|
|
426
|
-
cache_keys_to_add.append(cache_key)
|
|
427
|
-
|
|
428
|
-
# Execute all writes in a single batch
|
|
429
|
-
if put_ops:
|
|
430
|
-
await self._store.abatch(put_ops)
|
|
431
|
-
# Update cache after successful batch write
|
|
432
|
-
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
|
-
)
|
|
436
|
-
|
|
437
|
-
async def _download_asset(self, storage_path: str) -> str | None:
|
|
438
|
-
"""Download text asset from Supabase storage.
|
|
439
|
-
|
|
440
|
-
Args:
|
|
441
|
-
storage_path: Path in Supabase storage bucket.
|
|
442
|
-
|
|
443
|
-
Returns:
|
|
444
|
-
Asset content as string, or None on failure.
|
|
445
|
-
"""
|
|
446
|
-
if not self._supabase_url:
|
|
447
|
-
logger.warning("No Supabase URL configured, cannot download asset")
|
|
448
|
-
return None
|
|
449
|
-
|
|
450
|
-
import httpx
|
|
451
|
-
|
|
452
|
-
try:
|
|
453
|
-
supabase_url = self._supabase_url.rstrip("/")
|
|
454
|
-
url = f"{supabase_url}/storage/v1/object/public/skill-assets/{storage_path}"
|
|
455
|
-
|
|
456
|
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
457
|
-
response = await client.get(url)
|
|
458
|
-
if response.status_code == 200:
|
|
459
|
-
return response.text
|
|
460
|
-
else:
|
|
461
|
-
logger.warning(f"Failed to download asset: {response.status_code}")
|
|
462
|
-
return None
|
|
463
|
-
except Exception as e:
|
|
464
|
-
logger.warning(f"Error downloading asset: {e}")
|
|
465
|
-
return None
|
|
466
|
-
|
|
467
|
-
def _get_store_path(self, skill: dict[str, Any]) -> str:
|
|
468
|
-
"""Get the store path for a skill (without /skills/ prefix).
|
|
469
|
-
|
|
470
|
-
Args:
|
|
471
|
-
skill: Skill dict from API.
|
|
472
|
-
|
|
473
|
-
Returns:
|
|
474
|
-
Store path for the skill.
|
|
475
|
-
"""
|
|
476
|
-
skill_path = skill.get("path", "")
|
|
477
|
-
if skill_path.startswith("/skills/"):
|
|
478
|
-
return skill_path[len("/skills/") - 1 :] # Keep leading /
|
|
479
|
-
return skill_path
|
|
480
|
-
|
|
481
285
|
def _skill_matches_agent(self, skill: dict[str, Any], agent_name: str) -> bool:
|
|
482
286
|
"""Check if skill should be loaded for given agent.
|
|
483
287
|
|
|
@@ -534,9 +338,8 @@ class BasementSkillsLoader:
|
|
|
534
338
|
)
|
|
535
339
|
|
|
536
340
|
def clear_cache(self) -> None:
|
|
537
|
-
"""Clear the skills cache
|
|
341
|
+
"""Clear the skills cache."""
|
|
538
342
|
self._cache.clear()
|
|
539
|
-
self._written_skills.clear()
|
|
540
343
|
self._built_in_cache = None
|
|
541
344
|
|
|
542
345
|
|