lucidicai 2.0.1__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.
- lucidicai/__init__.py +351 -876
- lucidicai/api/__init__.py +1 -0
- lucidicai/api/client.py +218 -0
- lucidicai/api/resources/__init__.py +1 -0
- lucidicai/api/resources/dataset.py +192 -0
- lucidicai/api/resources/event.py +88 -0
- lucidicai/api/resources/session.py +126 -0
- lucidicai/client.py +4 -1
- lucidicai/core/__init__.py +1 -0
- lucidicai/core/config.py +223 -0
- lucidicai/core/errors.py +60 -0
- lucidicai/core/types.py +35 -0
- lucidicai/dataset.py +2 -0
- lucidicai/errors.py +6 -0
- lucidicai/feature_flag.py +8 -0
- lucidicai/sdk/__init__.py +1 -0
- lucidicai/sdk/context.py +144 -0
- lucidicai/sdk/decorators.py +187 -0
- lucidicai/sdk/error_boundary.py +299 -0
- lucidicai/sdk/event.py +122 -0
- lucidicai/sdk/event_builder.py +304 -0
- lucidicai/sdk/features/__init__.py +1 -0
- lucidicai/sdk/features/dataset.py +605 -0
- lucidicai/sdk/features/feature_flag.py +383 -0
- lucidicai/sdk/init.py +271 -0
- lucidicai/sdk/shutdown_manager.py +302 -0
- lucidicai/telemetry/context_bridge.py +82 -0
- lucidicai/telemetry/context_capture_processor.py +25 -9
- lucidicai/telemetry/litellm_bridge.py +18 -24
- lucidicai/telemetry/lucidic_exporter.py +51 -36
- lucidicai/telemetry/utils/model_pricing.py +278 -0
- lucidicai/utils/__init__.py +1 -0
- lucidicai/utils/images.py +337 -0
- lucidicai/utils/logger.py +168 -0
- lucidicai/utils/queue.py +393 -0
- {lucidicai-2.0.1.dist-info → lucidicai-2.1.0.dist-info}/METADATA +1 -1
- {lucidicai-2.0.1.dist-info → lucidicai-2.1.0.dist-info}/RECORD +39 -12
- {lucidicai-2.0.1.dist-info → lucidicai-2.1.0.dist-info}/WHEEL +0 -0
- {lucidicai-2.0.1.dist-info → lucidicai-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Decorators for the Lucidic SDK to create typed, nested events."""
|
|
2
|
+
import functools
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any, Callable, Optional, TypeVar
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
|
|
10
|
+
from .event import create_event
|
|
11
|
+
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
|
|
15
|
+
|
|
16
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
|
17
|
+
|
|
18
|
+
|
|
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]
|
|
26
|
+
try:
|
|
27
|
+
return json.loads(json.dumps(value, default=str))
|
|
28
|
+
except Exception:
|
|
29
|
+
return str(value)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def event(**decorator_kwargs) -> Callable[[F], F]:
|
|
33
|
+
"""Universal decorator creating FUNCTION_CALL events with nesting and error capture."""
|
|
34
|
+
|
|
35
|
+
def decorator(func: F) -> F:
|
|
36
|
+
@functools.wraps(func)
|
|
37
|
+
def sync_wrapper(*args, **kwargs):
|
|
38
|
+
session_id = get_session_id()
|
|
39
|
+
if not session_id:
|
|
40
|
+
return func(*args, **kwargs)
|
|
41
|
+
|
|
42
|
+
# Build arguments snapshot
|
|
43
|
+
sig = inspect.signature(func)
|
|
44
|
+
bound = sig.bind(*args, **kwargs)
|
|
45
|
+
bound.apply_defaults()
|
|
46
|
+
args_dict = {name: _serialize(val) for name, val in bound.arguments.items()}
|
|
47
|
+
|
|
48
|
+
parent_id = current_parent_event_id.get(None)
|
|
49
|
+
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)}")
|
|
51
|
+
start_time = datetime.now().astimezone()
|
|
52
|
+
result = None
|
|
53
|
+
error: Optional[BaseException] = None
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
with event_context(pre_event_id):
|
|
57
|
+
# Also inject into OpenTelemetry context for instrumentors
|
|
58
|
+
from ..telemetry.context_bridge import inject_lucidic_context
|
|
59
|
+
from opentelemetry import context as otel_context
|
|
60
|
+
|
|
61
|
+
otel_ctx = inject_lucidic_context()
|
|
62
|
+
token = otel_context.attach(otel_ctx)
|
|
63
|
+
try:
|
|
64
|
+
result = func(*args, **kwargs)
|
|
65
|
+
finally:
|
|
66
|
+
otel_context.detach(token)
|
|
67
|
+
return result
|
|
68
|
+
except Exception as e:
|
|
69
|
+
error = e
|
|
70
|
+
log_error(f"[Decorator] {func.__name__} raised exception: {e}")
|
|
71
|
+
raise
|
|
72
|
+
finally:
|
|
73
|
+
try:
|
|
74
|
+
# Store error as return value with type information
|
|
75
|
+
if error:
|
|
76
|
+
return_val = {
|
|
77
|
+
"error": str(error),
|
|
78
|
+
"error_type": type(error).__name__
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Create a separate error_traceback event for the exception
|
|
82
|
+
import traceback
|
|
83
|
+
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
|
|
89
|
+
)
|
|
90
|
+
debug(f"[Decorator] Created error_traceback event for {func.__name__}")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
debug(f"[Decorator] Failed to create error_traceback event: {e}")
|
|
93
|
+
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
|
+
)
|
|
106
|
+
debug(f"[Decorator] Created function_call event for {func.__name__}")
|
|
107
|
+
except Exception as e:
|
|
108
|
+
log_error(f"[Decorator] Failed to create function_call event: {e}")
|
|
109
|
+
|
|
110
|
+
@functools.wraps(func)
|
|
111
|
+
async def async_wrapper(*args, **kwargs):
|
|
112
|
+
session_id = get_session_id()
|
|
113
|
+
if not session_id:
|
|
114
|
+
return await func(*args, **kwargs)
|
|
115
|
+
|
|
116
|
+
sig = inspect.signature(func)
|
|
117
|
+
bound = sig.bind(*args, **kwargs)
|
|
118
|
+
bound.apply_defaults()
|
|
119
|
+
args_dict = {name: _serialize(val) for name, val in bound.arguments.items()}
|
|
120
|
+
|
|
121
|
+
parent_id = current_parent_event_id.get(None)
|
|
122
|
+
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)}")
|
|
124
|
+
start_time = datetime.now().astimezone()
|
|
125
|
+
result = None
|
|
126
|
+
error: Optional[BaseException] = None
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
async with event_context_async(pre_event_id):
|
|
130
|
+
# Also inject into OpenTelemetry context for instrumentors
|
|
131
|
+
from ..telemetry.context_bridge import inject_lucidic_context
|
|
132
|
+
from opentelemetry import context as otel_context
|
|
133
|
+
|
|
134
|
+
otel_ctx = inject_lucidic_context()
|
|
135
|
+
token = otel_context.attach(otel_ctx)
|
|
136
|
+
try:
|
|
137
|
+
result = await func(*args, **kwargs)
|
|
138
|
+
finally:
|
|
139
|
+
otel_context.detach(token)
|
|
140
|
+
return result
|
|
141
|
+
except Exception as e:
|
|
142
|
+
error = e
|
|
143
|
+
log_error(f"[Decorator] {func.__name__} raised exception: {e}")
|
|
144
|
+
raise
|
|
145
|
+
finally:
|
|
146
|
+
try:
|
|
147
|
+
# Store error as return value with type information
|
|
148
|
+
if error:
|
|
149
|
+
return_val = {
|
|
150
|
+
"error": str(error),
|
|
151
|
+
"error_type": type(error).__name__
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Create a separate error_traceback event for the exception
|
|
155
|
+
import traceback
|
|
156
|
+
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
|
|
162
|
+
)
|
|
163
|
+
debug(f"[Decorator] Created error_traceback event for {func.__name__}")
|
|
164
|
+
except Exception as e:
|
|
165
|
+
debug(f"[Decorator] Failed to create error_traceback event: {e}")
|
|
166
|
+
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
|
+
)
|
|
179
|
+
debug(f"[Decorator] Created function_call event for {func.__name__}")
|
|
180
|
+
except Exception as e:
|
|
181
|
+
log_error(f"[Decorator] Failed to create function_call event: {e}")
|
|
182
|
+
|
|
183
|
+
if inspect.iscoroutinefunction(func):
|
|
184
|
+
return async_wrapper # type: ignore
|
|
185
|
+
return sync_wrapper # type: ignore
|
|
186
|
+
|
|
187
|
+
return decorator
|
|
@@ -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
|