lucidicai 2.1.3__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 +31 -2
- 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 +391 -1
- lucidicai/sdk/features/feature_flag.py +344 -3
- lucidicai/sdk/init.py +49 -347
- 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 +27 -11
- lucidicai/utils/serialization.py +27 -0
- {lucidicai-2.1.3.dist-info → lucidicai-3.0.0.dist-info}/METADATA +1 -1
- {lucidicai-2.1.3.dist-info → lucidicai-3.0.0.dist-info}/RECORD +38 -29
- {lucidicai-2.1.3.dist-info → lucidicai-3.0.0.dist-info}/WHEEL +0 -0
- {lucidicai-2.1.3.dist-info → lucidicai-3.0.0.dist-info}/top_level.txt +0 -0
lucidicai/sdk/decorators.py
CHANGED
|
@@ -1,41 +1,136 @@
|
|
|
1
|
-
"""Decorators for the Lucidic SDK to create typed, nested events.
|
|
1
|
+
"""Decorators for the Lucidic SDK to create typed, nested events.
|
|
2
|
+
|
|
3
|
+
Supports both client-bound and context-based event tracking:
|
|
4
|
+
- Client-bound: Pass a LucidicAI client instance to bind events to that client
|
|
5
|
+
- Context-based: Uses global session context when no client is specified
|
|
6
|
+
"""
|
|
2
7
|
import functools
|
|
3
8
|
import inspect
|
|
4
|
-
import
|
|
9
|
+
import traceback
|
|
5
10
|
from datetime import datetime
|
|
6
11
|
import uuid
|
|
7
|
-
from typing import Any, Callable, Optional, TypeVar
|
|
8
|
-
from collections.abc import Iterable
|
|
12
|
+
from typing import Any, Callable, Optional, TypeVar, TYPE_CHECKING
|
|
9
13
|
|
|
10
|
-
from .
|
|
14
|
+
from .context import (
|
|
15
|
+
current_session_id,
|
|
16
|
+
current_client,
|
|
17
|
+
current_parent_event_id,
|
|
18
|
+
event_context,
|
|
19
|
+
event_context_async,
|
|
20
|
+
)
|
|
11
21
|
from .init import get_session_id
|
|
12
|
-
from ..
|
|
13
|
-
from .
|
|
14
|
-
|
|
22
|
+
from ..utils.serialization import serialize_value
|
|
23
|
+
from ..utils.logger import debug, error as log_error, truncate_id
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from ..client import LucidicAI
|
|
27
|
+
|
|
28
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _emit_event_to_client(
|
|
32
|
+
client: "LucidicAI",
|
|
33
|
+
session_id: str,
|
|
34
|
+
type: str,
|
|
35
|
+
**event_data,
|
|
36
|
+
) -> Optional[str]:
|
|
37
|
+
"""Emit an event using the client's event resource.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
client: The LucidicAI client
|
|
41
|
+
session_id: The session ID to associate the event with
|
|
42
|
+
type: The event type (e.g., "function_call", "error_traceback")
|
|
43
|
+
**event_data: Additional event data
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The event ID if created successfully, None otherwise
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
event_payload = {
|
|
50
|
+
"type": type,
|
|
51
|
+
"session_id": session_id,
|
|
52
|
+
**event_data,
|
|
53
|
+
}
|
|
54
|
+
response = client._resources["events"].create_event(event_payload)
|
|
55
|
+
return response.get("event_id") if response else None
|
|
56
|
+
except Exception as e:
|
|
57
|
+
debug(f"[Decorator] Failed to emit event: {e}")
|
|
58
|
+
return None
|
|
59
|
+
|
|
15
60
|
|
|
16
|
-
|
|
61
|
+
async def _aemit_event_to_client(
|
|
62
|
+
client: "LucidicAI",
|
|
63
|
+
session_id: str,
|
|
64
|
+
type: str,
|
|
65
|
+
**event_data,
|
|
66
|
+
) -> Optional[str]:
|
|
67
|
+
"""Emit an event using the client's event resource (async).
|
|
17
68
|
|
|
69
|
+
Args:
|
|
70
|
+
client: The LucidicAI client
|
|
71
|
+
session_id: The session ID to associate the event with
|
|
72
|
+
type: The event type (e.g., "function_call", "error_traceback")
|
|
73
|
+
**event_data: Additional event data
|
|
18
74
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if isinstance(value, dict):
|
|
23
|
-
return {k: _serialize(v) for k, v in value.items()}
|
|
24
|
-
if isinstance(value, Iterable) and not isinstance(value, (str, bytes)):
|
|
25
|
-
return [_serialize(v) for v in value]
|
|
75
|
+
Returns:
|
|
76
|
+
The event ID if created successfully, None otherwise
|
|
77
|
+
"""
|
|
26
78
|
try:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
79
|
+
event_payload = {
|
|
80
|
+
"type": type,
|
|
81
|
+
"session_id": session_id,
|
|
82
|
+
**event_data,
|
|
83
|
+
}
|
|
84
|
+
response = await client._resources["events"].acreate_event(event_payload)
|
|
85
|
+
return response.get("event_id") if response else None
|
|
86
|
+
except Exception as e:
|
|
87
|
+
debug(f"[Decorator] Failed to emit async event: {e}")
|
|
88
|
+
return None
|
|
30
89
|
|
|
31
90
|
|
|
32
|
-
def event(
|
|
33
|
-
""
|
|
91
|
+
def event(
|
|
92
|
+
client: Optional["LucidicAI"] = None, **decorator_kwargs
|
|
93
|
+
) -> Callable[[F], F]:
|
|
94
|
+
"""Universal decorator creating FUNCTION_CALL events with nesting and error capture.
|
|
95
|
+
|
|
96
|
+
Supports both client-bound and context-based modes:
|
|
97
|
+
- Client-bound: Pass a client instance to bind events to that specific client
|
|
98
|
+
- Context-based: Omit client to use global session context
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
client: Optional LucidicAI client to bind this decorator to
|
|
102
|
+
**decorator_kwargs: Additional keyword arguments passed to the event
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
A decorator function that wraps the target function
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
# Context-based (uses global session)
|
|
109
|
+
@event()
|
|
110
|
+
def my_function():
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
# Client-bound
|
|
114
|
+
@event(client=my_client)
|
|
115
|
+
def my_function():
|
|
116
|
+
pass
|
|
117
|
+
"""
|
|
34
118
|
|
|
35
119
|
def decorator(func: F) -> F:
|
|
36
120
|
@functools.wraps(func)
|
|
37
121
|
def sync_wrapper(*args, **kwargs):
|
|
38
|
-
|
|
122
|
+
# Determine session ID and whether we should track
|
|
123
|
+
if client:
|
|
124
|
+
# Client-bound mode: check if this client is active
|
|
125
|
+
active_client = current_client.get(None)
|
|
126
|
+
if active_client is not client:
|
|
127
|
+
# Not our client - just execute the function
|
|
128
|
+
return func(*args, **kwargs)
|
|
129
|
+
session_id = current_session_id.get(None)
|
|
130
|
+
else:
|
|
131
|
+
# Context-based mode: use global session
|
|
132
|
+
session_id = get_session_id()
|
|
133
|
+
|
|
39
134
|
if not session_id:
|
|
40
135
|
return func(*args, **kwargs)
|
|
41
136
|
|
|
@@ -43,11 +138,15 @@ def event(**decorator_kwargs) -> Callable[[F], F]:
|
|
|
43
138
|
sig = inspect.signature(func)
|
|
44
139
|
bound = sig.bind(*args, **kwargs)
|
|
45
140
|
bound.apply_defaults()
|
|
46
|
-
args_dict = {
|
|
141
|
+
args_dict = {
|
|
142
|
+
name: serialize_value(val) for name, val in bound.arguments.items()
|
|
143
|
+
}
|
|
47
144
|
|
|
48
145
|
parent_id = current_parent_event_id.get(None)
|
|
49
146
|
pre_event_id = str(uuid.uuid4())
|
|
50
|
-
debug(
|
|
147
|
+
debug(
|
|
148
|
+
f"[Decorator] Starting {func.__name__} with event ID {truncate_id(pre_event_id)}, parent: {truncate_id(parent_id)}"
|
|
149
|
+
)
|
|
51
150
|
start_time = datetime.now().astimezone()
|
|
52
151
|
result = None
|
|
53
152
|
error: Optional[BaseException] = None
|
|
@@ -57,7 +156,7 @@ def event(**decorator_kwargs) -> Callable[[F], F]:
|
|
|
57
156
|
# Also inject into OpenTelemetry context for instrumentors
|
|
58
157
|
from ..telemetry.context_bridge import inject_lucidic_context
|
|
59
158
|
from opentelemetry import context as otel_context
|
|
60
|
-
|
|
159
|
+
|
|
61
160
|
otel_ctx = inject_lucidic_context()
|
|
62
161
|
token = otel_context.attach(otel_ctx)
|
|
63
162
|
try:
|
|
@@ -75,52 +174,106 @@ def event(**decorator_kwargs) -> Callable[[F], F]:
|
|
|
75
174
|
if error:
|
|
76
175
|
return_val = {
|
|
77
176
|
"error": str(error),
|
|
78
|
-
"error_type": type(error).__name__
|
|
177
|
+
"error_type": type(error).__name__,
|
|
79
178
|
}
|
|
80
|
-
|
|
179
|
+
|
|
81
180
|
# Create a separate error_traceback event for the exception
|
|
82
|
-
import traceback
|
|
83
181
|
try:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
182
|
+
if client:
|
|
183
|
+
_emit_event_to_client(
|
|
184
|
+
client,
|
|
185
|
+
session_id,
|
|
186
|
+
type="error_traceback",
|
|
187
|
+
error=str(error),
|
|
188
|
+
traceback=traceback.format_exc(),
|
|
189
|
+
parent_event_id=pre_event_id,
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
from .event import emit_event
|
|
193
|
+
|
|
194
|
+
emit_event(
|
|
195
|
+
type="error_traceback",
|
|
196
|
+
error=str(error),
|
|
197
|
+
traceback=traceback.format_exc(),
|
|
198
|
+
parent_event_id=pre_event_id,
|
|
199
|
+
)
|
|
200
|
+
debug(
|
|
201
|
+
f"[Decorator] Created error_traceback event for {func.__name__}"
|
|
89
202
|
)
|
|
90
|
-
debug(f"[Decorator] Created error_traceback event for {func.__name__}")
|
|
91
203
|
except Exception as e:
|
|
92
|
-
debug(
|
|
204
|
+
debug(
|
|
205
|
+
f"[Decorator] Failed to create error_traceback event: {e}"
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
return_val = serialize_value(result)
|
|
209
|
+
|
|
210
|
+
# Emit the function_call event
|
|
211
|
+
if client:
|
|
212
|
+
_emit_event_to_client(
|
|
213
|
+
client,
|
|
214
|
+
session_id,
|
|
215
|
+
type="function_call",
|
|
216
|
+
event_id=pre_event_id,
|
|
217
|
+
parent_event_id=parent_id,
|
|
218
|
+
function_name=func.__name__,
|
|
219
|
+
arguments=args_dict,
|
|
220
|
+
return_value=return_val,
|
|
221
|
+
error=str(error) if error else None,
|
|
222
|
+
duration=(
|
|
223
|
+
datetime.now().astimezone() - start_time
|
|
224
|
+
).total_seconds(),
|
|
225
|
+
**decorator_kwargs,
|
|
226
|
+
)
|
|
93
227
|
else:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
228
|
+
from .event import emit_event
|
|
229
|
+
|
|
230
|
+
emit_event(
|
|
231
|
+
type="function_call",
|
|
232
|
+
event_id=pre_event_id,
|
|
233
|
+
parent_event_id=parent_id,
|
|
234
|
+
function_name=func.__name__,
|
|
235
|
+
arguments=args_dict,
|
|
236
|
+
return_value=return_val,
|
|
237
|
+
error=str(error) if error else None,
|
|
238
|
+
duration=(
|
|
239
|
+
datetime.now().astimezone() - start_time
|
|
240
|
+
).total_seconds(),
|
|
241
|
+
**decorator_kwargs,
|
|
242
|
+
)
|
|
106
243
|
debug(f"[Decorator] Created function_call event for {func.__name__}")
|
|
107
244
|
except Exception as e:
|
|
108
245
|
log_error(f"[Decorator] Failed to create function_call event: {e}")
|
|
109
246
|
|
|
110
247
|
@functools.wraps(func)
|
|
111
248
|
async def async_wrapper(*args, **kwargs):
|
|
112
|
-
|
|
249
|
+
# Determine session ID and whether we should track
|
|
250
|
+
if client:
|
|
251
|
+
# Client-bound mode: check if this client is active
|
|
252
|
+
active_client = current_client.get(None)
|
|
253
|
+
if active_client is not client:
|
|
254
|
+
# Not our client - just execute the function
|
|
255
|
+
return await func(*args, **kwargs)
|
|
256
|
+
session_id = current_session_id.get(None)
|
|
257
|
+
else:
|
|
258
|
+
# Context-based mode: use global session
|
|
259
|
+
session_id = get_session_id()
|
|
260
|
+
|
|
113
261
|
if not session_id:
|
|
114
262
|
return await func(*args, **kwargs)
|
|
115
263
|
|
|
264
|
+
# Build arguments snapshot
|
|
116
265
|
sig = inspect.signature(func)
|
|
117
266
|
bound = sig.bind(*args, **kwargs)
|
|
118
267
|
bound.apply_defaults()
|
|
119
|
-
args_dict = {
|
|
268
|
+
args_dict = {
|
|
269
|
+
name: serialize_value(val) for name, val in bound.arguments.items()
|
|
270
|
+
}
|
|
120
271
|
|
|
121
272
|
parent_id = current_parent_event_id.get(None)
|
|
122
273
|
pre_event_id = str(uuid.uuid4())
|
|
123
|
-
debug(
|
|
274
|
+
debug(
|
|
275
|
+
f"[Decorator] Starting {func.__name__} with event ID {truncate_id(pre_event_id)}, parent: {truncate_id(parent_id)}"
|
|
276
|
+
)
|
|
124
277
|
start_time = datetime.now().astimezone()
|
|
125
278
|
result = None
|
|
126
279
|
error: Optional[BaseException] = None
|
|
@@ -130,7 +283,7 @@ def event(**decorator_kwargs) -> Callable[[F], F]:
|
|
|
130
283
|
# Also inject into OpenTelemetry context for instrumentors
|
|
131
284
|
from ..telemetry.context_bridge import inject_lucidic_context
|
|
132
285
|
from opentelemetry import context as otel_context
|
|
133
|
-
|
|
286
|
+
|
|
134
287
|
otel_ctx = inject_lucidic_context()
|
|
135
288
|
token = otel_context.attach(otel_ctx)
|
|
136
289
|
try:
|
|
@@ -148,34 +301,72 @@ def event(**decorator_kwargs) -> Callable[[F], F]:
|
|
|
148
301
|
if error:
|
|
149
302
|
return_val = {
|
|
150
303
|
"error": str(error),
|
|
151
|
-
"error_type": type(error).__name__
|
|
304
|
+
"error_type": type(error).__name__,
|
|
152
305
|
}
|
|
153
|
-
|
|
306
|
+
|
|
154
307
|
# Create a separate error_traceback event for the exception
|
|
155
|
-
import traceback
|
|
156
308
|
try:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
309
|
+
if client:
|
|
310
|
+
await _aemit_event_to_client(
|
|
311
|
+
client,
|
|
312
|
+
session_id,
|
|
313
|
+
type="error_traceback",
|
|
314
|
+
error=str(error),
|
|
315
|
+
traceback=traceback.format_exc(),
|
|
316
|
+
parent_event_id=pre_event_id,
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
from .event import emit_event
|
|
320
|
+
|
|
321
|
+
emit_event(
|
|
322
|
+
type="error_traceback",
|
|
323
|
+
error=str(error),
|
|
324
|
+
traceback=traceback.format_exc(),
|
|
325
|
+
parent_event_id=pre_event_id,
|
|
326
|
+
)
|
|
327
|
+
debug(
|
|
328
|
+
f"[Decorator] Created error_traceback event for {func.__name__}"
|
|
162
329
|
)
|
|
163
|
-
debug(f"[Decorator] Created error_traceback event for {func.__name__}")
|
|
164
330
|
except Exception as e:
|
|
165
|
-
debug(
|
|
331
|
+
debug(
|
|
332
|
+
f"[Decorator] Failed to create error_traceback event: {e}"
|
|
333
|
+
)
|
|
334
|
+
else:
|
|
335
|
+
return_val = serialize_value(result)
|
|
336
|
+
|
|
337
|
+
# Emit the function_call event
|
|
338
|
+
if client:
|
|
339
|
+
await _aemit_event_to_client(
|
|
340
|
+
client,
|
|
341
|
+
session_id,
|
|
342
|
+
type="function_call",
|
|
343
|
+
event_id=pre_event_id,
|
|
344
|
+
parent_event_id=parent_id,
|
|
345
|
+
function_name=func.__name__,
|
|
346
|
+
arguments=args_dict,
|
|
347
|
+
return_value=return_val,
|
|
348
|
+
error=str(error) if error else None,
|
|
349
|
+
duration=(
|
|
350
|
+
datetime.now().astimezone() - start_time
|
|
351
|
+
).total_seconds(),
|
|
352
|
+
**decorator_kwargs,
|
|
353
|
+
)
|
|
166
354
|
else:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
355
|
+
from .event import emit_event
|
|
356
|
+
|
|
357
|
+
emit_event(
|
|
358
|
+
type="function_call",
|
|
359
|
+
event_id=pre_event_id,
|
|
360
|
+
parent_event_id=parent_id,
|
|
361
|
+
function_name=func.__name__,
|
|
362
|
+
arguments=args_dict,
|
|
363
|
+
return_value=return_val,
|
|
364
|
+
error=str(error) if error else None,
|
|
365
|
+
duration=(
|
|
366
|
+
datetime.now().astimezone() - start_time
|
|
367
|
+
).total_seconds(),
|
|
368
|
+
**decorator_kwargs,
|
|
369
|
+
)
|
|
179
370
|
debug(f"[Decorator] Created function_call event for {func.__name__}")
|
|
180
371
|
except Exception as e:
|
|
181
372
|
log_error(f"[Decorator] Failed to create function_call event: {e}")
|
|
@@ -184,4 +375,22 @@ def event(**decorator_kwargs) -> Callable[[F], F]:
|
|
|
184
375
|
return async_wrapper # type: ignore
|
|
185
376
|
return sync_wrapper # type: ignore
|
|
186
377
|
|
|
187
|
-
return decorator
|
|
378
|
+
return decorator
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# Backward compatibility alias
|
|
382
|
+
def create_bound_event_decorator(
|
|
383
|
+
client: "LucidicAI", **decorator_kwargs
|
|
384
|
+
) -> Callable[[F], F]:
|
|
385
|
+
"""Create an event decorator bound to a specific client.
|
|
386
|
+
|
|
387
|
+
This is a convenience wrapper around event() for backward compatibility.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
client: The LucidicAI client to bind this decorator to
|
|
391
|
+
**decorator_kwargs: Additional keyword arguments passed to the event
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
A decorator function that wraps the target function
|
|
395
|
+
"""
|
|
396
|
+
return event(client=client, **decorator_kwargs)
|