copilotkit-intelligence-runtime 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.
@@ -0,0 +1,263 @@
1
+ """Canonical CopilotKit analytics with bounded asynchronous delivery."""
2
+
3
+ import asyncio
4
+ import base64
5
+ import inspect
6
+ import json
7
+ import math
8
+ import os
9
+ import random
10
+ import re
11
+ import time
12
+ from collections.abc import Awaitable, Callable
13
+ from dataclasses import dataclass
14
+ from typing import Any, NoReturn
15
+ from urllib.parse import urlsplit
16
+
17
+ import httpx
18
+
19
+ from .models import Json
20
+
21
+ EventSink = Callable[[Json], Awaitable[None]]
22
+ _PREFIX = "oss.runtime."
23
+ _IDENTITY = re.compile(r"^[A-Za-z0-9_-]{1,128}$")
24
+ # ECMAScript WhiteSpace + LineTerminator; Python str.strip() differs for FEFF/NEL.
25
+ _JS_WHITESPACE = "\u0009\u000a\u000b\u000c\u000d\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
26
+
27
+
28
+ def _invalid_json_constant(value: str) -> NoReturn:
29
+ """Reject non-JSON numeric constants that Python's decoder otherwise accepts."""
30
+ raise ValueError("Invalid JSON constant")
31
+
32
+
33
+ def _license_identity(token: str | None) -> str | None:
34
+ """Read a safe legacy analytics claim, never verify a license or retain its token."""
35
+ if not isinstance(token, str):
36
+ return None
37
+ parts = token.split(".")
38
+ if len(parts) != 3:
39
+ return None
40
+ payload = parts[1]
41
+ if not re.fullmatch(r"[A-Za-z0-9_-]+", payload) or len(payload) % 4 == 1:
42
+ return None
43
+ try:
44
+ decoded = json.loads(
45
+ base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)).decode(
46
+ "utf-8", errors="replace"
47
+ ),
48
+ parse_constant=_invalid_json_constant,
49
+ )
50
+ value = decoded.get("telemetry_id") if isinstance(decoded, dict) else None
51
+ if isinstance(value, str) and _IDENTITY.fullmatch(value.strip(" \t")):
52
+ return value.strip(" \t")
53
+ except (ValueError, TypeError, RecursionError):
54
+ pass
55
+ return None
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class TelemetryStats:
60
+ """Local exporter diagnostics; these counters do not create analytics traffic."""
61
+
62
+ queued: int
63
+ sent: int
64
+ failed: int
65
+ dropped: int
66
+ sampled_out: int
67
+
68
+
69
+ class Telemetry:
70
+ """Enqueue canonical events without delaying request handling.
71
+
72
+ Unsampled by default: the sink is ours, so a real count beats one
73
+ extrapolated from a fraction of the population. ``sample_rate`` and
74
+ ``COPILOTKIT_TELEMETRY_SAMPLE_RATE`` still dial it down.
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ enabled: bool = True,
80
+ sink: EventSink | None = None,
81
+ *,
82
+ sample_rate: float = 1.0,
83
+ telemetry_id: str | None = None,
84
+ license_token: str | None = None,
85
+ url: str = "https://telemetry.copilotkit.ai/ingest",
86
+ queue_capacity: int = 256,
87
+ timeout: float = 3,
88
+ http_client: httpx.AsyncClient | None = None,
89
+ ) -> None:
90
+ self.enabled = enabled and not any(
91
+ os.getenv(key, "").lower() in ("true", "1")
92
+ for key in ("DO_NOT_TRACK", "COPILOTKIT_TELEMETRY_DISABLED")
93
+ )
94
+ if sink is not None and not (
95
+ inspect.iscoroutinefunction(sink)
96
+ or inspect.iscoroutinefunction(getattr(sink, "__call__", None))
97
+ ):
98
+ raise ValueError("Telemetry sink must be async")
99
+ override = os.getenv("COPILOTKIT_TELEMETRY_SAMPLE_RATE")
100
+ self.sample_rate = float(override) if override else sample_rate
101
+ if not math.isfinite(self.sample_rate) or not 0 <= self.sample_rate <= 1:
102
+ raise ValueError("Sample rate must be finite and between 0 and 1")
103
+ if queue_capacity < 1 or not math.isfinite(timeout) or not 0 < timeout <= 3:
104
+ raise ValueError("Telemetry queue must be positive and timeout within (0, 3] seconds")
105
+ self.telemetry_id = next(
106
+ (
107
+ candidate.strip(" \t")
108
+ for candidate in (telemetry_id, os.getenv("CPK_TELEMETRY_ID"))
109
+ if isinstance(candidate, str) and _IDENTITY.fullmatch(candidate.strip(" \t"))
110
+ ),
111
+ None,
112
+ )
113
+ self.identified = False
114
+ if self.telemetry_id is None:
115
+ self.telemetry_id = _license_identity(
116
+ next(
117
+ (
118
+ candidate
119
+ for candidate in (license_token, os.getenv("COPILOTKIT_LICENSE_TOKEN"))
120
+ if isinstance(candidate, str) and candidate.strip(_JS_WHITESPACE)
121
+ ),
122
+ None,
123
+ )
124
+ )
125
+ self.identified = self.telemetry_id is not None
126
+ if self.identified:
127
+ self.sample_rate = 1
128
+ self.url = os.getenv("COPILOTKIT_TELEMETRY_URL") or url
129
+ parsed = urlsplit(self.url)
130
+ if parsed.scheme not in ("http", "https") or not parsed.hostname or parsed.username:
131
+ raise ValueError("Invalid telemetry endpoint")
132
+ self.sink = sink
133
+ self.timeout = timeout
134
+ self._queue: asyncio.Queue[Json] = asyncio.Queue(queue_capacity)
135
+ self._worker: asyncio.Task[None] | None = None
136
+ self._client = http_client
137
+ self._owns_client = http_client is None
138
+ self._closed = False
139
+ self._sent = self._failed = self._dropped = self._sampled_out = 0
140
+
141
+ @property
142
+ def stats(self) -> TelemetryStats:
143
+ """Return a snapshot of queue pressure and transport outcomes."""
144
+ return TelemetryStats(
145
+ self._queue.qsize(), self._sent, self._failed, self._dropped, self._sampled_out
146
+ )
147
+
148
+ async def emit(self, name: str, **attributes: Any) -> None:
149
+ """Accept only canonical events and construct a fixed, content-free payload."""
150
+ if not self.enabled or self._closed:
151
+ return
152
+ event = name.removeprefix(_PREFIX)
153
+ properties: Json
154
+ if event == "instance_created":
155
+ count = attributes.get("agentsAmount", 0)
156
+ properties = {
157
+ "actionsAmount": 0,
158
+ "endpointTypes": [],
159
+ "endpointsAmount": 0,
160
+ "agentsAmount": count if type(count) is int and count >= 0 else 0,
161
+ "cloud.api_key_provided": False,
162
+ }
163
+ elif event == "copilot_request_created" and attributes.get("requestType") in (
164
+ "run",
165
+ "connect",
166
+ ):
167
+ properties = {
168
+ "requestType": attributes["requestType"],
169
+ "cloud.guardrails.enabled": False,
170
+ "cloud.api_key_provided": False,
171
+ }
172
+ elif event in ("agent_execution_stream_started", "agent_execution_stream_ended"):
173
+ properties = {}
174
+ elif event == "agent_execution_stream_errored":
175
+ properties = {
176
+ "error": attributes.get("error")
177
+ if attributes.get("error")
178
+ in ("AGENT_EXECUTION_FAILED", "RUN_STOPPED", "GATEWAY_START_FAILED")
179
+ else "AGENT_EXECUTION_FAILED"
180
+ }
181
+ else:
182
+ return
183
+ if self.sample_rate == 0 or random.random() >= self.sample_rate:
184
+ self._sampled_out += 1
185
+ return
186
+ envelope = {
187
+ "event": _PREFIX + event,
188
+ "properties": properties,
189
+ "ts": int(time.time()),
190
+ "package": {"name": "copilotkit-intelligence-runtime", "version": "0.1.0"},
191
+ "global_properties": {
192
+ "sampleRate": self.sample_rate,
193
+ "sampleRateAdjustmentFactor": 1 - self.sample_rate,
194
+ "sampleWeight": 1 / self.sample_rate,
195
+ "telemetry_identified": self.identified,
196
+ "telemetry_emitter": "runtime-python",
197
+ "telemetry_surface": "v2",
198
+ "telemetry_transport": "lambda",
199
+ },
200
+ }
201
+ try:
202
+ self._queue.put_nowait(envelope)
203
+ except asyncio.QueueFull:
204
+ self._dropped += 1
205
+ return
206
+ if self._worker is None or self._worker.done():
207
+ self._worker = asyncio.create_task(self._drain(), name="copilotkit-telemetry")
208
+
209
+ async def _drain(self) -> None:
210
+ """Send one item at a time with bounded timeout and no automatic redirects."""
211
+ while True:
212
+ envelope = await self._queue.get()
213
+ try:
214
+ async with asyncio.timeout(self.timeout):
215
+ if self.sink:
216
+ await self.sink(envelope)
217
+ else:
218
+ if self._client is None:
219
+ self._client = httpx.AsyncClient(follow_redirects=False)
220
+ headers = {
221
+ "Content-Type": "application/json",
222
+ "User-Agent": "CopilotKit-Runtime/0.1.0 (copilotkit-intelligence-runtime)",
223
+ }
224
+ if self.telemetry_id:
225
+ headers["X-CopilotKit-Telemetry-Id"] = self.telemetry_id
226
+ response = await self._client.post(
227
+ self.url,
228
+ json=envelope,
229
+ headers=headers,
230
+ timeout=self.timeout,
231
+ follow_redirects=False,
232
+ )
233
+ response.raise_for_status()
234
+ self._sent += 1
235
+ except Exception:
236
+ self._failed += 1
237
+ finally:
238
+ self._queue.task_done()
239
+
240
+ async def flush(self, timeout: float = 3) -> bool:
241
+ """Wait at most timeout seconds for the existing queue; return whether it drained."""
242
+ try:
243
+ async with asyncio.timeout(timeout):
244
+ await self._queue.join()
245
+ return True
246
+ except TimeoutError:
247
+ return False
248
+
249
+ async def aclose(self, timeout: float = 3) -> None:
250
+ """Stop accepting events and discard queued work after the shutdown deadline."""
251
+ if self._closed:
252
+ return
253
+ self._closed = True
254
+ await self.flush(timeout)
255
+ if self._worker:
256
+ self._worker.cancel()
257
+ await asyncio.gather(self._worker, return_exceptions=True)
258
+ while not self._queue.empty():
259
+ self._queue.get_nowait()
260
+ self._queue.task_done()
261
+ self._dropped += 1
262
+ if self._owns_client and self._client:
263
+ await self._client.aclose()