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.
Files changed (38) hide show
  1. lucidicai/__init__.py +32 -390
  2. lucidicai/api/client.py +31 -2
  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 +391 -1
  19. lucidicai/sdk/features/feature_flag.py +344 -3
  20. lucidicai/sdk/init.py +49 -347
  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 +27 -11
  34. lucidicai/utils/serialization.py +27 -0
  35. {lucidicai-2.1.3.dist-info → lucidicai-3.0.0.dist-info}/METADATA +1 -1
  36. {lucidicai-2.1.3.dist-info → lucidicai-3.0.0.dist-info}/RECORD +38 -29
  37. {lucidicai-2.1.3.dist-info → lucidicai-3.0.0.dist-info}/WHEEL +0 -0
  38. {lucidicai-2.1.3.dist-info → lucidicai-3.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,502 @@
1
+ """SDK session creation and management."""
2
+ import asyncio
3
+ import threading
4
+ import uuid
5
+ from typing import List, Optional, Set
6
+ from weakref import WeakSet
7
+
8
+ from ..core.config import SDKConfig, set_config
9
+ from ..utils.logger import debug, info, warning, error, truncate_id
10
+ from .context import set_active_session, clear_active_session
11
+ from .shutdown_manager import get_shutdown_manager, SessionState
12
+
13
+
14
+ # Track background threads for flush()
15
+ _background_threads: Set[threading.Thread] = WeakSet()
16
+
17
+
18
+ def _prepare_session_config(
19
+ api_key: Optional[str],
20
+ agent_id: Optional[str],
21
+ providers: Optional[List[str]],
22
+ production_monitoring: bool,
23
+ auto_end: bool,
24
+ capture_uncaught: bool,
25
+ ) -> SDKConfig:
26
+ """Prepare and validate SDK configuration.
27
+
28
+ Returns:
29
+ Validated SDKConfig instance
30
+ """
31
+ config = SDKConfig.from_env(
32
+ api_key=api_key,
33
+ agent_id=agent_id,
34
+ auto_end=auto_end,
35
+ production_monitoring=production_monitoring
36
+ )
37
+
38
+ if providers:
39
+ config.telemetry.providers = providers
40
+
41
+ config.error_handling.capture_uncaught = capture_uncaught
42
+
43
+ # Validate configuration
44
+ errors = config.validate()
45
+ if errors:
46
+ raise ValueError(f"Invalid configuration: {', '.join(errors)}")
47
+
48
+ return config
49
+
50
+
51
+ def _ensure_http_and_resources_initialized(config: SDKConfig) -> None:
52
+ """Ensure HTTP client and resources are initialized."""
53
+ from .init import get_http, get_resources, set_http, set_resources
54
+ from ..api.client import HttpClient
55
+ from ..api.resources.event import EventResource
56
+ from ..api.resources.session import SessionResource
57
+ from ..api.resources.dataset import DatasetResource
58
+
59
+ # Initialize HTTP client
60
+ if not get_http():
61
+ debug("[SDK] Initializing HTTP client")
62
+ set_http(HttpClient(config))
63
+
64
+ # Initialize resources
65
+ resources = get_resources()
66
+ if not resources:
67
+ http = get_http()
68
+ set_resources({
69
+ 'events': EventResource(http),
70
+ 'sessions': SessionResource(http),
71
+ 'datasets': DatasetResource(http)
72
+ })
73
+
74
+
75
+ def _build_session_params(
76
+ session_id: Optional[str],
77
+ session_name: Optional[str],
78
+ agent_id: str,
79
+ task: Optional[str],
80
+ tags: Optional[List],
81
+ experiment_id: Optional[str],
82
+ datasetitem_id: Optional[str],
83
+ evaluators: Optional[List],
84
+ production_monitoring: bool,
85
+ ) -> tuple[str, dict]:
86
+ """Build session parameters for API call.
87
+
88
+ Returns:
89
+ Tuple of (real_session_id, session_params)
90
+ """
91
+ # Create or retrieve session
92
+ if session_id:
93
+ real_session_id = session_id
94
+ else:
95
+ real_session_id = str(uuid.uuid4())
96
+
97
+ # Create session via API - only send non-None values
98
+ session_params = {
99
+ 'session_id': real_session_id,
100
+ 'session_name': session_name or 'Unnamed Session',
101
+ 'agent_id': agent_id,
102
+ }
103
+
104
+ # Only add optional fields if they have values
105
+ if task:
106
+ session_params['task'] = task
107
+ if tags:
108
+ session_params['tags'] = tags
109
+ if experiment_id:
110
+ session_params['experiment_id'] = experiment_id
111
+ if datasetitem_id:
112
+ session_params['datasetitem_id'] = datasetitem_id
113
+ if evaluators:
114
+ session_params['evaluators'] = evaluators
115
+ if production_monitoring:
116
+ session_params['production_monitoring'] = production_monitoring
117
+
118
+ return real_session_id, session_params
119
+
120
+
121
+ def _finalize_session(
122
+ real_session_id: str,
123
+ session_name: Optional[str],
124
+ auto_end: bool,
125
+ providers: Optional[List[str]],
126
+ ) -> str:
127
+ """Finalize session setup after API call."""
128
+ from .init import _sdk_state, _initialize_telemetry, get_resources
129
+
130
+ _sdk_state.session_id = real_session_id
131
+
132
+ info(f"[SDK] Session created: {truncate_id(real_session_id)} (name: {session_name or 'Unnamed Session'})")
133
+
134
+ # Set active session in context
135
+ set_active_session(real_session_id)
136
+
137
+ # Register session with shutdown manager
138
+ debug(f"[SDK] Registering session with shutdown manager (auto_end={auto_end})")
139
+ shutdown_manager = get_shutdown_manager()
140
+ session_state = SessionState(
141
+ session_id=real_session_id,
142
+ http_client=get_resources(),
143
+ auto_end=auto_end
144
+ )
145
+ shutdown_manager.register_session(real_session_id, session_state)
146
+
147
+ # Initialize telemetry if providers specified
148
+ if providers:
149
+ debug(f"[SDK] Initializing telemetry for providers: {providers}")
150
+ _initialize_telemetry(providers)
151
+
152
+ return real_session_id
153
+
154
+
155
+ def create_session(
156
+ session_name: Optional[str] = None,
157
+ session_id: Optional[str] = None,
158
+ api_key: Optional[str] = None,
159
+ agent_id: Optional[str] = None,
160
+ task: Optional[str] = None,
161
+ providers: Optional[List[str]] = None,
162
+ production_monitoring: bool = False,
163
+ experiment_id: Optional[str] = None,
164
+ evaluators: Optional[List] = None,
165
+ tags: Optional[List] = None,
166
+ datasetitem_id: Optional[str] = None,
167
+ masking_function: Optional[callable] = None,
168
+ auto_end: bool = True,
169
+ capture_uncaught: bool = True,
170
+ ) -> str:
171
+ """Create a new Lucidic session (synchronous).
172
+
173
+ Args:
174
+ session_name: Name for the session
175
+ session_id: Custom session ID (optional)
176
+ api_key: API key (uses env if not provided)
177
+ agent_id: Agent ID (uses env if not provided)
178
+ task: Task description
179
+ providers: List of telemetry providers to instrument
180
+ production_monitoring: Enable production monitoring
181
+ experiment_id: Experiment ID to associate with session
182
+ evaluators: Evaluators to use
183
+ tags: Session tags
184
+ datasetitem_id: Dataset item ID
185
+ masking_function: Function to mask sensitive data
186
+ auto_end: Automatically end session on exit
187
+ capture_uncaught: Capture uncaught exceptions
188
+
189
+ Returns:
190
+ Session ID
191
+
192
+ Raises:
193
+ APIKeyVerificationError: If API credentials are invalid
194
+ """
195
+ from .init import get_resources
196
+
197
+ # Prepare configuration
198
+ config = _prepare_session_config(
199
+ api_key, agent_id, providers, production_monitoring, auto_end, capture_uncaught
200
+ )
201
+ set_config(config)
202
+
203
+ # Ensure HTTP client and resources are initialized
204
+ _ensure_http_and_resources_initialized(config)
205
+
206
+ # Build session parameters
207
+ real_session_id, session_params = _build_session_params(
208
+ session_id, session_name, config.agent_id, task, tags,
209
+ experiment_id, datasetitem_id, evaluators, production_monitoring
210
+ )
211
+
212
+ # Create session via API (synchronous)
213
+ debug(f"[SDK] Creating session with params: {session_params}")
214
+ session_resource = get_resources()['sessions']
215
+ session_data = session_resource.create_session(session_params)
216
+
217
+ # Use the session_id returned by the backend
218
+ real_session_id = session_data.get('session_id', real_session_id)
219
+
220
+ return _finalize_session(real_session_id, session_name, auto_end, providers)
221
+
222
+
223
+ async def acreate_session(
224
+ session_name: Optional[str] = None,
225
+ session_id: Optional[str] = None,
226
+ api_key: Optional[str] = None,
227
+ agent_id: Optional[str] = None,
228
+ task: Optional[str] = None,
229
+ providers: Optional[List[str]] = None,
230
+ production_monitoring: bool = False,
231
+ experiment_id: Optional[str] = None,
232
+ evaluators: Optional[List] = None,
233
+ tags: Optional[List] = None,
234
+ datasetitem_id: Optional[str] = None,
235
+ masking_function: Optional[callable] = None,
236
+ auto_end: bool = True,
237
+ capture_uncaught: bool = True,
238
+ ) -> str:
239
+ """Create a new Lucidic session (asynchronous).
240
+
241
+ Args:
242
+ session_name: Name for the session
243
+ session_id: Custom session ID (optional)
244
+ api_key: API key (uses env if not provided)
245
+ agent_id: Agent ID (uses env if not provided)
246
+ task: Task description
247
+ providers: List of telemetry providers to instrument
248
+ production_monitoring: Enable production monitoring
249
+ experiment_id: Experiment ID to associate with session
250
+ evaluators: Evaluators to use
251
+ tags: Session tags
252
+ datasetitem_id: Dataset item ID
253
+ masking_function: Function to mask sensitive data
254
+ auto_end: Automatically end session on exit
255
+ capture_uncaught: Capture uncaught exceptions
256
+
257
+ Returns:
258
+ Session ID
259
+
260
+ Raises:
261
+ APIKeyVerificationError: If API credentials are invalid
262
+ """
263
+ from .init import get_resources
264
+
265
+ # Prepare configuration
266
+ config = _prepare_session_config(
267
+ api_key, agent_id, providers, production_monitoring, auto_end, capture_uncaught
268
+ )
269
+ set_config(config)
270
+
271
+ # Ensure HTTP client and resources are initialized
272
+ _ensure_http_and_resources_initialized(config)
273
+
274
+ # Build session parameters
275
+ real_session_id, session_params = _build_session_params(
276
+ session_id, session_name, config.agent_id, task, tags,
277
+ experiment_id, datasetitem_id, evaluators, production_monitoring
278
+ )
279
+
280
+ # Create session via API (asynchronous)
281
+ debug(f"[SDK] Creating session with params: {session_params}")
282
+ session_resource = get_resources()['sessions']
283
+ session_data = await session_resource.acreate_session(session_params)
284
+
285
+ # Use the session_id returned by the backend
286
+ real_session_id = session_data.get('session_id', real_session_id)
287
+
288
+ return _finalize_session(real_session_id, session_name, auto_end, providers)
289
+
290
+
291
+ def emit_session(
292
+ session_name: Optional[str] = None,
293
+ session_id: Optional[str] = None,
294
+ api_key: Optional[str] = None,
295
+ agent_id: Optional[str] = None,
296
+ task: Optional[str] = None,
297
+ providers: Optional[List[str]] = None,
298
+ production_monitoring: bool = False,
299
+ experiment_id: Optional[str] = None,
300
+ evaluators: Optional[List] = None,
301
+ tags: Optional[List] = None,
302
+ datasetitem_id: Optional[str] = None,
303
+ masking_function: Optional[callable] = None,
304
+ auto_end: bool = True,
305
+ capture_uncaught: bool = True,
306
+ ) -> str:
307
+ """Fire-and-forget session creation that returns instantly.
308
+
309
+ This function returns immediately with a session ID, while the actual
310
+ session creation happens in a background thread. Perfect for reducing
311
+ initialization latency.
312
+
313
+ Args:
314
+ session_name: Name for the session
315
+ session_id: Custom session ID (optional)
316
+ api_key: API key (uses env if not provided)
317
+ agent_id: Agent ID (uses env if not provided)
318
+ task: Task description
319
+ providers: List of telemetry providers to instrument
320
+ production_monitoring: Enable production monitoring
321
+ experiment_id: Experiment ID to associate with session
322
+ evaluators: Evaluators to use
323
+ tags: Session tags
324
+ datasetitem_id: Dataset item ID
325
+ masking_function: Function to mask sensitive data
326
+ auto_end: Automatically end session on exit
327
+ capture_uncaught: Capture uncaught exceptions
328
+
329
+ Returns:
330
+ Session ID - returned immediately
331
+ """
332
+ from .init import _sdk_state
333
+
334
+ # Pre-generate session ID for instant return
335
+ real_session_id = session_id or str(uuid.uuid4())
336
+
337
+ # Immediately set session state for subsequent operations
338
+ _sdk_state.session_id = real_session_id
339
+ set_active_session(real_session_id)
340
+
341
+ # Run async session creation in background thread
342
+ def _run():
343
+ try:
344
+ # Create new event loop for this thread
345
+ loop = asyncio.new_event_loop()
346
+ asyncio.set_event_loop(loop)
347
+ try:
348
+ loop.run_until_complete(
349
+ acreate_session(
350
+ session_name=session_name,
351
+ session_id=real_session_id, # Use pre-generated ID
352
+ api_key=api_key,
353
+ agent_id=agent_id,
354
+ task=task,
355
+ providers=providers,
356
+ production_monitoring=production_monitoring,
357
+ experiment_id=experiment_id,
358
+ evaluators=evaluators,
359
+ tags=tags,
360
+ datasetitem_id=datasetitem_id,
361
+ masking_function=masking_function,
362
+ auto_end=auto_end,
363
+ capture_uncaught=capture_uncaught,
364
+ )
365
+ )
366
+ finally:
367
+ loop.close()
368
+ except Exception as e:
369
+ error(f"[Session] Background emit failed for {truncate_id(real_session_id)}: {e}")
370
+
371
+ thread = threading.Thread(
372
+ target=_run,
373
+ daemon=True,
374
+ name=f"emit-session-{truncate_id(real_session_id)}"
375
+ )
376
+ _background_threads.add(thread)
377
+ thread.start()
378
+
379
+ info(f"[Session] Emitted session {truncate_id(real_session_id)} (name: {session_name or 'Unnamed Session'}, fire-and-forget)")
380
+ return real_session_id
381
+
382
+
383
+ def flush_sessions(timeout: float = 5.0) -> None:
384
+ """Wait for all background session creations to complete.
385
+
386
+ Args:
387
+ timeout: Maximum time to wait in seconds (default: 5.0)
388
+ """
389
+ import time
390
+
391
+ start_time = time.time()
392
+
393
+ # Wait for background threads
394
+ threads = list(_background_threads)
395
+ for thread in threads:
396
+ if thread.is_alive():
397
+ remaining = timeout - (time.time() - start_time)
398
+ if remaining > 0:
399
+ thread.join(timeout=remaining)
400
+ if thread.is_alive():
401
+ warning(f"[Session] Thread {thread.name} did not complete within timeout")
402
+
403
+ debug(f"[Session] Flush completed in {time.time() - start_time:.2f}s")
404
+
405
+
406
+ def end_session(
407
+ session_id: Optional[str] = None,
408
+ is_successful: Optional[bool] = None,
409
+ is_successful_reason: Optional[str] = None,
410
+ session_eval: Optional[float] = None,
411
+ session_eval_reason: Optional[str] = None,
412
+ ) -> None:
413
+ """End the current or specified session.
414
+
415
+ Args:
416
+ session_id: Session ID to end (uses current if not provided)
417
+ is_successful: Whether session was successful
418
+ is_successful_reason: Reason for success or failure
419
+ session_eval: Session evaluation score
420
+ session_eval_reason: Reason for evaluation
421
+ """
422
+ from .init import get_session_id, get_resources
423
+
424
+ # Get session ID
425
+ if not session_id:
426
+ session_id = get_session_id()
427
+
428
+ if not session_id:
429
+ warning("[Session] No active session to end")
430
+ return
431
+
432
+ # End session via API
433
+ resources = get_resources()
434
+ if resources and 'sessions' in resources:
435
+ try:
436
+ resources['sessions'].end_session(
437
+ session_id=session_id,
438
+ is_successful=is_successful,
439
+ is_successful_reason=is_successful_reason,
440
+ session_eval=session_eval,
441
+ session_eval_reason=session_eval_reason
442
+ )
443
+ info(f"[Session] Ended session {truncate_id(session_id)}")
444
+ except Exception as e:
445
+ error(f"[Session] Failed to end session {truncate_id(session_id)}: {e}")
446
+
447
+ # Unregister session from shutdown manager
448
+ shutdown_manager = get_shutdown_manager()
449
+ shutdown_manager.unregister_session(session_id)
450
+
451
+ # Clear active session
452
+ clear_active_session()
453
+
454
+
455
+
456
+ async def aend_session(
457
+ session_id: Optional[str] = None,
458
+ is_successful: Optional[bool] = None,
459
+ is_successful_reason: Optional[str] = None,
460
+ session_eval: Optional[float] = None,
461
+ session_eval_reason: Optional[str] = None,
462
+ ) -> None:
463
+ """End the current or specified session (asynchronous).
464
+
465
+ Args:
466
+ session_id: Session ID to end (uses current if not provided)
467
+ is_successful: Whether session was successful
468
+ is_successful_reason: Reason for success or failure
469
+ session_eval: Session evaluation score
470
+ session_eval_reason: Reason for evaluation
471
+ """
472
+ from .init import get_session_id, get_resources
473
+
474
+ # Get session ID
475
+ if not session_id:
476
+ session_id = get_session_id()
477
+
478
+ if not session_id:
479
+ warning("[Session] No active session to end")
480
+ return
481
+
482
+ # End session via API
483
+ resources = get_resources()
484
+ if resources and 'sessions' in resources:
485
+ try:
486
+ await resources['sessions'].aend_session(
487
+ session_id=session_id,
488
+ is_successful=is_successful,
489
+ is_successful_reason=is_successful_reason,
490
+ session_eval=session_eval,
491
+ session_eval_reason=session_eval_reason
492
+ )
493
+ info(f"[Session] Ended session {truncate_id(session_id)}")
494
+ except Exception as e:
495
+ error(f"[Session] Failed to end session {truncate_id(session_id)}: {e}")
496
+
497
+ # Unregister session from shutdown manager
498
+ shutdown_manager = get_shutdown_manager()
499
+ shutdown_manager.unregister_session(session_id)
500
+
501
+ # Clear active session
502
+ clear_active_session()