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.
- dna/__init__.py +5 -0
- dna/adapters/__init__.py +0 -0
- dna/adapters/_migrations.py +99 -0
- dna/adapters/async_adapter.py +118 -0
- dna/adapters/embedding/__init__.py +11 -0
- dna/adapters/embedding/onnx.py +54 -0
- dna/adapters/filesystem/__init__.py +5 -0
- dna/adapters/filesystem/cache.py +102 -0
- dna/adapters/filesystem/composite.py +387 -0
- dna/adapters/filesystem/source.py +499 -0
- dna/adapters/filesystem/writable.py +527 -0
- dna/adapters/postgres/__init__.py +11 -0
- dna/adapters/postgres/eventbus.py +322 -0
- dna/adapters/redis/__init__.py +0 -0
- dna/adapters/redis/cache.py +100 -0
- dna/adapters/resolvers/__init__.py +7 -0
- dna/adapters/resolvers/github.py +139 -0
- dna/adapters/resolvers/helix.py +76 -0
- dna/adapters/resolvers/http.py +184 -0
- dna/adapters/resolvers/local.py +143 -0
- dna/adapters/resolvers/registry.py +57 -0
- dna/adapters/s3/__init__.py +0 -0
- dna/adapters/s3/source.py +149 -0
- dna/adapters/search/__init__.py +21 -0
- dna/adapters/search/migrations.py +71 -0
- dna/adapters/search/pgvector.py +493 -0
- dna/adapters/search/pgvector_migrations.py +94 -0
- dna/adapters/search/rrf.py +54 -0
- dna/adapters/search/sqlite_vec.py +484 -0
- dna/adapters/sqlalchemy_/__init__.py +14 -0
- dna/adapters/sqlalchemy_/migrations.py +444 -0
- dna/adapters/sqlalchemy_/source.py +1616 -0
- dna/extensions/__init__.py +0 -0
- dna/extensions/_template_wrapper/__init__.py +166 -0
- dna/extensions/agentskills/DOCS.md +18 -0
- dna/extensions/agentskills/__init__.py +314 -0
- dna/extensions/agentsmd/DOCS.md +18 -0
- dna/extensions/agentsmd/__init__.py +185 -0
- dna/extensions/audit/__init__.py +128 -0
- dna/extensions/audit/kinds/audit-log.kind.yaml +148 -0
- dna/extensions/automation/__init__.py +74 -0
- dna/extensions/automation/kinds/automation.kind.yaml +296 -0
- dna/extensions/automation/query.py +83 -0
- dna/extensions/automation/write_guards.py +167 -0
- dna/extensions/collab/__init__.py +79 -0
- dna/extensions/doc/__init__.py +31 -0
- dna/extensions/doc/kinds/doc.kind.yaml +118 -0
- dna/extensions/eval/__init__.py +73 -0
- dna/extensions/eval/kinds/eval-baseline.kind.yaml +65 -0
- dna/extensions/eval/kinds/eval-case.kind.yaml +140 -0
- dna/extensions/eval/kinds/eval-run.kind.yaml +134 -0
- dna/extensions/eval/kinds/eval-suite.kind.yaml +87 -0
- dna/extensions/eval/runner.py +307 -0
- dna/extensions/evidence/__init__.py +109 -0
- dna/extensions/evidence/builder.py +15 -0
- dna/extensions/evidence/kinds/evidence.kind.yaml +92 -0
- dna/extensions/federation/__init__.py +211 -0
- dna/extensions/guardrails/DOCS.md +20 -0
- dna/extensions/guardrails/__init__.py +135 -0
- dna/extensions/helix/DOCS-Actor.md +20 -0
- dna/extensions/helix/DOCS-Agent.md +20 -0
- dna/extensions/helix/DOCS-Genome.md +20 -0
- dna/extensions/helix/DOCS-UseCase.md +20 -0
- dna/extensions/helix/__init__.py +1072 -0
- dna/extensions/helix/_extras.py +713 -0
- dna/extensions/helix/layers.py +33 -0
- dna/extensions/helix/write_guards.py +201 -0
- dna/extensions/hooks/__init__.py +159 -0
- dna/extensions/kinddef/DOCS.md +24 -0
- dna/extensions/kinddef/__init__.py +159 -0
- dna/extensions/lesson/__init__.py +263 -0
- dna/extensions/modelreg/__init__.py +45 -0
- dna/extensions/modelreg/kinds/model-profile.kind.yaml +155 -0
- dna/extensions/recognizer/__init__.py +172 -0
- dna/extensions/research/__init__.py +492 -0
- dna/extensions/safety/__init__.py +244 -0
- dna/extensions/safety/templates/ml-privacy-filter/README.md +19 -0
- dna/extensions/safety/templates/ml-privacy-filter/SAFETYPOLICY.md +56 -0
- dna/extensions/sdlc/__init__.py +2092 -0
- dna/extensions/sdlc/journey_derive.py +470 -0
- dna/extensions/sdlc/kinds/adr.kind.yaml +136 -0
- dna/extensions/sdlc/kinds/archive-proposal.kind.yaml +107 -0
- dna/extensions/sdlc/kinds/changelog.kind.yaml +108 -0
- dna/extensions/sdlc/kinds/cognitive-policy.kind.yaml +470 -0
- dna/extensions/sdlc/kinds/forecast.kind.yaml +125 -0
- dna/extensions/sdlc/kinds/insight.kind.yaml +87 -0
- dna/extensions/sdlc/kinds/kaizen.kind.yaml +106 -0
- dna/extensions/sdlc/kinds/lesson-learned.kind.yaml +270 -0
- dna/extensions/sdlc/kinds/narrative.kind.yaml +189 -0
- dna/extensions/sdlc/kinds/pattern-insight.kind.yaml +204 -0
- dna/extensions/sdlc/kinds/postmortem.kind.yaml +168 -0
- dna/extensions/sdlc/kinds/pre-mortem.kind.yaml +145 -0
- dna/extensions/sdlc/kinds/prompt-template.kind.yaml +109 -0
- dna/extensions/sdlc/kinds/retrospective.kind.yaml +186 -0
- dna/extensions/sdlc/kinds/risk-register.kind.yaml +178 -0
- dna/extensions/sdlc/kinds/saved-view.kind.yaml +127 -0
- dna/extensions/sdlc/kinds/status-report.kind.yaml +129 -0
- dna/extensions/sdlc/kinds/synthesis-run.kind.yaml +279 -0
- dna/extensions/sdlc/kinds/synthesizer-state.kind.yaml +196 -0
- dna/extensions/sdlc/kinds/workflow-event.kind.yaml +195 -0
- dna/extensions/sdlc/work_item_outputs.py +96 -0
- dna/extensions/sdlc/write_guards.py +65 -0
- dna/extensions/soulspec/DOCS.md +20 -0
- dna/extensions/soulspec/__init__.py +299 -0
- dna/extensions/tenant/__init__.py +507 -0
- dna/extensions/testkit/__init__.py +293 -0
- dna/kernel/__init__.py +2480 -0
- dna/kernel/_text.py +40 -0
- dna/kernel/bitemporal_guard.py +42 -0
- dna/kernel/bundle_handle.py +252 -0
- dna/kernel/bundle_io.py +174 -0
- dna/kernel/capabilities.py +502 -0
- dna/kernel/catalog_cache.py +160 -0
- dna/kernel/catalog_tier.py +85 -0
- dna/kernel/collaborator_ports.py +390 -0
- dna/kernel/composition.py +35 -0
- dna/kernel/composition_resolver.py +710 -0
- dna/kernel/descriptor_loader.py +56 -0
- dna/kernel/document.py +160 -0
- dna/kernel/embedding.py +75 -0
- dna/kernel/errors.py +67 -0
- dna/kernel/eventbus.py +141 -0
- dna/kernel/events.py +24 -0
- dna/kernel/evidence_capture.py +181 -0
- dna/kernel/generic_rw.py +505 -0
- dna/kernel/hooks.py +398 -0
- dna/kernel/instance.py +1100 -0
- dna/kernel/instance_builder.py +400 -0
- dna/kernel/invalidation.py +161 -0
- dna/kernel/kernel_bootstrap.py +228 -0
- dna/kernel/kernel_cache.py +167 -0
- dna/kernel/kind_base.py +289 -0
- dna/kernel/kind_definition_schema.py +75 -0
- dna/kernel/kind_registry.py +854 -0
- dna/kernel/layer_policy.py +159 -0
- dna/kernel/layer_resolver.py +322 -0
- dna/kernel/lock.py +140 -0
- dna/kernel/lock_manager.py +40 -0
- dna/kernel/meta.py +717 -0
- dna/kernel/models.py +1511 -0
- dna/kernel/module_lock.py +268 -0
- dna/kernel/nav_kernel.py +284 -0
- dna/kernel/navigator.py +174 -0
- dna/kernel/preview.py +113 -0
- dna/kernel/prompt_budget.py +50 -0
- dna/kernel/prompt_builder.py +495 -0
- dna/kernel/prompt_kernel.py +353 -0
- dna/kernel/protocols.py +1534 -0
- dna/kernel/query_engine.py +419 -0
- dna/kernel/query_fallback.py +179 -0
- dna/kernel/registry_accessor.py +140 -0
- dna/kernel/reports.py +248 -0
- dna/kernel/reports_kernel.py +283 -0
- dna/kernel/resolver.py +269 -0
- dna/kernel/resource.py +165 -0
- dna/kernel/runtime.py +35 -0
- dna/kernel/schemas/kind-definition.schema.json +253 -0
- dna/kernel/search_engine.py +123 -0
- dna/kernel/semver.py +259 -0
- dna/kernel/source_facade.py +97 -0
- dna/kernel/source_sync.py +217 -0
- dna/kernel/studio_ui.py +263 -0
- dna/kernel/templates.py +88 -0
- dna/kernel/tool_registry.py +62 -0
- dna/kernel/tools.py +210 -0
- dna/kernel/write_pipeline.py +525 -0
- dna/kernel/writer_helpers.py +69 -0
- dna/memory/__init__.py +99 -0
- dna/memory/decay.py +153 -0
- dna/memory/ecphory.py +317 -0
- dna/memory/encoding_context.py +97 -0
- dna/memory/memory_type.py +38 -0
- dna/memory/policy.py +80 -0
- dna/memory/retrieval.py +218 -0
- dna/memory/verbs.py +367 -0
- dna/safety/__init__.py +4 -0
- dna/safety/scanner.py +103 -0
- dna/safety/scanners/__init__.py +1 -0
- dna/safety/scanners/api_scanner.py +95 -0
- dna/safety/scanners/llm_judge.py +146 -0
- dna/safety/scanners/ml_scanner.py +194 -0
- dna/safety/scanners/regex_scanner.py +336 -0
- dna/sync/__init__.py +6 -0
- dna/sync/apply.py +89 -0
- dna/sync/diff.py +81 -0
- dna/sync/engine.py +161 -0
- dna/sync/hash.py +34 -0
- dna/sync/snapshot.py +56 -0
- dna/testing/__init__.py +74 -0
- dna/testing/record_search_conformance.py +312 -0
- dna/testing/rw_conformance.py +419 -0
- dna/testing/source_conformance.py +716 -0
- dna/testing/stubs.py +47 -0
- dna/viz/__init__.py +44 -0
- dna/viz/ascii.py +74 -0
- dna/viz/health.py +161 -0
- dna/viz/matrix.py +84 -0
- dna/viz/mermaid.py +777 -0
- dna_sdk-0.1.0.dist-info/METADATA +137 -0
- dna_sdk-0.1.0.dist-info/RECORD +203 -0
- dna_sdk-0.1.0.dist-info/WHEEL +4 -0
- dna_sdk-0.1.0.dist-info/entry_points.txt +23 -0
- dna_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
dna/__init__.py
ADDED
dna/adapters/__init__.py
ADDED
|
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)
|