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,299 @@
1
+ """Error boundary pattern for SDK error suppression.
2
+
3
+ Inspired by the TypeScript SDK's error-boundary.ts, this module provides
4
+ a clean, centralized way to handle SDK errors without affecting user code.
5
+ """
6
+ import functools
7
+ import logging
8
+ import threading
9
+ import traceback
10
+ import uuid
11
+ from dataclasses import dataclass
12
+ from datetime import datetime
13
+ from typing import Any, Callable, Dict, List, Optional, TypeVar
14
+
15
+ from ..core.config import get_config
16
+
17
+ logger = logging.getLogger("Lucidic")
18
+
19
+ F = TypeVar('F', bound=Callable[..., Any])
20
+
21
+
22
+ @dataclass
23
+ class ErrorContext:
24
+ """Context information about an SDK error."""
25
+ timestamp: datetime
26
+ module: str
27
+ function: str
28
+ error_type: str
29
+ error_message: str
30
+ traceback: str
31
+ suppressed: bool = True
32
+
33
+
34
+ class ErrorBoundary:
35
+ """Centralized error boundary for the SDK.
36
+
37
+ This class manages all error suppression, logging, and cleanup
38
+ in a single place, similar to the TypeScript implementation.
39
+ """
40
+
41
+ def __init__(self):
42
+ self.error_history: List[ErrorContext] = []
43
+ self.max_history_size = 100
44
+ self.cleanup_handlers: List[Callable] = []
45
+ self._lock = threading.Lock()
46
+ self._config = None # Lazy load config
47
+
48
+ def wrap_function(self, func: F, module: str = "unknown") -> F:
49
+ """Wrap a function with error boundary protection.
50
+
51
+ Args:
52
+ func: Function to wrap
53
+ module: Module name for error context
54
+
55
+ Returns:
56
+ Wrapped function that suppresses errors based on config
57
+ """
58
+ @functools.wraps(func)
59
+ def sync_wrapper(*args, **kwargs):
60
+ if not self.is_silent_mode():
61
+ return func(*args, **kwargs)
62
+
63
+ try:
64
+ return func(*args, **kwargs)
65
+ except Exception as e:
66
+ return self._handle_error(e, module, func.__name__, args, kwargs)
67
+
68
+ @functools.wraps(func)
69
+ async def async_wrapper(*args, **kwargs):
70
+ if not self.is_silent_mode():
71
+ return await func(*args, **kwargs)
72
+
73
+ try:
74
+ return await func(*args, **kwargs)
75
+ except Exception as e:
76
+ return self._handle_error(e, module, func.__name__, args, kwargs)
77
+
78
+ # Return appropriate wrapper based on function type
79
+ import asyncio
80
+ if asyncio.iscoroutinefunction(func):
81
+ return async_wrapper # type: ignore
82
+ return sync_wrapper # type: ignore
83
+
84
+ def wrap_module(self, module_dict: Dict[str, Any], module_name: str) -> Dict[str, Any]:
85
+ """Wrap all functions in a module dictionary.
86
+
87
+ Args:
88
+ module_dict: Dictionary of module exports
89
+ module_name: Name of the module
90
+
91
+ Returns:
92
+ Dictionary with all functions wrapped
93
+ """
94
+ if not self.is_silent_mode():
95
+ return module_dict
96
+
97
+ wrapped = {}
98
+ for name, obj in module_dict.items():
99
+ if callable(obj) and not name.startswith('_'):
100
+ wrapped[name] = self.wrap_function(obj, module_name)
101
+ else:
102
+ wrapped[name] = obj
103
+
104
+ return wrapped
105
+
106
+ @property
107
+ def config(self):
108
+ """Lazy load configuration."""
109
+ if self._config is None:
110
+ self._config = get_config()
111
+ return self._config
112
+
113
+ def is_silent_mode(self) -> bool:
114
+ """Check if SDK is in silent mode (error suppression enabled)."""
115
+ return self.config.error_handling.suppress_errors
116
+
117
+ def _handle_error(
118
+ self,
119
+ error: Exception,
120
+ module: str,
121
+ function: str,
122
+ args: tuple,
123
+ kwargs: dict
124
+ ) -> Any:
125
+ """Handle an error that occurred in SDK code.
126
+
127
+ Args:
128
+ error: The exception that occurred
129
+ module: Module where error occurred
130
+ function: Function where error occurred
131
+ args: Function arguments
132
+ kwargs: Function keyword arguments
133
+
134
+ Returns:
135
+ Default return value for the function
136
+ """
137
+ # Create error context
138
+ context = ErrorContext(
139
+ timestamp=datetime.now(),
140
+ module=module,
141
+ function=function,
142
+ error_type=type(error).__name__,
143
+ error_message=str(error),
144
+ traceback=traceback.format_exc(),
145
+ suppressed=True
146
+ )
147
+
148
+ # Add to history
149
+ with self._lock:
150
+ self.error_history.append(context)
151
+ if len(self.error_history) > self.max_history_size:
152
+ self.error_history.pop(0)
153
+
154
+ # Log if configured
155
+ if self.config.error_handling.log_suppressed:
156
+ logger.debug(
157
+ f"[ErrorBoundary] Suppressed {context.error_type} in {module}.{function}: "
158
+ f"{context.error_message}"
159
+ )
160
+ if self.config.debug:
161
+ logger.debug(f"[ErrorBoundary] Traceback:\n{context.traceback}")
162
+
163
+ # Perform cleanup if configured
164
+ if self.config.error_handling.cleanup_on_error:
165
+ self._perform_cleanup()
166
+
167
+ # Return appropriate default
168
+ return self._get_default_return(function)
169
+
170
+ def _get_default_return(self, function_name: str) -> Any:
171
+ """Get appropriate default return value for a function.
172
+
173
+ Args:
174
+ function_name: Name of the function
175
+
176
+ Returns:
177
+ Appropriate default value based on function name
178
+ """
179
+ # init function should return a fallback session ID
180
+ if function_name == 'init':
181
+ return f'fallback-session-{uuid.uuid4()}'
182
+
183
+ # Functions that return IDs
184
+ if any(x in function_name.lower() for x in ['id', 'create_experiment']):
185
+ return str(uuid.uuid4())
186
+
187
+ # Functions that return booleans
188
+ if any(x in function_name.lower() for x in ['is_', 'has_', 'can_', 'should_']):
189
+ return False
190
+
191
+ # Functions that return data
192
+ if function_name.lower().startswith('get_'):
193
+ if 'dataset' in function_name.lower():
194
+ return {}
195
+ elif 'prompt' in function_name.lower():
196
+ return ""
197
+ return None
198
+
199
+ # Default
200
+ return None
201
+
202
+ def _perform_cleanup(self) -> None:
203
+ """Perform cleanup after an error."""
204
+ # Run registered cleanup handlers
205
+ for handler in self.cleanup_handlers:
206
+ try:
207
+ handler()
208
+ except Exception as e:
209
+ logger.debug(f"[ErrorBoundary] Cleanup handler failed: {e}")
210
+
211
+ def register_cleanup_handler(self, handler: Callable) -> None:
212
+ """Register a cleanup handler to run on errors.
213
+
214
+ Args:
215
+ handler: Cleanup function to register
216
+ """
217
+ self.cleanup_handlers.append(handler)
218
+
219
+ def get_error_history(self) -> List[ErrorContext]:
220
+ """Get the error history.
221
+
222
+ Returns:
223
+ List of error contexts
224
+ """
225
+ with self._lock:
226
+ return list(self.error_history)
227
+
228
+ def clear_error_history(self) -> None:
229
+ """Clear the error history."""
230
+ with self._lock:
231
+ self.error_history.clear()
232
+
233
+ def reset_config(self) -> None:
234
+ """Reset cached configuration (for testing)."""
235
+ self._config = None
236
+
237
+
238
+ # Global error boundary instance
239
+ _error_boundary: Optional[ErrorBoundary] = None
240
+
241
+
242
+ def get_error_boundary() -> ErrorBoundary:
243
+ """Get the global error boundary instance."""
244
+ global _error_boundary
245
+ if _error_boundary is None:
246
+ _error_boundary = ErrorBoundary()
247
+ return _error_boundary
248
+
249
+
250
+ def wrap_sdk_function(func: F, module: str = "unknown") -> F:
251
+ """Wrap an SDK function with error boundary protection.
252
+
253
+ Args:
254
+ func: Function to wrap
255
+ module: Module name for error context
256
+
257
+ Returns:
258
+ Wrapped function
259
+ """
260
+ return get_error_boundary().wrap_function(func, module)
261
+
262
+
263
+ def wrap_sdk_module(module_dict: Dict[str, Any], module_name: str) -> Dict[str, Any]:
264
+ """Wrap all functions in an SDK module.
265
+
266
+ Args:
267
+ module_dict: Dictionary of module exports
268
+ module_name: Name of the module
269
+
270
+ Returns:
271
+ Dictionary with wrapped functions
272
+ """
273
+ return get_error_boundary().wrap_module(module_dict, module_name)
274
+
275
+
276
+ def is_silent_mode() -> bool:
277
+ """Check if SDK is in silent mode."""
278
+ return get_error_boundary().is_silent_mode()
279
+
280
+
281
+
282
+
283
+ def get_error_history() -> List[ErrorContext]:
284
+ """Get the SDK error history."""
285
+ return get_error_boundary().get_error_history()
286
+
287
+
288
+ def clear_error_history() -> None:
289
+ """Clear the SDK error history."""
290
+ get_error_boundary().clear_error_history()
291
+
292
+
293
+ def register_cleanup_handler(handler: Callable) -> None:
294
+ """Register a cleanup handler.
295
+
296
+ Args:
297
+ handler: Cleanup function to run on errors
298
+ """
299
+ get_error_boundary().register_cleanup_handler(handler)
lucidicai/sdk/event.py ADDED
@@ -0,0 +1,122 @@
1
+ """SDK event creation and management."""
2
+ import uuid
3
+ from datetime import datetime, timezone
4
+ from typing import Any, Dict, Optional, Union
5
+
6
+ from .context import current_parent_event_id
7
+ from ..core.config import get_config
8
+ from .event_builder import EventBuilder
9
+ from ..utils.logger import debug, truncate_id
10
+
11
+
12
+ def create_event(
13
+ type: str = "generic",
14
+ event_id: Optional[str] = None,
15
+ **kwargs
16
+ ) -> str:
17
+ """Create a new event.
18
+
19
+ Args:
20
+ type: Event type (llm_generation, function_call, error_traceback, generic)
21
+ event_id: Optional client event ID (will generate if not provided)
22
+ **kwargs: Event-specific fields
23
+
24
+ Returns:
25
+ Event ID (client-generated or provided UUID)
26
+ """
27
+ # Import here to avoid circular dependency
28
+ from ..sdk.init import get_session_id, get_event_queue
29
+
30
+ # Get current session
31
+ session_id = get_session_id()
32
+ if not session_id:
33
+ # No active session, return dummy ID
34
+ debug("[Event] No active session, returning dummy event ID")
35
+ return str(uuid.uuid4())
36
+
37
+ # Get parent event ID from context
38
+ parent_event_id = None
39
+ try:
40
+ parent_event_id = current_parent_event_id.get()
41
+ except Exception:
42
+ pass
43
+
44
+ # Use provided event ID or generate new one
45
+ client_event_id = event_id or str(uuid.uuid4())
46
+
47
+ # Build parameters for EventBuilder
48
+ params = {
49
+ 'type': type,
50
+ 'event_id': client_event_id,
51
+ 'parent_event_id': parent_event_id,
52
+ 'session_id': session_id,
53
+ 'occurred_at': kwargs.get('occurred_at') or datetime.now(timezone.utc).isoformat(),
54
+ **kwargs # Include all other kwargs
55
+ }
56
+
57
+ # Use EventBuilder to create normalized event request
58
+ event_request = EventBuilder.build(params)
59
+
60
+ debug(f"[Event] Creating {type} event {truncate_id(client_event_id)} (parent: {truncate_id(parent_event_id)}, session: {truncate_id(session_id)})")
61
+
62
+ # Queue event for async sending
63
+ event_queue = get_event_queue()
64
+ if event_queue:
65
+ event_queue.queue_event(event_request)
66
+
67
+ return client_event_id
68
+
69
+
70
+
71
+ def create_error_event(
72
+ error: Union[str, Exception],
73
+ parent_event_id: Optional[str] = None,
74
+ **kwargs
75
+ ) -> str:
76
+ """Create an error traceback event.
77
+
78
+ This is a convenience function for creating error events with proper
79
+ traceback information.
80
+
81
+ Args:
82
+ error: The error message or exception object
83
+ parent_event_id: Optional parent event ID for nesting
84
+ **kwargs: Additional event parameters
85
+
86
+ Returns:
87
+ Event ID of the created error event
88
+ """
89
+ import traceback
90
+
91
+ if isinstance(error, Exception):
92
+ error_str = str(error)
93
+ traceback_str = traceback.format_exc()
94
+ else:
95
+ error_str = str(error)
96
+ traceback_str = kwargs.pop('traceback', '')
97
+
98
+ return create_event(
99
+ type="error_traceback",
100
+ error=error_str,
101
+ traceback=traceback_str,
102
+ parent_event_id=parent_event_id,
103
+ **kwargs
104
+ )
105
+
106
+
107
+ def flush(timeout_seconds: float = 2.0) -> bool:
108
+ """Flush pending events.
109
+
110
+ Args:
111
+ timeout_seconds: Maximum time to wait for flush
112
+
113
+ Returns:
114
+ True if flush completed, False if timeout
115
+ """
116
+ from ..sdk.init import get_event_queue
117
+ event_queue = get_event_queue()
118
+ if event_queue:
119
+ debug(f"[Event] Forcing flush with {timeout_seconds}s timeout")
120
+ event_queue.force_flush(timeout_seconds)
121
+ return True
122
+ return False