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.
Files changed (39) hide show
  1. lucidicai/__init__.py +32 -390
  2. lucidicai/api/client.py +260 -92
  3. lucidicai/api/resources/__init__.py +16 -1
  4. lucidicai/api/resources/dataset.py +422 -82
  5. lucidicai/api/resources/event.py +399 -27
  6. lucidicai/api/resources/experiment.py +108 -0
  7. lucidicai/api/resources/feature_flag.py +78 -0
  8. lucidicai/api/resources/prompt.py +84 -0
  9. lucidicai/api/resources/session.py +545 -38
  10. lucidicai/client.py +395 -480
  11. lucidicai/core/config.py +73 -48
  12. lucidicai/core/errors.py +3 -3
  13. lucidicai/sdk/bound_decorators.py +321 -0
  14. lucidicai/sdk/context.py +20 -2
  15. lucidicai/sdk/decorators.py +283 -74
  16. lucidicai/sdk/event.py +538 -36
  17. lucidicai/sdk/event_builder.py +2 -4
  18. lucidicai/sdk/features/dataset.py +408 -232
  19. lucidicai/sdk/features/feature_flag.py +344 -3
  20. lucidicai/sdk/init.py +50 -279
  21. lucidicai/sdk/session.py +502 -0
  22. lucidicai/sdk/shutdown_manager.py +103 -46
  23. lucidicai/session_obj.py +321 -0
  24. lucidicai/telemetry/context_capture_processor.py +13 -6
  25. lucidicai/telemetry/extract.py +60 -63
  26. lucidicai/telemetry/litellm_bridge.py +3 -44
  27. lucidicai/telemetry/lucidic_exporter.py +143 -131
  28. lucidicai/telemetry/openai_agents_instrumentor.py +2 -2
  29. lucidicai/telemetry/openai_patch.py +7 -6
  30. lucidicai/telemetry/telemetry_manager.py +183 -0
  31. lucidicai/telemetry/utils/model_pricing.py +21 -30
  32. lucidicai/telemetry/utils/provider.py +77 -0
  33. lucidicai/utils/images.py +30 -14
  34. lucidicai/utils/queue.py +2 -2
  35. lucidicai/utils/serialization.py +27 -0
  36. {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/METADATA +1 -1
  37. {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/RECORD +39 -30
  38. {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/WHEEL +0 -0
  39. {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/top_level.txt +0 -0
@@ -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 json
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 .event import create_event
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 ..core.errors import LucidicNotInitializedError
13
- from .context import current_parent_event_id, event_context, event_context_async
14
- from ..utils.logger import debug, error as log_error, verbose, truncate_id
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
- F = TypeVar('F', bound=Callable[..., Any])
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
- def _serialize(value: Any):
20
- if isinstance(value, (str, int, float, bool)):
21
- return value
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
- return json.loads(json.dumps(value, default=str))
28
- except Exception:
29
- return str(value)
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(**decorator_kwargs) -> Callable[[F], F]:
33
- """Universal decorator creating FUNCTION_CALL events with nesting and error capture."""
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
- session_id = get_session_id()
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 = {name: _serialize(val) for name, val in bound.arguments.items()}
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(f"[Decorator] Starting {func.__name__} with event ID {truncate_id(pre_event_id)}, parent: {truncate_id(parent_id)}")
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
- create_event(
85
- type="error_traceback",
86
- error=str(error),
87
- traceback=traceback.format_exc(),
88
- parent_event_id=pre_event_id # Parent is the function that threw the error
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(f"[Decorator] Failed to create error_traceback event: {e}")
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
- return_val = _serialize(result)
95
-
96
- create_event(
97
- type="function_call",
98
- event_id=pre_event_id, # Use the pre-generated ID
99
- function_name=func.__name__,
100
- arguments=args_dict,
101
- return_value=return_val,
102
- error=str(error) if error else None,
103
- duration=(datetime.now().astimezone() - start_time).total_seconds(),
104
- **decorator_kwargs
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
- session_id = get_session_id()
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 = {name: _serialize(val) for name, val in bound.arguments.items()}
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(f"[Decorator] Starting {func.__name__} with event ID {truncate_id(pre_event_id)}, parent: {truncate_id(parent_id)}")
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
- create_event(
158
- type="error_traceback",
159
- error=str(error),
160
- traceback=traceback.format_exc(),
161
- parent_event_id=pre_event_id # Parent is the function that threw the error
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(f"[Decorator] Failed to create error_traceback event: {e}")
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
- return_val = _serialize(result)
168
-
169
- create_event(
170
- type="function_call",
171
- event_id=pre_event_id, # Use the pre-generated ID
172
- function_name=func.__name__,
173
- arguments=args_dict,
174
- return_value=return_val,
175
- error=str(error) if error else None,
176
- duration=(datetime.now().astimezone() - start_time).total_seconds(),
177
- **decorator_kwargs
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)