graviton-sdk 0.1.0__tar.gz

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.
@@ -0,0 +1,30 @@
1
+ # Python
2
+ __pycache__/
3
+ .pytest_cache/
4
+ *.pyc
5
+ *.pyo
6
+ *.pyd
7
+ .Python
8
+ env/
9
+ .venv/
10
+
11
+ # VS Code
12
+ .vscode/
13
+
14
+ # Build artifacts
15
+ artifacts/
16
+ infra/gcp/.terraform/
17
+ infra/cloudflare/.terraform/
18
+
19
+ # IDE
20
+ .idea/
21
+
22
+ # Mock GCS bucket
23
+ .mock_gcs/
24
+
25
+ # Secrets
26
+ .secrets
27
+ plan.out
28
+
29
+ # infra state
30
+ infra/gcp/current_state/projects/779429570375/SecretManagerSecret
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: graviton-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Graviton Supercompute
5
+ Project-URL: Homepage, https://www.g-qt.com
6
+ Project-URL: Repository, https://github.com/CodeZeroLabs/graviton
7
+ Author-email: CodeZero Labs <aaron@g-qt.com>
8
+ License: MIT
9
+ Requires-Python: >=3.8
10
+ Requires-Dist: ipython
11
+ Requires-Dist: requests
12
+ Requires-Dist: sympy>=1.12
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Graviton Python SDK
16
+
17
+ The Graviton SDK provides a seamless way to connect your local mathematical models (built with SymPy) to the Graviton Supercompute platform.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install graviton-sdk
23
+ ```
24
+
25
+ ## Quick Start (Jupyter/Colab)
26
+
27
+ The SDK is designed to work beautifully in interactive environments.
28
+
29
+ ```python
30
+ import sympy as sp
31
+ import graviton as gv
32
+
33
+ # 1. Authenticate (Use the public evaluation key for prototyping)
34
+ gv.login('a-little-more-art-than-science')
35
+
36
+ # 2. Define a complex model
37
+ x, y = sp.symbols('x y')
38
+ f = sp.sin(x)**2 + sp.exp(-y) * sp.cos(x)
39
+
40
+ # 3. Display with the Graviton "Compute" button
41
+ gv.expr(f)
42
+ ```
43
+
44
+ ## Programmatic Workflow
45
+
46
+ For advanced integrations, you can use the flattened API to evaluate and compute programmatically.
47
+
48
+ ```python
49
+ # 1. Evaluate to get JSON data
50
+ eval_data = gv.evaluate(f, options={"type": "JSON"})
51
+
52
+ # 2. Use flattened access for cost and ID
53
+ cost = eval_data['cost']
54
+ job_id = eval_data['id']
55
+
56
+ print(f"Estimated Cost: {cost:.2f} credits")
57
+
58
+ # 3. Compute using the ID directly
59
+ job = gv.compute(job_id, options={"auto_charge": True})
60
+ result = job.wait()
61
+ print(f"Result: {result}")
62
+ ```
63
+
64
+ ## How it Works
65
+
66
+ When you call `gv(formula)`, the SDK:
67
+ 1. Serializes the SymPy object into a structured `srepr` format.
68
+ 2. Generates a rich HTML representation with a LaTeX preview.
69
+ 3. Adds a secure button that opens the Graviton Dashboard with your formula pre-loaded.
70
+
71
+ This allows you to leverage Graviton's massive cloud compute resources for complexity analysis and evaluation without ever leaving your research environment.
72
+
73
+ ## Security
74
+
75
+ Graviton follows a "Data, not Code" security model. The SDK only transmits the mathematical structure of your model, not arbitrary Python code. Your local environment remains isolated, and all authentication is handled securely via the Graviton web platform.
@@ -0,0 +1,61 @@
1
+ # Graviton Python SDK
2
+
3
+ The Graviton SDK provides a seamless way to connect your local mathematical models (built with SymPy) to the Graviton Supercompute platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install graviton-sdk
9
+ ```
10
+
11
+ ## Quick Start (Jupyter/Colab)
12
+
13
+ The SDK is designed to work beautifully in interactive environments.
14
+
15
+ ```python
16
+ import sympy as sp
17
+ import graviton as gv
18
+
19
+ # 1. Authenticate (Use the public evaluation key for prototyping)
20
+ gv.login('a-little-more-art-than-science')
21
+
22
+ # 2. Define a complex model
23
+ x, y = sp.symbols('x y')
24
+ f = sp.sin(x)**2 + sp.exp(-y) * sp.cos(x)
25
+
26
+ # 3. Display with the Graviton "Compute" button
27
+ gv.expr(f)
28
+ ```
29
+
30
+ ## Programmatic Workflow
31
+
32
+ For advanced integrations, you can use the flattened API to evaluate and compute programmatically.
33
+
34
+ ```python
35
+ # 1. Evaluate to get JSON data
36
+ eval_data = gv.evaluate(f, options={"type": "JSON"})
37
+
38
+ # 2. Use flattened access for cost and ID
39
+ cost = eval_data['cost']
40
+ job_id = eval_data['id']
41
+
42
+ print(f"Estimated Cost: {cost:.2f} credits")
43
+
44
+ # 3. Compute using the ID directly
45
+ job = gv.compute(job_id, options={"auto_charge": True})
46
+ result = job.wait()
47
+ print(f"Result: {result}")
48
+ ```
49
+
50
+ ## How it Works
51
+
52
+ When you call `gv(formula)`, the SDK:
53
+ 1. Serializes the SymPy object into a structured `srepr` format.
54
+ 2. Generates a rich HTML representation with a LaTeX preview.
55
+ 3. Adds a secure button that opens the Graviton Dashboard with your formula pre-loaded.
56
+
57
+ This allows you to leverage Graviton's massive cloud compute resources for complexity analysis and evaluation without ever leaving your research environment.
58
+
59
+ ## Security
60
+
61
+ Graviton follows a "Data, not Code" security model. The SDK only transmits the mathematical structure of your model, not arbitrary Python code. Your local environment remains isolated, and all authentication is handled securely via the Graviton web platform.
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "graviton-sdk"
7
+ version = "0.1.0"
8
+ description = "Python SDK for Graviton Supercompute"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "CodeZero Labs", email = "aaron@g-qt.com"},
14
+ ]
15
+ dependencies = [
16
+ "sympy>=1.12",
17
+ "IPython",
18
+ "requests",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://www.g-qt.com"
23
+ Repository = "https://github.com/CodeZeroLabs/graviton"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/graviton"]
@@ -0,0 +1,134 @@
1
+ import os
2
+ import json
3
+ import requests
4
+ from typing import Optional, Dict, Any
5
+ import sympy as sp
6
+
7
+ # Internal Constants
8
+ CONFIG_PATH = os.path.expanduser("~/.graviton/config.json")
9
+
10
+ class GravitonSession:
11
+ """
12
+ Manages the authenticated state, API configuration, and user status
13
+ for the current SDK session.
14
+ """
15
+ _instance = None
16
+
17
+ def __new__(cls):
18
+ if cls._instance is None:
19
+ cls._instance = super(GravitonSession, cls).__new__(cls)
20
+ cls._instance._initialized = False
21
+ return cls._instance
22
+
23
+ def __init__(self):
24
+ if self._initialized:
25
+ return
26
+ self.api_key: Optional[str] = None
27
+ self.api_url: str = self._detect_api_url()
28
+ self.user_profile: Optional[Dict[str, Any]] = None
29
+ self._load_config()
30
+ self._initialized = True
31
+
32
+ def _detect_api_url(self) -> str:
33
+ if os.environ.get("GRAVITON_API_URL"):
34
+ return os.environ["GRAVITON_API_URL"].rstrip("/")
35
+
36
+ # Default to production-style URL for consistency.
37
+ # Inside the Docker swarm, this resolves to the Nginx gateway.
38
+ # In a host browser, it requires /etc/hosts (optional now with browse_web.py).
39
+ return "https://api.g-qt.com"
40
+
41
+ def _load_config(self):
42
+ if os.path.exists(CONFIG_PATH):
43
+ try:
44
+ with open(CONFIG_PATH, 'r') as f:
45
+ config = json.load(f)
46
+ self.api_key = config.get("api_key")
47
+ except Exception:
48
+ pass
49
+
50
+ def save_config(self, api_key: str):
51
+ self.api_key = api_key
52
+ os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
53
+ with open(CONFIG_PATH, 'w') as f:
54
+ json.dump({"api_key": api_key}, f)
55
+
56
+ def get_dashboard_url(self) -> str:
57
+ """Returns the correct dashboard URL for the current environment."""
58
+ # Always use the production URL. Routing is handled by the swarm DNS or host setup.
59
+ return "https://www.g-qt.com/dashboard.html"
60
+
61
+ @property
62
+ def verify(self) -> bool:
63
+ """Tolerates self-signed certificates in local dev environment."""
64
+ return False if os.environ.get("REGION") == "local-dev" else True
65
+
66
+ def get_status(self) -> Dict[str, Any]:
67
+ """Fetches the latest user credits and payment info from Shannon."""
68
+ if not self.api_key:
69
+ return {"authenticated": False}
70
+
71
+ try:
72
+ resp = requests.get(
73
+ f"{self.api_url}/user",
74
+ headers={"X-API-KEY": self.api_key},
75
+ timeout=5,
76
+ verify=self.verify
77
+ )
78
+ if resp.status_code == 200:
79
+ self.user_profile = resp.json()
80
+ return {
81
+ "authenticated": True,
82
+ "credits": self.user_profile.get("credits", 0),
83
+ "has_payment_method": bool(self.user_profile.get("payment_methods", {}).get("primary")),
84
+ "profile": self.user_profile
85
+ }
86
+ except Exception:
87
+ pass
88
+ return {"authenticated": False, "error": "Connection failed"}
89
+
90
+ # Global Session Instance
91
+ session = GravitonSession()
92
+
93
+ def login(api_key: str):
94
+ """
95
+ Authenticates the SDK with your Graviton API Key.
96
+ """
97
+ session.save_config(api_key)
98
+ print("✅ Authenticated with Graviton.")
99
+
100
+ def evaluate(expr: sp.Expr, options: Optional[dict] = None):
101
+ """
102
+ Evaluates a SymPy expression on Graviton.
103
+ """
104
+ from .main import gv
105
+ return gv(expr).evaluate(options=options)
106
+
107
+ def compute(expr_or_id: Any, options: Optional[Any] = None):
108
+ """
109
+ Directly executes a compute job.
110
+ Accepts either a SymPy expression or a job_id string.
111
+ Returns a GravitonJob handle.
112
+ """
113
+ from .main import gv, GravitonExpression
114
+ if isinstance(options, bool):
115
+ options = {"auto_charge": options}
116
+
117
+ if isinstance(expr_or_id, str):
118
+ auto_charge = options.get("auto_charge", False) if isinstance(options, dict) else False
119
+ return GravitonExpression._trigger_compute(expr_or_id, auto_charge=auto_charge)
120
+
121
+ return gv(expr_or_id).compute(options=options)
122
+
123
+ def status(job_id: str):
124
+ """Checks the status of a specific job."""
125
+ from .main import GravitonJob
126
+ return GravitonJob(job_id).status()
127
+
128
+ def result(job_id: str):
129
+ """Wait for and return the result of a specific job."""
130
+ from .main import GravitonJob
131
+ return GravitonJob(job_id).result()
132
+
133
+ # Legacy alias for backward compatibility
134
+ from .main import gv, expr
@@ -0,0 +1,440 @@
1
+ import sympy as sp
2
+ from IPython.display import display, HTML
3
+ import requests
4
+ import uuid
5
+ import time
6
+ import json
7
+ from typing import Optional, Dict, Any
8
+
9
+ class GravitonJob:
10
+ """
11
+ A handle for a Graviton compute job.
12
+ Allows for polling status and retrieving results asynchronously.
13
+ """
14
+ def __init__(self, job_id: str):
15
+ self.job_id = job_id
16
+ self._data = None
17
+
18
+ def status(self) -> str:
19
+ """Checks the current status of the job."""
20
+ from . import session
21
+ try:
22
+ resp = requests.get(
23
+ f"{session.api_url}/results/{self.job_id}",
24
+ headers={"X-API-KEY": session.api_key},
25
+ timeout=5,
26
+ verify=session.verify
27
+ )
28
+ if resp.status_code == 200:
29
+ self._data = resp.json()
30
+ return self._data.get("status", "UNKNOWN")
31
+ except Exception as e:
32
+ return f"ERROR: {str(e)}"
33
+ return "UNKNOWN"
34
+
35
+ def result(self, block: bool = True, poll_interval: float = 1.0) -> Any:
36
+ """
37
+ Retrieves the job result. If block=True, polls until completed.
38
+ """
39
+ from . import session
40
+
41
+ if block:
42
+ while True:
43
+ s = self.status()
44
+ if s == "COMPLETED":
45
+ break
46
+ if s in ["FAILED", "CANCELLED", "ERROR"]:
47
+ raise RuntimeError(f"Job {self.job_id} failed with status: {s}")
48
+ time.sleep(poll_interval)
49
+
50
+ # Download results
51
+ try:
52
+ resp = requests.get(
53
+ f"{session.api_url}/results/{self.job_id}/download",
54
+ headers={"X-API-KEY": session.api_key},
55
+ timeout=10,
56
+ verify=session.verify
57
+ )
58
+ if resp.status_code == 200:
59
+ # Attempt to parse as JSON if it looks like it
60
+ try:
61
+ return resp.json()
62
+ except:
63
+ return resp.text
64
+ else:
65
+ raise RuntimeError(f"Failed to download results: {resp.status_code}")
66
+ except Exception as e:
67
+ raise RuntimeError(f"Result retrieval failed: {e}")
68
+
69
+ def wait(self, poll_interval: float = 1.0) -> Any:
70
+ """Alias for result(block=True)."""
71
+ return self.result(block=True, poll_interval=poll_interval)
72
+
73
+ class GravitonEvaluation(dict):
74
+ """
75
+ A dictionary-like object that wraps the Graviton evaluation response
76
+ and provides flattened access to common fields like cost and ID.
77
+ """
78
+ @property
79
+ def id(self) -> str:
80
+ return super().get('id', '')
81
+
82
+ @property
83
+ def cost(self) -> float:
84
+ return super().get('cost', {}).get('total_credit_cost', 0.0)
85
+
86
+ @property
87
+ def estimated_time(self) -> float:
88
+ return super().get('estimated_duration_s', 0.1)
89
+
90
+ def __getitem__(self, key):
91
+ # Support flattened access for 'cost' and 'id'
92
+ if key == 'cost':
93
+ return self.cost
94
+ if key == 'id':
95
+ return self.id
96
+ return super().__getitem__(key)
97
+
98
+ def get(self, key, default=None):
99
+ if key == 'cost':
100
+ return self.cost
101
+ if key == 'id':
102
+ return self.id
103
+ return super().get(key, default)
104
+
105
+ class GravitonExpression:
106
+ """
107
+ A wrapper for SymPy expressions that provides interactive evaluation
108
+ and compute capabilities within notebooks.
109
+ """
110
+ def __init__(self, expr):
111
+ self.expr = expr
112
+ self.evaluation_result = None
113
+ self.error = None
114
+
115
+ @property
116
+ def cost(self) -> float:
117
+ """Returns the credit cost from the last evaluation."""
118
+ if not self.evaluation_result:
119
+ self.evaluate()
120
+ return self.evaluation_result.get('cost', {}).get('total_credit_cost', 0.0)
121
+
122
+ @property
123
+ def estimated_time(self) -> float:
124
+ """Returns the estimated duration in seconds from the last evaluation."""
125
+ if not self.evaluation_result:
126
+ self.evaluate()
127
+ return self.evaluation_result.get('estimated_duration_s', 0.1)
128
+
129
+ @property
130
+ def is_affordable(self) -> bool:
131
+ """Returns True if the user has enough credits for the last evaluation."""
132
+ from . import session
133
+ status = session.get_status()
134
+ return status.get('credits', 0) >= self.cost
135
+
136
+ def evaluate(self, options: Optional[dict] = None):
137
+ """
138
+ Performs a secure evaluation of the expression on Graviton.
139
+ """
140
+ from . import session
141
+
142
+ if not session.api_key:
143
+ self.error = "Unauthenticated. Please run 'gv.login(key)' first."
144
+ if options and options.get("type") == "JSON":
145
+ raise RuntimeError(self.error)
146
+ return self
147
+
148
+ try:
149
+ srepr_str = sp.srepr(self.expr)
150
+ response = requests.post(
151
+ f"{session.api_url}/eval",
152
+ json={"f": srepr_str},
153
+ headers={"X-API-KEY": session.api_key},
154
+ timeout=10,
155
+ verify=session.verify
156
+ )
157
+
158
+ if response.status_code in [200, 201]:
159
+ self.evaluation_result = response.json()
160
+ self.error = None
161
+ else:
162
+ try:
163
+ err_data = response.json()
164
+ err_msg = err_data.get('error', 'Unknown error')
165
+ self.error = f"API Error {response.status_code}: {err_msg}"
166
+ except:
167
+ self.error = f"API Error {response.status_code}"
168
+ except Exception as e:
169
+ self.error = f"Connection Error: {str(e)}"
170
+
171
+ if self.error and options and options.get("type") == "JSON":
172
+ raise RuntimeError(self.error)
173
+
174
+ if options and options.get("type") == "JSON":
175
+ return GravitonEvaluation(self.evaluation_result)
176
+
177
+ return self
178
+
179
+ def compute(self, options: Optional[Any] = None) -> GravitonJob:
180
+ """
181
+ Executes the compute job. If not yet evaluated, evaluates first.
182
+ """
183
+ from . import session
184
+
185
+ if isinstance(options, bool):
186
+ options = {"auto_charge": options}
187
+
188
+ auto_charge = False
189
+ if options and isinstance(options, dict):
190
+ auto_charge = options.get("auto_charge", False)
191
+
192
+ if not self.evaluation_result:
193
+ self.evaluate()
194
+ if self.error:
195
+ raise RuntimeError(self.error)
196
+
197
+ job_id = self.evaluation_result['id']
198
+
199
+ # Check if we need to auto-charge (client-side check for better DX)
200
+ if auto_charge:
201
+ cost = self.evaluation_result.get('cost', {}).get('total_credit_cost', 0)
202
+ status = session.get_status()
203
+ if status.get('credits', 0) < cost:
204
+ if not status.get('has_payment_method'):
205
+ dashboard_url = session.get_dashboard_url()
206
+ if session.api_key and "localhost" in dashboard_url:
207
+ dashboard_url += f"?dev_key={session.api_key}"
208
+ raise RuntimeError(
209
+ f"Insufficient Credits ({status.get('credits', 0):.2f}/{cost:.2f}). "
210
+ f"Auto-charging is not possible because no primary payment method is configured. "
211
+ f"Please add a payment method in your dashboard: {dashboard_url}"
212
+ )
213
+
214
+ # Call /compute
215
+ return self._trigger_compute(job_id, auto_charge)
216
+
217
+ @staticmethod
218
+ def _trigger_compute(job_id: str, auto_charge: bool = False) -> GravitonJob:
219
+ """Internal helper to trigger the /compute endpoint."""
220
+ from . import session
221
+
222
+ dashboard_url = session.get_dashboard_url()
223
+ if session.api_key and "localhost" in dashboard_url:
224
+ dashboard_url += f"?dev_key={session.api_key}"
225
+
226
+ resp = requests.post(
227
+ f"{session.api_url}/compute",
228
+ json={"job_id": job_id, "auto_charge": auto_charge},
229
+ headers={"X-API-KEY": session.api_key},
230
+ timeout=15,
231
+ verify=session.verify
232
+ )
233
+
234
+ if resp.status_code in [200, 201, 202]:
235
+ return GravitonJob(job_id)
236
+ else:
237
+ msg = resp.text
238
+ try:
239
+ err_data = resp.json()
240
+ if isinstance(err_data, dict):
241
+ msg = err_data.get('error', msg)
242
+ # If there's a more descriptive message, use it
243
+ if err_data.get('message'):
244
+ msg = f"{msg}: {err_data['message']}"
245
+ except:
246
+ pass
247
+
248
+ if "PAYMENT_METHOD_REQUIRED" in msg or "INSUFFICIENT_CREDITS" in msg:
249
+ msg = f"{msg}. Please visit your dashboard to manage credits: {dashboard_url}"
250
+
251
+ raise RuntimeError(f"Compute failed: {msg}")
252
+
253
+ def wait(self) -> Any:
254
+ """Executes the compute job and waits for the result."""
255
+ return self.compute().wait()
256
+
257
+ def _repr_html_(self):
258
+ from . import session
259
+
260
+ # 1. Prepare Base Info
261
+ try:
262
+ latex_str = sp.latex(self.expr)
263
+ srepr_str = sp.srepr(self.expr)
264
+ except Exception as e:
265
+ return f"<div style='color: #ba1a1a;'>Error preparing expression: {e}</div>"
266
+
267
+ # Unique ID for the card instance
268
+ card_id = f"graviton-card-{uuid.uuid4().hex[:8]}"
269
+ srepr_escaped = srepr_str.replace("'", "\\'")
270
+
271
+ # 2. Check Auth Status
272
+ auth_status = session.get_status()
273
+ if not auth_status.get('authenticated'):
274
+ return f"""
275
+ <div style="border: 1px solid #ccc; padding: 25px; border-radius: 16px; background: #fffbfa; font-family: 'Outfit', sans-serif; text-align: center; max-width: 500px;">
276
+ <div style="font-size: 1.4rem; margin-bottom: 10px; color: #1a1a1a;">$${latex_str}$$</div>
277
+ <div style="color: #666; margin-bottom: 20px;">Authenticate to access Graviton Supercomputing.</div>
278
+ <a href="http://www.g-qt.com" target="_blank" style="background: #1a1a1a; color: white; padding: 10px 25px; border-radius: 20px; text-decoration: none; font-weight: 600;">Login to Graviton</a>
279
+ </div>
280
+ """
281
+
282
+ # 3. Build the Information Grid
283
+ if self.evaluation_result:
284
+ res = self.evaluation_result
285
+ # Use estimated duration if available (mocked for now as 0.02s per op)
286
+ est_time = res.get('estimated_duration_s', 0.1)
287
+ cost = res.get('cost', {}).get('total_credit_cost', 0.0)
288
+
289
+ # Determine button state
290
+ has_credits = auth_status.get('credits', 0) >= cost
291
+ has_payment = auth_status.get('has_payment_method')
292
+
293
+ if has_credits:
294
+ btn_text = "Supercompute on Graviton"
295
+ btn_action = "runCompute()"
296
+ elif has_payment:
297
+ btn_text = "Purchase & Execute"
298
+ btn_action = "runPurchaseAndCompute()"
299
+ else:
300
+ btn_text = "Insufficient Credits"
301
+ dashboard_url = session.get_dashboard_url()
302
+ if session.api_key:
303
+ dashboard_url += f"?dev_key={session.api_key}"
304
+ btn_action = f"window.open('{dashboard_url}', '_blank')"
305
+
306
+ eval_html = f"""
307
+ <div id="{card_id}-metrics" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; width: 100%; margin-bottom: 15px; padding: 12px; background: rgba(0,0,0,0.03); border-radius: 8px; font-size: 0.9rem;">
308
+ <div><strong>Est. Time:</strong> {est_time:.1f}s</div>
309
+ <div><strong>Variables:</strong> {len(res.get('variables', []))}</div>
310
+ <div style="grid-column: span 2; text-align: center; border-top: 1px solid rgba(0,0,0,0.05); padding-top: 8px; margin-top: 4px; color: #2e7d32;">
311
+ <strong>Cost:</strong> {cost:.2f} Credits
312
+ </div>
313
+ </div>
314
+ """
315
+ else:
316
+ btn_text = "Evaluate on Graviton"
317
+ btn_action = "runEvaluate()"
318
+ eval_html = f"""
319
+ <div id="{card_id}-metrics" style="color: #666; font-size: 0.85rem; margin-bottom: 15px; text-align: center;">
320
+ Evaluate to see estimated time and costs.
321
+ </div>
322
+ """
323
+
324
+ # 4. Final HTML with JS Logic
325
+ return f"""
326
+ <div id="{card_id}" class="graviton-card" style="border: 1px solid #ccc; padding: 25px; border-radius: 16px; background: #fdfdfd; font-family: 'Outfit', sans-serif; display: flex; flex-direction: column; align-items: center; box-shadow: 0 4px 12px rgba(0,0,0,0.05); max-width: 500px;">
327
+ <div style="font-size: 1.4rem; margin-bottom: 15px; color: #1a1a1a;">$${latex_str}$$</div>
328
+
329
+ {eval_html}
330
+
331
+ <button id="{card_id}-btn" onclick="{btn_action}" style="background: #1a1a1a; color: #ffffff; border: 1px solid rgba(255,255,255,0.2); padding: 12px 32px; border-radius: 30px; cursor: pointer; font-weight: 600; font-size: 1rem; width: 100%; transition: all 0.2s;">
332
+ {btn_text}
333
+ </button>
334
+
335
+ <div id="{card_id}-status" style="font-size: 0.75rem; color: #777; margin-top: 12px; font-style: italic;">
336
+ Verified SymPy model ready for Graviton Acceleration
337
+ </div>
338
+
339
+ <script>
340
+ (function() {{
341
+ const card = document.getElementById('{card_id}');
342
+ const btn = document.getElementById('{card_id}-btn');
343
+ const status = document.getElementById('{card_id}-status');
344
+ const metrics = document.getElementById('{card_id}-metrics');
345
+ const api_url = '{session.api_url}';
346
+ const api_key = '{session.api_key}';
347
+
348
+ let currentJobId = '';
349
+ window.runEvaluate = async () => {{
350
+ btn.disabled = true;
351
+ btn.innerText = "Evaluating...";
352
+ try {{
353
+ const resp = await fetch(api_url + '/eval', {{
354
+ method: 'POST',
355
+ headers: {{ 'Content-Type': 'application/json', 'X-API-KEY': api_key }},
356
+ body: JSON.stringify({{ f: '{srepr_escaped}' }})
357
+ }});
358
+ if (resp.ok) {{
359
+ const data = await resp.json();
360
+ currentJobId = data.id;
361
+ status.innerText = "Evaluation complete. Ready to provision.";
362
+
363
+ if (data.result !== undefined) {{
364
+ metrics.innerHTML = '<div style="grid-column: span 2; text-align: center; font-size: 1.2rem; color: #1a1a1a;"><strong>Result:</strong> ' + data.result + '</div>';
365
+ btn.innerText = "Compute Finished";
366
+ }} else {{
367
+ let costStr = (data.cost && data.cost.total_credit_cost !== undefined) ? data.cost.total_credit_cost.toFixed(2) : "0.00";
368
+ let timeStr = data.estimated_duration_s !== undefined ? data.estimated_duration_s : "?";
369
+ metrics.innerHTML = 'Estimated Cost: ' + costStr + ' credits | Estimated Time: ' + timeStr + 's';
370
+ btn.innerText = "Compute on Swarm";
371
+ btn.onclick = window.runCompute;
372
+ btn.disabled = false;
373
+ }}
374
+ }} else {{
375
+ status.innerText = "Evaluation failed: " + resp.status;
376
+ btn.disabled = false;
377
+ }}
378
+ }} catch (e) {{
379
+ status.innerText = "Connection failed.";
380
+ btn.disabled = false;
381
+ }}
382
+ }};
383
+
384
+ window.runCompute = async () => {{
385
+ if (!currentJobId) return;
386
+ btn.disabled = true;
387
+ btn.innerText = "Provisioning...";
388
+ try {{
389
+ const resp = await fetch(api_url + '/compute', {{
390
+ method: 'POST',
391
+ headers: {{ 'Content-Type': 'application/json', 'X-API-KEY': api_key }},
392
+ body: JSON.stringify({{ job_id: currentJobId }})
393
+ }});
394
+ if (resp.ok) {{
395
+ pollResult(currentJobId);
396
+ }} else {{
397
+ status.innerText = "Compute failed.";
398
+ btn.disabled = false;
399
+ }}
400
+ }} catch (e) {{
401
+ status.innerText = "Network error.";
402
+ btn.disabled = false;
403
+ }}
404
+ }};
405
+
406
+ const pollResult = async (jobId) => {{
407
+ status.innerText = "Executing on Graviton Swarm...";
408
+ const timer = setInterval(async () => {{
409
+ const resp = await fetch(api_url + '/results/' + jobId, {{
410
+ headers: {{ 'X-API-KEY': api_key }}
411
+ }});
412
+ const data = await resp.json();
413
+ if (data.status === 'COMPLETED') {{
414
+ clearInterval(timer);
415
+ const resResp = await fetch(api_url + '/results/' + jobId + '/download', {{
416
+ headers: {{ 'X-API-KEY': api_key }}
417
+ }});
418
+ const result = await resResp.text();
419
+ metrics.innerHTML = '<div style="grid-column: span 2; text-align: center; font-size: 1.2rem; color: #1a1a1a;"><strong>Result:</strong> ' + result + '</div>';
420
+ btn.style.display = 'none';
421
+ status.innerText = "Job COMPLETED";
422
+ }} else if (data.status === 'FAILED') {{
423
+ clearInterval(timer);
424
+ status.innerText = "Job FAILED";
425
+ }}
426
+ }}, 1000);
427
+ }};
428
+ }})();
429
+ </script>
430
+ </div>
431
+ """
432
+
433
+ def gv(expr):
434
+ """
435
+ Wraps a SymPy expression for Graviton integration.
436
+ """
437
+ return GravitonExpression(expr)
438
+
439
+ # Alias for better readability
440
+ expr = gv