lucidicai 2.1.2__py3-none-any.whl → 3.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.
- lucidicai/__init__.py +32 -390
- lucidicai/api/client.py +260 -92
- lucidicai/api/resources/__init__.py +16 -1
- lucidicai/api/resources/dataset.py +422 -82
- lucidicai/api/resources/event.py +399 -27
- lucidicai/api/resources/experiment.py +108 -0
- lucidicai/api/resources/feature_flag.py +78 -0
- lucidicai/api/resources/prompt.py +84 -0
- lucidicai/api/resources/session.py +545 -38
- lucidicai/client.py +395 -480
- lucidicai/core/config.py +73 -48
- lucidicai/core/errors.py +3 -3
- lucidicai/sdk/bound_decorators.py +321 -0
- lucidicai/sdk/context.py +20 -2
- lucidicai/sdk/decorators.py +283 -74
- lucidicai/sdk/event.py +538 -36
- lucidicai/sdk/event_builder.py +2 -4
- lucidicai/sdk/features/dataset.py +408 -232
- lucidicai/sdk/features/feature_flag.py +344 -3
- lucidicai/sdk/init.py +50 -279
- lucidicai/sdk/session.py +502 -0
- lucidicai/sdk/shutdown_manager.py +103 -46
- lucidicai/session_obj.py +321 -0
- lucidicai/telemetry/context_capture_processor.py +13 -6
- lucidicai/telemetry/extract.py +60 -63
- lucidicai/telemetry/litellm_bridge.py +3 -44
- lucidicai/telemetry/lucidic_exporter.py +143 -131
- lucidicai/telemetry/openai_agents_instrumentor.py +2 -2
- lucidicai/telemetry/openai_patch.py +7 -6
- lucidicai/telemetry/telemetry_manager.py +183 -0
- lucidicai/telemetry/utils/model_pricing.py +21 -30
- lucidicai/telemetry/utils/provider.py +77 -0
- lucidicai/utils/images.py +30 -14
- lucidicai/utils/queue.py +2 -2
- lucidicai/utils/serialization.py +27 -0
- {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/METADATA +1 -1
- {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/RECORD +39 -30
- {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/WHEEL +0 -0
- {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/top_level.txt +0 -0
lucidicai/api/resources/event.py
CHANGED
|
@@ -1,24 +1,297 @@
|
|
|
1
1
|
"""Event resource API operations."""
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timezone
|
|
2
6
|
from typing import Any, Dict, Optional
|
|
3
|
-
from datetime import datetime
|
|
4
7
|
|
|
5
8
|
from ..client import HttpClient
|
|
6
9
|
|
|
10
|
+
logger = logging.getLogger("Lucidic")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _truncate_id(id_str: Optional[str]) -> str:
|
|
14
|
+
"""Truncate ID for logging."""
|
|
15
|
+
if not id_str:
|
|
16
|
+
return "None"
|
|
17
|
+
return f"{id_str[:8]}..." if len(id_str) > 8 else id_str
|
|
18
|
+
|
|
7
19
|
|
|
8
20
|
class EventResource:
|
|
9
21
|
"""Handle event-related API operations."""
|
|
10
|
-
|
|
11
|
-
def __init__(self, http: HttpClient):
|
|
22
|
+
|
|
23
|
+
def __init__(self, http: HttpClient, production: bool = False):
|
|
12
24
|
"""Initialize event resource.
|
|
13
|
-
|
|
25
|
+
|
|
14
26
|
Args:
|
|
15
27
|
http: HTTP client instance
|
|
28
|
+
production: Whether to suppress errors in production mode
|
|
16
29
|
"""
|
|
17
30
|
self.http = http
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
self._production = production
|
|
32
|
+
|
|
33
|
+
# ==================== High-Level Event Methods ====================
|
|
34
|
+
|
|
35
|
+
def create(
|
|
36
|
+
self,
|
|
37
|
+
type: str = "generic",
|
|
38
|
+
event_id: Optional[str] = None,
|
|
39
|
+
session_id: Optional[str] = None,
|
|
40
|
+
**kwargs,
|
|
41
|
+
) -> str:
|
|
20
42
|
"""Create a new event.
|
|
21
|
-
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
type: Event type (e.g., "llm_generation", "function_call", "error_traceback", "generic")
|
|
46
|
+
event_id: Optional client event ID (auto-generated if not provided)
|
|
47
|
+
session_id: Optional session ID (uses current context if not provided)
|
|
48
|
+
**kwargs: Event-specific fields
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Event ID (client-generated or provided UUID)
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
event_id = client.events.create(
|
|
55
|
+
type="custom_event",
|
|
56
|
+
data={"key": "value"}
|
|
57
|
+
)
|
|
58
|
+
"""
|
|
59
|
+
from ...sdk.context import current_session_id, current_parent_event_id
|
|
60
|
+
from ...sdk.event_builder import EventBuilder
|
|
61
|
+
|
|
62
|
+
# Generate event ID if not provided
|
|
63
|
+
client_event_id = event_id or str(uuid.uuid4())
|
|
64
|
+
|
|
65
|
+
# Get session from context if not provided
|
|
66
|
+
if not session_id:
|
|
67
|
+
session_id = current_session_id.get(None)
|
|
68
|
+
|
|
69
|
+
if not session_id:
|
|
70
|
+
logger.debug("[EventResource] No active session for create()")
|
|
71
|
+
return client_event_id
|
|
72
|
+
|
|
73
|
+
# Get parent event ID from context
|
|
74
|
+
parent_event_id = None
|
|
75
|
+
try:
|
|
76
|
+
parent_event_id = current_parent_event_id.get(None)
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
# Build event request
|
|
81
|
+
params = {
|
|
82
|
+
"type": type,
|
|
83
|
+
"event_id": client_event_id,
|
|
84
|
+
"parent_event_id": parent_event_id,
|
|
85
|
+
"session_id": session_id,
|
|
86
|
+
"occurred_at": kwargs.pop("occurred_at", None)
|
|
87
|
+
or datetime.now(timezone.utc).isoformat(),
|
|
88
|
+
**kwargs,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
event_request = EventBuilder.build(params)
|
|
93
|
+
self.create_event(event_request)
|
|
94
|
+
logger.debug(f"[EventResource] Created event {client_event_id[:8]}...")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
if self._production:
|
|
97
|
+
logger.error(f"[EventResource] Failed to create event: {e}")
|
|
98
|
+
else:
|
|
99
|
+
raise
|
|
100
|
+
|
|
101
|
+
return client_event_id
|
|
102
|
+
|
|
103
|
+
async def acreate(
|
|
104
|
+
self,
|
|
105
|
+
type: str = "generic",
|
|
106
|
+
event_id: Optional[str] = None,
|
|
107
|
+
session_id: Optional[str] = None,
|
|
108
|
+
**kwargs,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Create a new event (async version).
|
|
111
|
+
|
|
112
|
+
See create() for full documentation.
|
|
113
|
+
"""
|
|
114
|
+
from ...sdk.context import current_session_id, current_parent_event_id
|
|
115
|
+
from ...sdk.event_builder import EventBuilder
|
|
116
|
+
|
|
117
|
+
client_event_id = event_id or str(uuid.uuid4())
|
|
118
|
+
|
|
119
|
+
if not session_id:
|
|
120
|
+
session_id = current_session_id.get(None)
|
|
121
|
+
|
|
122
|
+
if not session_id:
|
|
123
|
+
logger.debug("[EventResource] No active session for acreate()")
|
|
124
|
+
return client_event_id
|
|
125
|
+
|
|
126
|
+
parent_event_id = None
|
|
127
|
+
try:
|
|
128
|
+
parent_event_id = current_parent_event_id.get(None)
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
params = {
|
|
133
|
+
"type": type,
|
|
134
|
+
"event_id": client_event_id,
|
|
135
|
+
"parent_event_id": parent_event_id,
|
|
136
|
+
"session_id": session_id,
|
|
137
|
+
"occurred_at": kwargs.pop("occurred_at", None)
|
|
138
|
+
or datetime.now(timezone.utc).isoformat(),
|
|
139
|
+
**kwargs,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
event_request = EventBuilder.build(params)
|
|
144
|
+
await self.acreate_event(event_request)
|
|
145
|
+
logger.debug(f"[EventResource] Created async event {client_event_id[:8]}...")
|
|
146
|
+
except Exception as e:
|
|
147
|
+
if self._production:
|
|
148
|
+
logger.error(f"[EventResource] Failed to create async event: {e}")
|
|
149
|
+
else:
|
|
150
|
+
raise
|
|
151
|
+
|
|
152
|
+
return client_event_id
|
|
153
|
+
|
|
154
|
+
def emit(
|
|
155
|
+
self,
|
|
156
|
+
type: str = "generic",
|
|
157
|
+
event_id: Optional[str] = None,
|
|
158
|
+
session_id: Optional[str] = None,
|
|
159
|
+
**kwargs,
|
|
160
|
+
) -> str:
|
|
161
|
+
"""Fire-and-forget event creation that returns instantly.
|
|
162
|
+
|
|
163
|
+
This function returns immediately with an event ID, while the actual
|
|
164
|
+
event creation happens in a background thread. Perfect for hot path
|
|
165
|
+
telemetry where latency is critical.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
type: Event type (e.g., "llm_generation", "function_call", "generic")
|
|
169
|
+
event_id: Optional client event ID (auto-generated if not provided)
|
|
170
|
+
session_id: Optional session ID (uses current context if not provided)
|
|
171
|
+
**kwargs: Event-specific fields
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Event ID (client-generated or provided UUID) - returned immediately
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
client.events.emit(type="log", message="Something happened")
|
|
178
|
+
"""
|
|
179
|
+
from ...sdk.context import current_session_id, current_parent_event_id
|
|
180
|
+
|
|
181
|
+
# Pre-generate event ID for instant return
|
|
182
|
+
client_event_id = event_id or str(uuid.uuid4())
|
|
183
|
+
|
|
184
|
+
# Capture context variables BEFORE creating the thread
|
|
185
|
+
captured_parent_id = kwargs.get("parent_event_id")
|
|
186
|
+
if captured_parent_id is None:
|
|
187
|
+
try:
|
|
188
|
+
captured_parent_id = current_parent_event_id.get(None)
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
# Get session from context if not provided
|
|
193
|
+
captured_session_id = session_id
|
|
194
|
+
if not captured_session_id:
|
|
195
|
+
captured_session_id = current_session_id.get(None)
|
|
196
|
+
|
|
197
|
+
if not captured_session_id:
|
|
198
|
+
logger.debug("[EventResource] No active session for emit()")
|
|
199
|
+
return client_event_id
|
|
200
|
+
|
|
201
|
+
# Capture all data for background thread
|
|
202
|
+
captured_kwargs = dict(kwargs)
|
|
203
|
+
captured_kwargs["parent_event_id"] = captured_parent_id
|
|
204
|
+
|
|
205
|
+
def _background_create():
|
|
206
|
+
try:
|
|
207
|
+
self.create(
|
|
208
|
+
type=type,
|
|
209
|
+
event_id=client_event_id,
|
|
210
|
+
session_id=captured_session_id,
|
|
211
|
+
**captured_kwargs,
|
|
212
|
+
)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.debug(f"[EventResource] Background emit() failed: {e}")
|
|
215
|
+
|
|
216
|
+
# Start background thread
|
|
217
|
+
thread = threading.Thread(target=_background_create, daemon=True)
|
|
218
|
+
thread.start()
|
|
219
|
+
|
|
220
|
+
return client_event_id
|
|
221
|
+
|
|
222
|
+
def create_error(
|
|
223
|
+
self,
|
|
224
|
+
error: Any,
|
|
225
|
+
parent_event_id: Optional[str] = None,
|
|
226
|
+
**kwargs,
|
|
227
|
+
) -> str:
|
|
228
|
+
"""Create an error traceback event.
|
|
229
|
+
|
|
230
|
+
Convenience method for creating error events with proper traceback information.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
error: The error message or exception object
|
|
234
|
+
parent_event_id: Optional parent event ID for nesting
|
|
235
|
+
**kwargs: Additional event parameters
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Event ID of the created error event
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
try:
|
|
242
|
+
risky_operation()
|
|
243
|
+
except Exception as e:
|
|
244
|
+
client.events.create_error(e)
|
|
245
|
+
"""
|
|
246
|
+
import traceback as tb
|
|
247
|
+
|
|
248
|
+
if isinstance(error, Exception):
|
|
249
|
+
error_str = str(error)
|
|
250
|
+
traceback_str = tb.format_exc()
|
|
251
|
+
else:
|
|
252
|
+
error_str = str(error)
|
|
253
|
+
traceback_str = kwargs.pop("traceback", "")
|
|
254
|
+
|
|
255
|
+
return self.create(
|
|
256
|
+
type="error_traceback",
|
|
257
|
+
error=error_str,
|
|
258
|
+
traceback=traceback_str,
|
|
259
|
+
parent_event_id=parent_event_id,
|
|
260
|
+
**kwargs,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
async def acreate_error(
|
|
264
|
+
self,
|
|
265
|
+
error: Any,
|
|
266
|
+
parent_event_id: Optional[str] = None,
|
|
267
|
+
**kwargs,
|
|
268
|
+
) -> str:
|
|
269
|
+
"""Create an error traceback event (async version).
|
|
270
|
+
|
|
271
|
+
See create_error() for full documentation.
|
|
272
|
+
"""
|
|
273
|
+
import traceback as tb
|
|
274
|
+
|
|
275
|
+
if isinstance(error, Exception):
|
|
276
|
+
error_str = str(error)
|
|
277
|
+
traceback_str = tb.format_exc()
|
|
278
|
+
else:
|
|
279
|
+
error_str = str(error)
|
|
280
|
+
traceback_str = kwargs.pop("traceback", "")
|
|
281
|
+
|
|
282
|
+
return await self.acreate(
|
|
283
|
+
type="error_traceback",
|
|
284
|
+
error=error_str,
|
|
285
|
+
traceback=traceback_str,
|
|
286
|
+
parent_event_id=parent_event_id,
|
|
287
|
+
**kwargs,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# ==================== Low-Level HTTP Methods ====================
|
|
291
|
+
|
|
292
|
+
def create_event(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
293
|
+
"""Create a new event via API.
|
|
294
|
+
|
|
22
295
|
Args:
|
|
23
296
|
params: Event parameters including:
|
|
24
297
|
- client_event_id: Client-generated event ID
|
|
@@ -27,36 +300,81 @@ class EventResource:
|
|
|
27
300
|
- occurred_at: When the event occurred
|
|
28
301
|
- payload: Event payload
|
|
29
302
|
- etc.
|
|
30
|
-
|
|
303
|
+
|
|
31
304
|
Returns:
|
|
32
305
|
API response with optional blob_url for large payloads
|
|
33
306
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
307
|
+
event_id = params.get("client_event_id")
|
|
308
|
+
session_id = params.get("session_id")
|
|
309
|
+
event_type = params.get("type")
|
|
310
|
+
parent_id = params.get("client_parent_event_id")
|
|
311
|
+
logger.debug(
|
|
312
|
+
f"[Event] create_event() called - "
|
|
313
|
+
f"event_id={_truncate_id(event_id)}, session_id={_truncate_id(session_id)}, "
|
|
314
|
+
f"type={event_type!r}, parent_id={_truncate_id(parent_id)}"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
response = self.http.post("events", params)
|
|
318
|
+
|
|
319
|
+
resp_event_id = response.get("event_id") if response else None
|
|
320
|
+
logger.debug(
|
|
321
|
+
f"[Event] create_event() response - "
|
|
322
|
+
f"event_id={_truncate_id(resp_event_id)}, response_keys={list(response.keys()) if response else 'None'}"
|
|
323
|
+
)
|
|
324
|
+
return response
|
|
325
|
+
|
|
326
|
+
async def acreate_event(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
327
|
+
"""Create a new event via API (asynchronous).
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
params: Event parameters
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
API response with optional blob_url for large payloads
|
|
334
|
+
"""
|
|
335
|
+
event_id = params.get("client_event_id")
|
|
336
|
+
session_id = params.get("session_id")
|
|
337
|
+
event_type = params.get("type")
|
|
338
|
+
parent_id = params.get("client_parent_event_id")
|
|
339
|
+
logger.debug(
|
|
340
|
+
f"[Event] acreate_event() called - "
|
|
341
|
+
f"event_id={_truncate_id(event_id)}, session_id={_truncate_id(session_id)}, "
|
|
342
|
+
f"type={event_type!r}, parent_id={_truncate_id(parent_id)}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
response = await self.http.apost("events", params)
|
|
346
|
+
|
|
347
|
+
resp_event_id = response.get("event_id") if response else None
|
|
348
|
+
logger.debug(
|
|
349
|
+
f"[Event] acreate_event() response - "
|
|
350
|
+
f"event_id={_truncate_id(resp_event_id)}, response_keys={list(response.keys()) if response else 'None'}"
|
|
351
|
+
)
|
|
352
|
+
return response
|
|
353
|
+
|
|
354
|
+
def get(self, event_id: str) -> Dict[str, Any]:
|
|
37
355
|
"""Get an event by ID.
|
|
38
|
-
|
|
356
|
+
|
|
39
357
|
Args:
|
|
40
358
|
event_id: Event ID
|
|
41
|
-
|
|
359
|
+
|
|
42
360
|
Returns:
|
|
43
361
|
Event data
|
|
44
362
|
"""
|
|
45
363
|
return self.http.get(f"events/{event_id}")
|
|
46
|
-
|
|
47
|
-
def
|
|
364
|
+
|
|
365
|
+
def update(self, event_id: str, **updates) -> Dict[str, Any]:
|
|
48
366
|
"""Update an existing event.
|
|
49
|
-
|
|
367
|
+
|
|
50
368
|
Args:
|
|
51
369
|
event_id: Event ID
|
|
52
|
-
updates: Fields to update
|
|
53
|
-
|
|
370
|
+
**updates: Fields to update
|
|
371
|
+
|
|
54
372
|
Returns:
|
|
55
373
|
Updated event data
|
|
56
374
|
"""
|
|
57
375
|
return self.http.put(f"events/{event_id}", updates)
|
|
58
|
-
|
|
59
|
-
def
|
|
376
|
+
|
|
377
|
+
def list(
|
|
60
378
|
self,
|
|
61
379
|
session_id: Optional[str] = None,
|
|
62
380
|
event_type: Optional[str] = None,
|
|
@@ -64,25 +382,79 @@ class EventResource:
|
|
|
64
382
|
offset: int = 0
|
|
65
383
|
) -> Dict[str, Any]:
|
|
66
384
|
"""List events with optional filters.
|
|
67
|
-
|
|
385
|
+
|
|
68
386
|
Args:
|
|
69
387
|
session_id: Filter by session ID
|
|
70
388
|
event_type: Filter by event type
|
|
71
389
|
limit: Maximum number of events to return
|
|
72
390
|
offset: Pagination offset
|
|
73
|
-
|
|
391
|
+
|
|
74
392
|
Returns:
|
|
75
393
|
List of events and pagination info
|
|
76
394
|
"""
|
|
77
|
-
params = {
|
|
395
|
+
params: Dict[str, Any] = {
|
|
78
396
|
"limit": limit,
|
|
79
397
|
"offset": offset
|
|
80
398
|
}
|
|
81
|
-
|
|
399
|
+
|
|
82
400
|
if session_id:
|
|
83
401
|
params["session_id"] = session_id
|
|
84
|
-
|
|
402
|
+
|
|
85
403
|
if event_type:
|
|
86
404
|
params["type"] = event_type
|
|
87
|
-
|
|
88
|
-
return self.http.get("events", params)
|
|
405
|
+
|
|
406
|
+
return self.http.get("events", params)
|
|
407
|
+
|
|
408
|
+
async def aget(self, event_id: str) -> Dict[str, Any]:
|
|
409
|
+
"""Get an event by ID (asynchronous).
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
event_id: Event ID
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Event data
|
|
416
|
+
"""
|
|
417
|
+
return await self.http.aget(f"events/{event_id}")
|
|
418
|
+
|
|
419
|
+
async def aupdate(self, event_id: str, **updates) -> Dict[str, Any]:
|
|
420
|
+
"""Update an existing event (asynchronous).
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
event_id: Event ID
|
|
424
|
+
**updates: Fields to update
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Updated event data
|
|
428
|
+
"""
|
|
429
|
+
return await self.http.aput(f"events/{event_id}", updates)
|
|
430
|
+
|
|
431
|
+
async def alist(
|
|
432
|
+
self,
|
|
433
|
+
session_id: Optional[str] = None,
|
|
434
|
+
event_type: Optional[str] = None,
|
|
435
|
+
limit: int = 100,
|
|
436
|
+
offset: int = 0
|
|
437
|
+
) -> Dict[str, Any]:
|
|
438
|
+
"""List events with optional filters (asynchronous).
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
session_id: Filter by session ID
|
|
442
|
+
event_type: Filter by event type
|
|
443
|
+
limit: Maximum number of events to return
|
|
444
|
+
offset: Pagination offset
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
List of events and pagination info
|
|
448
|
+
"""
|
|
449
|
+
params: Dict[str, Any] = {
|
|
450
|
+
"limit": limit,
|
|
451
|
+
"offset": offset
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if session_id:
|
|
455
|
+
params["session_id"] = session_id
|
|
456
|
+
|
|
457
|
+
if event_type:
|
|
458
|
+
params["type"] = event_type
|
|
459
|
+
|
|
460
|
+
return await self.http.aget("events", params)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Experiment resource API operations."""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from ..client import HttpClient
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("Lucidic")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExperimentResource:
|
|
11
|
+
"""Handle experiment-related API operations."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
http: HttpClient,
|
|
16
|
+
agent_id: Optional[str] = None,
|
|
17
|
+
production: bool = False,
|
|
18
|
+
):
|
|
19
|
+
"""Initialize experiment resource.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
http: HTTP client instance
|
|
23
|
+
agent_id: Default agent ID for experiments
|
|
24
|
+
production: Whether to suppress errors in production mode
|
|
25
|
+
"""
|
|
26
|
+
self.http = http
|
|
27
|
+
self._agent_id = agent_id
|
|
28
|
+
self._production = production
|
|
29
|
+
|
|
30
|
+
def create(
|
|
31
|
+
self,
|
|
32
|
+
experiment_name: str,
|
|
33
|
+
description: Optional[str] = None,
|
|
34
|
+
tags: Optional[List[str]] = None,
|
|
35
|
+
LLM_boolean_evaluators: Optional[List[str]] = None,
|
|
36
|
+
LLM_numeric_evaluators: Optional[List[str]] = None,
|
|
37
|
+
) -> Optional[str]:
|
|
38
|
+
"""Create a new experiment.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
experiment_name: Name of the experiment.
|
|
42
|
+
description: Optional description.
|
|
43
|
+
tags: Optional tags for filtering.
|
|
44
|
+
LLM_boolean_evaluators: Boolean evaluator names.
|
|
45
|
+
LLM_numeric_evaluators: Numeric evaluator names.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The experiment ID if created successfully, None otherwise.
|
|
49
|
+
"""
|
|
50
|
+
evaluator_names = []
|
|
51
|
+
if LLM_boolean_evaluators:
|
|
52
|
+
evaluator_names.extend(LLM_boolean_evaluators)
|
|
53
|
+
if LLM_numeric_evaluators:
|
|
54
|
+
evaluator_names.extend(LLM_numeric_evaluators)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
response = self.http.post(
|
|
58
|
+
"createexperiment",
|
|
59
|
+
{
|
|
60
|
+
"agent_id": self._agent_id,
|
|
61
|
+
"experiment_name": experiment_name,
|
|
62
|
+
"description": description or "",
|
|
63
|
+
"tags": tags or [],
|
|
64
|
+
"evaluator_names": evaluator_names,
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
return response.get("experiment_id")
|
|
68
|
+
except Exception as e:
|
|
69
|
+
if self._production:
|
|
70
|
+
logger.error(f"[ExperimentResource] Failed to create experiment: {e}")
|
|
71
|
+
return None
|
|
72
|
+
raise
|
|
73
|
+
|
|
74
|
+
async def acreate(
|
|
75
|
+
self,
|
|
76
|
+
experiment_name: str,
|
|
77
|
+
description: Optional[str] = None,
|
|
78
|
+
tags: Optional[List[str]] = None,
|
|
79
|
+
LLM_boolean_evaluators: Optional[List[str]] = None,
|
|
80
|
+
LLM_numeric_evaluators: Optional[List[str]] = None,
|
|
81
|
+
) -> Optional[str]:
|
|
82
|
+
"""Create a new experiment (asynchronous).
|
|
83
|
+
|
|
84
|
+
See create() for full documentation.
|
|
85
|
+
"""
|
|
86
|
+
evaluator_names = []
|
|
87
|
+
if LLM_boolean_evaluators:
|
|
88
|
+
evaluator_names.extend(LLM_boolean_evaluators)
|
|
89
|
+
if LLM_numeric_evaluators:
|
|
90
|
+
evaluator_names.extend(LLM_numeric_evaluators)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
response = await self.http.apost(
|
|
94
|
+
"createexperiment",
|
|
95
|
+
{
|
|
96
|
+
"agent_id": self._agent_id,
|
|
97
|
+
"experiment_name": experiment_name,
|
|
98
|
+
"description": description or "",
|
|
99
|
+
"tags": tags or [],
|
|
100
|
+
"evaluator_names": evaluator_names,
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
return response.get("experiment_id")
|
|
104
|
+
except Exception as e:
|
|
105
|
+
if self._production:
|
|
106
|
+
logger.error(f"[ExperimentResource] Failed to create experiment: {e}")
|
|
107
|
+
return None
|
|
108
|
+
raise
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Feature flag resource API operations."""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from ..client import HttpClient
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("Lucidic")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FeatureFlagResource:
|
|
11
|
+
"""Handle feature flag-related API operations."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
http: HttpClient,
|
|
16
|
+
agent_id: Optional[str] = None,
|
|
17
|
+
production: bool = False,
|
|
18
|
+
):
|
|
19
|
+
"""Initialize feature flag resource.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
http: HTTP client instance
|
|
23
|
+
agent_id: Default agent ID for feature flags
|
|
24
|
+
production: Whether to suppress errors in production mode
|
|
25
|
+
"""
|
|
26
|
+
self.http = http
|
|
27
|
+
self._agent_id = agent_id
|
|
28
|
+
self._production = production
|
|
29
|
+
|
|
30
|
+
def get(
|
|
31
|
+
self,
|
|
32
|
+
flag_name: str,
|
|
33
|
+
default: Any = None,
|
|
34
|
+
context: Optional[Dict[str, Any]] = None,
|
|
35
|
+
) -> Any:
|
|
36
|
+
"""Get a feature flag value.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
flag_name: Name of the feature flag.
|
|
40
|
+
default: Default value if flag is not found.
|
|
41
|
+
context: Optional context for flag evaluation.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The flag value or default.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
response = self.http.get(
|
|
48
|
+
"featureflags",
|
|
49
|
+
{"flag_name": flag_name, "agent_id": self._agent_id},
|
|
50
|
+
)
|
|
51
|
+
return response.get("value", default)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
if self._production:
|
|
54
|
+
logger.error(f"[FeatureFlagResource] Failed to get feature flag: {e}")
|
|
55
|
+
return default
|
|
56
|
+
raise
|
|
57
|
+
|
|
58
|
+
async def aget(
|
|
59
|
+
self,
|
|
60
|
+
flag_name: str,
|
|
61
|
+
default: Any = None,
|
|
62
|
+
context: Optional[Dict[str, Any]] = None,
|
|
63
|
+
) -> Any:
|
|
64
|
+
"""Get a feature flag value (asynchronous).
|
|
65
|
+
|
|
66
|
+
See get() for full documentation.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
response = await self.http.aget(
|
|
70
|
+
"featureflags",
|
|
71
|
+
{"flag_name": flag_name, "agent_id": self._agent_id},
|
|
72
|
+
)
|
|
73
|
+
return response.get("value", default)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
if self._production:
|
|
76
|
+
logger.error(f"[FeatureFlagResource] Failed to get feature flag: {e}")
|
|
77
|
+
return default
|
|
78
|
+
raise
|