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.
Files changed (46) hide show
  1. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/PKG-INFO +1 -1
  2. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/__init__.py +2 -0
  3. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/basement.py +13 -202
  4. deepanalysts-0.2.3/deepanalysts/backends/composite.py +422 -0
  5. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/store.py +189 -69
  6. deepanalysts-0.2.3/deepanalysts/backends/supabase_storage.py +507 -0
  7. deepanalysts-0.2.3/deepanalysts/middleware/_utils.py +82 -0
  8. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/memory.py +16 -2
  9. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/skills.py +15 -2
  10. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts.egg-info/PKG-INFO +1 -1
  11. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts.egg-info/SOURCES.txt +4 -0
  12. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/pyproject.toml +1 -1
  13. deepanalysts-0.2.3/tests/test_basement.py +171 -0
  14. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/tests/test_composite_backend.py +98 -1
  15. deepanalysts-0.2.3/tests/test_prompt_sections.py +177 -0
  16. deepanalysts-0.2.3/tests/test_store_backend.py +295 -0
  17. deepanalysts-0.2.3/tests/test_supabase_storage_backend.py +580 -0
  18. deepanalysts-0.2.1/deepanalysts/backends/composite.py +0 -281
  19. deepanalysts-0.2.1/deepanalysts/middleware/_utils.py +0 -26
  20. deepanalysts-0.2.1/tests/test_basement.py +0 -325
  21. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/README.md +0 -0
  22. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/__init__.py +0 -0
  23. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/filesystem.py +0 -0
  24. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/protocol.py +0 -0
  25. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/sandbox.py +0 -0
  26. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/state.py +0 -0
  27. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/backends/utils.py +0 -0
  28. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/clients/__init__.py +0 -0
  29. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/clients/basement.py +0 -0
  30. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/__init__.py +0 -0
  31. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/filesystem.py +0 -0
  32. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/patch_tool_calls.py +0 -0
  33. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/subagents.py +0 -0
  34. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/summarization.py +0 -0
  35. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/middleware/tool_errors.py +0 -0
  36. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/utils/__init__.py +0 -0
  37. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts/utils/retry.py +0 -0
  38. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts.egg-info/dependency_links.txt +0 -0
  39. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts.egg-info/requires.txt +0 -0
  40. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/deepanalysts.egg-info/top_level.txt +0 -0
  41. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/setup.cfg +0 -0
  42. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/tests/test_filesystem_middleware.py +0 -0
  43. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/tests/test_sandbox_backend.py +0 -0
  44. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/tests/test_skills_middleware.py +0 -0
  45. {deepanalysts-0.2.1 → deepanalysts-0.2.3}/tests/test_summarization_middleware.py +0 -0
  46. {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.1
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 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,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(user_id="user-123", agent_name="orchestrator")
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(user_id="user-123", agent_name="technical_analyst")
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 loader with optional store for caching.
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
- 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.
262
246
 
263
247
  Args:
264
248
  agent_name: Agent to filter skills for (e.g., 'technical_analyst').
265
- user_id: User ID for store namespace (multi-tenant isolation).
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 and written tracking."""
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