lucidicai 1.2.15__py3-none-any.whl → 1.2.17__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 (40) hide show
  1. lucidicai/__init__.py +111 -21
  2. lucidicai/client.py +22 -5
  3. lucidicai/decorators.py +357 -0
  4. lucidicai/event.py +2 -2
  5. lucidicai/image_upload.py +24 -1
  6. lucidicai/providers/anthropic_handler.py +0 -7
  7. lucidicai/providers/image_storage.py +45 -0
  8. lucidicai/providers/langchain.py +0 -78
  9. lucidicai/providers/lucidic_exporter.py +259 -0
  10. lucidicai/providers/lucidic_span_processor.py +648 -0
  11. lucidicai/providers/openai_agents_instrumentor.py +307 -0
  12. lucidicai/providers/openai_handler.py +1 -56
  13. lucidicai/providers/otel_handlers.py +266 -0
  14. lucidicai/providers/otel_init.py +197 -0
  15. lucidicai/providers/otel_provider.py +168 -0
  16. lucidicai/providers/pydantic_ai_handler.py +2 -19
  17. lucidicai/providers/text_storage.py +53 -0
  18. lucidicai/providers/universal_image_interceptor.py +276 -0
  19. lucidicai/session.py +17 -4
  20. lucidicai/step.py +4 -4
  21. lucidicai/streaming.py +2 -3
  22. lucidicai/telemetry/__init__.py +0 -0
  23. lucidicai/telemetry/base_provider.py +21 -0
  24. lucidicai/telemetry/lucidic_exporter.py +259 -0
  25. lucidicai/telemetry/lucidic_span_processor.py +665 -0
  26. lucidicai/telemetry/openai_agents_instrumentor.py +306 -0
  27. lucidicai/telemetry/opentelemetry_converter.py +436 -0
  28. lucidicai/telemetry/otel_handlers.py +266 -0
  29. lucidicai/telemetry/otel_init.py +197 -0
  30. lucidicai/telemetry/otel_provider.py +168 -0
  31. lucidicai/telemetry/pydantic_ai_handler.py +600 -0
  32. lucidicai/telemetry/utils/__init__.py +0 -0
  33. lucidicai/telemetry/utils/image_storage.py +45 -0
  34. lucidicai/telemetry/utils/text_storage.py +53 -0
  35. lucidicai/telemetry/utils/universal_image_interceptor.py +276 -0
  36. {lucidicai-1.2.15.dist-info → lucidicai-1.2.17.dist-info}/METADATA +1 -1
  37. lucidicai-1.2.17.dist-info/RECORD +49 -0
  38. lucidicai-1.2.15.dist-info/RECORD +0 -25
  39. {lucidicai-1.2.15.dist-info → lucidicai-1.2.17.dist-info}/WHEEL +0 -0
  40. {lucidicai-1.2.15.dist-info → lucidicai-1.2.17.dist-info}/top_level.txt +0 -0
lucidicai/__init__.py CHANGED
@@ -7,14 +7,24 @@ from typing import List, Literal, Optional
7
7
  from .client import Client
8
8
  from .errors import APIKeyVerificationError, InvalidOperationError, LucidicNotInitializedError, PromptError
9
9
  from .event import Event
10
- from .providers.anthropic_handler import AnthropicHandler
11
- from .providers.langchain import LucidicLangchainHandler
12
- from .providers.openai_handler import OpenAIHandler
13
- from .providers.openai_agents_handler import OpenAIAgentsHandler
14
- from .providers.pydantic_ai_handler import PydanticAIHandler
15
10
  from .session import Session
16
11
  from .step import Step
17
12
 
13
+ # Import OpenTelemetry-based handlers
14
+ from .telemetry.otel_handlers import (
15
+ OTelOpenAIHandler,
16
+ OTelAnthropicHandler,
17
+ OTelLangChainHandler,
18
+ OTelPydanticAIHandler,
19
+ OTelOpenAIAgentsHandler
20
+ )
21
+
22
+ # Import telemetry manager
23
+ from .telemetry.otel_init import LucidicTelemetry
24
+
25
+ # Import decorators
26
+ from .decorators import step, event
27
+
18
28
  ProviderType = Literal["openai", "anthropic", "langchain", "pydantic_ai", "openai_agents"]
19
29
 
20
30
  # Configure logging
@@ -37,31 +47,33 @@ def _setup_providers(client: Client, providers: List[ProviderType]) -> None:
37
47
  # Track which providers have been set up to avoid duplication
38
48
  setup_providers = set()
39
49
 
50
+ # Initialize telemetry if using OpenTelemetry
51
+ if providers:
52
+ telemetry = LucidicTelemetry()
53
+ if not telemetry.is_initialized():
54
+ telemetry.initialize(agent_id=client.agent_id)
55
+
40
56
  for provider in providers:
41
57
  if provider in setup_providers:
42
58
  continue
43
59
 
44
60
  if provider == "openai":
45
- client.set_provider(OpenAIHandler())
61
+ client.set_provider(OTelOpenAIHandler())
46
62
  setup_providers.add("openai")
47
63
  elif provider == "anthropic":
48
- client.set_provider(AnthropicHandler())
64
+ client.set_provider(OTelAnthropicHandler())
49
65
  setup_providers.add("anthropic")
50
66
  elif provider == "langchain":
67
+ client.set_provider(OTelLangChainHandler())
51
68
  logger.info("For LangChain, make sure to create a handler and attach it to your top-level Agent class.")
52
69
  setup_providers.add("langchain")
53
70
  elif provider == "pydantic_ai":
54
- client.set_provider(PydanticAIHandler())
71
+ client.set_provider(OTelPydanticAIHandler())
55
72
  setup_providers.add("pydantic_ai")
56
73
  elif provider == "openai_agents":
57
74
  try:
58
- # For OpenAI Agents SDK, we want both handlers
59
- client.set_provider(OpenAIAgentsHandler())
75
+ client.set_provider(OTelOpenAIAgentsHandler())
60
76
  setup_providers.add("openai_agents")
61
- # Also enable OpenAI handler if not already set up
62
- if "openai" not in setup_providers:
63
- client.set_provider(OpenAIHandler())
64
- setup_providers.add("openai")
65
77
  except Exception as e:
66
78
  logger.error(f"Failed to set up OpenAI Agents provider: {e}")
67
79
  raise
@@ -87,11 +99,8 @@ __all__ = [
87
99
  'LucidicNotInitializedError',
88
100
  'PromptError',
89
101
  'InvalidOperationError',
90
- 'LucidicLangchainHandler',
91
- 'AnthropicHandler',
92
- 'OpenAIHandler',
93
- 'OpenAIAgentsHandler',
94
- 'PydanticAIHandler'
102
+ 'step',
103
+ 'event',
95
104
  ]
96
105
 
97
106
 
@@ -101,9 +110,12 @@ def init(
101
110
  agent_id: Optional[str] = None,
102
111
  task: Optional[str] = None,
103
112
  providers: Optional[List[ProviderType]] = [],
113
+ production_monitoring: Optional[bool] = False,
104
114
  mass_sim_id: Optional[str] = None,
105
115
  rubrics: Optional[list] = None,
106
116
  tags: Optional[list] = None,
117
+ masking_function = None,
118
+ auto_end: Optional[bool] = True,
107
119
  ) -> str:
108
120
  """
109
121
  Initialize the Lucidic client.
@@ -117,6 +129,8 @@ def init(
117
129
  mass_sim_id: Optional mass simulation ID, if session is to be part of a mass simulation.
118
130
  rubrics: Optional rubrics for evaluation, list of strings.
119
131
  tags: Optional tags for the session, list of strings.
132
+ masking_function: Optional function to mask sensitive data.
133
+ auto_end: If True, automatically end the session on process exit. Defaults to True.
120
134
 
121
135
  Raises:
122
136
  InvalidOperationError: If the client is already initialized.
@@ -137,6 +151,17 @@ def init(
137
151
  if not getattr(client, 'initialized', False):
138
152
  client = Client(lucidic_api_key=lucidic_api_key, agent_id=agent_id)
139
153
 
154
+ if not production_monitoring:
155
+ production_monitoring = os.getenv("LUCIDIC_PRODUCTION_MONITORING", False)
156
+ if production_monitoring == "True":
157
+ production_monitoring = True
158
+ else:
159
+ production_monitoring = False
160
+
161
+ # Handle auto_end with environment variable support
162
+ if auto_end is None:
163
+ auto_end = os.getenv("LUCIDIC_AUTO_END", "True").lower() == "true"
164
+
140
165
  # Set up providers
141
166
  _setup_providers(client, providers)
142
167
  session_id = client.init_session(
@@ -144,8 +169,15 @@ def init(
144
169
  mass_sim_id=mass_sim_id,
145
170
  task=task,
146
171
  rubrics=rubrics,
147
- tags=tags
172
+ tags=tags,
173
+ production_monitoring=production_monitoring,
148
174
  )
175
+ if masking_function:
176
+ client.masking_function = masking_function
177
+
178
+ # Set the auto_end flag on the client
179
+ client.auto_end = auto_end
180
+
149
181
  logger.info("Session initialized successfully")
150
182
  return session_id
151
183
 
@@ -154,7 +186,9 @@ def continue_session(
154
186
  session_id: str,
155
187
  lucidic_api_key: Optional[str] = None,
156
188
  agent_id: Optional[str] = None,
157
- providers: Optional[List[ProviderType]] = []
189
+ providers: Optional[List[ProviderType]] = [],
190
+ masking_function = None,
191
+ auto_end: Optional[bool] = True,
158
192
  ):
159
193
  if lucidic_api_key is None:
160
194
  lucidic_api_key = os.getenv("LUCIDIC_API_KEY", None)
@@ -174,9 +208,19 @@ def continue_session(
174
208
  agent_id=agent_id,
175
209
  )
176
210
 
211
+ # Handle auto_end with environment variable support
212
+ if auto_end is None:
213
+ auto_end = os.getenv("LUCIDIC_AUTO_END", "True").lower() == "true"
214
+
177
215
  # Set up providers
178
216
  _setup_providers(client, providers)
179
217
  session_id = client.continue_session(session_id=session_id)
218
+ if masking_function:
219
+ client.masking_function = masking_function
220
+
221
+ # Set the auto_end flag on the client
222
+ client.auto_end = auto_end
223
+
180
224
  logger.info(f"Session {session_id} continuing...")
181
225
  return session_id # For consistency
182
226
 
@@ -233,9 +277,55 @@ def reset_sdk() -> None:
233
277
  client = Client()
234
278
  if not client.initialized:
235
279
  return
280
+
281
+ # Shutdown OpenTelemetry if it was initialized
282
+ telemetry = LucidicTelemetry()
283
+ if telemetry.is_initialized():
284
+ telemetry.uninstrument_all()
285
+
236
286
  client.clear()
237
287
 
238
288
 
289
+ def _cleanup_telemetry():
290
+ """Cleanup function for OpenTelemetry shutdown"""
291
+ try:
292
+ telemetry = LucidicTelemetry()
293
+ if telemetry.is_initialized():
294
+ telemetry.uninstrument_all()
295
+ logger.info("OpenTelemetry instrumentation cleaned up")
296
+ except Exception as e:
297
+ logger.error(f"Error during telemetry cleanup: {e}")
298
+
299
+
300
+ def _auto_end_session():
301
+ """Automatically end session on exit if auto_end is enabled"""
302
+ try:
303
+ client = Client()
304
+ if hasattr(client, 'auto_end') and client.auto_end and client.session and not client.session.is_finished:
305
+ logger.info("Auto-ending active session on exit")
306
+ end_session()
307
+ except Exception as e:
308
+ logger.debug(f"Error during auto-end session: {e}")
309
+
310
+
311
+ def _signal_handler(signum, frame):
312
+ """Handle interruption signals"""
313
+ _auto_end_session()
314
+ _cleanup_telemetry()
315
+ # Re-raise the signal for default handling
316
+ signal.signal(signum, signal.SIG_DFL)
317
+ os.kill(os.getpid(), signum)
318
+
319
+
320
+ # Register cleanup functions (auto-end runs first due to LIFO order)
321
+ atexit.register(_cleanup_telemetry)
322
+ atexit.register(_auto_end_session)
323
+
324
+ # Register signal handlers for graceful shutdown
325
+ signal.signal(signal.SIGINT, _signal_handler)
326
+ signal.signal(signal.SIGTERM, _signal_handler)
327
+
328
+
239
329
  def create_mass_sim(
240
330
  mass_sim_name: str,
241
331
  total_num_sessions: int,
lucidicai/client.py CHANGED
@@ -4,12 +4,13 @@ from datetime import datetime, timezone
4
4
  from typing import Optional, Tuple
5
5
 
6
6
  import requests
7
+ import logging
7
8
  from requests.adapters import HTTPAdapter, Retry
8
9
  from urllib3.util import Retry
9
10
 
10
11
 
11
12
  from .errors import APIKeyVerificationError, InvalidOperationError, LucidicNotInitializedError
12
- from .providers.base_providers import BaseProvider
13
+ from .telemetry.base_provider import BaseProvider
13
14
  from .session import Session
14
15
  from .singleton import singleton, clear_singletons
15
16
 
@@ -30,6 +31,8 @@ class Client:
30
31
  self.providers = []
31
32
  self.api_key = lucidic_api_key
32
33
  self.agent_id = agent_id
34
+ self.masking_function = None
35
+ self.auto_end = False # Default to False until explicitly set during init
33
36
  self.request_session = requests.Session()
34
37
  retry_cfg = Retry(
35
38
  total=3, # 3 attempts in total
@@ -73,7 +76,8 @@ class Client:
73
76
  mass_sim_id: Optional[str] = None,
74
77
  task: Optional[str] = None,
75
78
  rubrics: Optional[list] = None,
76
- tags: Optional[list] = None
79
+ tags: Optional[list] = None,
80
+ production_monitoring: Optional[bool] = False
77
81
  ) -> None:
78
82
  self.session = Session(
79
83
  agent_id=self.agent_id,
@@ -81,12 +85,13 @@ class Client:
81
85
  mass_sim_id=mass_sim_id,
82
86
  task=task,
83
87
  rubrics=rubrics,
84
- tags=tags
88
+ tags=tags,
89
+ production_monitoring=production_monitoring
85
90
  )
86
91
  self.initialized = True
87
92
  return self.session.session_id
88
93
 
89
- def continue_session(self, session_id: str) -> None:
94
+ def continue_session(self, session_id: str):
90
95
  self.session = Session(
91
96
  agent_id=self.agent_id,
92
97
  session_id=session_id
@@ -147,4 +152,16 @@ class Client:
147
152
  response.raise_for_status()
148
153
  except requests.exceptions.HTTPError as e:
149
154
  raise InvalidOperationError(f"Request to Lucidic AI Backend failed: {e.response.text}")
150
- return response.json()
155
+ return response.json()
156
+
157
+ def mask(self, data):
158
+ if not self.masking_function:
159
+ return data
160
+ if not data:
161
+ return data
162
+ try:
163
+ return self.masking_function(data)
164
+ except Exception as e:
165
+ logger = logging.getLogger('Lucidic')
166
+ logger.error(f"Error in custom masking function: {repr(e)}")
167
+ return "<Error in custom masking function, this is a fully-masked placeholder>"
@@ -0,0 +1,357 @@
1
+ """Decorators for the Lucidic SDK to simplify step and event tracking."""
2
+ import functools
3
+ import contextvars
4
+ import inspect
5
+ import json
6
+ import logging
7
+ from typing import Any, Callable, Optional, TypeVar, Union
8
+
9
+ from .client import Client
10
+ from .errors import LucidicNotInitializedError
11
+
12
+ logger = logging.getLogger("Lucidic")
13
+
14
+ F = TypeVar('F', bound=Callable[..., Any])
15
+
16
+ # Create context variables to store the current step and event
17
+ _current_step = contextvars.ContextVar("current_step", default=None)
18
+ _current_event = contextvars.ContextVar("current_event", default=None)
19
+
20
+ def get_decorator_step():
21
+ return _current_step.get()
22
+
23
+ def get_decorator_event():
24
+ return _current_event.get()
25
+
26
+
27
+ def step(
28
+ state: Optional[str] = None,
29
+ action: Optional[str] = None,
30
+ goal: Optional[str] = None,
31
+ screenshot_path: Optional[str] = None,
32
+ eval_score: Optional[float] = None,
33
+ eval_description: Optional[str] = None
34
+ ) -> Callable[[F], F]:
35
+ """
36
+ Decorator that wraps a function with step tracking.
37
+
38
+ The decorated function will be wrapped with create_step() at the start
39
+ and end_step() at the end, ensuring proper cleanup even on exceptions.
40
+
41
+ Args:
42
+ state: State description for the step
43
+ action: Action description for the step
44
+ goal: Goal description for the step
45
+ eval_score: Evaluation score for the step
46
+ eval_description: Evaluation description for the step
47
+
48
+ Example:
49
+ @lai.step(
50
+ state="Processing user input",
51
+ action="Validate and parse request",
52
+ goal="Extract intent from user message"
53
+ )
54
+ def process_user_input(message: str) -> dict:
55
+ # Function logic here
56
+ return parsed_intent
57
+ """
58
+ def decorator(func: F) -> F:
59
+ @functools.wraps(func)
60
+ def sync_wrapper(*args, **kwargs):
61
+ # Check if SDK is initialized
62
+ try:
63
+ client = Client()
64
+ if not client.session:
65
+ # No active session, run function normally
66
+ logger.warning("No active session, running function normally")
67
+ return func(*args, **kwargs)
68
+ except LucidicNotInitializedError:
69
+ # SDK not initialized, run function normally
70
+ logger.warning("Lucidic not initialized, running function normally")
71
+ return func(*args, **kwargs)
72
+
73
+ # Create the step
74
+ step_params = {
75
+ 'state': state,
76
+ 'action': action,
77
+ 'goal': goal,
78
+ 'screenshot_path': screenshot_path,
79
+ 'eval_score': eval_score,
80
+ 'eval_description': eval_description
81
+ }
82
+ # Remove None values
83
+ step_params = {k: v for k, v in step_params.items() if v is not None}
84
+
85
+ # Import here to avoid circular imports
86
+ from . import create_step, end_step
87
+ step_id = create_step(**step_params)
88
+ tok = _current_step.set(step_id)
89
+
90
+ try:
91
+ # Execute the wrapped function
92
+ result = func(*args, **kwargs)
93
+ # End step successfully
94
+ end_step(step_id=step_id)
95
+ _current_step.reset(tok)
96
+ return result
97
+ except Exception as e:
98
+ # End step with error indication
99
+ try:
100
+ end_step(
101
+ step_id=step_id,
102
+ eval_score=0.0,
103
+ eval_description=f"Step failed with error: {str(e)}"
104
+ )
105
+ _current_step.reset(tok)
106
+ except Exception:
107
+ # If end_step fails, just log it
108
+ logger.error(f"Failed to end step {step_id} after error")
109
+ raise
110
+
111
+ @functools.wraps(func)
112
+ async def async_wrapper(*args, **kwargs):
113
+ # Check if SDK is initialized
114
+ try:
115
+ client = Client()
116
+ if not client.session:
117
+ # No active session, run function normally
118
+ logger.warning("No active session, running function normally")
119
+ return await func(*args, **kwargs)
120
+ except LucidicNotInitializedError:
121
+ # SDK not initialized, run function normally
122
+ logger.warning("Lucidic not initialized, running function normally")
123
+ return await func(*args, **kwargs)
124
+
125
+ # Create the step
126
+ step_params = {
127
+ 'state': state,
128
+ 'action': action,
129
+ 'goal': goal,
130
+ 'screenshot_path': screenshot_path,
131
+ 'eval_score': eval_score,
132
+ 'eval_description': eval_description
133
+ }
134
+ # Remove None values
135
+ step_params = {k: v for k, v in step_params.items() if v is not None}
136
+
137
+ # Import here to avoid circular imports
138
+ from . import create_step, end_step
139
+
140
+ step_id = create_step(**step_params)
141
+ tok = _current_step.set(step_id)
142
+ try:
143
+ # Execute the wrapped function
144
+ result = await func(*args, **kwargs)
145
+ # End step successfully
146
+ end_step(step_id=step_id)
147
+ _current_step.reset(tok)
148
+ return result
149
+ except Exception as e:
150
+ # End step with error indication
151
+ try:
152
+ end_step(
153
+ step_id=step_id,
154
+ eval_score=0.0,
155
+ eval_description=f"Step failed with error: {str(e)}"
156
+ )
157
+ _current_step.reset(tok)
158
+ except Exception:
159
+ # If end_step fails, just log it
160
+ logger.error(f"Failed to end step {step_id} after error")
161
+ raise
162
+
163
+ # Return appropriate wrapper based on function type
164
+ if inspect.iscoroutinefunction(func):
165
+ return async_wrapper
166
+ else:
167
+ return sync_wrapper
168
+
169
+ return decorator
170
+
171
+
172
+ ### -- TODO -- Updating even within function causes function result to not be recorded.
173
+ def event(
174
+ description: Optional[str] = None,
175
+ result: Optional[str] = None,
176
+ model: Optional[str] = None,
177
+ cost_added: Optional[float] = 0
178
+ ) -> Callable[[F], F]:
179
+ """
180
+ Decorator that creates an event for a function call.
181
+
182
+ The decorated function will create an event that captures:
183
+ - Function inputs (as string representation) if description not provided
184
+ - Function output (as string representation) if result not provided
185
+
186
+ LLM calls within the function will create their own events as normal.
187
+
188
+ Args:
189
+ description: Custom description for the event. If not provided,
190
+ will use string representation of function inputs
191
+ result: Custom result for the event. If not provided,
192
+ will use string representation of function output
193
+ model: Model name if this function represents a model call (default: None)
194
+ cost_added: Cost to add for this event (default: 0)
195
+
196
+ Example:
197
+ @lai.event(description="Parse user query", model="custom-parser")
198
+ def parse_query(query: str) -> dict:
199
+ # Function logic here
200
+ return {"intent": "search", "query": query}
201
+ """
202
+ def decorator(func: F) -> F:
203
+ @functools.wraps(func)
204
+ def sync_wrapper(*args, **kwargs):
205
+ # Check if SDK is initialized
206
+ try:
207
+ client = Client()
208
+ if not client.session:
209
+ # No active session, run function normally
210
+ logger.warning("No active session, running function normally")
211
+ return func(*args, **kwargs)
212
+ except (LucidicNotInitializedError, AttributeError):
213
+ # SDK not initialized or no session, run function normally
214
+ logger.warning("Lucidic not initialized, running function normally")
215
+ return func(*args, **kwargs)
216
+
217
+ # Import here to avoid circular imports
218
+ from . import create_event, end_event
219
+
220
+ # Build event description from inputs if not provided
221
+ event_desc = description
222
+ if not event_desc:
223
+ # Get function signature
224
+ sig = inspect.signature(func)
225
+ bound_args = sig.bind(*args, **kwargs)
226
+ bound_args.apply_defaults()
227
+
228
+ # Create string representation of inputs
229
+ input_parts = []
230
+ for param_name, param_value in bound_args.arguments.items():
231
+ try:
232
+ input_parts.append(f"{param_name}={repr(param_value)}")
233
+ except Exception:
234
+ input_parts.append(f"{param_name}=<{type(param_value).__name__}>")
235
+
236
+ event_desc = f"{func.__name__}({', '.join(input_parts)})"
237
+
238
+ # Create the event
239
+ event_id = create_event(
240
+ description=event_desc,
241
+ model=model,
242
+ cost_added=cost_added
243
+ )
244
+ tok = _current_event.set(event_id)
245
+ try:
246
+ # Execute the wrapped function
247
+ function_result = func(*args, **kwargs)
248
+
249
+ # Build event result from output if not provided
250
+ event_result = result
251
+ if not event_result:
252
+ try:
253
+ event_result = repr(function_result)
254
+ except Exception:
255
+ event_result = str(function_result)
256
+
257
+ # Update and end the event
258
+ end_event(
259
+ event_id=event_id,
260
+ result=event_result,
261
+ )
262
+ _current_event.reset(tok)
263
+ return function_result
264
+
265
+ except Exception as e:
266
+ # Update event with error
267
+ try:
268
+ end_event(
269
+ event_id=event_id,
270
+ result=f"Error: {str(e)}",
271
+ )
272
+ _current_event.reset(tok)
273
+ except Exception:
274
+ logger.error(f"Failed to end event {event_id} after error")
275
+ raise
276
+
277
+ @functools.wraps(func)
278
+ async def async_wrapper(*args, **kwargs):
279
+ # Check if SDK is initialized
280
+ try:
281
+ client = Client()
282
+ if not client.session:
283
+ # No active session, run function normally
284
+ logger.warning("No active session, running function normally")
285
+ return await func(*args, **kwargs)
286
+ except (LucidicNotInitializedError, AttributeError):
287
+ # SDK not initialized or no session, run function normally
288
+ logger.warning("Lucidic not initialized, running function normally")
289
+ return await func(*args, **kwargs)
290
+
291
+ # Import here to avoid circular imports
292
+ from . import create_event, end_event
293
+
294
+ # Build event description from inputs if not provided
295
+ event_desc = description
296
+ if not event_desc:
297
+ # Get function signature
298
+ sig = inspect.signature(func)
299
+ bound_args = sig.bind(*args, **kwargs)
300
+ bound_args.apply_defaults()
301
+
302
+ # Create string representation of inputs
303
+ input_parts = []
304
+ for param_name, param_value in bound_args.arguments.items():
305
+ try:
306
+ input_parts.append(f"{param_name}={repr(param_value)}")
307
+ except Exception:
308
+ input_parts.append(f"{param_name}=<{type(param_value).__name__}>")
309
+
310
+ event_desc = f"{func.__name__}({', '.join(input_parts)})"
311
+
312
+ # Create the event
313
+ event_id = create_event(
314
+ description=event_desc,
315
+ model=model,
316
+ cost_added=cost_added
317
+ )
318
+ tok = _current_event.set(event_id)
319
+ try:
320
+ # Execute the wrapped function
321
+ function_result = await func(*args, **kwargs)
322
+
323
+ # Build event result from output if not provided
324
+ event_result = result
325
+ if not event_result:
326
+ try:
327
+ event_result = repr(function_result)
328
+ except Exception:
329
+ event_result = str(function_result)
330
+
331
+ # Update and end the event
332
+ end_event(
333
+ event_id=event_id,
334
+ result=event_result,
335
+ )
336
+ _current_event.reset(tok)
337
+ return function_result
338
+
339
+ except Exception as e:
340
+ # Update event with error
341
+ try:
342
+ end_event(
343
+ event_id=event_id,
344
+ result=f"Error: {str(e)}",
345
+ )
346
+ _current_event.reset(tok)
347
+ except Exception:
348
+ logger.error(f"Failed to end event {event_id} after error")
349
+ raise
350
+
351
+ # Return appropriate wrapper based on function type
352
+ if inspect.iscoroutinefunction(func):
353
+ return async_wrapper
354
+ else:
355
+ return sync_wrapper
356
+
357
+ return decorator
lucidicai/event.py CHANGED
@@ -40,8 +40,8 @@ class Event:
40
40
  self.is_finished = kwargs['is_finished']
41
41
  request_data = {
42
42
  "event_id": self.event_id,
43
- "description": kwargs.get("description", None),
44
- "result": kwargs.get("result", None),
43
+ "description": Client().mask(kwargs.get("description", None)),
44
+ "result": Client().mask(kwargs.get("result", None)),
45
45
  "is_finished": self.is_finished,
46
46
  "cost_added": kwargs.get("cost_added", None),
47
47
  "model": kwargs.get("model", None),
lucidicai/image_upload.py CHANGED
@@ -85,4 +85,27 @@ def screenshot_path_to_jpeg(screenshot_path):
85
85
  buffered = io.BytesIO()
86
86
  img.save(buffered, format="JPEG")
87
87
  img_byte = buffered.getvalue()
88
- return base64.b64encode(img_byte).decode('utf-8')
88
+ return base64.b64encode(img_byte).decode('utf-8')
89
+
90
+ def extract_base64_images(data):
91
+ """Extract base64 image URLs from various data structures
92
+
93
+ Args:
94
+ data: Can be a string, dict, list, or nested structure containing image data
95
+
96
+ Returns:
97
+ List of base64 image data URLs (data:image/...)
98
+ """
99
+ images = []
100
+
101
+ if isinstance(data, str):
102
+ if data.startswith('data:image'):
103
+ images.append(data)
104
+ elif isinstance(data, dict):
105
+ for value in data.values():
106
+ images.extend(extract_base64_images(value))
107
+ elif isinstance(data, list):
108
+ for item in data:
109
+ images.extend(extract_base64_images(item))
110
+
111
+ return images