build(agent): new-agents-2#7e3bbc iteration
This commit is contained in:
parent
1fed725255
commit
e11963ffb2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,18 +1,17 @@
|
|||
"""
|
||||
Public package for Interplanetary Edge Orchestrator (Privacy MVP).
|
||||
"""Interplanetary Edge Orchestrator - Privacy Package (Minimal UAV/Robotics MVP)
|
||||
|
||||
This package provides a tiny, production-oriented skeleton for
|
||||
privacy-preserving federated optimization with offline-first capability.
|
||||
It is intentionally minimal but well-structured to support future expansion
|
||||
into the full EnergiBridge/CatOpt و NovaPlan interop surface described in
|
||||
the MVP roadmap.
|
||||
"""
|
||||
|
||||
from .core import LocalProblem, SharedVariables, PlanDelta, DualVariables, PrivacyBudget, AuditLog, PolicyBlock
|
||||
from .federated import Client, Server
|
||||
from .core import Client, Server, OfflineCache, update_model
|
||||
|
||||
__all__ = [
|
||||
"LocalProblem",
|
||||
"SharedVariables",
|
||||
"PlanDelta",
|
||||
"DualVariables",
|
||||
"PrivacyBudget",
|
||||
"AuditLog",
|
||||
"PolicyBlock",
|
||||
"Client",
|
||||
"Server",
|
||||
"OfflineCache",
|
||||
"update_model",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
"""Adapters scaffold for canonical/interop bridging (toy implementation)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
|
||||
from .core import PlanDelta
|
||||
|
||||
|
||||
def to_contract_payload(plan_delta: PlanDelta) -> Dict[str, Any]:
|
||||
payload = plan_delta.to_dict()
|
||||
# attach a minimal signature-like tag for governance traceability
|
||||
payload["_interop"] = {
|
||||
"version": "0.1.0",
|
||||
"timestamp": plan_delta.timestamp,
|
||||
"signature_present": bool(plan_delta.signature),
|
||||
}
|
||||
return payload
|
||||
|
||||
|
||||
def from_contract_payload(payload: Dict[str, Any]) -> PlanDelta:
|
||||
delta = payload.get("delta", {})
|
||||
ts = payload.get("timestamp", None)
|
||||
author = payload.get("author", "")
|
||||
contract_id = payload.get("contract_id", "")
|
||||
signature = payload.get("signature", "")
|
||||
return PlanDelta(delta=delta, timestamp=ts, author=author, contract_id=contract_id, signature=signature)
|
||||
|
|
@ -1,62 +1,93 @@
|
|||
"""Minimal NumPy-free core primitives for the tests.
|
||||
|
||||
This module implements a lightweight, test-focused API surface for
|
||||
privacy-preserving federated optimization. It avoids external dependencies
|
||||
like NumPy and provides a deterministic, offline-capable workflow that the
|
||||
tests exercise.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Optional, List
|
||||
|
||||
import os
|
||||
import pickle
|
||||
import time
|
||||
import json
|
||||
import random
|
||||
import math
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
def _clip_norm_vec(vec: List[float], max_norm: Optional[float]) -> List[float]:
|
||||
if max_norm is None or max_norm <= 0:
|
||||
return vec
|
||||
norm = math.sqrt(sum(v * v for v in vec))
|
||||
if norm <= max_norm:
|
||||
return vec
|
||||
scale = max_norm / (norm + 1e-12)
|
||||
return [v * scale for v in vec]
|
||||
|
||||
|
||||
# Canonical IR primitives (minimal)
|
||||
@dataclass
|
||||
class LocalProblem:
|
||||
id: str
|
||||
domain: str
|
||||
assets: List[str]
|
||||
objective: str
|
||||
constraints: Optional[Dict[str, Any]] = field(default_factory=dict)
|
||||
solver_hint: Optional[str] = None
|
||||
"""A minimal LocalProblem: id, domain, assets, objective, constraints."""
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
def __init__(self, id: str, domain: str, assets: List[str], objective: str, constraints: Optional[str] = None):
|
||||
self.id = id
|
||||
self.domain = domain
|
||||
self.assets = assets
|
||||
self.objective = objective
|
||||
self.constraints = constraints or ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"domain": self.domain,
|
||||
"assets": self.assets,
|
||||
"objective": self.objective,
|
||||
"constraints": self.constraints or {},
|
||||
"solver_hint": self.solver_hint,
|
||||
"constraints": self.constraints,
|
||||
}
|
||||
|
||||
@dataclass
|
||||
|
||||
class SharedVariables:
|
||||
# Lightweight, versioned signals
|
||||
versions: Dict[str, int] = field(default_factory=dict)
|
||||
payloads: Dict[str, Any] = field(default_factory=dict)
|
||||
encryption_schema: Optional[str] = None
|
||||
"""Versioned shared signals between domains (test-focused placeholder)."""
|
||||
|
||||
def __init__(self, versions: Optional[dict] = None):
|
||||
self.versions = versions or {}
|
||||
self.payloads = {}
|
||||
|
||||
def update(self, key: str, value):
|
||||
self.versions[key] = value
|
||||
|
||||
def bump_version(self, key: str) -> int:
|
||||
self.versions[key] = self.versions.get(key, 0) + 1
|
||||
return self.versions[key]
|
||||
cur = self.versions.get(key, 0)
|
||||
cur += 1
|
||||
self.versions[key] = cur
|
||||
return cur
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {"payloads": self.payloads}
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"versions": self.versions,
|
||||
"payloads": self.payloads,
|
||||
"encryption_schema": self.encryption_schema,
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class PlanDelta:
|
||||
delta: Dict[str, Any]
|
||||
timestamp: float = field(default_factory=lambda: time.time())
|
||||
author: Optional[str] = None
|
||||
contract_id: Optional[str] = None
|
||||
signature: Optional[str] = None
|
||||
"""Incremental plan delta with provenance fields."""
|
||||
|
||||
def sign(self, signer: str) -> None:
|
||||
# Simple deterministic "signature" for demo purposes
|
||||
payload = json.dumps({"delta": self.delta, "timestamp": self.timestamp, "author": signer}, sort_keys=True)
|
||||
self.signature = f"sig-{abs(hash(payload))}"
|
||||
self.author = signer
|
||||
def __init__(
|
||||
self,
|
||||
delta: dict,
|
||||
timestamp: Optional[float] = None,
|
||||
author: str = "",
|
||||
contract_id: str = "",
|
||||
signature: str = "",
|
||||
):
|
||||
self.delta = delta
|
||||
self.timestamp = timestamp or float("nan")
|
||||
self.author = author
|
||||
self.contract_id = contract_id
|
||||
self.signature = signature
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
def sign(self, author: str) -> str:
|
||||
self.author = author
|
||||
self.signature = f"signed-by-{author}"
|
||||
return self.signature
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"delta": self.delta,
|
||||
"timestamp": self.timestamp,
|
||||
|
|
@ -65,37 +96,48 @@ class PlanDelta:
|
|||
"signature": self.signature,
|
||||
}
|
||||
|
||||
@dataclass
|
||||
|
||||
class DualVariables:
|
||||
multipliers: Dict[str, float] = field(default_factory=dict)
|
||||
"""Multipliers or dual variables for optimization (placeholder)."""
|
||||
|
||||
def set(self, name: str, value: float) -> None:
|
||||
self.multipliers[name] = value
|
||||
def __init__(self, multipliers: Optional[dict] = None):
|
||||
self.multipliers = multipliers or {}
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
def set(self, key: str, value):
|
||||
self.multipliers[key] = value
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {"multipliers": self.multipliers}
|
||||
|
||||
@dataclass
|
||||
|
||||
class PrivacyBudget:
|
||||
signal: str
|
||||
budget: float
|
||||
expiry: float # epoch
|
||||
"""Budget for privacy budget per signal (placeholder)."""
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
return time.time() > self.expiry
|
||||
def __init__(self, signal: str, budget: float, expiry: Optional[float] = None):
|
||||
self.signal = signal
|
||||
self.budget = budget
|
||||
self.expiry = expiry
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
def to_dict(self) -> dict:
|
||||
return {"signal": self.signal, "budget": self.budget, "expiry": self.expiry}
|
||||
|
||||
@dataclass
|
||||
class AuditLog:
|
||||
entry: str
|
||||
signer: str
|
||||
timestamp: float
|
||||
contract_id: Optional[str] = None
|
||||
version: Optional[str] = None
|
||||
def is_expired(self) -> bool:
|
||||
if self.expiry is None:
|
||||
return False
|
||||
return time.time() > self.expiry
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
|
||||
class AuditLog:
|
||||
"""Provenance/log of operations for governance (placeholder)."""
|
||||
|
||||
def __init__(self, entry: str, signer: str, timestamp: Optional[float] = None, contract_id: str = "", version: str = ""):
|
||||
self.entry = entry
|
||||
self.signer = signer
|
||||
self.timestamp = timestamp or float("nan")
|
||||
self.contract_id = contract_id
|
||||
self.version = version
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"entry": self.entry,
|
||||
"signer": self.signer,
|
||||
|
|
@ -104,19 +146,123 @@ class AuditLog:
|
|||
"version": self.version,
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class PolicyBlock:
|
||||
safety: Optional[str] = None
|
||||
exposure_controls: Optional[Dict[str, Any]] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"safety": self.safety,
|
||||
"exposure_controls": self.exposure_controls or {},
|
||||
}
|
||||
class OfflineCache:
|
||||
"""Simple disk-backed cache for offline updates per client."""
|
||||
|
||||
# Tiny helper for serialization (to aid tests)
|
||||
def serialize(obj: Any) -> str:
|
||||
if hasattr(obj, "to_dict"):
|
||||
return json.dumps(obj.to_dict(), sort_keys=True)
|
||||
return json.dumps(obj, default=lambda o: o.__dict__, sort_keys=True)
|
||||
def __init__(self, base_dir: Optional[str] = None):
|
||||
self.base = base_dir or "."
|
||||
os.makedirs(self.base, exist_ok=True)
|
||||
self.cache_dir = self.base
|
||||
|
||||
def cache_update(self, client_id: str, delta: List[float]) -> str:
|
||||
os.makedirs(self.cache_dir, exist_ok=True)
|
||||
fname = os.path.join(self.cache_dir, f"{client_id}_delta.pkl")
|
||||
with open(fname, "wb") as f:
|
||||
pickle.dump(delta, f)
|
||||
return fname
|
||||
|
||||
def load_update(self, client_id: str) -> Optional[List[float]]:
|
||||
fname = os.path.join(self.cache_dir, f"{client_id}_delta.pkl")
|
||||
if not os.path.exists(fname):
|
||||
return None
|
||||
with open(fname, "rb") as f:
|
||||
return pickle.load(f)
|
||||
|
||||
def clear(self, client_id: str) -> None:
|
||||
fname = os.path.join(self.cache_dir, f"{client_id}_delta.pkl")
|
||||
if os.path.exists(fname):
|
||||
os.remove(fname)
|
||||
|
||||
|
||||
class Server:
|
||||
"""Simple server that aggregates deltas from clients with optional clipping and noise."""
|
||||
|
||||
def __init__(self, dim: int):
|
||||
self.dim = dim
|
||||
self.w = [0.0 for _ in range(dim)]
|
||||
|
||||
def aggregate(
|
||||
self,
|
||||
deltas: List[List[float]],
|
||||
clip_norm: Optional[float] = None,
|
||||
noise_scale: float = 0.0,
|
||||
seed: Optional[int] = None,
|
||||
) -> List[float]:
|
||||
if not deltas:
|
||||
return self.w
|
||||
total = [0.0 for _ in range(self.dim)]
|
||||
for d in deltas:
|
||||
for i in range(self.dim):
|
||||
total[i] += d[i]
|
||||
if clip_norm is not None:
|
||||
total = _clip_norm_vec(total, clip_norm)
|
||||
if noise_scale and noise_scale > 0:
|
||||
rnd = random.Random(seed)
|
||||
total = [v + rnd.gauss(0.0, noise_scale) for v in total]
|
||||
self.w = [self.w[i] + total[i] for i in range(self.dim)]
|
||||
return self.w
|
||||
|
||||
|
||||
class Client:
|
||||
"""Lightweight client for local training on a toy dataset."""
|
||||
|
||||
def __init__(self, client_id, data_X, data_y, connected: bool = True, cache_dir: Optional[str] = None):
|
||||
self.client_id = client_id
|
||||
self.X = data_X
|
||||
self.y = data_y
|
||||
self.connected = connected
|
||||
self.cache_dir = cache_dir or "."
|
||||
self.n_features = None
|
||||
self.w = None
|
||||
# Simple per-client cache
|
||||
self.cache = OfflineCache(self.cache_dir)
|
||||
|
||||
# Backward-compatible alias used by tests
|
||||
def initialize(self, n_features: int):
|
||||
self.n_features = int(n_features)
|
||||
self.w = [0.0 for _ in range(self.n_features)]
|
||||
|
||||
def train(self, model, lr: float = 0.01, epochs: int = 1, clip_norm: Optional[float] = None) -> List[float]:
|
||||
if self.n_features is None:
|
||||
# Infer from provided model if not initialized yet
|
||||
if model is not None:
|
||||
self.initialize(len(model))
|
||||
else:
|
||||
self.initialize(len(self.X[0]) if isinstance(self.X, list) and len(self.X) > 0 else 0)
|
||||
|
||||
# Local model start point
|
||||
w = list(model)
|
||||
delta_total = [0.0 for _ in range(self.n_features)]
|
||||
N = len(self.X) if self.X else 0
|
||||
for _ in range(epochs):
|
||||
if N == 0:
|
||||
break
|
||||
grad = [0.0 for _ in range(self.n_features)]
|
||||
for i in range(N):
|
||||
xi = self.X[i]
|
||||
yi = self.y[i]
|
||||
pred = sum(xi[j] * w[j] for j in range(self.n_features))
|
||||
residual = pred - yi
|
||||
for j in range(self.n_features):
|
||||
grad[j] += xi[j] * residual
|
||||
grad = [g / float(N) for g in grad]
|
||||
delta_epoch = [-lr * g for g in grad]
|
||||
if clip_norm is not None:
|
||||
delta_epoch = _clip_norm_vec(delta_epoch, clip_norm)
|
||||
delta_total = [delta_total[i] + delta_epoch[i] for i in range(self.n_features)]
|
||||
w = [w[i] + delta_epoch[i] for i in range(self.n_features)]
|
||||
self.w = w
|
||||
# Persist delta for offline caching as required by tests
|
||||
self.cache.cache_update(self.client_id, delta_total)
|
||||
return delta_total
|
||||
|
||||
def load_update(self):
|
||||
return self.cache.load_update(self.client_id)
|
||||
|
||||
|
||||
def update_model(model, delta):
|
||||
"""Apply a delta to a model (minimal list-like semantics)."""
|
||||
if model is None:
|
||||
return delta
|
||||
return [m + d for m, d in zip(model, delta)]
|
||||
|
|
|
|||
|
|
@ -1,71 +1,43 @@
|
|||
"""Minimal DSL sketch for LocalProblem / SharedVariables / PlanDelta.
|
||||
|
||||
This is intentionally lightweight and dependency-free. It provides a
|
||||
canonical, vendor-agnostic contract surface that adapters can map to/from
|
||||
their internal representations.
|
||||
"""
|
||||
"""Tiny DSL seeds for LocalProblem and related primitives."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Dict
|
||||
|
||||
@dataclass
|
||||
class LocalProblem:
|
||||
"""Represents a per-agent optimization task.
|
||||
|
||||
- problem_id: unique identifier for the local problem
|
||||
- features: simple feature vector or dictionary describing the task
|
||||
- objective: optional objective descriptor (string or structured)
|
||||
- metadata: extensible metadata for compatibility checks
|
||||
"""
|
||||
problem_id: str
|
||||
features: List[float] | Dict[str, Any] = field(default_factory=list)
|
||||
objective: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
class LocalProblemDSL:
|
||||
id: str
|
||||
domain: str
|
||||
assets: List[str]
|
||||
objective: str
|
||||
constraints: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class SharedVariables:
|
||||
"""Represents shared summaries, priors, or signals exchanged between agents."""
|
||||
version: int
|
||||
data: Dict[str, Any] = field(default_factory=dict)
|
||||
class SharedVariablesDSL:
|
||||
versions: Dict[str, int]
|
||||
|
||||
@dataclass
|
||||
class PlanDeltaDSL:
|
||||
delta: Dict
|
||||
timestamp: Optional[float] = None
|
||||
|
||||
author: str = ""
|
||||
contract_id: str = ""
|
||||
signature: str = ""
|
||||
|
||||
@dataclass
|
||||
class PlanDelta:
|
||||
"""Represents an incremental plan change derived from optimization.
|
||||
|
||||
Extended with optional provenance fields to support auditing and offline replay:
|
||||
- timestamp: when the delta was created
|
||||
- author: identifier of the delta creator
|
||||
- contract_id: contract/session identifier
|
||||
- signature: cryptographic signature proving integrity
|
||||
"""
|
||||
version: int
|
||||
delta: Dict[str, Any] = field(default_factory=dict)
|
||||
insight: Optional[str] = None
|
||||
# Provenance / governance fields (optional)
|
||||
timestamp: Optional[float] = None
|
||||
author: Optional[str] = None
|
||||
contract_id: Optional[str] = None
|
||||
signature: Optional[str] = None
|
||||
|
||||
class DualVariablesDSL:
|
||||
multipliers: Dict[str, float]
|
||||
|
||||
@dataclass
|
||||
class PrivacyBudget:
|
||||
"""Governance/privacy budget block for a contract message."""
|
||||
class PrivacyBudgetDSL:
|
||||
signal: str
|
||||
budget: float
|
||||
expiry: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditLog:
|
||||
"""Tamper-evident audit log entry for governance provenance."""
|
||||
class AuditLogDSL:
|
||||
entry: str
|
||||
signer: str
|
||||
timestamp: float
|
||||
contract_id: str
|
||||
version: str
|
||||
timestamp: Optional[float] = None
|
||||
contract_id: str = ""
|
||||
version: str = ""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
"""Toy example/demo runner for the privacy MVP."""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from .core import Client, Server
|
||||
|
||||
|
||||
def run_toy_demo():
|
||||
# Simple synthetic dataset: y = 2x with noise
|
||||
rng = np.random.default_rng(42)
|
||||
X = rng.normal(size=(100, 3))
|
||||
true_w = np.array([1.5, -2.0, 0.5])
|
||||
y = X @ true_w + rng.normal(scale=0.5, size=100)
|
||||
|
||||
client = Client("client-1", X, y, seed=7)
|
||||
server = Server(dim=X.shape[1])
|
||||
|
||||
# simulate three rounds of local training
|
||||
for rnd in range(3):
|
||||
delta = client.train(learning_rate=0.05, clip_norm=1.0, noise_scale=0.01)
|
||||
server.aggregate([delta], clip_norm=None, noise_scale=0.0)
|
||||
print("Toy demo finished. Global model:", server.global_model)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
"""Utility helpers for the toy MVP."""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def gaussian_noise(size, scale: float):
|
||||
if scale <= 0:
|
||||
return np.zeros(size)
|
||||
return np.random.normal(0.0, scale, size)
|
||||
Loading…
Reference in New Issue