build(agent): melter#14fd4b iteration
This commit is contained in:
parent
4f6b7f1226
commit
b9b2d9cc64
|
|
@ -20,6 +20,7 @@ from .adapters import MLFlowAdapter, WandBAdapter, Adapter
|
||||||
from .registry import ContractRegistry
|
from .registry import ContractRegistry
|
||||||
from .util import environment_hash
|
from .util import environment_hash
|
||||||
from .governance import DID, GovernanceLogEntry
|
from .governance import DID, GovernanceLogEntry
|
||||||
|
from .reprobundle import ReproBundle
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Ledger",
|
"Ledger",
|
||||||
|
|
@ -39,4 +40,5 @@ __all__ = [
|
||||||
"environment_hash",
|
"environment_hash",
|
||||||
"DID",
|
"DID",
|
||||||
"GovernanceLogEntry",
|
"GovernanceLogEntry",
|
||||||
|
"ReproBundle",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from .contracts import Environment
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ReproBundle:
|
||||||
|
"""A compact, content-addressed snapshot describing everything needed to
|
||||||
|
deterministically replay a run (code commit, environment, dataset refs,
|
||||||
|
model ref, and a minimal run manifest).
|
||||||
|
|
||||||
|
This is intentionally small and deterministic so it can be cheaply
|
||||||
|
exchanged between peers and used to compute Merkle-like bundle hashes.
|
||||||
|
"""
|
||||||
|
code_commit: str
|
||||||
|
environment: Environment
|
||||||
|
dataset_refs: List[Dict[str, Any]]
|
||||||
|
model_ref: Dict[str, Any]
|
||||||
|
run_manifest: Dict[str, Any]
|
||||||
|
container_hash: Optional[str] = None
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
# Convert to a canonical dict suitable for hashing/serialization.
|
||||||
|
return {
|
||||||
|
"code_commit": self.code_commit,
|
||||||
|
"environment": self.environment.to_dict(),
|
||||||
|
"container_hash": self.container_hash,
|
||||||
|
"dataset_refs": sorted(self.dataset_refs, key=lambda d: d.get("id") or ""),
|
||||||
|
"model_ref": self.model_ref,
|
||||||
|
"run_manifest": self.run_manifest,
|
||||||
|
}
|
||||||
|
|
||||||
|
def compute_bundle_hash(self) -> str:
|
||||||
|
"""Compute a deterministic SHA-256 digest over the canonical bundle
|
||||||
|
representation. Uses JSON with sorted keys so identical bundles always
|
||||||
|
produce the same digest.
|
||||||
|
"""
|
||||||
|
payload = self.to_dict()
|
||||||
|
serialized = json.dumps(payload, sort_keys=True, default=str).encode("utf-8")
|
||||||
|
return hashlib.sha256(serialized).hexdigest()
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
from mltrail_verifiable_provenance_ledger_for.reprobundle import ReproBundle
|
||||||
|
from mltrail_verifiable_provenance_ledger_for.contracts import Environment
|
||||||
|
|
||||||
|
|
||||||
|
def make_sample_environment():
|
||||||
|
return Environment(
|
||||||
|
id="env1",
|
||||||
|
language="python",
|
||||||
|
version="3.9",
|
||||||
|
dependencies={"numpy": "1.24.0", "pandas": "1.5.3"},
|
||||||
|
container_hash=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bundle_hash_deterministic():
|
||||||
|
env = make_sample_environment()
|
||||||
|
bundle1 = ReproBundle(
|
||||||
|
code_commit="deadbeef",
|
||||||
|
environment=env,
|
||||||
|
dataset_refs=[{"id": "ds1", "fingerprint": "fp1"}],
|
||||||
|
model_ref={"id": "m1", "fingerprint": "mf1"},
|
||||||
|
run_manifest={"seed": 42, "cmd": "python train.py"},
|
||||||
|
)
|
||||||
|
bundle2 = ReproBundle(
|
||||||
|
code_commit="deadbeef",
|
||||||
|
environment=make_sample_environment(),
|
||||||
|
dataset_refs=[{"id": "ds1", "fingerprint": "fp1"}],
|
||||||
|
model_ref={"id": "m1", "fingerprint": "mf1"},
|
||||||
|
run_manifest={"seed": 42, "cmd": "python train.py"},
|
||||||
|
)
|
||||||
|
|
||||||
|
h1 = bundle1.compute_bundle_hash()
|
||||||
|
h2 = bundle2.compute_bundle_hash()
|
||||||
|
assert h1 == h2
|
||||||
|
|
||||||
|
|
||||||
|
def test_bundle_hash_changes_on_difference():
|
||||||
|
env = make_sample_environment()
|
||||||
|
base = ReproBundle(
|
||||||
|
code_commit="deadbeef",
|
||||||
|
environment=env,
|
||||||
|
dataset_refs=[{"id": "ds1", "fingerprint": "fp1"}],
|
||||||
|
model_ref={"id": "m1", "fingerprint": "mf1"},
|
||||||
|
run_manifest={"seed": 42, "cmd": "python train.py"},
|
||||||
|
)
|
||||||
|
modified = ReproBundle(
|
||||||
|
code_commit="cafebabe", # different commit
|
||||||
|
environment=env,
|
||||||
|
dataset_refs=[{"id": "ds1", "fingerprint": "fp1"}],
|
||||||
|
model_ref={"id": "m1", "fingerprint": "mf1"},
|
||||||
|
run_manifest={"seed": 42, "cmd": "python train.py"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert base.compute_bundle_hash() != modified.compute_bundle_hash()
|
||||||
Loading…
Reference in New Issue