dna-sdk 0.1.0__py3-none-any.whl

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 (203) hide show
  1. dna/__init__.py +5 -0
  2. dna/adapters/__init__.py +0 -0
  3. dna/adapters/_migrations.py +99 -0
  4. dna/adapters/async_adapter.py +118 -0
  5. dna/adapters/embedding/__init__.py +11 -0
  6. dna/adapters/embedding/onnx.py +54 -0
  7. dna/adapters/filesystem/__init__.py +5 -0
  8. dna/adapters/filesystem/cache.py +102 -0
  9. dna/adapters/filesystem/composite.py +387 -0
  10. dna/adapters/filesystem/source.py +499 -0
  11. dna/adapters/filesystem/writable.py +527 -0
  12. dna/adapters/postgres/__init__.py +11 -0
  13. dna/adapters/postgres/eventbus.py +322 -0
  14. dna/adapters/redis/__init__.py +0 -0
  15. dna/adapters/redis/cache.py +100 -0
  16. dna/adapters/resolvers/__init__.py +7 -0
  17. dna/adapters/resolvers/github.py +139 -0
  18. dna/adapters/resolvers/helix.py +76 -0
  19. dna/adapters/resolvers/http.py +184 -0
  20. dna/adapters/resolvers/local.py +143 -0
  21. dna/adapters/resolvers/registry.py +57 -0
  22. dna/adapters/s3/__init__.py +0 -0
  23. dna/adapters/s3/source.py +149 -0
  24. dna/adapters/search/__init__.py +21 -0
  25. dna/adapters/search/migrations.py +71 -0
  26. dna/adapters/search/pgvector.py +493 -0
  27. dna/adapters/search/pgvector_migrations.py +94 -0
  28. dna/adapters/search/rrf.py +54 -0
  29. dna/adapters/search/sqlite_vec.py +484 -0
  30. dna/adapters/sqlalchemy_/__init__.py +14 -0
  31. dna/adapters/sqlalchemy_/migrations.py +444 -0
  32. dna/adapters/sqlalchemy_/source.py +1616 -0
  33. dna/extensions/__init__.py +0 -0
  34. dna/extensions/_template_wrapper/__init__.py +166 -0
  35. dna/extensions/agentskills/DOCS.md +18 -0
  36. dna/extensions/agentskills/__init__.py +314 -0
  37. dna/extensions/agentsmd/DOCS.md +18 -0
  38. dna/extensions/agentsmd/__init__.py +185 -0
  39. dna/extensions/audit/__init__.py +128 -0
  40. dna/extensions/audit/kinds/audit-log.kind.yaml +148 -0
  41. dna/extensions/automation/__init__.py +74 -0
  42. dna/extensions/automation/kinds/automation.kind.yaml +296 -0
  43. dna/extensions/automation/query.py +83 -0
  44. dna/extensions/automation/write_guards.py +167 -0
  45. dna/extensions/collab/__init__.py +79 -0
  46. dna/extensions/doc/__init__.py +31 -0
  47. dna/extensions/doc/kinds/doc.kind.yaml +118 -0
  48. dna/extensions/eval/__init__.py +73 -0
  49. dna/extensions/eval/kinds/eval-baseline.kind.yaml +65 -0
  50. dna/extensions/eval/kinds/eval-case.kind.yaml +140 -0
  51. dna/extensions/eval/kinds/eval-run.kind.yaml +134 -0
  52. dna/extensions/eval/kinds/eval-suite.kind.yaml +87 -0
  53. dna/extensions/eval/runner.py +307 -0
  54. dna/extensions/evidence/__init__.py +109 -0
  55. dna/extensions/evidence/builder.py +15 -0
  56. dna/extensions/evidence/kinds/evidence.kind.yaml +92 -0
  57. dna/extensions/federation/__init__.py +211 -0
  58. dna/extensions/guardrails/DOCS.md +20 -0
  59. dna/extensions/guardrails/__init__.py +135 -0
  60. dna/extensions/helix/DOCS-Actor.md +20 -0
  61. dna/extensions/helix/DOCS-Agent.md +20 -0
  62. dna/extensions/helix/DOCS-Genome.md +20 -0
  63. dna/extensions/helix/DOCS-UseCase.md +20 -0
  64. dna/extensions/helix/__init__.py +1072 -0
  65. dna/extensions/helix/_extras.py +713 -0
  66. dna/extensions/helix/layers.py +33 -0
  67. dna/extensions/helix/write_guards.py +201 -0
  68. dna/extensions/hooks/__init__.py +159 -0
  69. dna/extensions/kinddef/DOCS.md +24 -0
  70. dna/extensions/kinddef/__init__.py +159 -0
  71. dna/extensions/lesson/__init__.py +263 -0
  72. dna/extensions/modelreg/__init__.py +45 -0
  73. dna/extensions/modelreg/kinds/model-profile.kind.yaml +155 -0
  74. dna/extensions/recognizer/__init__.py +172 -0
  75. dna/extensions/research/__init__.py +492 -0
  76. dna/extensions/safety/__init__.py +244 -0
  77. dna/extensions/safety/templates/ml-privacy-filter/README.md +19 -0
  78. dna/extensions/safety/templates/ml-privacy-filter/SAFETYPOLICY.md +56 -0
  79. dna/extensions/sdlc/__init__.py +2092 -0
  80. dna/extensions/sdlc/journey_derive.py +470 -0
  81. dna/extensions/sdlc/kinds/adr.kind.yaml +136 -0
  82. dna/extensions/sdlc/kinds/archive-proposal.kind.yaml +107 -0
  83. dna/extensions/sdlc/kinds/changelog.kind.yaml +108 -0
  84. dna/extensions/sdlc/kinds/cognitive-policy.kind.yaml +470 -0
  85. dna/extensions/sdlc/kinds/forecast.kind.yaml +125 -0
  86. dna/extensions/sdlc/kinds/insight.kind.yaml +87 -0
  87. dna/extensions/sdlc/kinds/kaizen.kind.yaml +106 -0
  88. dna/extensions/sdlc/kinds/lesson-learned.kind.yaml +270 -0
  89. dna/extensions/sdlc/kinds/narrative.kind.yaml +189 -0
  90. dna/extensions/sdlc/kinds/pattern-insight.kind.yaml +204 -0
  91. dna/extensions/sdlc/kinds/postmortem.kind.yaml +168 -0
  92. dna/extensions/sdlc/kinds/pre-mortem.kind.yaml +145 -0
  93. dna/extensions/sdlc/kinds/prompt-template.kind.yaml +109 -0
  94. dna/extensions/sdlc/kinds/retrospective.kind.yaml +186 -0
  95. dna/extensions/sdlc/kinds/risk-register.kind.yaml +178 -0
  96. dna/extensions/sdlc/kinds/saved-view.kind.yaml +127 -0
  97. dna/extensions/sdlc/kinds/status-report.kind.yaml +129 -0
  98. dna/extensions/sdlc/kinds/synthesis-run.kind.yaml +279 -0
  99. dna/extensions/sdlc/kinds/synthesizer-state.kind.yaml +196 -0
  100. dna/extensions/sdlc/kinds/workflow-event.kind.yaml +195 -0
  101. dna/extensions/sdlc/work_item_outputs.py +96 -0
  102. dna/extensions/sdlc/write_guards.py +65 -0
  103. dna/extensions/soulspec/DOCS.md +20 -0
  104. dna/extensions/soulspec/__init__.py +299 -0
  105. dna/extensions/tenant/__init__.py +507 -0
  106. dna/extensions/testkit/__init__.py +293 -0
  107. dna/kernel/__init__.py +2480 -0
  108. dna/kernel/_text.py +40 -0
  109. dna/kernel/bitemporal_guard.py +42 -0
  110. dna/kernel/bundle_handle.py +252 -0
  111. dna/kernel/bundle_io.py +174 -0
  112. dna/kernel/capabilities.py +502 -0
  113. dna/kernel/catalog_cache.py +160 -0
  114. dna/kernel/catalog_tier.py +85 -0
  115. dna/kernel/collaborator_ports.py +390 -0
  116. dna/kernel/composition.py +35 -0
  117. dna/kernel/composition_resolver.py +710 -0
  118. dna/kernel/descriptor_loader.py +56 -0
  119. dna/kernel/document.py +160 -0
  120. dna/kernel/embedding.py +75 -0
  121. dna/kernel/errors.py +67 -0
  122. dna/kernel/eventbus.py +141 -0
  123. dna/kernel/events.py +24 -0
  124. dna/kernel/evidence_capture.py +181 -0
  125. dna/kernel/generic_rw.py +505 -0
  126. dna/kernel/hooks.py +398 -0
  127. dna/kernel/instance.py +1100 -0
  128. dna/kernel/instance_builder.py +400 -0
  129. dna/kernel/invalidation.py +161 -0
  130. dna/kernel/kernel_bootstrap.py +228 -0
  131. dna/kernel/kernel_cache.py +167 -0
  132. dna/kernel/kind_base.py +289 -0
  133. dna/kernel/kind_definition_schema.py +75 -0
  134. dna/kernel/kind_registry.py +854 -0
  135. dna/kernel/layer_policy.py +159 -0
  136. dna/kernel/layer_resolver.py +322 -0
  137. dna/kernel/lock.py +140 -0
  138. dna/kernel/lock_manager.py +40 -0
  139. dna/kernel/meta.py +717 -0
  140. dna/kernel/models.py +1511 -0
  141. dna/kernel/module_lock.py +268 -0
  142. dna/kernel/nav_kernel.py +284 -0
  143. dna/kernel/navigator.py +174 -0
  144. dna/kernel/preview.py +113 -0
  145. dna/kernel/prompt_budget.py +50 -0
  146. dna/kernel/prompt_builder.py +495 -0
  147. dna/kernel/prompt_kernel.py +353 -0
  148. dna/kernel/protocols.py +1534 -0
  149. dna/kernel/query_engine.py +419 -0
  150. dna/kernel/query_fallback.py +179 -0
  151. dna/kernel/registry_accessor.py +140 -0
  152. dna/kernel/reports.py +248 -0
  153. dna/kernel/reports_kernel.py +283 -0
  154. dna/kernel/resolver.py +269 -0
  155. dna/kernel/resource.py +165 -0
  156. dna/kernel/runtime.py +35 -0
  157. dna/kernel/schemas/kind-definition.schema.json +253 -0
  158. dna/kernel/search_engine.py +123 -0
  159. dna/kernel/semver.py +259 -0
  160. dna/kernel/source_facade.py +97 -0
  161. dna/kernel/source_sync.py +217 -0
  162. dna/kernel/studio_ui.py +263 -0
  163. dna/kernel/templates.py +88 -0
  164. dna/kernel/tool_registry.py +62 -0
  165. dna/kernel/tools.py +210 -0
  166. dna/kernel/write_pipeline.py +525 -0
  167. dna/kernel/writer_helpers.py +69 -0
  168. dna/memory/__init__.py +99 -0
  169. dna/memory/decay.py +153 -0
  170. dna/memory/ecphory.py +317 -0
  171. dna/memory/encoding_context.py +97 -0
  172. dna/memory/memory_type.py +38 -0
  173. dna/memory/policy.py +80 -0
  174. dna/memory/retrieval.py +218 -0
  175. dna/memory/verbs.py +367 -0
  176. dna/safety/__init__.py +4 -0
  177. dna/safety/scanner.py +103 -0
  178. dna/safety/scanners/__init__.py +1 -0
  179. dna/safety/scanners/api_scanner.py +95 -0
  180. dna/safety/scanners/llm_judge.py +146 -0
  181. dna/safety/scanners/ml_scanner.py +194 -0
  182. dna/safety/scanners/regex_scanner.py +336 -0
  183. dna/sync/__init__.py +6 -0
  184. dna/sync/apply.py +89 -0
  185. dna/sync/diff.py +81 -0
  186. dna/sync/engine.py +161 -0
  187. dna/sync/hash.py +34 -0
  188. dna/sync/snapshot.py +56 -0
  189. dna/testing/__init__.py +74 -0
  190. dna/testing/record_search_conformance.py +312 -0
  191. dna/testing/rw_conformance.py +419 -0
  192. dna/testing/source_conformance.py +716 -0
  193. dna/testing/stubs.py +47 -0
  194. dna/viz/__init__.py +44 -0
  195. dna/viz/ascii.py +74 -0
  196. dna/viz/health.py +161 -0
  197. dna/viz/matrix.py +84 -0
  198. dna/viz/mermaid.py +777 -0
  199. dna_sdk-0.1.0.dist-info/METADATA +137 -0
  200. dna_sdk-0.1.0.dist-info/RECORD +203 -0
  201. dna_sdk-0.1.0.dist-info/WHEEL +4 -0
  202. dna_sdk-0.1.0.dist-info/entry_points.txt +23 -0
  203. dna_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
dna/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """DNA SDK v3 — Microkernel + Extensions for declarative agent configuration."""
2
+ from dna.kernel import Kernel
3
+ from dna.kernel.runtime import Runtime
4
+
5
+ __all__ = ["Kernel", "Runtime"]
File without changes
@@ -0,0 +1,99 @@
1
+ """Shared forward-only schema-migration runner (s-dna-migration-contract).
2
+
3
+ THE single implementation of the migration *algorithm* the two retired raw
4
+ SQL adapters previously hand-rolled in parallel; today its only caller is
5
+ ``SqlAlchemySource`` (one call per dialect). The contract it encodes
6
+ (documented in ``docs/PORT-CONTRACT.md`` § "Schema migrations"):
7
+
8
+ - **Forward-only, numbered.** Migrations are a ``Mapping[int, payload]``
9
+ keyed by positive integer version. They are applied in ascending
10
+ numeric order. There is no downgrade path — recovery is
11
+ backup/re-seed.
12
+ - **Append-only.** A version already recorded in the adapter's control
13
+ table is NEVER re-applied (and must never be edited in code — add a
14
+ new version instead).
15
+ - **Idempotent boot.** Running against an up-to-date store applies
16
+ nothing and returns ``[]`` — this is what every service boot does.
17
+ - **Control table owned by the adapter.** The helper never touches
18
+ storage itself; the adapter supplies three async callables bound to
19
+ its own connection/control-table dialect. Existing control tables
20
+ (SQLite ``schema_migrations``, Postgres ``{schema}.dna_schema_migrations``)
21
+ keep their exact name and shape — full compat with existing DBs.
22
+
23
+ Why callables instead of a driver abstraction: the two callers have
24
+ deliberately different atomicity semantics (SQLite ``executescript`` per
25
+ version + separate record/commit; Postgres one transaction per version
26
+ wrapping statements + record) and different payload shapes (one SQL
27
+ script string vs a list of statements with a ``{schema}`` placeholder).
28
+ ``apply_version`` owns "apply + record, with MY atomicity" so the helper
29
+ can unify ordering/skip/reporting without flattening those semantics.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import logging
34
+ from typing import Awaitable, Callable, Iterable, Mapping, TypeVar
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ PayloadT = TypeVar("PayloadT")
39
+
40
+
41
+ async def run_migrations(
42
+ migrations: Mapping[int, PayloadT],
43
+ *,
44
+ ensure_control_table: Callable[[], Awaitable[None]],
45
+ fetch_applied: Callable[[], Awaitable[Iterable[int]]],
46
+ apply_version: Callable[[int, PayloadT], Awaitable[None]],
47
+ dialect: str = "SQL",
48
+ ) -> list[int]:
49
+ """Apply every pending migration in ascending version order.
50
+
51
+ Args:
52
+ migrations: version → payload. Keys MUST be positive ints; the
53
+ payload shape is the adapter's business (SQLite: one script
54
+ ``str``; Postgres: ``list[str]`` of statements).
55
+ ensure_control_table: create the adapter's control table if it
56
+ doesn't exist yet (bootstrap — runs FIRST, exactly once).
57
+ fetch_applied: return the version numbers already recorded in
58
+ the control table.
59
+ apply_version: apply ONE version's payload AND record it in the
60
+ control table, honoring the adapter's own atomicity
61
+ (transaction) semantics. Called once per pending version,
62
+ in ascending order. An exception aborts the run — versions
63
+ already applied stay recorded; the failed one is retried on
64
+ the next boot.
65
+ dialect: human label for log lines (``"SQLite"``, ``"Postgres"``).
66
+
67
+ Returns:
68
+ The version numbers applied by THIS run, in application order.
69
+ ``[]`` means the store was already up to date (the idempotent
70
+ re-boot case).
71
+ """
72
+ bad = [v for v in migrations if not isinstance(v, int) or v < 1]
73
+ if bad:
74
+ raise ValueError(
75
+ f"migration versions must be positive ints, got {sorted(bad, key=repr)!r}"
76
+ )
77
+
78
+ await ensure_control_table()
79
+ applied = {int(v) for v in await fetch_applied()}
80
+
81
+ unknown = applied - set(migrations)
82
+ if unknown:
83
+ # Forward-only tolerance: an OLDER binary booting against a NEWER
84
+ # store must not crash (nor try to "fix" anything). Surface it,
85
+ # because writes from old code against a newer schema may misbehave.
86
+ logger.warning(
87
+ "%s control table records migration version(s) %s unknown to this "
88
+ "code (store is newer than the binary) — leaving them untouched.",
89
+ dialect, sorted(unknown),
90
+ )
91
+
92
+ applied_now: list[int] = []
93
+ for version in sorted(migrations):
94
+ if version in applied:
95
+ continue
96
+ logger.info("Applying %s migration v%d", dialect, version)
97
+ await apply_version(version, migrations[version])
98
+ applied_now.append(version)
99
+ return applied_now
@@ -0,0 +1,118 @@
1
+ """AsyncSourceAdapter — wraps a sync SourcePort for async callers.
2
+
3
+ Uses asyncio.to_thread() to run blocking I/O in a thread pool,
4
+ preventing event loop blocking in FastAPI/Starlette.
5
+
6
+ Usage:
7
+ from dna.adapters.async_adapter import AsyncSourceAdapter
8
+ from dna.adapters.s3.source import S3Source
9
+
10
+ sync_source = S3Source(bucket="my-manifests") # sync SourcePort
11
+ async_source = AsyncSourceAdapter(sync_source)
12
+
13
+ docs = await async_source.load_all("my-scope")
14
+
15
+ Design (s-dna-source-conformance-kit): the adapter is a TRANSPARENT
16
+ proxy — it structurally mirrors whatever the wrapped source implements,
17
+ via a table-driven ``__getattr__`` that thread-hops known SourcePort /
18
+ WritableSourcePort methods and forwards everything else untouched.
19
+
20
+ This is the ONE in-repo source adapter that deliberately does NOT
21
+ subclass the ``SourcePort`` Protocol: inheriting would attach the
22
+ Protocol's no-op method stubs to the class, which would (a) shadow the
23
+ ``__getattr__`` forwarding and (b) make the wrapper claim methods its
24
+ inner source doesn't have. Structural transparency is the contract —
25
+ ``isinstance(AsyncSourceAdapter(s), SourcePort)`` reports the truth
26
+ about ``s``, and ``derive_capabilities`` sees the inner source's real
27
+ surface (hops carry the inner signature via ``functools.wraps``).
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import asyncio
32
+ import functools
33
+ import inspect
34
+ from typing import TYPE_CHECKING, Any
35
+
36
+ if TYPE_CHECKING:
37
+ from dna.kernel.capabilities import SourceCapabilities
38
+
39
+
40
+ # Sync methods of the Source/WritableSource contract that must be executed
41
+ # off the event loop. Anything callable NOT in this set is forwarded as-is
42
+ # (helpers, sync capability methods like fetch_bundle_entry — the
43
+ # BundleEntryReadable protocol explicitly allows sync returns).
44
+ _THREAD_HOPPED: frozenset[str] = frozenset({
45
+ # SourcePort
46
+ "load_bootstrap_docs", "load_all", "resolve_ref", "load_layer",
47
+ "close", "list_doc_refs", "load_one", "count",
48
+ # WritableSourcePort
49
+ "save_document", "delete_document", "save_manifest", "list_versions",
50
+ "get_version", "publish", "load_drafts", "list_scopes",
51
+ })
52
+
53
+ # `query` is special: the port promises an AsyncIterator, so the sync
54
+ # result (an iterable / generator) is materialized in the worker thread
55
+ # and re-emitted as an async generator.
56
+ _ASYNC_ITER_HOPPED: frozenset[str] = frozenset({"query"})
57
+
58
+
59
+ class AsyncSourceAdapter:
60
+ """Async transparent proxy around any sync SourcePort."""
61
+
62
+ def __init__(self, source: Any) -> None:
63
+ self._source = source
64
+
65
+ @property
66
+ def supports_readers(self) -> bool:
67
+ return getattr(self._source, "supports_readers", False)
68
+
69
+ def capabilities(self) -> "SourceCapabilities":
70
+ """Sync, typed capabilities of the WRAPPED source.
71
+
72
+ Passthrough when the inner source declares; otherwise falls back
73
+ to reflection-derivation over the inner source (external sync
74
+ sources — e.g. ``S3Source`` — predate the declaration contract).
75
+ Both answers match the wrapper's own structural surface, since
76
+ the proxy mirrors the inner source member-for-member.
77
+ """
78
+ fn = getattr(self._source, "capabilities", None)
79
+ if callable(fn) and not inspect.iscoroutinefunction(fn):
80
+ caps = fn()
81
+ from dna.kernel.capabilities import SourceCapabilities
82
+ if isinstance(caps, SourceCapabilities):
83
+ return caps
84
+ from dna.kernel.capabilities import derive_capabilities
85
+ return derive_capabilities(
86
+ self._source, label=type(self._source).__name__,
87
+ )
88
+
89
+ # -- Transparent forwarding ------------------------------------------
90
+
91
+ def __getattr__(self, name: str) -> Any:
92
+ """Mirror the wrapped source: thread-hop known sync port methods,
93
+ forward everything else untouched. Missing members raise
94
+ ``AttributeError`` naturally — the proxy never invents surface."""
95
+ attr = getattr(self._source, name)
96
+ if not callable(attr):
97
+ return attr
98
+ # Already-async inner methods need no hop (mixed sources).
99
+ if inspect.iscoroutinefunction(attr) or inspect.isasyncgenfunction(attr):
100
+ return attr
101
+
102
+ if name in _ASYNC_ITER_HOPPED:
103
+ @functools.wraps(attr)
104
+ async def _aiter_hop(*args: Any, **kwargs: Any):
105
+ rows = await asyncio.to_thread(
106
+ lambda: list(attr(*args, **kwargs))
107
+ )
108
+ for row in rows:
109
+ yield row
110
+ return _aiter_hop
111
+
112
+ if name in _THREAD_HOPPED:
113
+ @functools.wraps(attr)
114
+ async def _hop(*args: Any, **kwargs: Any) -> Any:
115
+ return await asyncio.to_thread(attr, *args, **kwargs)
116
+ return _hop
117
+
118
+ return attr
@@ -0,0 +1,11 @@
1
+ """Real ``EmbeddingPort`` adapters (opt-in extras).
2
+
3
+ Nothing here is imported by the default SDK — importing ``dna`` or booting a
4
+ kernel must never pull ONNX/torch/fastembed (guard:
5
+ ``tests/test_embedding_import_isolation.py``). Install the extra and import the
6
+ adapter explicitly to register a real provider:
7
+
8
+ pip install "dna-sdk[embed-onnx]"
9
+ from dna.adapters.embedding.onnx import OnnxEmbeddingProvider
10
+ kernel.embedding_provider(OnnxEmbeddingProvider())
11
+ """
@@ -0,0 +1,54 @@
1
+ """ONNX all-MiniLM-L6-v2 embedding provider (opt-in ``embed-onnx`` extra).
2
+
3
+ The REAL embedder (rec-embedding-port): the same ``sentence-transformers/
4
+ all-MiniLM-L6-v2`` ONNX artifact run by ``fastembed`` (Py) and
5
+ ``@huggingface/transformers`` (TS) — parity by artifact, not reimplementation.
6
+ Cosine ≈ 1 across the two runtimes (see ``tests/test_embedding_onnx_parity.py``,
7
+ which is network-gated and skips offline).
8
+
9
+ Lazy-download + cache, the Chroma pattern: the model is a downloaded artifact,
10
+ never an install-time dependency. ``fastembed`` fetches and caches the ONNX on
11
+ the FIRST ``embed`` call, not at import — so importing this module (with the
12
+ extra installed) still costs nothing until you actually embed.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ # Same model + vector width as the TS twin (src/adapters/embedding/onnx.ts) and
17
+ # the fake floor (FAKE_EMBEDDING_DIMS) so providers are swap-compatible.
18
+ ONNX_MODEL_ID = "sentence-transformers/all-MiniLM-L6-v2"
19
+ ONNX_DIMS = 384
20
+
21
+
22
+ class OnnxEmbeddingProvider:
23
+ """``EmbeddingPort`` backed by fastembed's ONNX runtime.
24
+
25
+ Structurally satisfies ``dna.kernel.protocols.EmbeddingPort``. The heavy
26
+ import (``fastembed``, which drags ``onnxruntime``) is deferred to first
27
+ use so merely importing this module never pulls ML deps into a process
28
+ that does not embed."""
29
+
30
+ def __init__(self, model_id: str = ONNX_MODEL_ID, dims: int = ONNX_DIMS) -> None:
31
+ self.model_id = model_id
32
+ self.dims = dims
33
+ self._model = None # lazily constructed on first embed()
34
+
35
+ def _ensure_model(self):
36
+ if self._model is None:
37
+ try:
38
+ from fastembed import TextEmbedding
39
+ except ImportError as exc: # pragma: no cover - exercised via extra
40
+ raise ImportError(
41
+ "OnnxEmbeddingProvider needs the 'embed-onnx' extra: "
42
+ "pip install 'dna-sdk[embed-onnx]'"
43
+ ) from exc
44
+ # Downloads + caches the ONNX artifact on first construction.
45
+ self._model = TextEmbedding(model_name=self.model_id)
46
+ return self._model
47
+
48
+ async def embed(self, texts: list[str]) -> list[list[float]]:
49
+ if not texts:
50
+ return []
51
+ model = self._ensure_model()
52
+ # fastembed yields L2-normalized mean-pooled sentence vectors (same
53
+ # recipe transformers.js uses with pooling='mean', normalize=true).
54
+ return [list(map(float, vec)) for vec in model.embed(list(texts))]
@@ -0,0 +1,5 @@
1
+ from dna.adapters.filesystem.source import FilesystemSource
2
+ from dna.adapters.filesystem.cache import FilesystemCache
3
+ from dna.adapters.filesystem.writable import FilesystemWritableSource
4
+
5
+ __all__ = ["FilesystemSource", "FilesystemCache", "FilesystemWritableSource"]
@@ -0,0 +1,102 @@
1
+ """FilesystemCache — CachePort backed by .dna-cache/ directories."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import aiofiles
10
+ import yaml
11
+
12
+ from dna.kernel.protocols import CacheItem
13
+ from dna.kernel.bundle_handle import FilesystemBundleHandle
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class FilesystemCache:
19
+ """Stores and loads cached dependencies from .dna-cache/<scope>/."""
20
+
21
+ def __init__(self, base_dir: str | Path) -> None:
22
+ self._base = Path(base_dir).resolve().parent / ".dna-cache"
23
+
24
+ async def has(self, scope: str, key: str) -> bool:
25
+ return (self._base / scope / key).exists()
26
+
27
+ async def store(self, scope: str, key: str, items: list[CacheItem]) -> None:
28
+ dest_base = self._base / scope / key
29
+ dest_base.mkdir(parents=True, exist_ok=True)
30
+ for item in items:
31
+ # When kind is known, organize by kind (skills/, souls/)
32
+ # When kind is "" (scanner determines later), store flat by name
33
+ if item.kind:
34
+ sub_dir = dest_base / (item.kind.lower() + "s")
35
+ else:
36
+ sub_dir = dest_base
37
+ dest = sub_dir / item.name
38
+ if dest.exists():
39
+ shutil.rmtree(dest)
40
+ dest.parent.mkdir(parents=True, exist_ok=True)
41
+ shutil.copytree(item.content_path, dest)
42
+
43
+ async def load_key(
44
+ self, scope: str, key: str, readers: list | None = None,
45
+ ) -> list[dict[str, Any]]:
46
+ key_dir = self._base / scope / key
47
+ if not key_dir.exists():
48
+ return []
49
+ documents: list[dict[str, Any]] = []
50
+ await self._read_tree(key_dir, readers or [], documents)
51
+ return documents
52
+
53
+ async def load_all(
54
+ self, scope: str, readers: list | None = None,
55
+ ) -> list[dict[str, Any]]:
56
+ scope_dir = self._base / scope
57
+ if not scope_dir.exists():
58
+ return []
59
+
60
+ documents: list[dict[str, Any]] = []
61
+ readers = readers or []
62
+ await self._read_tree(scope_dir, readers, documents)
63
+ return documents
64
+
65
+ async def _read_tree(
66
+ self, directory: Path, readers: list,
67
+ documents: list[dict[str, Any]],
68
+ ) -> None:
69
+ """Recursively read directories for bundles and YAMLs."""
70
+ for subdir in sorted(directory.iterdir()):
71
+ if not subdir.is_dir():
72
+ continue
73
+ # Try readers first
74
+ matched = False
75
+ bundle = FilesystemBundleHandle(subdir)
76
+ for reader in readers:
77
+ try:
78
+ if reader.detect(bundle):
79
+ doc = reader.read(bundle)
80
+ if isinstance(doc, dict) and "kind" in doc:
81
+ documents.append(doc)
82
+ matched = True
83
+ break
84
+ except Exception as e:
85
+ logger.warning("Reader error on %s: %s", subdir, e)
86
+ if matched:
87
+ continue
88
+ # Check for YAML files with kind:
89
+ has_yaml = False
90
+ for yf in sorted(subdir.glob("*.yaml")):
91
+ try:
92
+ async with aiofiles.open(yf, "r") as f:
93
+ raw = await f.read()
94
+ content = yaml.safe_load(raw)
95
+ if isinstance(content, dict) and "kind" in content:
96
+ documents.append(content)
97
+ has_yaml = True
98
+ except yaml.YAMLError:
99
+ pass
100
+ # Recurse deeper if nothing found at this level
101
+ if not has_yaml:
102
+ await self._read_tree(subdir, readers, documents)