deepanalysts 0.2.1__tar.gz → 0.2.3__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.1 → deepanalysts-0.2.3}/PKG-INFO +1 -1
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/__init__.py +2 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/basement.py +13 -202
- deepanalysts-0.2.3/deepanalysts/backends/composite.py +422 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/store.py +189 -69
- deepanalysts-0.2.3/deepanalysts/backends/supabase_storage.py +507 -0
- deepanalysts-0.2.3/deepanalysts/middleware/_utils.py +82 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/memory.py +16 -2
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/skills.py +15 -2
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts.egg-info/PKG-INFO +1 -1
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts.egg-info/SOURCES.txt +4 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/pyproject.toml +1 -1
- deepanalysts-0.2.3/tests/test_basement.py +171 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/tests/test_composite_backend.py +98 -1
- deepanalysts-0.2.3/tests/test_prompt_sections.py +177 -0
- deepanalysts-0.2.3/tests/test_store_backend.py +295 -0
- deepanalysts-0.2.3/tests/test_supabase_storage_backend.py +580 -0
- deepanalysts-0.2.1/deepanalysts/backends/composite.py +0 -281
- deepanalysts-0.2.1/deepanalysts/middleware/_utils.py +0 -26
- deepanalysts-0.2.1/tests/test_basement.py +0 -325
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/README.md +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/__init__.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/filesystem.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/protocol.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/sandbox.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/state.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/utils.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/clients/__init__.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/clients/basement.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/__init__.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/filesystem.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/patch_tool_calls.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/subagents.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/summarization.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/tool_errors.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/utils/__init__.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/utils/retry.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts.egg-info/dependency_links.txt +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts.egg-info/requires.txt +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts.egg-info/top_level.txt +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/setup.cfg +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/tests/test_filesystem_middleware.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/tests/test_sandbox_backend.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/tests/test_skills_middleware.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/tests/test_summarization_middleware.py +0 -0
- {deepanalysts-0.2.1 → deepanalysts-0.2.3}/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.3
|
|
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,16 +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(
|
|
128
|
+
orchestrator_skills = await loader.load_skills(agent_name="orchestrator")
|
|
134
129
|
|
|
135
130
|
# For technical analyst (only skills targeting technical_analyst)
|
|
136
|
-
ta_skills = await loader.load_skills(
|
|
131
|
+
ta_skills = await loader.load_skills(agent_name="technical_analyst")
|
|
137
132
|
```
|
|
138
133
|
"""
|
|
139
134
|
|
|
@@ -142,32 +137,19 @@ class BasementSkillsLoader:
|
|
|
142
137
|
*,
|
|
143
138
|
client: BasementClient | None = None,
|
|
144
139
|
token_provider: Callable[[], str | None] | None = None,
|
|
145
|
-
store: BaseStore | None = None,
|
|
146
|
-
store_namespace: str = "filesystem",
|
|
147
|
-
supabase_url: str | None = None,
|
|
148
140
|
built_in_dirs: list[str] | None = None,
|
|
149
141
|
) -> None:
|
|
150
|
-
"""Initialize
|
|
142
|
+
"""Initialize the skills loader.
|
|
151
143
|
|
|
152
144
|
Args:
|
|
153
145
|
client: BasementClient instance to use. Defaults to global basement_client.
|
|
154
146
|
token_provider: Callable that returns JWT token.
|
|
155
|
-
store: LangGraph Store instance for writing skill content.
|
|
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
|
-
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._store_namespace = store_namespace
|
|
169
|
-
self._supabase_url = supabase_url
|
|
170
|
-
self._written_skills: set[str] = set() # Track which skills already written
|
|
171
153
|
self._built_in_dirs = built_in_dirs or []
|
|
172
154
|
self._built_in_cache: list[dict[str, Any]] | None = None
|
|
173
155
|
|
|
@@ -258,11 +240,13 @@ class BasementSkillsLoader:
|
|
|
258
240
|
) -> list[SkillMetadata]:
|
|
259
241
|
"""Load skills filtered for specific agent.
|
|
260
242
|
|
|
261
|
-
|
|
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.
|
|
262
246
|
|
|
263
247
|
Args:
|
|
264
248
|
agent_name: Agent to filter skills for (e.g., 'technical_analyst').
|
|
265
|
-
user_id: User ID for
|
|
249
|
+
user_id: User ID (kept for API compatibility, not used for storage).
|
|
266
250
|
|
|
267
251
|
Returns:
|
|
268
252
|
List of SkillMetadata dicts filtered by target_agents.
|
|
@@ -289,18 +273,6 @@ class BasementSkillsLoader:
|
|
|
289
273
|
if bi_skill["name"] not in api_names:
|
|
290
274
|
all_skills.append(bi_skill)
|
|
291
275
|
|
|
292
|
-
# Write skills to store for read_file access (only on first load per user)
|
|
293
|
-
if self._store and user_id:
|
|
294
|
-
needs_write = any(
|
|
295
|
-
f"{user_id}:{self._get_store_path(skill)}/SKILL.md" not in self._written_skills
|
|
296
|
-
for skill in all_skills
|
|
297
|
-
if skill.get("path")
|
|
298
|
-
)
|
|
299
|
-
if needs_write:
|
|
300
|
-
await self._write_skills_to_store(all_skills, user_id)
|
|
301
|
-
elif not user_id and self._store:
|
|
302
|
-
logger.warning("No user_id provided to load_skills, skipping store write")
|
|
303
|
-
|
|
304
276
|
# Filter by target_agents and convert to SkillMetadata
|
|
305
277
|
filtered: list[SkillMetadata] = []
|
|
306
278
|
for skill in all_skills:
|
|
@@ -310,166 +282,6 @@ class BasementSkillsLoader:
|
|
|
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, self._store_namespace)
|
|
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(PutOp(namespace=namespace, key=file_path, value=store_value))
|
|
358
|
-
cache_keys_to_add.append(cache_key)
|
|
359
|
-
|
|
360
|
-
# Process assets
|
|
361
|
-
assets = skill.get("assets", []) or skill.get("skill_assets", []) or []
|
|
362
|
-
for asset in assets:
|
|
363
|
-
asset_path = asset.get("path", "")
|
|
364
|
-
asset_type = asset.get("type", "")
|
|
365
|
-
storage_path = asset.get("storage_path", "")
|
|
366
|
-
|
|
367
|
-
if not asset_path or not storage_path:
|
|
368
|
-
continue
|
|
369
|
-
|
|
370
|
-
full_path = store_skill_path.rstrip("/") + "/" + asset_path.lstrip("/")
|
|
371
|
-
cache_key = f"{user_id}:{full_path}"
|
|
372
|
-
|
|
373
|
-
if cache_key in self._written_skills:
|
|
374
|
-
continue
|
|
375
|
-
|
|
376
|
-
# Queue text asset downloads for parallel execution
|
|
377
|
-
if asset_type in ("script", "markdown"):
|
|
378
|
-
asset_download_tasks.append((full_path, cache_key, storage_path, asset_type))
|
|
379
|
-
# For images, create reference file immediately (no download needed)
|
|
380
|
-
elif asset_type == "image" and self._supabase_url:
|
|
381
|
-
supabase_url = self._supabase_url.rstrip("/")
|
|
382
|
-
public_url = f"{supabase_url}/storage/v1/object/public/skill-assets/{storage_path}"
|
|
383
|
-
ref_content = f"# Image Asset Reference\n\nURL: {public_url}\nType: {asset.get('mime_type', 'image')}\nSize: {asset.get('size_bytes', 0)} bytes"
|
|
384
|
-
file_data = create_file_data(ref_content)
|
|
385
|
-
store_value = {
|
|
386
|
-
"content": file_data["content"],
|
|
387
|
-
"created_at": file_data["created_at"],
|
|
388
|
-
"modified_at": file_data["modified_at"],
|
|
389
|
-
}
|
|
390
|
-
put_ops.append(
|
|
391
|
-
PutOp(
|
|
392
|
-
namespace=namespace,
|
|
393
|
-
key=full_path + ".ref",
|
|
394
|
-
value=store_value,
|
|
395
|
-
)
|
|
396
|
-
)
|
|
397
|
-
cache_keys_to_add.append(cache_key)
|
|
398
|
-
|
|
399
|
-
# Download text assets in parallel
|
|
400
|
-
if asset_download_tasks:
|
|
401
|
-
download_results = await asyncio.gather(
|
|
402
|
-
*[self._download_asset(task[2]) for task in asset_download_tasks],
|
|
403
|
-
return_exceptions=True,
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
for (full_path, cache_key, storage_path, _), result in zip(
|
|
407
|
-
asset_download_tasks, download_results, strict=False
|
|
408
|
-
):
|
|
409
|
-
if isinstance(result, Exception):
|
|
410
|
-
logger.warning(f"Failed to download asset {storage_path}: {result}")
|
|
411
|
-
continue
|
|
412
|
-
if result:
|
|
413
|
-
file_data = create_file_data(result)
|
|
414
|
-
store_value = {
|
|
415
|
-
"content": file_data["content"],
|
|
416
|
-
"created_at": file_data["created_at"],
|
|
417
|
-
"modified_at": file_data["modified_at"],
|
|
418
|
-
}
|
|
419
|
-
put_ops.append(PutOp(namespace=namespace, key=full_path, value=store_value))
|
|
420
|
-
cache_keys_to_add.append(cache_key)
|
|
421
|
-
|
|
422
|
-
# Execute all writes in a single batch
|
|
423
|
-
if put_ops:
|
|
424
|
-
await self._store.abatch(put_ops)
|
|
425
|
-
# Update cache after successful batch write
|
|
426
|
-
self._written_skills.update(cache_keys_to_add)
|
|
427
|
-
logger.debug(f"Wrote {len(put_ops)} skill files to store for user {user_id}")
|
|
428
|
-
|
|
429
|
-
async def _download_asset(self, storage_path: str) -> str | None:
|
|
430
|
-
"""Download text asset from Supabase storage.
|
|
431
|
-
|
|
432
|
-
Args:
|
|
433
|
-
storage_path: Path in Supabase storage bucket.
|
|
434
|
-
|
|
435
|
-
Returns:
|
|
436
|
-
Asset content as string, or None on failure.
|
|
437
|
-
"""
|
|
438
|
-
if not self._supabase_url:
|
|
439
|
-
logger.warning("No Supabase URL configured, cannot download asset")
|
|
440
|
-
return None
|
|
441
|
-
|
|
442
|
-
import httpx
|
|
443
|
-
|
|
444
|
-
try:
|
|
445
|
-
supabase_url = self._supabase_url.rstrip("/")
|
|
446
|
-
url = f"{supabase_url}/storage/v1/object/public/skill-assets/{storage_path}"
|
|
447
|
-
|
|
448
|
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
449
|
-
response = await client.get(url)
|
|
450
|
-
if response.status_code == 200:
|
|
451
|
-
return response.text
|
|
452
|
-
else:
|
|
453
|
-
logger.warning(f"Failed to download asset: {response.status_code}")
|
|
454
|
-
return None
|
|
455
|
-
except Exception as e:
|
|
456
|
-
logger.warning(f"Error downloading asset: {e}")
|
|
457
|
-
return None
|
|
458
|
-
|
|
459
|
-
def _get_store_path(self, skill: dict[str, Any]) -> str:
|
|
460
|
-
"""Get the store path for a skill (without /skills/ prefix).
|
|
461
|
-
|
|
462
|
-
Args:
|
|
463
|
-
skill: Skill dict from API.
|
|
464
|
-
|
|
465
|
-
Returns:
|
|
466
|
-
Store path for the skill.
|
|
467
|
-
"""
|
|
468
|
-
skill_path = skill.get("path", "")
|
|
469
|
-
if skill_path.startswith("/skills/"):
|
|
470
|
-
return skill_path[len("/skills/") - 1 :] # Keep leading /
|
|
471
|
-
return skill_path
|
|
472
|
-
|
|
473
285
|
def _skill_matches_agent(self, skill: dict[str, Any], agent_name: str) -> bool:
|
|
474
286
|
"""Check if skill should be loaded for given agent.
|
|
475
287
|
|
|
@@ -526,9 +338,8 @@ class BasementSkillsLoader:
|
|
|
526
338
|
)
|
|
527
339
|
|
|
528
340
|
def clear_cache(self) -> None:
|
|
529
|
-
"""Clear the skills cache
|
|
341
|
+
"""Clear the skills cache."""
|
|
530
342
|
self._cache.clear()
|
|
531
|
-
self._written_skills.clear()
|
|
532
343
|
self._built_in_cache = None
|
|
533
344
|
|
|
534
345
|
|