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.
- copilotkit_intelligence/__init__.py +62 -0
- copilotkit_intelligence/client.py +805 -0
- copilotkit_intelligence/entitlements.py +142 -0
- copilotkit_intelligence/inspector.py +182 -0
- copilotkit_intelligence/learned_skills.py +98 -0
- copilotkit_intelligence/py.typed +0 -0
- copilotkit_intelligence/resources.py +134 -0
- copilotkit_intelligence_runtime-0.1.0.dist-info/METADATA +403 -0
- copilotkit_intelligence_runtime-0.1.0.dist-info/RECORD +22 -0
- copilotkit_intelligence_runtime-0.1.0.dist-info/WHEEL +4 -0
- copilotkit_intelligence_runtime-0.1.0.dist-info/licenses/LICENSE +21 -0
- copilotkit_runtime/__init__.py +27 -0
- copilotkit_runtime/a2ui.py +559 -0
- copilotkit_runtime/agents.py +64 -0
- copilotkit_runtime/finalizer.py +75 -0
- copilotkit_runtime/gateway.py +287 -0
- copilotkit_runtime/mcp_apps.py +299 -0
- copilotkit_runtime/models.py +77 -0
- copilotkit_runtime/platform.py +67 -0
- copilotkit_runtime/py.typed +0 -0
- copilotkit_runtime/runtime.py +878 -0
- copilotkit_runtime/telemetry.py +263 -0
|
@@ -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()
|