auraone-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.
aura/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """AuraOne Python SDK.
2
+
3
+ This package provides the AuraOne client, CLI helpers, and integration shims.
4
+ """
5
+
6
+ from .client import AuraClient
7
+
8
+ __all__ = ["AuraClient"]
aura/cli.py ADDED
@@ -0,0 +1,61 @@
1
+ import argparse
2
+ import sys
3
+ from typing import List
4
+ from .client import AuraClient
5
+
6
+
7
+ def build_parser() -> argparse.ArgumentParser:
8
+ parser = argparse.ArgumentParser(prog="aura")
9
+ parser.add_argument("--base-url", default="http://localhost:8000")
10
+ parser.add_argument("--org-id", default="public")
11
+ parser.add_argument("--api-key", default=None)
12
+
13
+ sub = parser.add_subparsers(dest="cmd", required=True)
14
+
15
+ sub.add_parser("list-templates")
16
+ sub.add_parser("quotas")
17
+ sub.add_parser("system-health")
18
+
19
+ eval_p = sub.add_parser("evaluate")
20
+ eval_p.add_argument("--template-id", required=True)
21
+ eval_p.add_argument("--agent-bundle-url", required=True)
22
+ eval_p.add_argument("--wait", action="store_true", default=False)
23
+ eval_p.add_argument("--robust", action="store_true", default=False)
24
+
25
+ return parser
26
+
27
+
28
+ def run_cli(argv: List[str]) -> int:
29
+ args = build_parser().parse_args(argv)
30
+ client = AuraClient(api_key=args.api_key, base_url=args.base_url, org_id=args.org_id)
31
+
32
+ if args.cmd == "list-templates":
33
+ items = client.list_templates()
34
+ for t in items:
35
+ print(f"{t.id}\t{t.name}\t{t.version}\t{t.domain or ''}")
36
+ return 0
37
+
38
+ if args.cmd == "evaluate":
39
+ if args.robust:
40
+ res = client.evaluate_robust(template_id=args.template_id, agent_bundle_url=args.agent_bundle_url, wait=args.wait)
41
+ else:
42
+ res = client.evaluate(template_id=args.template_id, agent_bundle_url=args.agent_bundle_url, wait=args.wait)
43
+ print(res)
44
+ return 0
45
+
46
+ if args.cmd == "quotas":
47
+ q = client.get_quotas()
48
+ print(q)
49
+ return 0
50
+
51
+ if args.cmd == "system-health":
52
+ h = client.get_system_health()
53
+ print(h)
54
+ return 0
55
+
56
+ return 1
57
+
58
+
59
+ def main() -> None:
60
+ sys.exit(run_cli(sys.argv[1:]))
61
+
aura/client.py ADDED
@@ -0,0 +1,487 @@
1
+ from typing import Optional, Dict, Any, List, Callable
2
+ from dataclasses import dataclass
3
+ from enum import Enum
4
+ import time
5
+ import uuid
6
+ import os
7
+ import secrets
8
+ import httpx
9
+ from httpx import HTTPError, TimeoutException
10
+
11
+
12
+ class EvaluationStatus(Enum):
13
+ QUEUED = "queued"
14
+ RUNNING = "running"
15
+ COMPLETED = "completed"
16
+ FAILED = "failed"
17
+
18
+
19
+ @dataclass
20
+ class EvaluationResult:
21
+ id: str
22
+ status: EvaluationStatus
23
+ score: Optional[float] = None
24
+ metrics: Optional[Dict[str, float]] = None
25
+ artifacts_url: Optional[str] = None
26
+
27
+
28
+ @dataclass
29
+ class Template:
30
+ id: str
31
+ name: str
32
+ version: str
33
+ domain: Optional[str] = None
34
+
35
+
36
+ class AuraClient:
37
+ def __init__(
38
+ self,
39
+ api_key: Optional[str] = None,
40
+ base_url: str = "https://api.auraone.ai",
41
+ org_id: Optional[str] = None,
42
+ timeout: float = 10.0,
43
+ retry_attempts: int = 3,
44
+ backoff_ms: int = 300,
45
+ ):
46
+ """
47
+ Python SDK client for AuraOne.
48
+
49
+ Args:
50
+ api_key: API key or bearer JWT. If None, 'test' is used for local.
51
+ base_url: Base API URL (no trailing slash required).
52
+ org_id: Organization/tenant identifier; defaults to 'public'.
53
+ timeout: Per-request timeout in seconds.
54
+ retry_attempts: How many times to retry transient failures (>=500 or timeouts).
55
+ backoff_ms: Base backoff in milliseconds; grows linearly by attempt.
56
+ """
57
+ # Public attributes
58
+ self.api_key = api_key or "test"
59
+ self.base_url = base_url.rstrip("/")
60
+ self.org_id = org_id or "public"
61
+ # Back-compat private aliases expected by tests/tooling
62
+ self._api_key = self.api_key
63
+ self._base_url = self.base_url
64
+ self._org_id = self.org_id
65
+ self._client = httpx.Client(timeout=timeout)
66
+ self._retry_attempts = max(1, retry_attempts)
67
+ self._backoff_ms = max(0, backoff_ms)
68
+
69
+ def _headers(self) -> Dict[str, str]:
70
+ h = {"X-Org-Id": self.org_id}
71
+ h["Authorization"] = f"Bearer {self.api_key}"
72
+ if self._otel_enabled():
73
+ h["traceparent"] = self._traceparent()
74
+ return h
75
+
76
+ def _otel_enabled(self) -> bool:
77
+ if os.environ.get("OTEL_DISABLED") == "true":
78
+ return False
79
+ if os.environ.get("OTEL_ENABLED") == "true":
80
+ return True
81
+ return (os.environ.get("NODE_ENV") or "development") == "production"
82
+
83
+ def _traceparent(self) -> str:
84
+ trace_id = uuid.uuid4().hex
85
+ span_id = secrets.token_hex(8)
86
+ return f"00-{trace_id}-{span_id}-01"
87
+
88
+ def _request(
89
+ self,
90
+ method: str,
91
+ path: str,
92
+ *,
93
+ json: Optional[Dict[str, Any]] = None,
94
+ params: Optional[Dict[str, Any]] = None,
95
+ headers: Optional[Dict[str, str]] = None,
96
+ idempotency: bool = False,
97
+ ) -> httpx.Response:
98
+ url = f"{self.base_url}{path}"
99
+ hdrs = {**self._headers(), **(headers or {})}
100
+ if idempotency:
101
+ hdrs.setdefault("X-Idempotency-Key", str(uuid.uuid4()))
102
+
103
+ last_exc: Optional[Exception] = None
104
+ for attempt in range(1, self._retry_attempts + 1):
105
+ try:
106
+ # Some tests inject a lightweight client without a generic 'request' method.
107
+ # Support both httpx.Client.request and simple .get/.post mocks.
108
+ if hasattr(self._client, "request"):
109
+ resp = self._client.request(method, url, json=json, params=params, headers=hdrs)
110
+ else:
111
+ call = getattr(self._client, method.lower())
112
+ resp = call(url, json=json, params=params, headers=hdrs)
113
+ # Retry on 5xx when available on the response object
114
+ status_code = getattr(resp, "status_code", None)
115
+ if isinstance(status_code, int) and 500 <= status_code < 600:
116
+ raise HTTPError(f"Server error {status_code}")
117
+ return resp
118
+ except (TimeoutException, HTTPError) as exc:
119
+ last_exc = exc
120
+ if attempt >= self._retry_attempts:
121
+ raise
122
+ # Linear backoff (simple, deterministic)
123
+ time.sleep((self._backoff_ms * attempt) / 1000.0)
124
+ # Should not reach here
125
+ raise last_exc or RuntimeError("request failed without exception")
126
+
127
+ def evaluate(
128
+ self,
129
+ template_id: str,
130
+ agent_bundle_url: str,
131
+ reward_spec_id: Optional[str] = None,
132
+ config: Optional[Dict[str, Any]] = None,
133
+ wait: bool = True,
134
+ timeout: int = 300,
135
+ ) -> EvaluationResult:
136
+ payload: Dict[str, Any] = {
137
+ "template_id": template_id,
138
+ "agent_bundle_url": agent_bundle_url,
139
+ }
140
+ if reward_spec_id:
141
+ payload["reward_spec_id"] = reward_spec_id
142
+ if config:
143
+ payload["config"] = config
144
+ r = self._request("POST", "/v1/evaluations", json=payload, headers={}, idempotency=True)
145
+ r.raise_for_status()
146
+ data = r.json()
147
+ status = data.get("status") or data.get("state") or "queued"
148
+ result = EvaluationResult(id=data["id"], status=EvaluationStatus(status))
149
+ if not wait:
150
+ return result
151
+ # Poll
152
+ end = time.time() + timeout
153
+ while time.time() < end:
154
+ res = self.get_status(result.id)
155
+ if res.status in (EvaluationStatus.COMPLETED, EvaluationStatus.FAILED):
156
+ return res
157
+ time.sleep(0.5)
158
+ return self.get_status(result.id)
159
+
160
+ def evaluate_robust(
161
+ self,
162
+ template_id: str,
163
+ agent_bundle_url: str,
164
+ *,
165
+ reward_spec_id: Optional[str] = None,
166
+ config: Optional[Dict[str, Any]] = None,
167
+ max_retries: int = 3,
168
+ backoff_ms: int = 500,
169
+ wait: bool = False,
170
+ ) -> EvaluationResult:
171
+ """Create an evaluation with client-side retry/backoff (POST only).
172
+
173
+ This mirrors TS SDK's robust create helper, and always uses idempotency keys.
174
+ """
175
+ last_exc: Optional[Exception] = None
176
+ for attempt in range(1, max_retries + 1):
177
+ try:
178
+ return self.evaluate(
179
+ template_id=template_id,
180
+ agent_bundle_url=agent_bundle_url,
181
+ reward_spec_id=reward_spec_id,
182
+ config=config,
183
+ wait=wait,
184
+ )
185
+ except Exception as exc: # noqa: BLE001
186
+ last_exc = exc
187
+ if attempt >= max_retries:
188
+ raise
189
+ time.sleep((backoff_ms * attempt) / 1000.0)
190
+ # Should not reach here
191
+ if last_exc:
192
+ raise last_exc
193
+ raise RuntimeError("evaluate_robust failed without exception")
194
+
195
+ def get_status(self, evaluation_id: str) -> EvaluationResult:
196
+ r = self._request("GET", f"/v1/evaluations/{evaluation_id}")
197
+ r.raise_for_status()
198
+ data = r.json()
199
+ status = EvaluationStatus((data.get("status") or data.get("state") or "queued"))
200
+ return EvaluationResult(
201
+ id=data["id"],
202
+ status=status,
203
+ score=data.get("score"),
204
+ metrics=data.get("metrics"),
205
+ artifacts_url=data.get("artifacts_url"),
206
+ )
207
+
208
+ def cancel(self, evaluation_id: str) -> EvaluationResult:
209
+ r = self._request("POST", f"/v1/evaluations/{evaluation_id}:cancel")
210
+ r.raise_for_status()
211
+ raw = r.json()
212
+ # Some APIs return the updated resource; otherwise fetch
213
+ if isinstance(raw, dict) and raw.get("id"):
214
+ s = raw.get("status") or raw.get("state") or "cancelled"
215
+ return EvaluationResult(
216
+ id=raw["id"],
217
+ status=EvaluationStatus(s),
218
+ score=raw.get("score"),
219
+ metrics=raw.get("metrics"),
220
+ artifacts_url=raw.get("artifacts_url"),
221
+ )
222
+ return self.get_status(evaluation_id)
223
+
224
+ def list_templates(self, domain: Optional[str] = None, page: int = 1, per_page: int = 20) -> List[Template]:
225
+ params: Dict[str, Any] = {"page": page, "per_page": per_page}
226
+ if domain:
227
+ params["domain"] = domain
228
+ r = self._request("GET", "/v1/templates", params=params)
229
+ r.raise_for_status()
230
+ payload = r.json()
231
+ data = payload if isinstance(payload, list) else payload.get("templates") or payload.get("data") or []
232
+ items: List[Template] = []
233
+ for t in data:
234
+ items.append(Template(id=t.get("id"), name=t["name"], version=t.get("version") or "1.0.0", domain=t.get("domain")))
235
+ return items
236
+
237
+ def get_template(self, template_id: str) -> Template:
238
+ r = self._request("GET", f"/v1/templates/{template_id}")
239
+ r.raise_for_status()
240
+ raw = r.json()
241
+ t = raw.get("template") if isinstance(raw, dict) and raw.get("template") else raw
242
+ return Template(id=t.get("id"), name=t["name"], version=t.get("version") or "1.0.0", domain=t.get("domain"))
243
+
244
+ def get_quotas(self) -> Dict[str, Any]:
245
+ r = self._request("GET", "/v1/quotas")
246
+ r.raise_for_status()
247
+ return r.json()
248
+
249
+ def get_attestation(self, evaluation_id: str) -> Dict[str, Any]:
250
+ r = self._request("GET", f"/v1/attestations/{evaluation_id}")
251
+ r.raise_for_status()
252
+ return r.json()
253
+
254
+ # ---- Advanced endpoints: analytics, algorithms, benchmarks, safety, admin ----
255
+
256
+ def get_evaluation_analytics(self, timeframe: str = "24h") -> Dict[str, Any]:
257
+ r = self._request("GET", "/v1/analytics/evaluation-metrics", params={"timeframe": timeframe})
258
+ r.raise_for_status()
259
+ return r.json()
260
+
261
+ def get_template_analytics(self, template_id: str, time_range: str = "30d") -> Dict[str, Any]:
262
+ r = self._request("GET", f"/v1/templates/{template_id}/analytics", params={"timeRange": time_range})
263
+ r.raise_for_status()
264
+ return r.json()
265
+
266
+ def get_org_templates_analytics(self, org_id: Optional[str] = None, time_range: str = "30d") -> Dict[str, Any]:
267
+ oid = org_id or self.org_id
268
+ r = self._request("GET", f"/v1/organizations/{oid}/templates/analytics", params={"timeRange": time_range})
269
+ r.raise_for_status()
270
+ return r.json()
271
+
272
+ def get_algorithm_metrics(self, org_id: Optional[str] = None) -> Dict[str, Any]:
273
+ oid = org_id or self.org_id
274
+ r = self._request("GET", f"/v1/organizations/{oid}/algorithms/metrics")
275
+ r.raise_for_status()
276
+ return r.json()
277
+
278
+ def compare_algorithms(self, algorithm_ids: List[str], metrics: Optional[List[str]] = None) -> Dict[str, Any]:
279
+ payload = {"algorithm_ids": algorithm_ids, "metrics": metrics or ["accuracy", "precision", "recall", "f1Score"]}
280
+ r = self._request("POST", "/v1/algorithms/compare", json=payload)
281
+ r.raise_for_status()
282
+ return r.json()
283
+
284
+ def get_competitive_benchmarks(self) -> Dict[str, Any]:
285
+ r = self._request("GET", "/v1/competitive/benchmarks")
286
+ r.raise_for_status()
287
+ return r.json()
288
+
289
+ def get_safety_metrics(self, org_id: Optional[str] = None, time_range: str = "30d") -> Dict[str, Any]:
290
+ oid = org_id or self.org_id
291
+ r = self._request("GET", f"/v1/organizations/{oid}/safety/metrics", params={"timeRange": time_range})
292
+ r.raise_for_status()
293
+ return r.json()
294
+
295
+ def get_safety_incidents(self, org_id: Optional[str] = None, status: str = "open") -> Dict[str, Any]:
296
+ oid = org_id or self.org_id
297
+ r = self._request("GET", f"/v1/organizations/{oid}/safety/incidents", params={"status": status})
298
+ r.raise_for_status()
299
+ return r.json()
300
+
301
+ def get_compliance_reports(self, org_id: Optional[str] = None) -> Dict[str, Any]:
302
+ oid = org_id or self.org_id
303
+ r = self._request("GET", f"/v1/organizations/{oid}/safety/compliance")
304
+ r.raise_for_status()
305
+ return r.json()
306
+
307
+ def get_organization_metrics(self, org_id: Optional[str] = None) -> Dict[str, Any]:
308
+ oid = org_id or self.org_id
309
+ r = self._request("GET", f"/v1/admin/organizations/{oid}/metrics")
310
+ r.raise_for_status()
311
+ return r.json()
312
+
313
+ def get_system_health(self) -> Dict[str, Any]:
314
+ r = self._request("GET", "/v1/admin/system/health")
315
+ r.raise_for_status()
316
+ return r.json()
317
+
318
+ # ---- Federation endpoints ----
319
+
320
+ def get_federation_nodes(self, org_id: Optional[str] = None) -> Dict[str, Any]:
321
+ """Get federation nodes for an organization."""
322
+ oid = org_id or self.org_id
323
+ r = self._request("GET", f"/v1/organizations/{oid}/federation/nodes")
324
+ r.raise_for_status()
325
+ return r.json()
326
+
327
+ def get_federation_rounds(self, org_id: Optional[str] = None, limit: int = 10, status: Optional[str] = None) -> Dict[str, Any]:
328
+ """Get federation rounds for an organization."""
329
+ oid = org_id or self.org_id
330
+ params = {"limit": limit}
331
+ if status:
332
+ params["status"] = status
333
+ r = self._request("GET", f"/v1/organizations/{oid}/federation/rounds", params=params)
334
+ r.raise_for_status()
335
+ return r.json()
336
+
337
+ def get_federation_metrics(self, org_id: Optional[str] = None, timeframe: str = "30d") -> Dict[str, Any]:
338
+ """Get federation metrics for an organization."""
339
+ oid = org_id or self.org_id
340
+ r = self._request("GET", f"/v1/organizations/{oid}/federation/metrics", params={"timeframe": timeframe})
341
+ r.raise_for_status()
342
+ return r.json()
343
+
344
+ # ---- Chemistry & 3D Visualization ----
345
+
346
+ def parse_molecular_file(self, file_path: str, format_type: Optional[str] = None, enable_caching: bool = True) -> Dict[str, Any]:
347
+ """Parse molecular file and extract chemical structure data."""
348
+ with open(file_path, 'rb') as f:
349
+ files = {'file': f}
350
+ data = {}
351
+ if format_type:
352
+ data['format_type'] = format_type
353
+ if enable_caching:
354
+ data['enable_caching'] = 'true'
355
+
356
+ r = self._request("POST", "/v1/chemistry/parse-molecular-file-optimized", files=files, data=data)
357
+ r.raise_for_status()
358
+ return r.json()
359
+
360
+ def get_chemistry_performance_status(self) -> Dict[str, Any]:
361
+ """Get chemistry service performance status."""
362
+ r = self._request("GET", "/v1/chemistry/performance-status")
363
+ r.raise_for_status()
364
+ return r.json()
365
+
366
+ # ---- RFT Automation (Reward Function Tooling) ----
367
+
368
+ def generate_rft_environment(self, evaluation_id: str, configuration: Dict[str, Any]) -> Dict[str, Any]:
369
+ """Generate RFT environment scaffold for an evaluation."""
370
+ payload = {
371
+ "evaluation_id": evaluation_id,
372
+ "configuration": configuration
373
+ }
374
+ r = self._request("POST", "/v1/rft/generate-environment", json=payload)
375
+ r.raise_for_status()
376
+ return r.json()
377
+
378
+ def build_reward_function(self, strategy: str, metrics: List[str], configuration: Dict[str, Any]) -> Dict[str, Any]:
379
+ """Build reward function configuration."""
380
+ payload = {
381
+ "strategy": strategy,
382
+ "metrics": metrics,
383
+ "configuration": configuration
384
+ }
385
+ r = self._request("POST", "/v1/rft/build-reward", json=payload)
386
+ r.raise_for_status()
387
+ return r.json()
388
+
389
+ # ---- Rewards & Compilation ----
390
+
391
+ def compile_reward(self, yaml_spec: str) -> Dict[str, Any]:
392
+ """Compile and validate reward function YAML specification."""
393
+ payload = {"yaml_spec": yaml_spec}
394
+ r = self._request("POST", "/v1/rewards/compile", json=payload)
395
+ r.raise_for_status()
396
+ return r.json()
397
+
398
+ def list_reward_specs(self) -> Dict[str, Any]:
399
+ """List available reward specifications."""
400
+ r = self._request("GET", "/v1/rewards")
401
+ r.raise_for_status()
402
+ return r.json()
403
+
404
+ # ---- Advanced Analytics ----
405
+
406
+ def get_template_usage_metrics(self, template_id: str) -> Dict[str, Any]:
407
+ """Get detailed usage metrics for a template."""
408
+ r = self._request("GET", f"/v1/templates/{template_id}/usage-metrics")
409
+ r.raise_for_status()
410
+ return r.json()
411
+
412
+ # ---- Batch Operations ----
413
+
414
+ def create_multiple_evaluations(self, evaluation_requests: List[Dict[str, Any]]) -> List[EvaluationResult]:
415
+ """Create multiple evaluations in batch."""
416
+ results = []
417
+ for req in evaluation_requests:
418
+ try:
419
+ result = self.evaluate(
420
+ template_id=req["template_id"],
421
+ agent_bundle_url=req["agent_bundle_url"],
422
+ reward_spec_id=req.get("reward_spec_id"),
423
+ config=req.get("config", {})
424
+ )
425
+ results.append(result)
426
+ except Exception as e:
427
+ # Continue with other evaluations even if one fails
428
+ print(f"Failed to create evaluation: {e}")
429
+ results.append(EvaluationResult(
430
+ id=f"failed-{len(results)}",
431
+ status=EvaluationStatus.FAILED,
432
+ score=None,
433
+ metrics=None
434
+ ))
435
+ return results
436
+
437
+ def wait_for_multiple_completions(self, evaluation_ids: List[str], timeout_seconds: int = 300) -> List[EvaluationResult]:
438
+ """Wait for multiple evaluations to complete."""
439
+ results = []
440
+ for eval_id in evaluation_ids:
441
+ try:
442
+ result = self.wait_for_completion(eval_id, timeout_seconds)
443
+ results.append(result)
444
+ except Exception as e:
445
+ print(f"Failed to wait for evaluation {eval_id}: {e}")
446
+ results.append(EvaluationResult(
447
+ id=eval_id,
448
+ status=EvaluationStatus.FAILED,
449
+ score=None,
450
+ metrics=None
451
+ ))
452
+ return results
453
+
454
+ # ---- Context Managers ----
455
+
456
+ def __enter__(self):
457
+ """Context manager entry."""
458
+ return self
459
+
460
+ def __exit__(self, exc_type, exc_val, exc_tb):
461
+ """Context manager exit - cleanup resources."""
462
+ if hasattr(self, '_client'):
463
+ self._client.close()
464
+
465
+ # ---- Utility helpers ----
466
+
467
+ def wait_for_completion(self, evaluation_id: str, timeout_seconds: int = 300, poll_interval: float = 1.0) -> EvaluationResult:
468
+ """Poll evaluation status until terminal state or timeout.
469
+
470
+ Terminal states: COMPLETED, FAILED, CANCELLED (if exposed by API).
471
+ """
472
+ end = time.time() + timeout_seconds
473
+ while time.time() < end:
474
+ res = self.get_status(evaluation_id)
475
+ if res.status in (
476
+ EvaluationStatus.COMPLETED,
477
+ EvaluationStatus.FAILED,
478
+ ):
479
+ return res
480
+ # Some APIs may return cancelled
481
+ try:
482
+ if res.status.name == "CANCELLED": # type: ignore[attr-defined]
483
+ return res
484
+ except Exception:
485
+ pass
486
+ time.sleep(max(0.1, poll_interval))
487
+ return self.get_status(evaluation_id)