apical 0.2.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.
- apical/__init__.py +88 -0
- apical/classifier.py +205 -0
- apical/client.py +371 -0
- apical/exceptions.py +66 -0
- apical/orthogonal.py +93 -0
- apical/plexus.py +288 -0
- apical/router.py +150 -0
- apical/verifier.py +88 -0
- apical-0.2.0.dist-info/METADATA +132 -0
- apical-0.2.0.dist-info/RECORD +12 -0
- apical-0.2.0.dist-info/WHEEL +5 -0
- apical-0.2.0.dist-info/top_level.txt +1 -0
apical/__init__.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Apical Labs — Zero-Retention Continual Memory & Embedding Router.
|
|
2
|
+
|
|
3
|
+
Client SDK providing:
|
|
4
|
+
- ApicalClient: Main client connection pool
|
|
5
|
+
- Classifier: Continual classification with instant GDPR Art. 17 unlearning
|
|
6
|
+
- MemoryRouter: Real-time dynamic embedding routing and reinforcement
|
|
7
|
+
- OrthogonalPrivacyProjector: Client-side mathematical input de-identification
|
|
8
|
+
- verify_zero_retention_receipt: Offline Ed25519 receipt verification
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from apical.classifier import (
|
|
14
|
+
Classifier,
|
|
15
|
+
LearnResult,
|
|
16
|
+
PredictionResult,
|
|
17
|
+
UnlearnResult,
|
|
18
|
+
)
|
|
19
|
+
from apical.client import ApicalClient, ArborNamespace, BridgeResult
|
|
20
|
+
|
|
21
|
+
# Backward-compatible convenience alias
|
|
22
|
+
Client = ApicalClient
|
|
23
|
+
from apical.exceptions import (
|
|
24
|
+
APIError,
|
|
25
|
+
ApicalError,
|
|
26
|
+
AuthenticationError,
|
|
27
|
+
FixedByteInvariantError,
|
|
28
|
+
ModelNotFoundError,
|
|
29
|
+
ProductNotEntitledError,
|
|
30
|
+
RateLimitError,
|
|
31
|
+
SparseProbeError,
|
|
32
|
+
SweepDetectedError,
|
|
33
|
+
VerificationError,
|
|
34
|
+
)
|
|
35
|
+
from apical.orthogonal import OrthogonalPrivacyProjector
|
|
36
|
+
from apical.plexus import (
|
|
37
|
+
PlexusClassifier,
|
|
38
|
+
PlexusLearnResult,
|
|
39
|
+
PlexusNetwork,
|
|
40
|
+
PlexusPredictionResult,
|
|
41
|
+
PlexusRouteResult,
|
|
42
|
+
PlexusRouter,
|
|
43
|
+
PlexusUnlearnResult,
|
|
44
|
+
)
|
|
45
|
+
from apical.router import (
|
|
46
|
+
MemoryRouter,
|
|
47
|
+
ReinforceResult,
|
|
48
|
+
RouteItem,
|
|
49
|
+
RouteResult,
|
|
50
|
+
)
|
|
51
|
+
from apical.verifier import verify_zero_retention_receipt
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__version__ = "0.2.0"
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"APIError",
|
|
58
|
+
"ApicalClient",
|
|
59
|
+
"ApicalError",
|
|
60
|
+
"ArborNamespace",
|
|
61
|
+
"AuthenticationError",
|
|
62
|
+
"BridgeResult",
|
|
63
|
+
"Classifier",
|
|
64
|
+
"Client",
|
|
65
|
+
"FixedByteInvariantError",
|
|
66
|
+
"LearnResult",
|
|
67
|
+
"MemoryRouter",
|
|
68
|
+
"ModelNotFoundError",
|
|
69
|
+
"OrthogonalPrivacyProjector",
|
|
70
|
+
"PlexusClassifier",
|
|
71
|
+
"PlexusLearnResult",
|
|
72
|
+
"PlexusNetwork",
|
|
73
|
+
"PlexusPredictionResult",
|
|
74
|
+
"PlexusRouteResult",
|
|
75
|
+
"PlexusRouter",
|
|
76
|
+
"PlexusUnlearnResult",
|
|
77
|
+
"PredictionResult",
|
|
78
|
+
"ProductNotEntitledError",
|
|
79
|
+
"RateLimitError",
|
|
80
|
+
"ReinforceResult",
|
|
81
|
+
"RouteItem",
|
|
82
|
+
"RouteResult",
|
|
83
|
+
"SparseProbeError",
|
|
84
|
+
"SweepDetectedError",
|
|
85
|
+
"UnlearnResult",
|
|
86
|
+
"VerificationError",
|
|
87
|
+
"verify_zero_retention_receipt",
|
|
88
|
+
]
|
apical/classifier.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Classifier Surface for the Apical Labs Platform."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, NamedTuple, Optional, Sequence, Union
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from apical.orthogonal import OrthogonalPrivacyProjector
|
|
9
|
+
from apical.verifier import verify_zero_retention_receipt
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PredictionResult(NamedTuple):
|
|
13
|
+
"""Result of a classifier inference pass."""
|
|
14
|
+
prediction: int
|
|
15
|
+
confidence: float
|
|
16
|
+
receipt: Dict[str, Any]
|
|
17
|
+
crystallization_pct: Optional[float] = None
|
|
18
|
+
mean_plasticity: Optional[float] = None
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def signature(self) -> str:
|
|
22
|
+
"""Return the Ed25519 signature from the cryptographic attestation receipt."""
|
|
23
|
+
return str(self.receipt.get("ed25519_signature", ""))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LearnResult(NamedTuple):
|
|
27
|
+
"""Result of a single continual update."""
|
|
28
|
+
status: str
|
|
29
|
+
target_class: int
|
|
30
|
+
winner_class: int
|
|
31
|
+
confidence: float
|
|
32
|
+
update_count: int
|
|
33
|
+
persisted_bytes_delta: int
|
|
34
|
+
receipt: Dict[str, Any]
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def signature(self) -> str:
|
|
38
|
+
"""Return the Ed25519 signature from the cryptographic attestation receipt."""
|
|
39
|
+
return str(self.receipt.get("ed25519_signature", ""))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class UnlearnResult(NamedTuple):
|
|
43
|
+
"""Result of an instant GDPR Article 17 class erasure."""
|
|
44
|
+
status: str
|
|
45
|
+
erased_class_id: int
|
|
46
|
+
persisted_bytes_delta: int
|
|
47
|
+
receipt: Dict[str, Any]
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def signature(self) -> str:
|
|
51
|
+
"""Return the Ed25519 signature from the cryptographic attestation receipt."""
|
|
52
|
+
return str(self.receipt.get("ed25519_signature", ""))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Classifier:
|
|
56
|
+
"""Client interface for continuous classification models."""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
client: Any,
|
|
61
|
+
model_id: str,
|
|
62
|
+
in_features: Optional[int] = None,
|
|
63
|
+
num_classes: Optional[int] = None,
|
|
64
|
+
privacy_secret: Optional[str] = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
self.client = client
|
|
67
|
+
self.model_id = model_id
|
|
68
|
+
self.in_features = in_features
|
|
69
|
+
self.num_classes = num_classes
|
|
70
|
+
self.privacy_secret = privacy_secret
|
|
71
|
+
|
|
72
|
+
self._projector: Optional[OrthogonalPrivacyProjector] = None
|
|
73
|
+
if self.privacy_secret and self.in_features:
|
|
74
|
+
self._projector = OrthogonalPrivacyProjector(self.privacy_secret, self.in_features)
|
|
75
|
+
|
|
76
|
+
def _prepare_vector(self, vector: Union[Sequence[float], np.ndarray]) -> list[float]:
|
|
77
|
+
"""Optionally apply orthogonal privacy projection, returning a JSON-serializable list."""
|
|
78
|
+
arr = np.asarray(vector, dtype=np.float32).ravel()
|
|
79
|
+
|
|
80
|
+
if self._projector is not None:
|
|
81
|
+
arr = self._projector.project(arr)
|
|
82
|
+
elif self.privacy_secret and not self._projector:
|
|
83
|
+
# Lazy initialize projector once vector dimension is known
|
|
84
|
+
self.in_features = arr.size
|
|
85
|
+
self._projector = OrthogonalPrivacyProjector(self.privacy_secret, self.in_features)
|
|
86
|
+
arr = self._projector.project(arr)
|
|
87
|
+
|
|
88
|
+
return arr.tolist()
|
|
89
|
+
|
|
90
|
+
def create(self, in_features: int, num_classes: int) -> Dict[str, Any]:
|
|
91
|
+
"""Create and initialize a new fixed-byte model container on Apical Cloud."""
|
|
92
|
+
self.in_features = in_features
|
|
93
|
+
self.num_classes = num_classes
|
|
94
|
+
|
|
95
|
+
if self.privacy_secret and not self._projector:
|
|
96
|
+
self._projector = OrthogonalPrivacyProjector(self.privacy_secret, self.in_features)
|
|
97
|
+
|
|
98
|
+
payload = {
|
|
99
|
+
"model_id": self.model_id,
|
|
100
|
+
"in_features": in_features,
|
|
101
|
+
"out_features": num_classes,
|
|
102
|
+
}
|
|
103
|
+
resp = self.client.request("POST", f"/api/v1/models", payload)
|
|
104
|
+
data = resp.get("data", resp)
|
|
105
|
+
|
|
106
|
+
if "receipt" in data:
|
|
107
|
+
verify_zero_retention_receipt(data["receipt"])
|
|
108
|
+
|
|
109
|
+
return data
|
|
110
|
+
|
|
111
|
+
def predict(
|
|
112
|
+
self,
|
|
113
|
+
vector: Union[Sequence[float], np.ndarray],
|
|
114
|
+
tier: int = 0,
|
|
115
|
+
verify_receipt: bool = True,
|
|
116
|
+
) -> PredictionResult:
|
|
117
|
+
"""Execute directional classification, returning class prediction and signed receipt."""
|
|
118
|
+
prep_vec = self._prepare_vector(vector)
|
|
119
|
+
payload = {
|
|
120
|
+
"model_id": self.model_id,
|
|
121
|
+
"vector": prep_vec,
|
|
122
|
+
"tier": tier,
|
|
123
|
+
}
|
|
124
|
+
resp = self.client.request("POST", f"/api/v1/predict", payload)
|
|
125
|
+
data = resp.get("data", resp)
|
|
126
|
+
|
|
127
|
+
receipt = data.get("receipt", {})
|
|
128
|
+
if verify_receipt and receipt:
|
|
129
|
+
verify_zero_retention_receipt(receipt)
|
|
130
|
+
|
|
131
|
+
return PredictionResult(
|
|
132
|
+
prediction=int(data["prediction"]),
|
|
133
|
+
confidence=float(data["confidence"]),
|
|
134
|
+
receipt=receipt,
|
|
135
|
+
crystallization_pct=data.get("crystallization_pct"),
|
|
136
|
+
mean_plasticity=data.get("mean_plasticity"),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def learn(
|
|
140
|
+
self,
|
|
141
|
+
vector: Union[Sequence[float], np.ndarray],
|
|
142
|
+
target_class: int,
|
|
143
|
+
tier: int = 0,
|
|
144
|
+
verify_receipt: bool = True,
|
|
145
|
+
) -> LearnResult:
|
|
146
|
+
"""Stream a single continual update, updating model state under zero retention."""
|
|
147
|
+
prep_vec = self._prepare_vector(vector)
|
|
148
|
+
payload = {
|
|
149
|
+
"model_id": self.model_id,
|
|
150
|
+
"vector": prep_vec,
|
|
151
|
+
"target_class": target_class,
|
|
152
|
+
"tier": tier,
|
|
153
|
+
}
|
|
154
|
+
resp = self.client.request("POST", f"/api/v1/learn", payload)
|
|
155
|
+
data = resp.get("data", resp)
|
|
156
|
+
|
|
157
|
+
receipt = data.get("receipt", {})
|
|
158
|
+
if verify_receipt and receipt:
|
|
159
|
+
verify_zero_retention_receipt(receipt)
|
|
160
|
+
|
|
161
|
+
return LearnResult(
|
|
162
|
+
status=str(data.get("status", "updated")),
|
|
163
|
+
target_class=int(data["target_class"]),
|
|
164
|
+
winner_class=int(data["winner_class"]),
|
|
165
|
+
confidence=float(data["confidence"]),
|
|
166
|
+
update_count=int(data.get("update_count", 0)),
|
|
167
|
+
persisted_bytes_delta=int(data.get("persisted_bytes_delta", 0)),
|
|
168
|
+
receipt=receipt,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def unlearn(
|
|
172
|
+
self,
|
|
173
|
+
class_id: int,
|
|
174
|
+
verify_receipt: bool = True,
|
|
175
|
+
) -> UnlearnResult:
|
|
176
|
+
"""Execute instant GDPR Article 17 Machine Unlearning for a specific class coordinate."""
|
|
177
|
+
payload = {
|
|
178
|
+
"model_id": self.model_id,
|
|
179
|
+
"class_id": class_id,
|
|
180
|
+
}
|
|
181
|
+
resp = self.client.request("POST", f"/api/v1/unlearn", payload)
|
|
182
|
+
data = resp.get("data", resp)
|
|
183
|
+
|
|
184
|
+
receipt = data.get("receipt", {})
|
|
185
|
+
if verify_receipt and receipt:
|
|
186
|
+
verify_zero_retention_receipt(receipt)
|
|
187
|
+
|
|
188
|
+
return UnlearnResult(
|
|
189
|
+
status=str(data.get("status", "erased")),
|
|
190
|
+
erased_class_id=int(data.get("erased_class_id", class_id)),
|
|
191
|
+
persisted_bytes_delta=int(data.get("persisted_bytes_delta", 0)),
|
|
192
|
+
receipt=receipt,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def erase_class(
|
|
196
|
+
self,
|
|
197
|
+
target_class: Optional[int] = None,
|
|
198
|
+
class_id: Optional[int] = None,
|
|
199
|
+
verify_receipt: bool = True,
|
|
200
|
+
) -> UnlearnResult:
|
|
201
|
+
"""Execute instant GDPR Article 17 Machine Unlearning for a specific class coordinate (alias for unlearn)."""
|
|
202
|
+
target = target_class if target_class is not None else class_id
|
|
203
|
+
if target is None:
|
|
204
|
+
raise ValueError("Either target_class or class_id must be provided")
|
|
205
|
+
return self.unlearn(class_id=target, verify_receipt=verify_receipt)
|
apical/client.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Base HTTP Client for the Apical Labs Platform."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Dict, NamedTuple, Optional, Sequence, Union
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from apical.exceptions import (
|
|
10
|
+
APIError,
|
|
11
|
+
ApicalError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
ModelNotFoundError,
|
|
14
|
+
ProductNotEntitledError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
SparseProbeError,
|
|
17
|
+
SweepDetectedError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_BASE_URL = "https://api.apical-labs.com"
|
|
22
|
+
DEFAULT_TIMEOUT_SECONDS = 10.0
|
|
23
|
+
DEFAULT_MAX_RETRIES = 3
|
|
24
|
+
SDK_VERSION = "0.2.0"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BridgeResult(NamedTuple):
|
|
28
|
+
"""Result of an arbor.Bridge cross-backbone migration."""
|
|
29
|
+
status: str
|
|
30
|
+
model_id: str
|
|
31
|
+
alignment_score: float
|
|
32
|
+
persisted_bytes_delta: int
|
|
33
|
+
update_count: int
|
|
34
|
+
receipt: Dict[str, Any]
|
|
35
|
+
latency_ms: float = 0.0
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def signature(self) -> str:
|
|
39
|
+
"""Return the Ed25519 signature from the cryptographic attestation receipt."""
|
|
40
|
+
return str(self.receipt.get("ed25519_signature", ""))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ArborNamespace:
|
|
44
|
+
"""Namespace for Arbor (SCPP v1) components matching the landing page and developer ergonomics."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, client: "ApicalClient") -> None:
|
|
47
|
+
self._client = client
|
|
48
|
+
|
|
49
|
+
def create_model(
|
|
50
|
+
self,
|
|
51
|
+
model_id: str,
|
|
52
|
+
in_features: Optional[int] = None,
|
|
53
|
+
out_features: Optional[int] = None,
|
|
54
|
+
input_dim: Optional[int] = None,
|
|
55
|
+
num_classes: Optional[int] = None,
|
|
56
|
+
privacy_secret: Optional[str] = None,
|
|
57
|
+
) -> Any:
|
|
58
|
+
return self._client.create_model(
|
|
59
|
+
model_id=model_id,
|
|
60
|
+
in_features=in_features,
|
|
61
|
+
out_features=out_features,
|
|
62
|
+
input_dim=input_dim,
|
|
63
|
+
num_classes=num_classes,
|
|
64
|
+
privacy_secret=privacy_secret,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def classifier(
|
|
68
|
+
self,
|
|
69
|
+
model_id: str,
|
|
70
|
+
in_features: Optional[int] = None,
|
|
71
|
+
num_classes: Optional[int] = None,
|
|
72
|
+
privacy_secret: Optional[str] = None,
|
|
73
|
+
) -> Any:
|
|
74
|
+
return self._client.classifier(
|
|
75
|
+
model_id=model_id,
|
|
76
|
+
in_features=in_features,
|
|
77
|
+
num_classes=num_classes,
|
|
78
|
+
privacy_secret=privacy_secret,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def memory_router(
|
|
82
|
+
self,
|
|
83
|
+
router_id: str,
|
|
84
|
+
embedding_dim: Optional[int] = None,
|
|
85
|
+
num_rules: Optional[int] = None,
|
|
86
|
+
privacy_secret: Optional[str] = None,
|
|
87
|
+
) -> Any:
|
|
88
|
+
return self._client.memory_router(
|
|
89
|
+
router_id=router_id,
|
|
90
|
+
embedding_dim=embedding_dim,
|
|
91
|
+
num_rules=num_rules,
|
|
92
|
+
privacy_secret=privacy_secret,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def bridge(
|
|
96
|
+
self,
|
|
97
|
+
model_id: str,
|
|
98
|
+
source_anchors: Optional[Any] = None,
|
|
99
|
+
target_anchors: Optional[Any] = None,
|
|
100
|
+
public_anchors: Optional[Any] = None,
|
|
101
|
+
tier: int = 0,
|
|
102
|
+
verify_receipt: bool = True,
|
|
103
|
+
) -> BridgeResult:
|
|
104
|
+
return self._client.bridge(
|
|
105
|
+
model_id=model_id,
|
|
106
|
+
source_anchors=source_anchors,
|
|
107
|
+
target_anchors=target_anchors,
|
|
108
|
+
public_anchors=public_anchors,
|
|
109
|
+
tier=tier,
|
|
110
|
+
verify_receipt=verify_receipt,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ApicalClient:
|
|
115
|
+
"""Primary client for interacting with the Apical Labs API Gateway.
|
|
116
|
+
|
|
117
|
+
Manages HTTP/2 connection pooling, API key authentication, and retry policies.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
api_key: str,
|
|
123
|
+
privacy_secret: Optional[str] = None,
|
|
124
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
125
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
126
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
127
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
128
|
+
) -> None:
|
|
129
|
+
if not api_key:
|
|
130
|
+
raise ValueError("api_key cannot be empty")
|
|
131
|
+
|
|
132
|
+
self.api_key = api_key
|
|
133
|
+
self.privacy_secret = privacy_secret
|
|
134
|
+
self.base_url = base_url.rstrip("/")
|
|
135
|
+
self.timeout = timeout
|
|
136
|
+
self.max_retries = max_retries
|
|
137
|
+
|
|
138
|
+
headers = {
|
|
139
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
140
|
+
"X-Client-Version": SDK_VERSION,
|
|
141
|
+
"Accept": "application/json",
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
self._client = httpx.Client(
|
|
146
|
+
base_url=self.base_url,
|
|
147
|
+
headers=headers,
|
|
148
|
+
timeout=self.timeout,
|
|
149
|
+
transport=transport,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def arbor(self) -> ArborNamespace:
|
|
154
|
+
"""Access the Arbor (SCPP v1) namespace (classifier, memory_router, bridge)."""
|
|
155
|
+
return ArborNamespace(self)
|
|
156
|
+
|
|
157
|
+
def create_model(
|
|
158
|
+
self,
|
|
159
|
+
model_id: str,
|
|
160
|
+
in_features: Optional[int] = None,
|
|
161
|
+
out_features: Optional[int] = None,
|
|
162
|
+
input_dim: Optional[int] = None,
|
|
163
|
+
num_classes: Optional[int] = None,
|
|
164
|
+
privacy_secret: Optional[str] = None,
|
|
165
|
+
) -> Any:
|
|
166
|
+
"""Create and initialize a fixed-byte model container on the Enclave, returning a Classifier."""
|
|
167
|
+
in_dim = in_features or input_dim
|
|
168
|
+
out_dim = out_features or num_classes
|
|
169
|
+
if in_dim is None or out_dim is None:
|
|
170
|
+
raise ValueError("Both input dimensions and output classes must be specified to create a model container.")
|
|
171
|
+
|
|
172
|
+
payload = {
|
|
173
|
+
"model_id": model_id,
|
|
174
|
+
"in_features": in_dim,
|
|
175
|
+
"out_features": out_dim,
|
|
176
|
+
}
|
|
177
|
+
self.request("POST", "/api/v1/models", payload)
|
|
178
|
+
|
|
179
|
+
return self.classifier(
|
|
180
|
+
model_id=model_id,
|
|
181
|
+
in_features=in_dim,
|
|
182
|
+
num_classes=out_dim,
|
|
183
|
+
privacy_secret=privacy_secret,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def classifier(
|
|
187
|
+
self,
|
|
188
|
+
model_id: str,
|
|
189
|
+
in_features: Optional[int] = None,
|
|
190
|
+
num_classes: Optional[int] = None,
|
|
191
|
+
privacy_secret: Optional[str] = None,
|
|
192
|
+
) -> "Classifier":
|
|
193
|
+
"""Return a Classifier wrapper for classification and continuous learning."""
|
|
194
|
+
from apical.classifier import Classifier
|
|
195
|
+
|
|
196
|
+
secret = privacy_secret or self.privacy_secret
|
|
197
|
+
return Classifier(
|
|
198
|
+
client=self,
|
|
199
|
+
model_id=model_id,
|
|
200
|
+
in_features=in_features,
|
|
201
|
+
num_classes=num_classes,
|
|
202
|
+
privacy_secret=secret,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def memory_router(
|
|
206
|
+
self,
|
|
207
|
+
router_id: str,
|
|
208
|
+
embedding_dim: Optional[int] = None,
|
|
209
|
+
num_rules: Optional[int] = None,
|
|
210
|
+
privacy_secret: Optional[str] = None,
|
|
211
|
+
) -> "MemoryRouter":
|
|
212
|
+
"""Return a MemoryRouter wrapper for embedding routing and reinforcement."""
|
|
213
|
+
from apical.router import MemoryRouter
|
|
214
|
+
|
|
215
|
+
secret = privacy_secret or self.privacy_secret
|
|
216
|
+
return MemoryRouter(
|
|
217
|
+
client=self,
|
|
218
|
+
router_id=router_id,
|
|
219
|
+
embedding_dim=embedding_dim,
|
|
220
|
+
num_rules=num_rules,
|
|
221
|
+
privacy_secret=secret,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def plexus(
|
|
225
|
+
self,
|
|
226
|
+
model_id: str,
|
|
227
|
+
in_features: Optional[int] = None,
|
|
228
|
+
hidden_dim: int = 2048,
|
|
229
|
+
out_features: Optional[int] = None,
|
|
230
|
+
assembly_k: int = 64,
|
|
231
|
+
privacy_secret: Optional[str] = None,
|
|
232
|
+
) -> "PlexusNetwork":
|
|
233
|
+
"""Return a PlexusNetwork wrapper for SCPP v2 multi-layer continual learning and routing."""
|
|
234
|
+
from apical.plexus import PlexusNetwork
|
|
235
|
+
|
|
236
|
+
secret = privacy_secret or self.privacy_secret
|
|
237
|
+
return PlexusNetwork(
|
|
238
|
+
client=self,
|
|
239
|
+
model_id=model_id,
|
|
240
|
+
in_features=in_features,
|
|
241
|
+
hidden_dim=hidden_dim,
|
|
242
|
+
out_features=out_features,
|
|
243
|
+
assembly_k=assembly_k,
|
|
244
|
+
privacy_secret=secret,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def bridge(
|
|
248
|
+
self,
|
|
249
|
+
model_id: str,
|
|
250
|
+
source_anchors: Optional[Any] = None,
|
|
251
|
+
target_anchors: Optional[Any] = None,
|
|
252
|
+
public_anchors: Optional[Any] = None,
|
|
253
|
+
tier: int = 0,
|
|
254
|
+
verify_receipt: bool = True,
|
|
255
|
+
) -> BridgeResult:
|
|
256
|
+
"""Execute cross-backbone model migration via Orthogonal Procrustes alignment."""
|
|
257
|
+
from apical.verifier import verify_zero_retention_receipt
|
|
258
|
+
|
|
259
|
+
# Unpack public_anchors if provided as dict or tuple
|
|
260
|
+
if public_anchors is not None:
|
|
261
|
+
if isinstance(public_anchors, dict):
|
|
262
|
+
source_anchors = public_anchors.get("source") or public_anchors.get("source_anchors")
|
|
263
|
+
target_anchors = public_anchors.get("target") or public_anchors.get("target_anchors")
|
|
264
|
+
elif isinstance(public_anchors, (list, tuple)) and len(public_anchors) == 2:
|
|
265
|
+
source_anchors, target_anchors = public_anchors[0], public_anchors[1]
|
|
266
|
+
|
|
267
|
+
if source_anchors is None or target_anchors is None:
|
|
268
|
+
raise ValueError("Both source_anchors and target_anchors must be provided (or public_anchors)")
|
|
269
|
+
|
|
270
|
+
def _to_list_matrix(m: Any) -> list[list[float]]:
|
|
271
|
+
if hasattr(m, "tolist"):
|
|
272
|
+
return m.tolist()
|
|
273
|
+
if isinstance(m, (list, tuple)):
|
|
274
|
+
return [list(row) if hasattr(row, "__iter__") else [float(row)] for row in m]
|
|
275
|
+
return list(m)
|
|
276
|
+
|
|
277
|
+
src_list = _to_list_matrix(source_anchors)
|
|
278
|
+
tgt_list = _to_list_matrix(target_anchors)
|
|
279
|
+
|
|
280
|
+
payload = {
|
|
281
|
+
"model_id": model_id,
|
|
282
|
+
"source_anchors": src_list,
|
|
283
|
+
"target_anchors": tgt_list,
|
|
284
|
+
"tier": tier,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
resp = self.request("POST", "/api/v1/bridge", payload)
|
|
288
|
+
data = resp.get("data", resp)
|
|
289
|
+
|
|
290
|
+
receipt = data.get("receipt", {})
|
|
291
|
+
if verify_receipt and receipt:
|
|
292
|
+
verify_zero_retention_receipt(receipt)
|
|
293
|
+
|
|
294
|
+
return BridgeResult(
|
|
295
|
+
status=str(data.get("status", "bridged")),
|
|
296
|
+
model_id=str(data.get("model_id", model_id)),
|
|
297
|
+
alignment_score=float(data.get("alignment_score", 1.0)),
|
|
298
|
+
persisted_bytes_delta=int(data.get("persisted_bytes_delta", 0)),
|
|
299
|
+
update_count=int(data.get("update_count", 0)),
|
|
300
|
+
receipt=receipt,
|
|
301
|
+
latency_ms=float(data.get("latency_ms", 0.0)),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def request(
|
|
305
|
+
self,
|
|
306
|
+
method: str,
|
|
307
|
+
path: str,
|
|
308
|
+
json_body: Optional[Dict[str, Any]] = None,
|
|
309
|
+
) -> Dict[str, Any]:
|
|
310
|
+
"""Execute an HTTP request with automatic retry on transient network failures."""
|
|
311
|
+
clean_path = "/" + path.lstrip("/")
|
|
312
|
+
retries = 0
|
|
313
|
+
|
|
314
|
+
while True:
|
|
315
|
+
try:
|
|
316
|
+
response = self._client.request(
|
|
317
|
+
method=method,
|
|
318
|
+
url=clean_path,
|
|
319
|
+
json=json_body,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if response.status_code == 200 or response.status_code == 201:
|
|
323
|
+
return response.json()
|
|
324
|
+
|
|
325
|
+
self._handle_error_response(response)
|
|
326
|
+
|
|
327
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
|
328
|
+
retries += 1
|
|
329
|
+
if retries >= self.max_retries:
|
|
330
|
+
raise APIError(
|
|
331
|
+
f"Network connection failed after {retries} retries: {e}",
|
|
332
|
+
status_code=None,
|
|
333
|
+
error_code="ERR_NETWORK_FAILURE",
|
|
334
|
+
) from e
|
|
335
|
+
time.sleep(0.1 * (2 ** retries))
|
|
336
|
+
|
|
337
|
+
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
338
|
+
"""Parse structured API gateway errors and raise appropriate typed exceptions."""
|
|
339
|
+
try:
|
|
340
|
+
body = response.json()
|
|
341
|
+
except Exception:
|
|
342
|
+
body = {"error": response.text}
|
|
343
|
+
|
|
344
|
+
code = body.get("code", "")
|
|
345
|
+
message = body.get("error", response.text)
|
|
346
|
+
status = response.status_code
|
|
347
|
+
|
|
348
|
+
if status == 401:
|
|
349
|
+
raise AuthenticationError(message, status_code=status, error_code=code, details=body)
|
|
350
|
+
elif code == "ERR_PRODUCT_NOT_ENTITLED":
|
|
351
|
+
raise ProductNotEntitledError(message, status_code=status, error_code=code, details=body)
|
|
352
|
+
elif status == 404:
|
|
353
|
+
raise ModelNotFoundError(message, status_code=status, error_code=code, details=body)
|
|
354
|
+
elif status == 429:
|
|
355
|
+
raise RateLimitError(message, status_code=status, error_code=code, details=body)
|
|
356
|
+
elif code == "ERR_ORACLE_SPARSE_PROBE":
|
|
357
|
+
raise SparseProbeError(message, status_code=status, error_code=code, details=body)
|
|
358
|
+
elif code == "ERR_ORACLE_SWEEP_DETECTED":
|
|
359
|
+
raise SweepDetectedError(message, status_code=status, error_code=code, details=body)
|
|
360
|
+
else:
|
|
361
|
+
raise APIError(message, status_code=status, error_code=code, details=body)
|
|
362
|
+
|
|
363
|
+
def close(self) -> None:
|
|
364
|
+
"""Close the underlying HTTP connection pool."""
|
|
365
|
+
self._client.close()
|
|
366
|
+
|
|
367
|
+
def __enter__(self) -> "ApicalClient":
|
|
368
|
+
return self
|
|
369
|
+
|
|
370
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
371
|
+
self.close()
|