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
lucidicai/sdk/init.py CHANGED
@@ -1,264 +1,34 @@
1
- """SDK initialization module.
1
+ """SDK utilities module.
2
2
 
3
- This module handles SDK initialization, separating concerns from the main __init__.py
3
+ This module provides utility functions for session and context management.
4
+ The main entry point for the SDK is the LucidicAI class in client.py.
5
+
6
+ Note: Global session creation functions (create_session, init) have been removed.
7
+ Use the LucidicAI class for all SDK operations.
4
8
  """
5
- import uuid
6
- from typing import List, Optional
7
9
  import asyncio
8
10
  import threading
11
+ from typing import Optional
9
12
  from weakref import WeakKeyDictionary
10
13
 
11
- from ..api.client import HttpClient
12
- from ..api.resources.event import EventResource
13
- from ..api.resources.session import SessionResource
14
- from ..api.resources.dataset import DatasetResource
15
- from ..core.config import SDKConfig, get_config, set_config
16
- from ..utils.queue import EventQueue
17
- from ..utils.logger import debug, info, warning, error, truncate_id
18
- from .context import set_active_session, current_session_id
19
- from .error_boundary import register_cleanup_handler
20
- from .shutdown_manager import get_shutdown_manager, SessionState
21
- from ..telemetry.telemetry_init import instrument_providers
22
- from opentelemetry.sdk.trace import TracerProvider
23
-
24
-
25
- class SDKState:
26
- """Container for SDK runtime state."""
27
-
28
- def __init__(self):
29
- self.http: Optional[HttpClient] = None
30
- self.event_queue: Optional[EventQueue] = None
31
- self.session_id: Optional[str] = None
32
- self.tracer_provider: Optional[TracerProvider] = None
33
- self.resources = {}
34
- # Task-local storage for async task isolation
35
- self.task_sessions: WeakKeyDictionary = WeakKeyDictionary()
36
- # Thread-local storage for thread isolation
37
- self.thread_local = threading.local()
38
-
39
- def reset(self):
40
- """Reset SDK state."""
41
- # Shutdown telemetry first to ensure all spans are exported
42
- if self.tracer_provider:
43
- try:
44
- # Force flush all pending spans with 5 second timeout
45
- debug("[SDK] Flushing OpenTelemetry spans...")
46
- self.tracer_provider.force_flush(timeout_millis=5000)
47
- # Shutdown the tracer provider and all processors
48
- self.tracer_provider.shutdown()
49
- debug("[SDK] TracerProvider shutdown complete")
50
- except Exception as e:
51
- error(f"[SDK] Error shutting down TracerProvider: {e}")
52
-
53
- if self.event_queue:
54
- self.event_queue.shutdown()
55
- if self.http:
56
- self.http.close()
57
-
58
- self.http = None
59
- self.event_queue = None
60
- self.session_id = None
61
- self.tracer_provider = None
62
- self.resources = {}
63
- self.task_sessions.clear()
64
- # Clear thread-local storage for current thread
65
- if hasattr(self.thread_local, 'session_id'):
66
- delattr(self.thread_local, 'session_id')
67
-
68
-
69
- # Global SDK state
70
- _sdk_state = SDKState()
71
-
72
-
73
- def init(
74
- session_name: Optional[str] = None,
75
- session_id: Optional[str] = None,
76
- api_key: Optional[str] = None,
77
- agent_id: Optional[str] = None,
78
- task: Optional[str] = None,
79
- providers: Optional[List[str]] = None,
80
- production_monitoring: bool = False,
81
- experiment_id: Optional[str] = None,
82
- evaluators: Optional[List] = None,
83
- tags: Optional[List] = None,
84
- datasetitem_id: Optional[str] = None,
85
- masking_function: Optional[callable] = None,
86
- auto_end: bool = True,
87
- capture_uncaught: bool = True,
88
- ) -> str:
89
- """Initialize the Lucidic SDK.
90
-
91
- Args:
92
- session_name: Name for the session
93
- session_id: Custom session ID (optional)
94
- api_key: API key (uses env if not provided)
95
- agent_id: Agent ID (uses env if not provided)
96
- task: Task description
97
- providers: List of telemetry providers to instrument
98
- production_monitoring: Enable production monitoring
99
- experiment_id: Experiment ID to associate with session
100
- evaluators: Ealuators to use
101
- tags: Session tags
102
- datasetitem_id: Dataset item ID
103
- masking_function: Function to mask sensitive data
104
- auto_end: Automatically end session on exit
105
- capture_uncaught: Capture uncaught exceptions
106
-
107
- Returns:
108
- Session ID
109
-
110
- Raises:
111
- APIKeyVerificationError: If API credentials are invalid
112
- """
113
- global _sdk_state
114
-
115
- # Create or update configuration
116
- config = SDKConfig.from_env(
117
- api_key=api_key,
118
- agent_id=agent_id,
119
- auto_end=auto_end,
120
- production_monitoring=production_monitoring
121
- )
122
-
123
- if providers:
124
- config.telemetry.providers = providers
125
-
126
- config.error_handling.capture_uncaught = capture_uncaught
127
-
128
- # Validate configuration
129
- errors = config.validate()
130
- if errors:
131
- raise ValueError(f"Invalid configuration: {', '.join(errors)}")
132
-
133
- # Set global config
134
- set_config(config)
135
-
136
- # Initialize HTTP client
137
- if not _sdk_state.http:
138
- debug("[SDK] Initializing HTTP client")
139
- _sdk_state.http = HttpClient(config)
140
-
141
- # Initialize resources
142
- if not _sdk_state.resources:
143
- _sdk_state.resources = {
144
- 'events': EventResource(_sdk_state.http),
145
- 'sessions': SessionResource(_sdk_state.http),
146
- 'datasets': DatasetResource(_sdk_state.http)
147
- }
148
-
149
- # Initialize event queue
150
- if not _sdk_state.event_queue:
151
- debug("[SDK] Initializing event queue")
152
- # Create a mock client object for backward compatibility
153
- # The queue needs a client with make_request method
154
- class ClientAdapter:
155
- def make_request(self, endpoint, method, data):
156
- return _sdk_state.http.request(method, endpoint, json=data)
157
-
158
- _sdk_state.event_queue = EventQueue(ClientAdapter())
159
-
160
- # Register cleanup handler
161
- register_cleanup_handler(lambda: _sdk_state.event_queue.force_flush())
162
- debug("[SDK] Event queue initialized and cleanup handler registered")
163
-
164
- # Create or retrieve session
165
- if session_id:
166
- # Use provided session ID
167
- real_session_id = session_id
168
- else:
169
- # Create new session
170
- real_session_id = str(uuid.uuid4())
171
-
172
- # Create session via API - only send non-None values
173
- session_params = {
174
- 'session_id': real_session_id,
175
- 'session_name': session_name or 'Unnamed Session',
176
- 'agent_id': config.agent_id,
177
- }
178
-
179
- # Only add optional fields if they have values
180
- if task:
181
- session_params['task'] = task
182
- if tags:
183
- session_params['tags'] = tags
184
- if experiment_id:
185
- session_params['experiment_id'] = experiment_id
186
- if datasetitem_id:
187
- session_params['datasetitem_id'] = datasetitem_id
188
- if evaluators:
189
- session_params['evaluators'] = evaluators
190
- if production_monitoring:
191
- session_params['production_monitoring'] = production_monitoring
192
-
193
- debug(f"[SDK] Creating session with params: {session_params}")
194
- session_resource = _sdk_state.resources['sessions']
195
- session_data = session_resource.create_session(session_params)
196
-
197
- # Use the session_id returned by the backend
198
- real_session_id = session_data.get('session_id', real_session_id)
199
- _sdk_state.session_id = real_session_id
200
-
201
- info(f"[SDK] Session created: {truncate_id(real_session_id)} (name: {session_name or 'Unnamed Session'})")
202
-
203
- # Set active session in context
204
- set_active_session(real_session_id)
205
-
206
- # Register session with shutdown manager
207
- debug(f"[SDK] Registering session with shutdown manager (auto_end={auto_end})")
208
- shutdown_manager = get_shutdown_manager()
209
- session_state = SessionState(
210
- session_id=real_session_id,
211
- http_client=_sdk_state.resources, # Pass resources dict which has sessions
212
- event_queue=_sdk_state.event_queue,
213
- auto_end=auto_end
214
- )
215
- shutdown_manager.register_session(real_session_id, session_state)
216
-
217
- # Initialize telemetry if providers specified
218
- if providers:
219
- debug(f"[SDK] Initializing telemetry for providers: {providers}")
220
- _initialize_telemetry(providers)
221
-
222
- return real_session_id
223
-
224
-
225
- def _initialize_telemetry(providers: List[str]) -> None:
226
- """Initialize telemetry providers.
227
-
228
- Args:
229
- providers: List of provider names
230
- """
231
- global _sdk_state
232
-
233
- if not _sdk_state.tracer_provider:
234
- # Import here to avoid circular dependency
235
- from ..telemetry.lucidic_exporter import LucidicSpanExporter
236
- from ..telemetry.context_capture_processor import ContextCaptureProcessor
237
- from opentelemetry.sdk.trace.export import BatchSpanProcessor
238
-
239
- # Create tracer provider with our processors
240
- _sdk_state.tracer_provider = TracerProvider()
241
-
242
- # Add context capture processor FIRST to capture context before export
243
- context_processor = ContextCaptureProcessor()
244
- _sdk_state.tracer_provider.add_span_processor(context_processor)
245
-
246
- # Add exporter processor
247
- exporter = LucidicSpanExporter()
248
- export_processor = BatchSpanProcessor(exporter)
249
- _sdk_state.tracer_provider.add_span_processor(export_processor)
250
-
251
- # Instrument providers
252
- instrument_providers(providers, _sdk_state.tracer_provider, {})
253
-
254
- info(f"[Telemetry] Initialized for providers: {providers}")
14
+ from ..utils.logger import debug, truncate_id
15
+ from .context import current_session_id
16
+
17
+ # Module-level storage for async task isolation
18
+ _task_sessions: WeakKeyDictionary = WeakKeyDictionary()
19
+
20
+ # Module-level thread-local storage
21
+ _thread_local = threading.local()
22
+
23
+ # Reference to tracer provider (set by TelemetryManager)
24
+ _tracer_provider = None
255
25
 
256
26
 
257
27
  def set_task_session(session_id: str) -> None:
258
28
  """Set session ID for current async task (if in async context)."""
259
29
  try:
260
30
  if task := asyncio.current_task():
261
- _sdk_state.task_sessions[task] = session_id
31
+ _task_sessions[task] = session_id
262
32
  debug(f"[SDK] Set task-local session {truncate_id(session_id)} for task {task.get_name()}")
263
33
  except RuntimeError:
264
34
  # Not in async context, ignore
@@ -269,7 +39,7 @@ def clear_task_session() -> None:
269
39
  """Clear session ID for current async task (if in async context)."""
270
40
  try:
271
41
  if task := asyncio.current_task():
272
- _sdk_state.task_sessions.pop(task, None)
42
+ _task_sessions.pop(task, None)
273
43
  debug(f"[SDK] Cleared task-local session for task {task.get_name()}")
274
44
  except RuntimeError:
275
45
  # Not in async context, ignore
@@ -281,22 +51,22 @@ def set_thread_session(session_id: str) -> None:
281
51
 
282
52
  This provides true thread-local storage that doesn't inherit from parent thread.
283
53
  """
284
- _sdk_state.thread_local.session_id = session_id
54
+ _thread_local.session_id = session_id
285
55
  current_thread = threading.current_thread()
286
56
  debug(f"[SDK] Set thread-local session {truncate_id(session_id)} for thread {current_thread.name}")
287
57
 
288
58
 
289
59
  def clear_thread_session() -> None:
290
60
  """Clear session ID for current thread."""
291
- if hasattr(_sdk_state.thread_local, 'session_id'):
292
- delattr(_sdk_state.thread_local, 'session_id')
61
+ if hasattr(_thread_local, 'session_id'):
62
+ delattr(_thread_local, 'session_id')
293
63
  current_thread = threading.current_thread()
294
64
  debug(f"[SDK] Cleared thread-local session for thread {current_thread.name}")
295
65
 
296
66
 
297
67
  def get_thread_session() -> Optional[str]:
298
68
  """Get session ID from thread-local storage."""
299
- return getattr(_sdk_state.thread_local, 'session_id', None)
69
+ return getattr(_thread_local, 'session_id', None)
300
70
 
301
71
 
302
72
  def is_main_thread() -> bool:
@@ -310,13 +80,12 @@ def get_session_id() -> Optional[str]:
310
80
  Priority:
311
81
  1. Task-local session (for async tasks)
312
82
  2. Thread-local session (for threads) - NO FALLBACK for threads
313
- 3. SDK state session (for main thread)
314
- 4. Context variable session (fallback for main thread only)
83
+ 3. Context variable session (for main thread)
315
84
  """
316
85
  # First check task-local storage for async isolation
317
86
  try:
318
87
  if task := asyncio.current_task():
319
- if task_session := _sdk_state.task_sessions.get(task):
88
+ if task_session := _task_sessions.get(task):
320
89
  debug(f"[SDK] Using task-local session {truncate_id(task_session)}")
321
90
  return task_session
322
91
  except RuntimeError:
@@ -334,102 +103,35 @@ def get_session_id() -> Optional[str]:
334
103
  debug(f"[SDK] Thread {threading.current_thread().name} has no thread-local session")
335
104
  return thread_session # Return None if not set - don't fall back!
336
105
 
337
- # For main thread only: fall back to SDK state or context variable
338
- return _sdk_state.session_id or current_session_id.get()
339
-
106
+ # For main thread: use context variable
107
+ return current_session_id.get(None)
340
108
 
341
- def get_http() -> Optional[HttpClient]:
342
- """Get the HTTP client instance."""
343
- return _sdk_state.http
344
109
 
110
+ def get_tracer_provider():
111
+ """Get the tracer provider instance.
345
112
 
346
- def get_event_queue() -> Optional[EventQueue]:
347
- """Get the event queue instance."""
348
- return _sdk_state.event_queue
349
-
350
-
351
- def get_resources() -> dict:
352
- """Get API resource instances."""
353
- return _sdk_state.resources
354
-
113
+ Returns the tracer provider set by the TelemetryManager.
114
+ """
115
+ global _tracer_provider
116
+ if _tracer_provider is not None:
117
+ return _tracer_provider
355
118
 
356
- def set_http(http: HttpClient) -> None:
357
- """Set the HTTP client instance in SDK state."""
358
- global _sdk_state
359
- _sdk_state.http = http
119
+ # Try to get from TelemetryManager
120
+ try:
121
+ from ..telemetry.telemetry_manager import get_telemetry_manager
122
+ manager = get_telemetry_manager()
123
+ if manager._tracer_provider:
124
+ return manager._tracer_provider
125
+ except Exception:
126
+ pass
360
127
 
128
+ return None
361
129
 
362
- def set_resources(resources: dict) -> None:
363
- """Set API resource instances in SDK state."""
364
- global _sdk_state
365
- _sdk_state.resources = resources
366
130
 
131
+ def set_tracer_provider(provider) -> None:
132
+ """Set the tracer provider instance.
367
133
 
368
- def ensure_http_and_resources(api_key: Optional[str] = None, agent_id: Optional[str] = None) -> dict:
369
- """Ensure HTTP client and resources are initialized, creating them if needed.
370
-
371
- This function checks if the HTTP client and resources already exist in SDK state.
372
- If not, it creates them and stores them in SDK state for reuse.
373
-
374
- Args:
375
- api_key: API key (uses env if not provided)
376
- agent_id: Agent ID (uses env if not provided)
377
-
378
- Returns:
379
- Dictionary of API resources with 'datasets' key
380
-
381
- Raises:
382
- APIKeyVerificationError: If API key is not available
134
+ Called by TelemetryManager when initializing telemetry.
383
135
  """
384
- global _sdk_state
385
-
386
- # If we already have resources with datasets, return them
387
- if _sdk_state.resources and 'datasets' in _sdk_state.resources:
388
- return _sdk_state.resources
389
-
390
- # Need to create HTTP client and resources
391
- from dotenv import load_dotenv
392
- import os
393
- from ..core.errors import APIKeyVerificationError
394
-
395
- load_dotenv()
396
-
397
- # Get credentials
398
- if api_key is None:
399
- api_key = os.getenv("LUCIDIC_API_KEY", None)
400
- if api_key is None:
401
- raise APIKeyVerificationError(
402
- "Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
403
- )
404
-
405
- if agent_id is None:
406
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
407
-
408
- # Create or reuse HTTP client
409
- if not _sdk_state.http:
410
- debug("[SDK] Creating HTTP client for standalone use")
411
- config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
412
- _sdk_state.http = HttpClient(config)
413
-
414
- # Create resources if not already present
415
- if not _sdk_state.resources:
416
- _sdk_state.resources = {}
417
-
418
- if 'datasets' not in _sdk_state.resources:
419
- debug("[SDK] Creating DatasetResource for standalone use")
420
- _sdk_state.resources['datasets'] = DatasetResource(_sdk_state.http)
421
-
422
- return _sdk_state.resources
423
-
424
-
425
- def get_tracer_provider() -> Optional[TracerProvider]:
426
- """Get the tracer provider instance."""
427
- return _sdk_state.tracer_provider
428
-
429
-
430
- def clear_state() -> None:
431
- """Clear SDK state (for testing)."""
432
- global _sdk_state
433
- debug("[SDK] Clearing SDK state")
434
- _sdk_state.reset()
435
- _sdk_state = SDKState()
136
+ global _tracer_provider
137
+ _tracer_provider = provider