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.
Files changed (44) hide show
  1. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/PKG-INFO +1 -1
  2. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/__init__.py +2 -0
  3. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/basement.py +25 -222
  4. deepanalysts-0.2.2/deepanalysts/backends/composite.py +422 -0
  5. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/sandbox.py +5 -19
  6. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/store.py +189 -69
  7. deepanalysts-0.2.2/deepanalysts/backends/supabase_storage.py +507 -0
  8. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/_utils.py +1 -3
  9. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/memory.py +5 -15
  10. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/patch_tool_calls.py +1 -5
  11. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/skills.py +11 -34
  12. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/summarization.py +9 -31
  13. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts.egg-info/PKG-INFO +1 -1
  14. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts.egg-info/SOURCES.txt +3 -0
  15. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/pyproject.toml +1 -1
  16. deepanalysts-0.2.2/tests/test_basement.py +171 -0
  17. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/tests/test_composite_backend.py +98 -1
  18. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/tests/test_filesystem_middleware.py +1 -3
  19. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/tests/test_sandbox_backend.py +1 -4
  20. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/tests/test_skills_middleware.py +9 -28
  21. deepanalysts-0.2.2/tests/test_store_backend.py +295 -0
  22. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/tests/test_summarization_middleware.py +6 -13
  23. deepanalysts-0.2.2/tests/test_supabase_storage_backend.py +580 -0
  24. deepanalysts-0.2.0/deepanalysts/backends/composite.py +0 -281
  25. deepanalysts-0.2.0/tests/test_basement.py +0 -292
  26. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/README.md +0 -0
  27. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/__init__.py +0 -0
  28. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/filesystem.py +0 -0
  29. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/protocol.py +0 -0
  30. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/state.py +0 -0
  31. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/backends/utils.py +0 -0
  32. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/clients/__init__.py +0 -0
  33. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/clients/basement.py +0 -0
  34. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/__init__.py +0 -0
  35. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/filesystem.py +0 -0
  36. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/subagents.py +0 -0
  37. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/middleware/tool_errors.py +0 -0
  38. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/utils/__init__.py +0 -0
  39. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts/utils/retry.py +0 -0
  40. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts.egg-info/dependency_links.txt +0 -0
  41. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts.egg-info/requires.txt +0 -0
  42. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/deepanalysts.egg-info/top_level.txt +0 -0
  43. {deepanalysts-0.2.0 → deepanalysts-0.2.2}/setup.cfg +0 -0
  44. {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.0
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 TYPE_CHECKING, Any, Protocol, runtime_checkable
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
- Optionally writes skill content to a LangGraph Store so the `read_file` tool
118
- can access skill files during conversations.
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 loader with optional store for caching.
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
- "name": name,
239
- "description": description,
240
- "content": content,
241
- "path": f"/skills/{name}",
242
- "target_agents": [], # Built-in skills available to all agents
243
- "metadata": frontmatter.get("metadata", {}),
244
- "assets": [],
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
- Writes skill content to store for read_file access if store is configured.
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 store namespace (multi-tenant isolation).
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 and written tracking."""
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