hyperaim-sdk 1.0.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.
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hyperaim-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: HyperSDK — AIM Intelligence Middleware. Add telemetry, quality scoring, rate limiting, anomaly detection, and demand prediction to any HyperCycle AIM with 2 lines of code.
|
|
5
|
+
Author-email: QyraTech <info@qyratech.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/qyratech/hyperaim-sdk
|
|
8
|
+
Project-URL: Documentation, http://62.171.162.38:8095/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/qyratech/hyperaim-sdk
|
|
10
|
+
Keywords: hypercycle,aim,ai,telemetry,intelligence,sdk,hypersdk,middleware
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Classifier: Topic :: System :: Monitoring
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
Requires-Dist: fastapi>=0.100.0
|
|
21
|
+
|
|
22
|
+
# HyperSDK
|
|
23
|
+
|
|
24
|
+
**AIM Intelligence Middleware for the HyperCycle IoAI Network.**
|
|
25
|
+
|
|
26
|
+
Add intelligence to any AIM with 2 lines of code.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from hyperaim_sdk import HyperSDK
|
|
30
|
+
hs = HyperSDK(app, api_key="qt_your_key_here", aim_name="my-aim")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## What You Get (Free Tier)
|
|
34
|
+
|
|
35
|
+
- **Automatic telemetry** — every request tracked: latency, errors, callers, endpoints
|
|
36
|
+
- **Live /health endpoint** — uptime, error rate, avg latency, unique callers
|
|
37
|
+
- **Live /qt/stats endpoint** — detailed rolling metrics with percentiles
|
|
38
|
+
- **Per-caller rate limiting** — protect your AIM from abuse
|
|
39
|
+
- **Request ID tracking** — trace any request via response headers
|
|
40
|
+
- **Batched telemetry upload** — minimal overhead, flushes every 60s
|
|
41
|
+
|
|
42
|
+
## Intelligence Engine (powered by QyraTech)
|
|
43
|
+
|
|
44
|
+
Your telemetry feeds the QyraTech Intelligence Server, which provides:
|
|
45
|
+
|
|
46
|
+
- **Quality scoring** — reliability, performance, demand, consistency (0-100 each)
|
|
47
|
+
- **Anomaly detection** — latency spikes, error bursts, traffic drops detected automatically
|
|
48
|
+
- **Demand prediction** — peak hours, trend direction, future call volume
|
|
49
|
+
- **Network rankings** — how your AIM compares to all others on the IoAI
|
|
50
|
+
- **Cross-AIM patterns** — which AIMs are frequently used together
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install hyperaim-sdk
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from fastapi import FastAPI
|
|
62
|
+
from hyperaim_sdk import HyperSDK
|
|
63
|
+
|
|
64
|
+
app = FastAPI(title="My AIM")
|
|
65
|
+
|
|
66
|
+
# Register at http://62.171.162.38:8095/register to get your API key
|
|
67
|
+
hs = HyperSDK(
|
|
68
|
+
app,
|
|
69
|
+
api_key="qt_your_key_here",
|
|
70
|
+
aim_name="my-awesome-aim",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@app.post("/my-endpoint")
|
|
74
|
+
async def my_endpoint():
|
|
75
|
+
# Your AIM logic here — HyperSDK tracks everything automatically
|
|
76
|
+
return {"result": "hello"}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
hs = HyperSDK(
|
|
83
|
+
app,
|
|
84
|
+
api_key="qt_...", # Required: your API key
|
|
85
|
+
aim_name="my-aim", # Name shown in dashboards
|
|
86
|
+
server_url="http://62.171.162.38:8095", # Intelligence server
|
|
87
|
+
rate_limit=100, # Max requests per window per caller
|
|
88
|
+
rate_window=60, # Rate limit window in seconds
|
|
89
|
+
flush_interval=60, # Telemetry flush interval in seconds
|
|
90
|
+
enable_telemetry=True, # Send telemetry to server
|
|
91
|
+
enable_rate_limiting=True, # Enable per-caller rate limiting
|
|
92
|
+
enable_health=True, # Add /health endpoint
|
|
93
|
+
enable_stats=True, # Add /qt/stats endpoint
|
|
94
|
+
excluded_paths=["/health", "/docs"], # Paths to skip tracking
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Response Headers
|
|
99
|
+
|
|
100
|
+
Every tracked request gets these headers:
|
|
101
|
+
|
|
102
|
+
| Header | Description |
|
|
103
|
+
|--------|-------------|
|
|
104
|
+
| `X-Request-ID` | Unique request identifier |
|
|
105
|
+
| `X-HyperSDK` | SDK version |
|
|
106
|
+
| `X-Response-Time-Ms` | Request latency in milliseconds |
|
|
107
|
+
|
|
108
|
+
## Endpoints Added
|
|
109
|
+
|
|
110
|
+
| Endpoint | Description |
|
|
111
|
+
|----------|-------------|
|
|
112
|
+
| `GET /health` | Health check with live stats |
|
|
113
|
+
| `GET /qt/stats` | Detailed rolling metrics |
|
|
114
|
+
| `GET /qt/rate-limit` | Current rate limit status for caller |
|
|
115
|
+
|
|
116
|
+
## Get Your API Key
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
curl -X POST http://62.171.162.38:8095/register \
|
|
120
|
+
-H 'Content-Type: application/json' \
|
|
121
|
+
-d '{"name": "my-aim", "owner": "your-name", "url": "http://your-server:port"}'
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## View Your Dashboard
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
http://62.171.162.38:8095/dashboard/{your_aim_id}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Built By
|
|
131
|
+
|
|
132
|
+
**QyraTech** — AI Agent Intelligence Infrastructure on the HyperCycle IoAI Network.
|
|
133
|
+
|
|
134
|
+
- Intelligence Server: http://62.171.162.38:8095
|
|
135
|
+
- QyraTech Home: http://62.171.162.38:8085
|
|
136
|
+
- HyperCycle: https://www.hypercycle.ai
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
hyperaim_sdk.py,sha256=rUXEY5J4RQ1MMfsDanBvTwt8cdwIAiuQj9Htp1Tg9r4,12978
|
|
2
|
+
hyperaim_sdk-1.0.0.dist-info/METADATA,sha256=kWPa3G8oHKoireM01KOhGlksUEB7JKOvqduN93_fqUg,4536
|
|
3
|
+
hyperaim_sdk-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
hyperaim_sdk-1.0.0.dist-info/top_level.txt,sha256=KX5AIpd0HEnm-kgb_IyV2Z8fqovTmMf9n-CjD1Pv-oM,13
|
|
5
|
+
hyperaim_sdk-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hyperaim_sdk
|
hyperaim_sdk.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HyperSDK v1.0.0 — AIM Intelligence Middleware
|
|
3
|
+
|
|
4
|
+
Add intelligence to any HyperCycle AIM with 2 lines of code:
|
|
5
|
+
|
|
6
|
+
from hypersdk import HyperSDK
|
|
7
|
+
hs = HyperSDK(app, api_key="qt_your_key_here")
|
|
8
|
+
|
|
9
|
+
What you get for free:
|
|
10
|
+
- Automatic request telemetry (latency, errors, callers)
|
|
11
|
+
- /health endpoint with live stats
|
|
12
|
+
- /qt/stats endpoint with detailed metrics
|
|
13
|
+
- Rate limiting per caller
|
|
14
|
+
- Request ID tracking
|
|
15
|
+
- Automatic batched telemetry upload
|
|
16
|
+
|
|
17
|
+
What the intelligence server does with your data:
|
|
18
|
+
- Quality scoring (reliability, performance, demand, consistency)
|
|
19
|
+
- Anomaly detection (latency spikes, error bursts, traffic drops)
|
|
20
|
+
- Demand prediction (when your peak hours are, trend direction)
|
|
21
|
+
- Network-wide AIM rankings
|
|
22
|
+
- Cross-AIM correlation patterns
|
|
23
|
+
|
|
24
|
+
Published by QyraTech — AI Agent Intelligence Infrastructure
|
|
25
|
+
https://github.com/qyratech/hypersdk
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import time
|
|
29
|
+
import uuid
|
|
30
|
+
import hashlib
|
|
31
|
+
import asyncio
|
|
32
|
+
import threading
|
|
33
|
+
import statistics
|
|
34
|
+
from collections import deque, defaultdict
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
from typing import Optional, Callable
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
import httpx
|
|
40
|
+
except ImportError:
|
|
41
|
+
httpx = None
|
|
42
|
+
|
|
43
|
+
__version__ = "1.0.0"
|
|
44
|
+
__author__ = "QyraTech"
|
|
45
|
+
|
|
46
|
+
# Default intelligence server
|
|
47
|
+
DEFAULT_SERVER = "http://62.171.162.38:8095"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TelemetryBuffer:
|
|
51
|
+
"""Thread-safe buffer that batches telemetry events and flushes periodically."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, server_url: str, api_key: str, flush_interval: int = 60,
|
|
54
|
+
max_buffer: int = 500):
|
|
55
|
+
self.server_url = server_url
|
|
56
|
+
self.api_key = api_key
|
|
57
|
+
self.flush_interval = flush_interval
|
|
58
|
+
self.max_buffer = max_buffer
|
|
59
|
+
self.buffer = deque(maxlen=max_buffer)
|
|
60
|
+
self._running = False
|
|
61
|
+
self._flush_task = None
|
|
62
|
+
|
|
63
|
+
def add(self, event: dict):
|
|
64
|
+
self.buffer.append(event)
|
|
65
|
+
if len(self.buffer) >= self.max_buffer:
|
|
66
|
+
asyncio.ensure_future(self.flush())
|
|
67
|
+
|
|
68
|
+
async def flush(self):
|
|
69
|
+
if not self.buffer or not httpx:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
events = []
|
|
73
|
+
while self.buffer:
|
|
74
|
+
try:
|
|
75
|
+
events.append(self.buffer.popleft())
|
|
76
|
+
except IndexError:
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
if not events:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
async with httpx.AsyncClient() as client:
|
|
84
|
+
await client.post(
|
|
85
|
+
f"{self.server_url}/telemetry",
|
|
86
|
+
json={"events": events},
|
|
87
|
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
88
|
+
timeout=10,
|
|
89
|
+
)
|
|
90
|
+
except Exception:
|
|
91
|
+
# Re-add failed events (best effort)
|
|
92
|
+
for e in events[:50]:
|
|
93
|
+
self.buffer.appendleft(e)
|
|
94
|
+
|
|
95
|
+
async def start_periodic_flush(self):
|
|
96
|
+
self._running = True
|
|
97
|
+
while self._running:
|
|
98
|
+
await asyncio.sleep(self.flush_interval)
|
|
99
|
+
await self.flush()
|
|
100
|
+
|
|
101
|
+
def stop(self):
|
|
102
|
+
self._running = False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class RateLimiter:
|
|
106
|
+
"""Per-caller sliding window rate limiter."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, max_requests: int = 100, window_seconds: int = 60):
|
|
109
|
+
self.max_requests = max_requests
|
|
110
|
+
self.window = window_seconds
|
|
111
|
+
self.callers = defaultdict(deque)
|
|
112
|
+
|
|
113
|
+
def is_allowed(self, caller_id: str) -> bool:
|
|
114
|
+
now = time.time()
|
|
115
|
+
window = self.callers[caller_id]
|
|
116
|
+
|
|
117
|
+
# Remove expired entries
|
|
118
|
+
while window and window[0] < now - self.window:
|
|
119
|
+
window.popleft()
|
|
120
|
+
|
|
121
|
+
if len(window) >= self.max_requests:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
window.append(now)
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
def get_usage(self, caller_id: str) -> dict:
|
|
128
|
+
now = time.time()
|
|
129
|
+
window = self.callers[caller_id]
|
|
130
|
+
while window and window[0] < now - self.window:
|
|
131
|
+
window.popleft()
|
|
132
|
+
return {
|
|
133
|
+
"used": len(window),
|
|
134
|
+
"limit": self.max_requests,
|
|
135
|
+
"remaining": max(0, self.max_requests - len(window)),
|
|
136
|
+
"window_seconds": self.window,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class LiveStats:
|
|
141
|
+
"""Real-time rolling statistics for the AIM."""
|
|
142
|
+
|
|
143
|
+
def __init__(self, window_size: int = 1000):
|
|
144
|
+
self.window_size = window_size
|
|
145
|
+
self.latencies = deque(maxlen=window_size)
|
|
146
|
+
self.status_codes = deque(maxlen=window_size)
|
|
147
|
+
self.endpoints = defaultdict(int)
|
|
148
|
+
self.callers = set()
|
|
149
|
+
self.total_requests = 0
|
|
150
|
+
self.total_errors = 0
|
|
151
|
+
self.start_time = time.time()
|
|
152
|
+
self._hourly_calls = defaultdict(int)
|
|
153
|
+
|
|
154
|
+
def record(self, latency_ms: float, status_code: int, endpoint: str, caller: str):
|
|
155
|
+
self.latencies.append(latency_ms)
|
|
156
|
+
self.status_codes.append(status_code)
|
|
157
|
+
self.endpoints[endpoint] += 1
|
|
158
|
+
self.callers.add(caller)
|
|
159
|
+
self.total_requests += 1
|
|
160
|
+
if status_code >= 400:
|
|
161
|
+
self.total_errors += 1
|
|
162
|
+
|
|
163
|
+
hour = datetime.now(timezone.utc).strftime("%H")
|
|
164
|
+
self._hourly_calls[hour] += 1
|
|
165
|
+
|
|
166
|
+
def get_stats(self) -> dict:
|
|
167
|
+
uptime = time.time() - self.start_time
|
|
168
|
+
lats = list(self.latencies)
|
|
169
|
+
sorted_lats = sorted(lats) if lats else [0]
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"uptime_seconds": round(uptime),
|
|
173
|
+
"total_requests": self.total_requests,
|
|
174
|
+
"total_errors": self.total_errors,
|
|
175
|
+
"error_rate_pct": round(self.total_errors / max(self.total_requests, 1) * 100, 2),
|
|
176
|
+
"unique_callers": len(self.callers),
|
|
177
|
+
"requests_per_minute": round(self.total_requests / max(uptime / 60, 1), 2),
|
|
178
|
+
"latency": {
|
|
179
|
+
"avg_ms": round(statistics.mean(lats), 1) if lats else 0,
|
|
180
|
+
"p50_ms": round(sorted_lats[len(sorted_lats) // 2], 1) if sorted_lats else 0,
|
|
181
|
+
"p95_ms": round(sorted_lats[int(len(sorted_lats) * 0.95)], 1) if len(sorted_lats) > 1 else 0,
|
|
182
|
+
"p99_ms": round(sorted_lats[int(len(sorted_lats) * 0.99)], 1) if len(sorted_lats) > 1 else 0,
|
|
183
|
+
"min_ms": round(min(lats), 1) if lats else 0,
|
|
184
|
+
"max_ms": round(max(lats), 1) if lats else 0,
|
|
185
|
+
},
|
|
186
|
+
"top_endpoints": dict(sorted(self.endpoints.items(),
|
|
187
|
+
key=lambda x: x[1], reverse=True)[:10]),
|
|
188
|
+
"hourly_distribution": dict(self._hourly_calls),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class HyperSDK:
|
|
193
|
+
"""
|
|
194
|
+
HyperSDK — AIM Intelligence Middleware.
|
|
195
|
+
|
|
196
|
+
Wraps a FastAPI app with automatic telemetry, rate limiting,
|
|
197
|
+
health endpoints, and intelligence server reporting.
|
|
198
|
+
|
|
199
|
+
Usage:
|
|
200
|
+
from fastapi import FastAPI
|
|
201
|
+
from hypersdk import HyperSDK
|
|
202
|
+
|
|
203
|
+
app = FastAPI()
|
|
204
|
+
hs = HyperSDK(
|
|
205
|
+
app,
|
|
206
|
+
api_key="qt_your_key_here",
|
|
207
|
+
aim_name="my-awesome-aim",
|
|
208
|
+
)
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
app,
|
|
214
|
+
api_key: str,
|
|
215
|
+
aim_name: str = "unknown",
|
|
216
|
+
server_url: str = DEFAULT_SERVER,
|
|
217
|
+
rate_limit: int = 100,
|
|
218
|
+
rate_window: int = 60,
|
|
219
|
+
flush_interval: int = 60,
|
|
220
|
+
enable_telemetry: bool = True,
|
|
221
|
+
enable_rate_limiting: bool = True,
|
|
222
|
+
enable_health: bool = True,
|
|
223
|
+
enable_stats: bool = True,
|
|
224
|
+
excluded_paths: list = None,
|
|
225
|
+
):
|
|
226
|
+
self.app = app
|
|
227
|
+
self.api_key = api_key
|
|
228
|
+
self.aim_name = aim_name
|
|
229
|
+
self.server_url = server_url
|
|
230
|
+
self.enable_telemetry = enable_telemetry
|
|
231
|
+
self.enable_rate_limiting = enable_rate_limiting
|
|
232
|
+
self.excluded_paths = set(excluded_paths or [
|
|
233
|
+
"/health", "/qt/stats", "/qt/rate-limit", "/docs",
|
|
234
|
+
"/openapi.json", "/favicon.ico",
|
|
235
|
+
])
|
|
236
|
+
|
|
237
|
+
self.stats = LiveStats()
|
|
238
|
+
self.rate_limiter = RateLimiter(rate_limit, rate_window) if enable_rate_limiting else None
|
|
239
|
+
self.telemetry = TelemetryBuffer(server_url, api_key, flush_interval) if enable_telemetry else None
|
|
240
|
+
|
|
241
|
+
# Add middleware
|
|
242
|
+
self._add_middleware()
|
|
243
|
+
|
|
244
|
+
# Add endpoints
|
|
245
|
+
if enable_health:
|
|
246
|
+
self._add_health_endpoint()
|
|
247
|
+
if enable_stats:
|
|
248
|
+
self._add_stats_endpoint()
|
|
249
|
+
if enable_rate_limiting:
|
|
250
|
+
self._add_rate_limit_endpoint()
|
|
251
|
+
|
|
252
|
+
# Start periodic flush
|
|
253
|
+
if self.telemetry:
|
|
254
|
+
@app.on_event("startup")
|
|
255
|
+
async def _start_telemetry():
|
|
256
|
+
asyncio.create_task(self.telemetry.start_periodic_flush())
|
|
257
|
+
|
|
258
|
+
@app.on_event("shutdown")
|
|
259
|
+
async def _stop_telemetry():
|
|
260
|
+
await self.telemetry.flush()
|
|
261
|
+
self.telemetry.stop()
|
|
262
|
+
|
|
263
|
+
def _caller_hash(self, request) -> str:
|
|
264
|
+
ip = request.client.host if request.client else "unknown"
|
|
265
|
+
return hashlib.md5(ip.encode()).hexdigest()[:12]
|
|
266
|
+
|
|
267
|
+
def _add_middleware(self):
|
|
268
|
+
sdk = self
|
|
269
|
+
|
|
270
|
+
@self.app.middleware("http")
|
|
271
|
+
async def qyratech_middleware(request, call_next):
|
|
272
|
+
path = request.url.path
|
|
273
|
+
caller = sdk._caller_hash(request)
|
|
274
|
+
|
|
275
|
+
# Skip excluded paths
|
|
276
|
+
if path in sdk.excluded_paths:
|
|
277
|
+
return await call_next(request)
|
|
278
|
+
|
|
279
|
+
# Rate limiting
|
|
280
|
+
if sdk.rate_limiter and not sdk.rate_limiter.is_allowed(caller):
|
|
281
|
+
from fastapi.responses import JSONResponse
|
|
282
|
+
return JSONResponse(
|
|
283
|
+
status_code=429,
|
|
284
|
+
content={
|
|
285
|
+
"error": "Rate limit exceeded",
|
|
286
|
+
"retry_after_seconds": sdk.rate_limiter.callers[caller][0] + sdk.rate_limiter.window - time.time()
|
|
287
|
+
if sdk.rate_limiter.callers.get(caller) else 60,
|
|
288
|
+
},
|
|
289
|
+
headers={"X-HyperSDK": __version__},
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Track request
|
|
293
|
+
request_id = str(uuid.uuid4())[:8]
|
|
294
|
+
start = time.time()
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
response = await call_next(request)
|
|
298
|
+
latency_ms = round((time.time() - start) * 1000, 2)
|
|
299
|
+
status_code = response.status_code
|
|
300
|
+
|
|
301
|
+
# Record stats
|
|
302
|
+
sdk.stats.record(latency_ms, status_code, path, caller)
|
|
303
|
+
|
|
304
|
+
# Buffer telemetry event
|
|
305
|
+
if sdk.telemetry:
|
|
306
|
+
sdk.telemetry.add({
|
|
307
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
308
|
+
"endpoint": path,
|
|
309
|
+
"method": request.method,
|
|
310
|
+
"status_code": status_code,
|
|
311
|
+
"latency_ms": latency_ms,
|
|
312
|
+
"caller_hash": caller,
|
|
313
|
+
"error_type": None,
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
# Add tracking headers
|
|
317
|
+
response.headers["X-Request-ID"] = request_id
|
|
318
|
+
response.headers["X-HyperSDK"] = __version__
|
|
319
|
+
response.headers["X-Response-Time-Ms"] = str(latency_ms)
|
|
320
|
+
|
|
321
|
+
return response
|
|
322
|
+
|
|
323
|
+
except Exception as e:
|
|
324
|
+
latency_ms = round((time.time() - start) * 1000, 2)
|
|
325
|
+
sdk.stats.record(latency_ms, 500, path, caller)
|
|
326
|
+
|
|
327
|
+
if sdk.telemetry:
|
|
328
|
+
sdk.telemetry.add({
|
|
329
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
330
|
+
"endpoint": path,
|
|
331
|
+
"method": request.method,
|
|
332
|
+
"status_code": 500,
|
|
333
|
+
"latency_ms": latency_ms,
|
|
334
|
+
"caller_hash": caller,
|
|
335
|
+
"error_type": type(e).__name__,
|
|
336
|
+
})
|
|
337
|
+
raise
|
|
338
|
+
|
|
339
|
+
def _add_health_endpoint(self):
|
|
340
|
+
sdk = self
|
|
341
|
+
|
|
342
|
+
@self.app.get("/health")
|
|
343
|
+
async def qt_health():
|
|
344
|
+
stats = sdk.stats.get_stats()
|
|
345
|
+
return {
|
|
346
|
+
"status": "ok",
|
|
347
|
+
"aim": sdk.aim_name,
|
|
348
|
+
"sdk_version": __version__,
|
|
349
|
+
"uptime_seconds": stats["uptime_seconds"],
|
|
350
|
+
"total_requests": stats["total_requests"],
|
|
351
|
+
"error_rate_pct": stats["error_rate_pct"],
|
|
352
|
+
"avg_latency_ms": stats["latency"]["avg_ms"],
|
|
353
|
+
"unique_callers": stats["unique_callers"],
|
|
354
|
+
"intelligence_server": sdk.server_url,
|
|
355
|
+
"telemetry_enabled": sdk.enable_telemetry,
|
|
356
|
+
"rate_limiting_enabled": sdk.enable_rate_limiting,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
def _add_stats_endpoint(self):
|
|
360
|
+
sdk = self
|
|
361
|
+
|
|
362
|
+
@self.app.get("/qt/stats")
|
|
363
|
+
async def qt_stats():
|
|
364
|
+
return {
|
|
365
|
+
"aim": sdk.aim_name,
|
|
366
|
+
"sdk_version": __version__,
|
|
367
|
+
"stats": sdk.stats.get_stats(),
|
|
368
|
+
"telemetry_buffer_size": len(sdk.telemetry.buffer) if sdk.telemetry else 0,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
def _add_rate_limit_endpoint(self):
|
|
372
|
+
sdk = self
|
|
373
|
+
from fastapi import Request as FastAPIRequest
|
|
374
|
+
|
|
375
|
+
@self.app.get("/qt/rate-limit")
|
|
376
|
+
async def qt_rate_limit(request: FastAPIRequest):
|
|
377
|
+
caller = sdk._caller_hash(request)
|
|
378
|
+
if sdk.rate_limiter:
|
|
379
|
+
return sdk.rate_limiter.get_usage(caller)
|
|
380
|
+
return {"rate_limiting": "disabled"}
|