lucidicai 2.0.2__py3-none-any.whl → 2.1.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 (35) hide show
  1. lucidicai/__init__.py +350 -899
  2. lucidicai/api/__init__.py +1 -0
  3. lucidicai/api/client.py +218 -0
  4. lucidicai/api/resources/__init__.py +1 -0
  5. lucidicai/api/resources/dataset.py +192 -0
  6. lucidicai/api/resources/event.py +88 -0
  7. lucidicai/api/resources/session.py +126 -0
  8. lucidicai/core/__init__.py +1 -0
  9. lucidicai/core/config.py +223 -0
  10. lucidicai/core/errors.py +60 -0
  11. lucidicai/core/types.py +35 -0
  12. lucidicai/sdk/__init__.py +1 -0
  13. lucidicai/sdk/context.py +144 -0
  14. lucidicai/sdk/decorators.py +187 -0
  15. lucidicai/sdk/error_boundary.py +299 -0
  16. lucidicai/sdk/event.py +122 -0
  17. lucidicai/sdk/event_builder.py +304 -0
  18. lucidicai/sdk/features/__init__.py +1 -0
  19. lucidicai/sdk/features/dataset.py +605 -0
  20. lucidicai/sdk/features/feature_flag.py +383 -0
  21. lucidicai/sdk/init.py +271 -0
  22. lucidicai/sdk/shutdown_manager.py +302 -0
  23. lucidicai/telemetry/context_bridge.py +82 -0
  24. lucidicai/telemetry/context_capture_processor.py +25 -9
  25. lucidicai/telemetry/litellm_bridge.py +18 -24
  26. lucidicai/telemetry/lucidic_exporter.py +51 -36
  27. lucidicai/telemetry/utils/model_pricing.py +278 -0
  28. lucidicai/utils/__init__.py +1 -0
  29. lucidicai/utils/images.py +337 -0
  30. lucidicai/utils/logger.py +168 -0
  31. lucidicai/utils/queue.py +393 -0
  32. {lucidicai-2.0.2.dist-info → lucidicai-2.1.0.dist-info}/METADATA +1 -1
  33. {lucidicai-2.0.2.dist-info → lucidicai-2.1.0.dist-info}/RECORD +35 -8
  34. {lucidicai-2.0.2.dist-info → lucidicai-2.1.0.dist-info}/WHEEL +0 -0
  35. {lucidicai-2.0.2.dist-info → lucidicai-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,383 @@
1
+ import os
2
+ import logging
3
+ import time
4
+ from typing import Union, List, Dict, Any, Optional, overload, Tuple, Literal
5
+ from dotenv import load_dotenv
6
+
7
+ from ..init import get_http
8
+ from ...core.errors import APIKeyVerificationError, FeatureFlagError
9
+
10
+ logger = logging.getLogger("Lucidic")
11
+
12
+ # Cache implementation
13
+ class FeatureFlagCache:
14
+ def __init__(self):
15
+ self._cache: Dict[str, tuple[Any, float]] = {}
16
+ self._default_ttl = 300 # 5 minutes
17
+
18
+ def get(self, key: str) -> Optional[Any]:
19
+ if key in self._cache:
20
+ value, expiry = self._cache[key]
21
+ if time.time() < expiry:
22
+ return value
23
+ else:
24
+ del self._cache[key]
25
+ return None
26
+
27
+ def set(self, key: str, value: Any, ttl: int = None):
28
+ if ttl is None:
29
+ ttl = self._default_ttl
30
+ if ttl > 0:
31
+ self._cache[key] = (value, time.time() + ttl)
32
+
33
+ def clear(self):
34
+ self._cache.clear()
35
+
36
+ # Global cache instance
37
+ _flag_cache = FeatureFlagCache()
38
+
39
+ # Sentinel value to distinguish None from missing
40
+ MISSING = object()
41
+
42
+ # Function overloads for type safety
43
+ @overload
44
+ def get_feature_flag(
45
+ flag_name: str,
46
+ default: Any = ...,
47
+ *,
48
+ return_missing: Literal[False] = False,
49
+ cache_ttl: Optional[int] = 300,
50
+ api_key: Optional[str] = None,
51
+ agent_id: Optional[str] = None,
52
+ ) -> Any:
53
+ """Get a single feature flag."""
54
+ ...
55
+
56
+ @overload
57
+ def get_feature_flag(
58
+ flag_name: str,
59
+ default: Any = ...,
60
+ *,
61
+ return_missing: Literal[True],
62
+ cache_ttl: Optional[int] = 300,
63
+ api_key: Optional[str] = None,
64
+ agent_id: Optional[str] = None,
65
+ ) -> Tuple[Any, List[str]]:
66
+ """Get a single feature flag with missing info."""
67
+ ...
68
+
69
+ @overload
70
+ def get_feature_flag(
71
+ flag_name: List[str],
72
+ defaults: Optional[Dict[str, Any]] = None,
73
+ *,
74
+ return_missing: Literal[False] = False,
75
+ cache_ttl: Optional[int] = 300,
76
+ api_key: Optional[str] = None,
77
+ agent_id: Optional[str] = None,
78
+ ) -> Dict[str, Any]:
79
+ """Get multiple feature flags."""
80
+ ...
81
+
82
+ @overload
83
+ def get_feature_flag(
84
+ flag_name: List[str],
85
+ defaults: Optional[Dict[str, Any]] = None,
86
+ *,
87
+ return_missing: Literal[True],
88
+ cache_ttl: Optional[int] = 300,
89
+ api_key: Optional[str] = None,
90
+ agent_id: Optional[str] = None,
91
+ ) -> Tuple[Dict[str, Any], List[str]]:
92
+ """Get multiple feature flags with missing info."""
93
+ ...
94
+
95
+ def get_feature_flag(
96
+ flag_name: Union[str, List[str]],
97
+ default_or_defaults: Any = MISSING,
98
+ *,
99
+ return_missing: bool = False,
100
+ cache_ttl: Optional[int] = 300,
101
+ api_key: Optional[str] = None,
102
+ agent_id: Optional[str] = None,
103
+ ) -> Union[Any, Tuple[Any, List[str]], Dict[str, Any], Tuple[Dict[str, Any], List[str]]]:
104
+ """
105
+ Get feature flag(s) from backend. Raises FeatureFlagError on failure unless default provided.
106
+
107
+ Args:
108
+ flag_name: Single flag name (str) or list of flag names
109
+ default_or_defaults:
110
+ - If flag_name is str: default value for that flag (optional)
111
+ - If flag_name is List[str]: dict of defaults {flag_name: default_value}
112
+ cache_ttl: Cache time-to-live in seconds (0 to disable, -1 for forever)
113
+ api_key: Optional API key
114
+ agent_id: Optional agent ID
115
+
116
+ Returns:
117
+ - If flag_name is str: The flag value (or tuple with missing list if return_missing=True)
118
+ - If flag_name is List[str]: Dict mapping flag_name -> value (or tuple with missing list if return_missing=True)
119
+
120
+ Raises:
121
+ FeatureFlagError: If fetch fails and no default provided
122
+ APIKeyVerificationError: If credentials missing
123
+
124
+ Examples:
125
+ # Single flag with default
126
+ retries = lai.get_feature_flag("max_retries", default=3)
127
+
128
+ # Single flag without default (can raise)
129
+ retries = lai.get_feature_flag("max_retries")
130
+
131
+ # Multiple flags
132
+ flags = lai.get_feature_flag(
133
+ ["max_retries", "timeout"],
134
+ defaults={"max_retries": 3}
135
+ )
136
+ """
137
+
138
+ load_dotenv()
139
+
140
+ # Determine if single or batch
141
+ is_single = isinstance(flag_name, str)
142
+ flag_names = [flag_name] if is_single else flag_name
143
+
144
+ # Parse defaults
145
+ if is_single:
146
+ has_default = default_or_defaults is not MISSING
147
+ defaults = {flag_name: default_or_defaults} if has_default else {}
148
+ else:
149
+ defaults = default_or_defaults if default_or_defaults not in (None, MISSING) else {}
150
+
151
+ # Track missing flags
152
+ missing_flags = []
153
+
154
+ # Check cache first
155
+ uncached_flags = []
156
+ cached_results = {}
157
+
158
+ if cache_ttl != 0:
159
+ for name in flag_names:
160
+ cache_key = f"{agent_id}:{name}"
161
+ cached_value = _flag_cache.get(cache_key)
162
+ if cached_value is not None:
163
+ cached_results[name] = cached_value
164
+ else:
165
+ uncached_flags.append(name)
166
+ else:
167
+ uncached_flags = flag_names
168
+
169
+ # Fetch uncached flags if needed
170
+ if uncached_flags:
171
+ # Get credentials
172
+ if api_key is None:
173
+ api_key = os.getenv("LUCIDIC_API_KEY", None)
174
+ if api_key is None:
175
+ raise APIKeyVerificationError(
176
+ "Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
177
+ )
178
+
179
+ if agent_id is None:
180
+ agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
181
+ if agent_id is None:
182
+ raise APIKeyVerificationError(
183
+ "Lucidic agent ID not specified. Make sure to either pass your agent ID or set the LUCIDIC_AGENT_ID environment variable."
184
+ )
185
+
186
+ # Get HTTP client
187
+ http = get_http()
188
+ if not http:
189
+ from ..init import init
190
+ init(api_key=api_key, agent_id=agent_id)
191
+ http = get_http()
192
+
193
+ # check for active session
194
+ from ..init import get_session_id
195
+ session_id = get_session_id()
196
+
197
+ try:
198
+ if len(uncached_flags) == 1:
199
+ # Single flag evaluation
200
+ if session_id:
201
+ # Use session-based evaluation for consistency
202
+ response = http.post('evaluatefeatureflag', {
203
+ 'session_id': session_id,
204
+ 'flag_name': uncached_flags[0],
205
+ 'context': {},
206
+ 'default': defaults.get(uncached_flags[0])
207
+ })
208
+ else:
209
+ # Use stateless evaluation as fallback
210
+ response = http.post('evaluatefeatureflagstateless', {
211
+ 'agent_id': agent_id,
212
+ 'flag_name': uncached_flags[0],
213
+ 'context': {},
214
+ 'default': defaults.get(uncached_flags[0])
215
+ })
216
+
217
+ # Extract value from response
218
+ if 'value' in response:
219
+ value = response['value']
220
+ cached_results[uncached_flags[0]] = value
221
+
222
+ # Cache the result
223
+ if cache_ttl != 0:
224
+ cache_key = f"{agent_id}:{uncached_flags[0]}"
225
+ _flag_cache.set(cache_key, value, ttl=cache_ttl if cache_ttl > 0 else None)
226
+ elif 'error' in response:
227
+ # Flag not found or error
228
+ logger.warning(f"Feature flag error: {response['error']}")
229
+ missing_flags.append(uncached_flags[0])
230
+
231
+ else:
232
+ # Batch evaluation
233
+ if session_id:
234
+ # Use session-based batch evaluation
235
+ response = http.post('evaluatebatchfeatureflags', {
236
+ 'session_id': session_id,
237
+ 'flag_names': uncached_flags,
238
+ 'context': {},
239
+ 'defaults': {k: v for k, v in defaults.items() if k in uncached_flags}
240
+ })
241
+ else:
242
+ # Use stateless batch evaluation
243
+ response = http.post('evaluatebatchfeatureflagsstateless', {
244
+ 'agent_id': agent_id,
245
+ 'flag_names': uncached_flags,
246
+ 'context': {},
247
+ 'defaults': {k: v for k, v in defaults.items() if k in uncached_flags}
248
+ })
249
+
250
+ # Process batch response
251
+ if 'flags' in response:
252
+ for name in uncached_flags:
253
+ flag_data = response['flags'].get(name)
254
+ if flag_data and 'value' in flag_data:
255
+ value = flag_data['value']
256
+ cached_results[name] = value
257
+
258
+ # Cache it
259
+ if cache_ttl != 0:
260
+ cache_key = f"{agent_id}:{name}"
261
+ _flag_cache.set(cache_key, value, ttl=cache_ttl if cache_ttl > 0 else None)
262
+ else:
263
+ missing_flags.append(name)
264
+
265
+ except Exception as e:
266
+ # HTTP client raises on errors, fall back to defaults
267
+ logger.error(f"Failed to fetch feature flags: {e}")
268
+
269
+ # Use defaults for all uncached flags
270
+ for name in uncached_flags:
271
+ if name in defaults:
272
+ cached_results[name] = defaults[name]
273
+ else:
274
+ missing_flags.append(name)
275
+ if is_single and not return_missing:
276
+ raise FeatureFlagError(f"'{name}': {e}") from e
277
+
278
+ # Build final result
279
+ result = {}
280
+ for name in flag_names:
281
+ if name in cached_results:
282
+ result[name] = cached_results[name]
283
+ elif name in defaults:
284
+ result[name] = defaults[name]
285
+ else:
286
+ # No value and no default
287
+ missing_flags.append(name)
288
+ if is_single and not return_missing:
289
+ raise FeatureFlagError(f"'{name}' not found and no default provided")
290
+ else:
291
+ result[name] = None
292
+
293
+ # Return based on input type and return_missing flag
294
+ if return_missing:
295
+ return (result[flag_names[0]] if is_single else result, missing_flags)
296
+ else:
297
+ return result[flag_names[0]] if is_single else result
298
+
299
+
300
+ # Typed convenience functions
301
+ def get_bool_flag(flag_name: str, default: Optional[bool] = None, **kwargs) -> bool:
302
+ """
303
+ Get a boolean feature flag with type validation.
304
+
305
+ Raises:
306
+ FeatureFlagError: If fetch fails and no default provided
307
+ TypeError: If flag value is not a boolean
308
+ """
309
+ value = get_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
310
+ if not isinstance(value, bool):
311
+ if default is not None:
312
+ logger.warning(f"Feature flag '{flag_name}' is not a boolean, using default")
313
+ return default
314
+ raise TypeError(f"Feature flag '{flag_name}' expected boolean, got {type(value).__name__}")
315
+ return value
316
+
317
+
318
+ def get_int_flag(flag_name: str, default: Optional[int] = None, **kwargs) -> int:
319
+ """
320
+ Get an integer feature flag with type validation.
321
+
322
+ Raises:
323
+ FeatureFlagError: If fetch fails and no default provided
324
+ TypeError: If flag value is not an integer
325
+ """
326
+ value = get_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
327
+ if not isinstance(value, int) or isinstance(value, bool): # bool is subclass of int
328
+ if default is not None:
329
+ logger.warning(f"Feature flag '{flag_name}' is not an integer, using default")
330
+ return default
331
+ raise TypeError(f"Feature flag '{flag_name}' expected integer, got {type(value).__name__}")
332
+ return value
333
+
334
+
335
+ def get_float_flag(flag_name: str, default: Optional[float] = None, **kwargs) -> float:
336
+ """
337
+ Get a float feature flag with type validation.
338
+
339
+ Raises:
340
+ FeatureFlagError: If fetch fails and no default provided
341
+ TypeError: If flag value is not a float
342
+ """
343
+ value = get_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
344
+ if not isinstance(value, (int, float)) or isinstance(value, bool):
345
+ if default is not None:
346
+ logger.warning(f"Feature flag '{flag_name}' is not a float, using default")
347
+ return default
348
+ raise TypeError(f"Feature flag '{flag_name}' expected float, got {type(value).__name__}")
349
+ return float(value)
350
+
351
+
352
+ def get_string_flag(flag_name: str, default: Optional[str] = None, **kwargs) -> str:
353
+ """
354
+ Get a string feature flag with type validation.
355
+
356
+ Raises:
357
+ FeatureFlagError: If fetch fails and no default provided
358
+ TypeError: If flag value is not a string
359
+ """
360
+ value = get_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
361
+ if not isinstance(value, str):
362
+ if default is not None:
363
+ logger.warning(f"Feature flag '{flag_name}' is not a string, using default")
364
+ return default
365
+ raise TypeError(f"Feature flag '{flag_name}' expected string, got {type(value).__name__}")
366
+ return value
367
+
368
+
369
+ def get_json_flag(flag_name: str, default: Optional[dict] = None, **kwargs) -> dict:
370
+ """
371
+ Get a JSON object feature flag.
372
+
373
+ Raises:
374
+ FeatureFlagError: If fetch fails and no default provided
375
+ """
376
+ value = get_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
377
+ return value
378
+
379
+
380
+ def clear_feature_flag_cache():
381
+ """Clear the feature flag cache."""
382
+ _flag_cache.clear()
383
+ logger.debug("Feature flag cache cleared")
lucidicai/sdk/init.py ADDED
@@ -0,0 +1,271 @@
1
+ """SDK initialization module.
2
+
3
+ This module handles SDK initialization, separating concerns from the main __init__.py
4
+ """
5
+ import uuid
6
+ from typing import List, Optional
7
+
8
+ from ..api.client import HttpClient
9
+ from ..api.resources.event import EventResource
10
+ from ..api.resources.session import SessionResource
11
+ from ..api.resources.dataset import DatasetResource
12
+ from ..core.config import SDKConfig, get_config, set_config
13
+ from ..utils.queue import EventQueue
14
+ from ..utils.logger import debug, info, warning, error, truncate_id
15
+ from .context import set_active_session, current_session_id
16
+ from .error_boundary import register_cleanup_handler
17
+ from .shutdown_manager import get_shutdown_manager, SessionState
18
+ from ..telemetry.telemetry_init import instrument_providers
19
+ from opentelemetry.sdk.trace import TracerProvider
20
+
21
+
22
+ class SDKState:
23
+ """Container for SDK runtime state."""
24
+
25
+ def __init__(self):
26
+ self.http: Optional[HttpClient] = None
27
+ self.event_queue: Optional[EventQueue] = None
28
+ self.session_id: Optional[str] = None
29
+ self.tracer_provider: Optional[TracerProvider] = None
30
+ self.resources = {}
31
+
32
+ def reset(self):
33
+ """Reset SDK state."""
34
+ # Shutdown telemetry first to ensure all spans are exported
35
+ if self.tracer_provider:
36
+ try:
37
+ # Force flush all pending spans with 5 second timeout
38
+ debug("[SDK] Flushing OpenTelemetry spans...")
39
+ self.tracer_provider.force_flush(timeout_millis=5000)
40
+ # Shutdown the tracer provider and all processors
41
+ self.tracer_provider.shutdown()
42
+ debug("[SDK] TracerProvider shutdown complete")
43
+ except Exception as e:
44
+ error(f"[SDK] Error shutting down TracerProvider: {e}")
45
+
46
+ if self.event_queue:
47
+ self.event_queue.shutdown()
48
+ if self.http:
49
+ self.http.close()
50
+
51
+ self.http = None
52
+ self.event_queue = None
53
+ self.session_id = None
54
+ self.tracer_provider = None
55
+ self.resources = {}
56
+
57
+
58
+ # Global SDK state
59
+ _sdk_state = SDKState()
60
+
61
+
62
+ def init(
63
+ session_name: Optional[str] = None,
64
+ session_id: Optional[str] = None,
65
+ api_key: Optional[str] = None,
66
+ agent_id: Optional[str] = None,
67
+ task: Optional[str] = None,
68
+ providers: Optional[List[str]] = None,
69
+ production_monitoring: bool = False,
70
+ experiment_id: Optional[str] = None,
71
+ evaluators: Optional[List] = None,
72
+ tags: Optional[List] = None,
73
+ datasetitem_id: Optional[str] = None,
74
+ masking_function: Optional[callable] = None,
75
+ auto_end: bool = True,
76
+ capture_uncaught: bool = True,
77
+ ) -> str:
78
+ """Initialize the Lucidic SDK.
79
+
80
+ Args:
81
+ session_name: Name for the session
82
+ session_id: Custom session ID (optional)
83
+ api_key: API key (uses env if not provided)
84
+ agent_id: Agent ID (uses env if not provided)
85
+ task: Task description
86
+ providers: List of telemetry providers to instrument
87
+ production_monitoring: Enable production monitoring
88
+ experiment_id: Experiment ID to associate with session
89
+ evaluators: Ealuators to use
90
+ tags: Session tags
91
+ datasetitem_id: Dataset item ID
92
+ masking_function: Function to mask sensitive data
93
+ auto_end: Automatically end session on exit
94
+ capture_uncaught: Capture uncaught exceptions
95
+
96
+ Returns:
97
+ Session ID
98
+
99
+ Raises:
100
+ APIKeyVerificationError: If API credentials are invalid
101
+ """
102
+ global _sdk_state
103
+
104
+ # Create or update configuration
105
+ config = SDKConfig.from_env(
106
+ api_key=api_key,
107
+ agent_id=agent_id,
108
+ auto_end=auto_end,
109
+ production_monitoring=production_monitoring
110
+ )
111
+
112
+ if providers:
113
+ config.telemetry.providers = providers
114
+
115
+ config.error_handling.capture_uncaught = capture_uncaught
116
+
117
+ # Validate configuration
118
+ errors = config.validate()
119
+ if errors:
120
+ raise ValueError(f"Invalid configuration: {', '.join(errors)}")
121
+
122
+ # Set global config
123
+ set_config(config)
124
+
125
+ # Initialize HTTP client
126
+ if not _sdk_state.http:
127
+ debug("[SDK] Initializing HTTP client")
128
+ _sdk_state.http = HttpClient(config)
129
+
130
+ # Initialize resources
131
+ if not _sdk_state.resources:
132
+ _sdk_state.resources = {
133
+ 'events': EventResource(_sdk_state.http),
134
+ 'sessions': SessionResource(_sdk_state.http),
135
+ 'datasets': DatasetResource(_sdk_state.http)
136
+ }
137
+
138
+ # Initialize event queue
139
+ if not _sdk_state.event_queue:
140
+ debug("[SDK] Initializing event queue")
141
+ # Create a mock client object for backward compatibility
142
+ # The queue needs a client with make_request method
143
+ class ClientAdapter:
144
+ def make_request(self, endpoint, method, data):
145
+ return _sdk_state.http.request(method, endpoint, json=data)
146
+
147
+ _sdk_state.event_queue = EventQueue(ClientAdapter())
148
+
149
+ # Register cleanup handler
150
+ register_cleanup_handler(lambda: _sdk_state.event_queue.force_flush())
151
+ debug("[SDK] Event queue initialized and cleanup handler registered")
152
+
153
+ # Create or retrieve session
154
+ if session_id:
155
+ # Use provided session ID
156
+ real_session_id = session_id
157
+ else:
158
+ # Create new session
159
+ real_session_id = str(uuid.uuid4())
160
+
161
+ # Create session via API - only send non-None values
162
+ session_params = {
163
+ 'session_id': real_session_id,
164
+ 'session_name': session_name or 'Unnamed Session',
165
+ 'agent_id': config.agent_id,
166
+ }
167
+
168
+ # Only add optional fields if they have values
169
+ if task:
170
+ session_params['task'] = task
171
+ if tags:
172
+ session_params['tags'] = tags
173
+ if experiment_id:
174
+ session_params['experiment_id'] = experiment_id
175
+ if datasetitem_id:
176
+ session_params['datasetitem_id'] = datasetitem_id
177
+ if evaluators:
178
+ session_params['evaluators'] = evaluators
179
+ if production_monitoring:
180
+ session_params['production_monitoring'] = production_monitoring
181
+
182
+ debug(f"[SDK] Creating session with params: {session_params}")
183
+ session_resource = _sdk_state.resources['sessions']
184
+ session_data = session_resource.create_session(session_params)
185
+
186
+ # Use the session_id returned by the backend
187
+ real_session_id = session_data.get('session_id', real_session_id)
188
+ _sdk_state.session_id = real_session_id
189
+
190
+ info(f"[SDK] Session created: {truncate_id(real_session_id)} (name: {session_name or 'Unnamed Session'})")
191
+
192
+ # Set active session in context
193
+ set_active_session(real_session_id)
194
+
195
+ # Register session with shutdown manager
196
+ debug(f"[SDK] Registering session with shutdown manager (auto_end={auto_end})")
197
+ shutdown_manager = get_shutdown_manager()
198
+ session_state = SessionState(
199
+ session_id=real_session_id,
200
+ http_client=_sdk_state.resources, # Pass resources dict which has sessions
201
+ event_queue=_sdk_state.event_queue,
202
+ auto_end=auto_end
203
+ )
204
+ shutdown_manager.register_session(real_session_id, session_state)
205
+
206
+ # Initialize telemetry if providers specified
207
+ if providers:
208
+ debug(f"[SDK] Initializing telemetry for providers: {providers}")
209
+ _initialize_telemetry(providers)
210
+
211
+ return real_session_id
212
+
213
+
214
+ def _initialize_telemetry(providers: List[str]) -> None:
215
+ """Initialize telemetry providers.
216
+
217
+ Args:
218
+ providers: List of provider names
219
+ """
220
+ global _sdk_state
221
+
222
+ if not _sdk_state.tracer_provider:
223
+ # Import here to avoid circular dependency
224
+ from ..telemetry.lucidic_exporter import LucidicSpanExporter
225
+ from ..telemetry.context_capture_processor import ContextCaptureProcessor
226
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
227
+
228
+ # Create tracer provider with our processors
229
+ _sdk_state.tracer_provider = TracerProvider()
230
+
231
+ # Add context capture processor FIRST to capture context before export
232
+ context_processor = ContextCaptureProcessor()
233
+ _sdk_state.tracer_provider.add_span_processor(context_processor)
234
+
235
+ # Add exporter processor
236
+ exporter = LucidicSpanExporter()
237
+ export_processor = BatchSpanProcessor(exporter)
238
+ _sdk_state.tracer_provider.add_span_processor(export_processor)
239
+
240
+ # Instrument providers
241
+ instrument_providers(providers, _sdk_state.tracer_provider, {})
242
+
243
+ info(f"[Telemetry] Initialized for providers: {providers}")
244
+
245
+
246
+ def get_session_id() -> Optional[str]:
247
+ """Get the current session ID."""
248
+ return _sdk_state.session_id or current_session_id.get()
249
+
250
+
251
+ def get_http() -> Optional[HttpClient]:
252
+ """Get the HTTP client instance."""
253
+ return _sdk_state.http
254
+
255
+
256
+ def get_event_queue() -> Optional[EventQueue]:
257
+ """Get the event queue instance."""
258
+ return _sdk_state.event_queue
259
+
260
+
261
+ def get_resources() -> dict:
262
+ """Get API resource instances."""
263
+ return _sdk_state.resources
264
+
265
+
266
+ def clear_state() -> None:
267
+ """Clear SDK state (for testing)."""
268
+ global _sdk_state
269
+ debug("[SDK] Clearing SDK state")
270
+ _sdk_state.reset()
271
+ _sdk_state = SDKState()