lucidicai 1.3.5__py3-none-any.whl → 2.0.2__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 +475 -398
- lucidicai/client.py +328 -50
- lucidicai/constants.py +7 -37
- lucidicai/context.py +25 -0
- lucidicai/dataset.py +114 -0
- lucidicai/decorators.py +96 -325
- lucidicai/errors.py +39 -0
- lucidicai/event.py +50 -59
- lucidicai/event_queue.py +466 -0
- lucidicai/feature_flag.py +344 -0
- lucidicai/session.py +9 -71
- lucidicai/singleton.py +20 -17
- lucidicai/streaming.py +15 -50
- lucidicai/telemetry/context_capture_processor.py +65 -0
- lucidicai/telemetry/extract.py +192 -0
- lucidicai/telemetry/litellm_bridge.py +80 -45
- lucidicai/telemetry/lucidic_exporter.py +125 -142
- lucidicai/telemetry/telemetry_init.py +189 -0
- {lucidicai-1.3.5.dist-info → lucidicai-2.0.2.dist-info}/METADATA +1 -1
- {lucidicai-1.3.5.dist-info → lucidicai-2.0.2.dist-info}/RECORD +22 -16
- {lucidicai-1.3.5.dist-info → lucidicai-2.0.2.dist-info}/WHEEL +0 -0
- {lucidicai-1.3.5.dist-info → lucidicai-2.0.2.dist-info}/top_level.txt +0 -0
lucidicai/dataset.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional, Dict, List, Any
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
from .client import Client
|
|
7
|
+
from .errors import APIKeyVerificationError
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("Lucidic")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_dataset(
|
|
13
|
+
dataset_id: str,
|
|
14
|
+
api_key: Optional[str] = None,
|
|
15
|
+
agent_id: Optional[str] = None,
|
|
16
|
+
) -> Dict[str, Any]:
|
|
17
|
+
"""
|
|
18
|
+
Get a dataset by ID with all its items.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
dataset_id: The ID of the dataset to retrieve (required).
|
|
22
|
+
api_key: API key for authentication. If not provided, will use the LUCIDIC_API_KEY environment variable.
|
|
23
|
+
agent_id: Agent ID. If not provided, will use the LUCIDIC_AGENT_ID environment variable.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A dictionary containing the dataset information including:
|
|
27
|
+
- dataset_id: The dataset ID
|
|
28
|
+
- name: Dataset name
|
|
29
|
+
- description: Dataset description
|
|
30
|
+
- tags: List of tags
|
|
31
|
+
- created_at: Creation timestamp
|
|
32
|
+
- updated_at: Last update timestamp
|
|
33
|
+
- num_items: Number of items in the dataset
|
|
34
|
+
- items: List of dataset items
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
APIKeyVerificationError: If API key or agent ID is missing or invalid.
|
|
38
|
+
ValueError: If dataset_id is not provided.
|
|
39
|
+
"""
|
|
40
|
+
return # no op for now
|
|
41
|
+
load_dotenv()
|
|
42
|
+
|
|
43
|
+
# Validation
|
|
44
|
+
if not dataset_id:
|
|
45
|
+
raise ValueError("Dataset ID is required")
|
|
46
|
+
|
|
47
|
+
# Get credentials
|
|
48
|
+
if api_key is None:
|
|
49
|
+
api_key = os.getenv("LUCIDIC_API_KEY", None)
|
|
50
|
+
if api_key is None:
|
|
51
|
+
raise APIKeyVerificationError(
|
|
52
|
+
"Make sure to either pass your API key into get_dataset() or set the LUCIDIC_API_KEY environment variable."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if agent_id is None:
|
|
56
|
+
agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
|
|
57
|
+
if agent_id is None:
|
|
58
|
+
raise APIKeyVerificationError(
|
|
59
|
+
"Lucidic agent ID not specified. Make sure to either pass your agent ID into get_dataset() or set the LUCIDIC_AGENT_ID environment variable."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Get current client or create a new one
|
|
63
|
+
client = Client()
|
|
64
|
+
# If not yet initialized or still the NullClient -> create a real client
|
|
65
|
+
if not getattr(client, 'initialized', False):
|
|
66
|
+
client = Client(api_key=api_key, agent_id=agent_id)
|
|
67
|
+
else:
|
|
68
|
+
# Already initialized, check if we need to update credentials
|
|
69
|
+
if api_key is not None and agent_id is not None and (api_key != client.api_key or agent_id != client.agent_id):
|
|
70
|
+
client.set_api_key(api_key)
|
|
71
|
+
client.agent_id = agent_id
|
|
72
|
+
|
|
73
|
+
# Make request to get dataset
|
|
74
|
+
response = client.make_request(
|
|
75
|
+
'getdataset',
|
|
76
|
+
'GET',
|
|
77
|
+
{'dataset_id': dataset_id}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
logger.info(f"Retrieved dataset {dataset_id} with {response.get('num_items', 0)} items")
|
|
81
|
+
return response
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_dataset_items(
|
|
85
|
+
dataset_id: str,
|
|
86
|
+
api_key: Optional[str] = None,
|
|
87
|
+
agent_id: Optional[str] = None,
|
|
88
|
+
) -> List[Dict[str, Any]]:
|
|
89
|
+
"""
|
|
90
|
+
Convenience function to get just the items from a dataset.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
dataset_id: The ID of the dataset to retrieve items from (required).
|
|
94
|
+
api_key: API key for authentication. If not provided, will use the LUCIDIC_API_KEY environment variable.
|
|
95
|
+
agent_id: Agent ID. If not provided, will use the LUCIDIC_AGENT_ID environment variable.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
A list of dataset items, where each item contains:
|
|
99
|
+
- dataset_item_id: The item ID
|
|
100
|
+
- name: Item name
|
|
101
|
+
- description: Item description
|
|
102
|
+
- tags: List of tags
|
|
103
|
+
- input: Input data for the item
|
|
104
|
+
- expected_output: Expected output data
|
|
105
|
+
- metadata: Additional metadata
|
|
106
|
+
- created_at: Creation timestamp
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
APIKeyVerificationError: If API key or agent ID is missing or invalid.
|
|
110
|
+
ValueError: If dataset_id is not provided.
|
|
111
|
+
"""
|
|
112
|
+
return # no op for now
|
|
113
|
+
dataset = get_dataset(dataset_id, api_key, agent_id)
|
|
114
|
+
return dataset.get('items', [])
|
lucidicai/decorators.py
CHANGED
|
@@ -1,375 +1,146 @@
|
|
|
1
|
-
"""Decorators for the Lucidic SDK to
|
|
1
|
+
"""Decorators for the Lucidic SDK to create typed, nested events."""
|
|
2
2
|
import functools
|
|
3
|
-
import contextvars
|
|
4
3
|
import inspect
|
|
5
4
|
import json
|
|
6
5
|
import logging
|
|
7
|
-
from
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Any, Callable, Optional, TypeVar
|
|
8
9
|
from collections.abc import Iterable
|
|
9
10
|
|
|
10
11
|
from .client import Client
|
|
11
12
|
from .errors import LucidicNotInitializedError
|
|
13
|
+
from .context import current_parent_event_id, event_context, event_context_async
|
|
12
14
|
|
|
13
15
|
logger = logging.getLogger("Lucidic")
|
|
14
16
|
|
|
15
17
|
F = TypeVar('F', bound=Callable[..., Any])
|
|
16
18
|
|
|
17
|
-
# Create context variables to store the current step and event
|
|
18
|
-
_current_step = contextvars.ContextVar("current_step", default=None)
|
|
19
|
-
_current_event = contextvars.ContextVar("current_event", default=None)
|
|
20
19
|
|
|
21
|
-
def
|
|
22
|
-
|
|
20
|
+
def _serialize(value: Any):
|
|
21
|
+
if isinstance(value, (str, int, float, bool)):
|
|
22
|
+
return value
|
|
23
|
+
if isinstance(value, dict):
|
|
24
|
+
return {k: _serialize(v) for k, v in value.items()}
|
|
25
|
+
if isinstance(value, Iterable) and not isinstance(value, (str, bytes)):
|
|
26
|
+
return [_serialize(v) for v in value]
|
|
27
|
+
try:
|
|
28
|
+
return json.loads(json.dumps(value, default=str))
|
|
29
|
+
except Exception:
|
|
30
|
+
return str(value)
|
|
23
31
|
|
|
24
|
-
def get_decorator_event():
|
|
25
|
-
return _current_event.get()
|
|
26
32
|
|
|
33
|
+
def event(**decorator_kwargs) -> Callable[[F], F]:
|
|
34
|
+
"""Universal decorator creating FUNCTION_CALL events with nesting and error capture."""
|
|
27
35
|
|
|
28
|
-
def step(
|
|
29
|
-
state: Optional[str] = None,
|
|
30
|
-
action: Optional[str] = None,
|
|
31
|
-
goal: Optional[str] = None,
|
|
32
|
-
screenshot_path: Optional[str] = None,
|
|
33
|
-
eval_score: Optional[float] = None,
|
|
34
|
-
eval_description: Optional[str] = None
|
|
35
|
-
) -> Callable[[F], F]:
|
|
36
|
-
"""
|
|
37
|
-
Decorator that wraps a function with step tracking.
|
|
38
|
-
|
|
39
|
-
The decorated function will be wrapped with create_step() at the start
|
|
40
|
-
and end_step() at the end, ensuring proper cleanup even on exceptions.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
state: State description for the step
|
|
44
|
-
action: Action description for the step
|
|
45
|
-
goal: Goal description for the step
|
|
46
|
-
eval_score: Evaluation score for the step
|
|
47
|
-
eval_description: Evaluation description for the step
|
|
48
|
-
|
|
49
|
-
Example:
|
|
50
|
-
@lai.step(
|
|
51
|
-
state="Processing user input",
|
|
52
|
-
action="Validate and parse request",
|
|
53
|
-
goal="Extract intent from user message"
|
|
54
|
-
)
|
|
55
|
-
def process_user_input(message: str) -> dict:
|
|
56
|
-
# Function logic here
|
|
57
|
-
return parsed_intent
|
|
58
|
-
"""
|
|
59
36
|
def decorator(func: F) -> F:
|
|
60
37
|
@functools.wraps(func)
|
|
61
38
|
def sync_wrapper(*args, **kwargs):
|
|
62
|
-
# Check if SDK is initialized
|
|
63
39
|
try:
|
|
64
40
|
client = Client()
|
|
65
41
|
if not client.session:
|
|
66
|
-
# No active session, run function normally
|
|
67
|
-
logger.warning("No active session, running function normally")
|
|
68
42
|
return func(*args, **kwargs)
|
|
69
|
-
except LucidicNotInitializedError:
|
|
70
|
-
# SDK not initialized, run function normally
|
|
71
|
-
logger.warning("Lucidic not initialized, running function normally")
|
|
43
|
+
except (LucidicNotInitializedError, AttributeError):
|
|
72
44
|
return func(*args, **kwargs)
|
|
73
|
-
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# Import here to avoid circular imports
|
|
87
|
-
from . import create_step, end_step
|
|
88
|
-
step_id = create_step(**step_params)
|
|
89
|
-
tok = _current_step.set(step_id)
|
|
90
|
-
|
|
45
|
+
|
|
46
|
+
# Build arguments snapshot
|
|
47
|
+
sig = inspect.signature(func)
|
|
48
|
+
bound = sig.bind(*args, **kwargs)
|
|
49
|
+
bound.apply_defaults()
|
|
50
|
+
args_dict = {name: _serialize(val) for name, val in bound.arguments.items()}
|
|
51
|
+
|
|
52
|
+
parent_id = current_parent_event_id.get(None)
|
|
53
|
+
pre_event_id = str(uuid.uuid4())
|
|
54
|
+
start_time = datetime.now().astimezone()
|
|
55
|
+
result = None
|
|
56
|
+
error: Optional[BaseException] = None
|
|
57
|
+
|
|
91
58
|
try:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# End step successfully
|
|
95
|
-
end_step(step_id=step_id)
|
|
96
|
-
_current_step.reset(tok)
|
|
59
|
+
with event_context(pre_event_id):
|
|
60
|
+
result = func(*args, **kwargs)
|
|
97
61
|
return result
|
|
98
62
|
except Exception as e:
|
|
99
|
-
|
|
63
|
+
error = e
|
|
64
|
+
raise
|
|
65
|
+
finally:
|
|
100
66
|
try:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
67
|
+
# Store error as return value with type information
|
|
68
|
+
if error:
|
|
69
|
+
return_val = {
|
|
70
|
+
"error": str(error),
|
|
71
|
+
"error_type": type(error).__name__
|
|
72
|
+
}
|
|
73
|
+
else:
|
|
74
|
+
return_val = _serialize(result)
|
|
75
|
+
|
|
76
|
+
client.create_event(
|
|
77
|
+
type="function_call",
|
|
78
|
+
event_id=pre_event_id,
|
|
79
|
+
function_name=func.__name__,
|
|
80
|
+
arguments=args_dict,
|
|
81
|
+
return_value=return_val,
|
|
82
|
+
error=str(error) if error else None,
|
|
83
|
+
parent_event_id=parent_id,
|
|
84
|
+
duration=(datetime.now().astimezone() - start_time).total_seconds(),
|
|
85
|
+
**decorator_kwargs
|
|
105
86
|
)
|
|
106
|
-
_current_step.reset(tok)
|
|
107
87
|
except Exception:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
raise
|
|
111
|
-
|
|
88
|
+
pass
|
|
89
|
+
|
|
112
90
|
@functools.wraps(func)
|
|
113
91
|
async def async_wrapper(*args, **kwargs):
|
|
114
|
-
# Check if SDK is initialized
|
|
115
92
|
try:
|
|
116
93
|
client = Client()
|
|
117
94
|
if not client.session:
|
|
118
|
-
# No active session, run function normally
|
|
119
|
-
logger.warning("No active session, running function normally")
|
|
120
95
|
return await func(*args, **kwargs)
|
|
121
|
-
except LucidicNotInitializedError:
|
|
122
|
-
# SDK not initialized, run function normally
|
|
123
|
-
logger.warning("Lucidic not initialized, running function normally")
|
|
96
|
+
except (LucidicNotInitializedError, AttributeError):
|
|
124
97
|
return await func(*args, **kwargs)
|
|
125
|
-
|
|
126
|
-
# Create the step
|
|
127
|
-
step_params = {
|
|
128
|
-
'state': state,
|
|
129
|
-
'action': action,
|
|
130
|
-
'goal': goal,
|
|
131
|
-
'screenshot_path': screenshot_path,
|
|
132
|
-
'eval_score': eval_score,
|
|
133
|
-
'eval_description': eval_description
|
|
134
|
-
}
|
|
135
|
-
# Remove None values
|
|
136
|
-
step_params = {k: v for k, v in step_params.items() if v is not None}
|
|
137
|
-
|
|
138
|
-
# Import here to avoid circular imports
|
|
139
|
-
from . import create_step, end_step
|
|
140
|
-
|
|
141
|
-
step_id = create_step(**step_params)
|
|
142
|
-
tok = _current_step.set(step_id)
|
|
143
|
-
try:
|
|
144
|
-
# Execute the wrapped function
|
|
145
|
-
result = await func(*args, **kwargs)
|
|
146
|
-
# End step successfully
|
|
147
|
-
end_step(step_id=step_id)
|
|
148
|
-
_current_step.reset(tok)
|
|
149
|
-
return result
|
|
150
|
-
except Exception as e:
|
|
151
|
-
# End step with error indication
|
|
152
|
-
try:
|
|
153
|
-
end_step(
|
|
154
|
-
step_id=step_id,
|
|
155
|
-
eval_score=0.0,
|
|
156
|
-
eval_description=f"Step failed with error: {str(e)}"
|
|
157
|
-
)
|
|
158
|
-
_current_step.reset(tok)
|
|
159
|
-
except Exception:
|
|
160
|
-
# If end_step fails, just log it
|
|
161
|
-
logger.error(f"Failed to end step {step_id} after error")
|
|
162
|
-
raise
|
|
163
|
-
|
|
164
|
-
# Return appropriate wrapper based on function type
|
|
165
|
-
if inspect.iscoroutinefunction(func):
|
|
166
|
-
return async_wrapper
|
|
167
|
-
else:
|
|
168
|
-
return sync_wrapper
|
|
169
|
-
|
|
170
|
-
return decorator
|
|
171
98
|
|
|
172
|
-
|
|
173
|
-
### -- TODO -- Updating even within function causes function result to not be recorded.
|
|
174
|
-
def event(
|
|
175
|
-
description: Optional[str] = None,
|
|
176
|
-
result: Optional[str] = None,
|
|
177
|
-
model: Optional[str] = None,
|
|
178
|
-
cost_added: Optional[float] = 0
|
|
179
|
-
) -> Callable[[F], F]:
|
|
180
|
-
"""
|
|
181
|
-
Decorator that creates an event for a function call.
|
|
182
|
-
|
|
183
|
-
The decorated function will create an event that captures:
|
|
184
|
-
- Function inputs (as string representation) if description not provided
|
|
185
|
-
- Function output (as string representation) if result not provided
|
|
186
|
-
|
|
187
|
-
LLM calls within the function will create their own events as normal.
|
|
188
|
-
|
|
189
|
-
Args:
|
|
190
|
-
description: Custom description for the event. If not provided,
|
|
191
|
-
will use string representation of function inputs
|
|
192
|
-
result: Custom result for the event. If not provided,
|
|
193
|
-
will use string representation of function output
|
|
194
|
-
model: Model name if this function represents a model call (default: None)
|
|
195
|
-
cost_added: Cost to add for this event (default: 0)
|
|
196
|
-
|
|
197
|
-
Example:
|
|
198
|
-
@lai.event(description="Parse user query", model="custom-parser")
|
|
199
|
-
def parse_query(query: str) -> dict:
|
|
200
|
-
# Function logic here
|
|
201
|
-
return {"intent": "search", "query": query}
|
|
202
|
-
"""
|
|
203
|
-
def decorator(func: F) -> F:
|
|
204
|
-
@functools.wraps(func)
|
|
205
|
-
def sync_wrapper(*args, **kwargs):
|
|
206
|
-
# Check if SDK is initialized
|
|
207
|
-
try:
|
|
208
|
-
client = Client()
|
|
209
|
-
if not client.session:
|
|
210
|
-
# No active session, run function normally
|
|
211
|
-
logger.warning("No active session, running function normally")
|
|
212
|
-
return func(*args, **kwargs)
|
|
213
|
-
except (LucidicNotInitializedError, AttributeError):
|
|
214
|
-
# SDK not initialized or no session, run function normally
|
|
215
|
-
logger.warning("Lucidic not initialized, running function normally")
|
|
216
|
-
return func(*args, **kwargs)
|
|
217
|
-
|
|
218
|
-
# Import here to avoid circular imports
|
|
219
|
-
from . import create_event, end_event
|
|
220
|
-
|
|
221
|
-
# Build event description from inputs if not provided
|
|
222
|
-
event_desc = description
|
|
223
|
-
function_name = func.__name__
|
|
224
|
-
|
|
225
|
-
# Get function signature
|
|
226
99
|
sig = inspect.signature(func)
|
|
227
|
-
|
|
228
|
-
|
|
100
|
+
bound = sig.bind(*args, **kwargs)
|
|
101
|
+
bound.apply_defaults()
|
|
102
|
+
args_dict = {name: _serialize(val) for name, val in bound.arguments.items()}
|
|
229
103
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if isinstance(value, float):
|
|
236
|
-
return value
|
|
237
|
-
if isinstance(value, bool):
|
|
238
|
-
return value
|
|
239
|
-
if isinstance(value, dict):
|
|
240
|
-
return {k: serialize(v) for k, v in value.items()}
|
|
241
|
-
if isinstance(value, Iterable):
|
|
242
|
-
return [serialize(v) for v in value]
|
|
243
|
-
return str(value)
|
|
104
|
+
parent_id = current_parent_event_id.get(None)
|
|
105
|
+
pre_event_id = str(uuid.uuid4())
|
|
106
|
+
start_time = datetime.now().astimezone()
|
|
107
|
+
result = None
|
|
108
|
+
error: Optional[BaseException] = None
|
|
244
109
|
|
|
245
|
-
# Construct JSONable object of args
|
|
246
|
-
args_dict = {
|
|
247
|
-
param_name: serialize(param_value) # Recursive - maybe change later
|
|
248
|
-
for param_name, param_value in bound_args.arguments.items()
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if not event_desc:
|
|
252
|
-
event_desc = f"Function {function_name}({json.dumps(args_dict)})"
|
|
253
|
-
|
|
254
|
-
# Create the event
|
|
255
|
-
event_id = create_event(
|
|
256
|
-
description=event_desc,
|
|
257
|
-
model=model,
|
|
258
|
-
cost_added=cost_added,
|
|
259
|
-
function_name=function_name,
|
|
260
|
-
arguments=args_dict,
|
|
261
|
-
)
|
|
262
|
-
tok = _current_event.set(event_id)
|
|
263
110
|
try:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
# Build event result from output if not provided
|
|
268
|
-
event_result = result
|
|
269
|
-
if not event_result:
|
|
270
|
-
try:
|
|
271
|
-
event_result = repr(function_result)
|
|
272
|
-
except Exception:
|
|
273
|
-
event_result = str(function_result)
|
|
274
|
-
|
|
275
|
-
# Update and end the event
|
|
276
|
-
end_event(
|
|
277
|
-
event_id=event_id,
|
|
278
|
-
result=event_result,
|
|
279
|
-
)
|
|
280
|
-
_current_event.reset(tok)
|
|
281
|
-
return function_result
|
|
282
|
-
|
|
111
|
+
async with event_context_async(pre_event_id):
|
|
112
|
+
result = await func(*args, **kwargs)
|
|
113
|
+
return result
|
|
283
114
|
except Exception as e:
|
|
284
|
-
|
|
285
|
-
try:
|
|
286
|
-
end_event(
|
|
287
|
-
event_id=event_id,
|
|
288
|
-
result=f"Error: {str(e)}",
|
|
289
|
-
)
|
|
290
|
-
_current_event.reset(tok)
|
|
291
|
-
except Exception:
|
|
292
|
-
logger.error(f"Failed to end event {event_id} after error")
|
|
115
|
+
error = e
|
|
293
116
|
raise
|
|
294
|
-
|
|
295
|
-
@functools.wraps(func)
|
|
296
|
-
async def async_wrapper(*args, **kwargs):
|
|
297
|
-
# Check if SDK is initialized
|
|
298
|
-
try:
|
|
299
|
-
client = Client()
|
|
300
|
-
if not client.session:
|
|
301
|
-
# No active session, run function normally
|
|
302
|
-
logger.warning("No active session, running function normally")
|
|
303
|
-
return await func(*args, **kwargs)
|
|
304
|
-
except (LucidicNotInitializedError, AttributeError):
|
|
305
|
-
# SDK not initialized or no session, run function normally
|
|
306
|
-
logger.warning("Lucidic not initialized, running function normally")
|
|
307
|
-
return await func(*args, **kwargs)
|
|
308
|
-
|
|
309
|
-
# Import here to avoid circular imports
|
|
310
|
-
from . import create_event, end_event
|
|
311
|
-
|
|
312
|
-
# Build event description from inputs if not provided
|
|
313
|
-
event_desc = description
|
|
314
|
-
if not event_desc:
|
|
315
|
-
# Get function signature
|
|
316
|
-
sig = inspect.signature(func)
|
|
317
|
-
bound_args = sig.bind(*args, **kwargs)
|
|
318
|
-
bound_args.apply_defaults()
|
|
319
|
-
|
|
320
|
-
# Create string representation of inputs
|
|
321
|
-
input_parts = []
|
|
322
|
-
for param_name, param_value in bound_args.arguments.items():
|
|
323
|
-
try:
|
|
324
|
-
input_parts.append(f"{param_name}={repr(param_value)}")
|
|
325
|
-
except Exception:
|
|
326
|
-
input_parts.append(f"{param_name}=<{type(param_value).__name__}>")
|
|
327
|
-
|
|
328
|
-
event_desc = f"{func.__name__}({', '.join(input_parts)})"
|
|
329
|
-
|
|
330
|
-
# Create the event
|
|
331
|
-
event_id = create_event(
|
|
332
|
-
description=event_desc,
|
|
333
|
-
model=model,
|
|
334
|
-
cost_added=cost_added
|
|
335
|
-
)
|
|
336
|
-
tok = _current_event.set(event_id)
|
|
337
|
-
try:
|
|
338
|
-
# Execute the wrapped function
|
|
339
|
-
function_result = await func(*args, **kwargs)
|
|
340
|
-
|
|
341
|
-
# Build event result from output if not provided
|
|
342
|
-
event_result = result
|
|
343
|
-
if not event_result:
|
|
344
|
-
try:
|
|
345
|
-
event_result = repr(function_result)
|
|
346
|
-
except Exception:
|
|
347
|
-
event_result = str(function_result)
|
|
348
|
-
|
|
349
|
-
# Update and end the event
|
|
350
|
-
end_event(
|
|
351
|
-
event_id=event_id,
|
|
352
|
-
result=event_result,
|
|
353
|
-
)
|
|
354
|
-
_current_event.reset(tok)
|
|
355
|
-
return function_result
|
|
356
|
-
|
|
357
|
-
except Exception as e:
|
|
358
|
-
# Update event with error
|
|
117
|
+
finally:
|
|
359
118
|
try:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
119
|
+
# Store error as return value with type information
|
|
120
|
+
if error:
|
|
121
|
+
return_val = {
|
|
122
|
+
"error": str(error),
|
|
123
|
+
"error_type": type(error).__name__
|
|
124
|
+
}
|
|
125
|
+
else:
|
|
126
|
+
return_val = _serialize(result)
|
|
127
|
+
|
|
128
|
+
client.create_event(
|
|
129
|
+
type="function_call",
|
|
130
|
+
event_id=pre_event_id,
|
|
131
|
+
function_name=func.__name__,
|
|
132
|
+
arguments=args_dict,
|
|
133
|
+
return_value=return_val,
|
|
134
|
+
error=str(error) if error else None,
|
|
135
|
+
parent_event_id=parent_id,
|
|
136
|
+
duration=(datetime.now().astimezone() - start_time).total_seconds(),
|
|
137
|
+
**decorator_kwargs
|
|
363
138
|
)
|
|
364
|
-
_current_event.reset(tok)
|
|
365
139
|
except Exception:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
# Return appropriate wrapper based on function type
|
|
140
|
+
pass
|
|
141
|
+
|
|
370
142
|
if inspect.iscoroutinefunction(func):
|
|
371
|
-
return async_wrapper
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
143
|
+
return async_wrapper # type: ignore
|
|
144
|
+
return sync_wrapper # type: ignore
|
|
145
|
+
|
|
375
146
|
return decorator
|
lucidicai/errors.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
|
+
import sys
|
|
3
|
+
import traceback
|
|
2
4
|
|
|
3
5
|
class APIKeyVerificationError(Exception):
|
|
4
6
|
"""Exception for API key verification errors"""
|
|
@@ -19,3 +21,40 @@ class InvalidOperationError(Exception):
|
|
|
19
21
|
"Exception for errors resulting from attempting an invalid operation"
|
|
20
22
|
def __init__(self, message: str):
|
|
21
23
|
super().__init__(f"An invalid Lucidic operation was attempted: {message}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FeatureFlagError(Exception):
|
|
27
|
+
"""Exception for feature flag fetch failures"""
|
|
28
|
+
def __init__(self, message: str):
|
|
29
|
+
super().__init__(f"Failed to fetch feature flag: {message}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def install_error_handler():
|
|
33
|
+
"""Install global handler to create ERROR_TRACEBACK events for uncaught exceptions."""
|
|
34
|
+
from .client import Client
|
|
35
|
+
from .context import current_parent_event_id
|
|
36
|
+
|
|
37
|
+
def handle_exception(exc_type, exc_value, exc_traceback):
|
|
38
|
+
try:
|
|
39
|
+
client = Client()
|
|
40
|
+
if client and client.session:
|
|
41
|
+
tb = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
|
|
42
|
+
parent_id = None
|
|
43
|
+
try:
|
|
44
|
+
parent_id = current_parent_event_id.get(None)
|
|
45
|
+
except Exception:
|
|
46
|
+
parent_id = None
|
|
47
|
+
client.create_event(
|
|
48
|
+
type="error_traceback",
|
|
49
|
+
error=str(exc_value),
|
|
50
|
+
traceback=tb,
|
|
51
|
+
parent_event_id=parent_id
|
|
52
|
+
)
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
try:
|
|
56
|
+
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
sys.excepthook = handle_exception
|