lucidicai 1.2.16__py3-none-any.whl → 1.2.18__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 +105 -30
- lucidicai/client.py +10 -4
- lucidicai/decorators.py +357 -0
- lucidicai/image_upload.py +24 -1
- lucidicai/providers/image_storage.py +45 -0
- lucidicai/providers/lucidic_exporter.py +259 -0
- lucidicai/providers/lucidic_span_processor.py +648 -0
- lucidicai/providers/openai_agents_instrumentor.py +307 -0
- lucidicai/providers/otel_handlers.py +266 -0
- lucidicai/providers/otel_init.py +197 -0
- lucidicai/providers/otel_provider.py +168 -0
- lucidicai/providers/pydantic_ai_handler.py +1 -1
- lucidicai/providers/text_storage.py +53 -0
- lucidicai/providers/universal_image_interceptor.py +276 -0
- lucidicai/session.py +9 -1
- lucidicai/telemetry/__init__.py +0 -0
- lucidicai/telemetry/base_provider.py +21 -0
- lucidicai/telemetry/lucidic_exporter.py +259 -0
- lucidicai/telemetry/lucidic_span_processor.py +665 -0
- lucidicai/telemetry/openai_agents_instrumentor.py +306 -0
- lucidicai/telemetry/opentelemetry_converter.py +436 -0
- lucidicai/telemetry/otel_handlers.py +266 -0
- lucidicai/telemetry/otel_init.py +197 -0
- lucidicai/telemetry/otel_provider.py +168 -0
- lucidicai/telemetry/pydantic_ai_handler.py +600 -0
- lucidicai/telemetry/utils/__init__.py +0 -0
- lucidicai/telemetry/utils/image_storage.py +45 -0
- lucidicai/telemetry/utils/text_storage.py +53 -0
- lucidicai/telemetry/utils/universal_image_interceptor.py +276 -0
- {lucidicai-1.2.16.dist-info → lucidicai-1.2.18.dist-info}/METADATA +1 -1
- lucidicai-1.2.18.dist-info/RECORD +49 -0
- lucidicai-1.2.16.dist-info/RECORD +0 -25
- {lucidicai-1.2.16.dist-info → lucidicai-1.2.18.dist-info}/WHEEL +0 -0
- {lucidicai-1.2.16.dist-info → lucidicai-1.2.18.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(
|
|
61
|
+
client.set_provider(OTelOpenAIHandler())
|
|
46
62
|
setup_providers.add("openai")
|
|
47
63
|
elif provider == "anthropic":
|
|
48
|
-
client.set_provider(
|
|
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(
|
|
71
|
+
client.set_provider(OTelPydanticAIHandler())
|
|
55
72
|
setup_providers.add("pydantic_ai")
|
|
56
73
|
elif provider == "openai_agents":
|
|
57
74
|
try:
|
|
58
|
-
|
|
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,16 +99,14 @@ __all__ = [
|
|
|
87
99
|
'LucidicNotInitializedError',
|
|
88
100
|
'PromptError',
|
|
89
101
|
'InvalidOperationError',
|
|
90
|
-
'
|
|
91
|
-
'
|
|
92
|
-
'OpenAIHandler',
|
|
93
|
-
'OpenAIAgentsHandler',
|
|
94
|
-
'PydanticAIHandler'
|
|
102
|
+
'step',
|
|
103
|
+
'event',
|
|
95
104
|
]
|
|
96
105
|
|
|
97
106
|
|
|
98
107
|
def init(
|
|
99
|
-
session_name: str,
|
|
108
|
+
session_name: Optional[str] = None,
|
|
109
|
+
session_id: Optional[str] = None,
|
|
100
110
|
lucidic_api_key: Optional[str] = None,
|
|
101
111
|
agent_id: Optional[str] = None,
|
|
102
112
|
task: Optional[str] = None,
|
|
@@ -106,12 +116,14 @@ def init(
|
|
|
106
116
|
rubrics: Optional[list] = None,
|
|
107
117
|
tags: Optional[list] = None,
|
|
108
118
|
masking_function = None,
|
|
119
|
+
auto_end: Optional[bool] = True,
|
|
109
120
|
) -> str:
|
|
110
121
|
"""
|
|
111
122
|
Initialize the Lucidic client.
|
|
112
123
|
|
|
113
124
|
Args:
|
|
114
|
-
session_name: The name of the session.
|
|
125
|
+
session_name: The display name of the session.
|
|
126
|
+
session_id: Custom ID of the session. If not provided, a random ID will be generated.
|
|
115
127
|
lucidic_api_key: API key for authentication. If not provided, will use the LUCIDIC_API_KEY environment variable.
|
|
116
128
|
agent_id: Agent ID. If not provided, will use the LUCIDIC_AGENT_ID environment variable.
|
|
117
129
|
task: Task description.
|
|
@@ -120,6 +132,7 @@ def init(
|
|
|
120
132
|
rubrics: Optional rubrics for evaluation, list of strings.
|
|
121
133
|
tags: Optional tags for the session, list of strings.
|
|
122
134
|
masking_function: Optional function to mask sensitive data.
|
|
135
|
+
auto_end: If True, automatically end the session on process exit. Defaults to True.
|
|
123
136
|
|
|
124
137
|
Raises:
|
|
125
138
|
InvalidOperationError: If the client is already initialized.
|
|
@@ -147,6 +160,10 @@ def init(
|
|
|
147
160
|
else:
|
|
148
161
|
production_monitoring = False
|
|
149
162
|
|
|
163
|
+
# Handle auto_end with environment variable support
|
|
164
|
+
if auto_end is None:
|
|
165
|
+
auto_end = os.getenv("LUCIDIC_AUTO_END", "True").lower() == "true"
|
|
166
|
+
|
|
150
167
|
# Set up providers
|
|
151
168
|
_setup_providers(client, providers)
|
|
152
169
|
session_id = client.init_session(
|
|
@@ -156,9 +173,14 @@ def init(
|
|
|
156
173
|
rubrics=rubrics,
|
|
157
174
|
tags=tags,
|
|
158
175
|
production_monitoring=production_monitoring,
|
|
176
|
+
custom_session_id=session_id,
|
|
159
177
|
)
|
|
160
178
|
if masking_function:
|
|
161
179
|
client.masking_function = masking_function
|
|
180
|
+
|
|
181
|
+
# Set the auto_end flag on the client
|
|
182
|
+
client.auto_end = auto_end
|
|
183
|
+
|
|
162
184
|
logger.info("Session initialized successfully")
|
|
163
185
|
return session_id
|
|
164
186
|
|
|
@@ -169,6 +191,7 @@ def continue_session(
|
|
|
169
191
|
agent_id: Optional[str] = None,
|
|
170
192
|
providers: Optional[List[ProviderType]] = [],
|
|
171
193
|
masking_function = None,
|
|
194
|
+
auto_end: Optional[bool] = True,
|
|
172
195
|
):
|
|
173
196
|
if lucidic_api_key is None:
|
|
174
197
|
lucidic_api_key = os.getenv("LUCIDIC_API_KEY", None)
|
|
@@ -178,21 +201,27 @@ def continue_session(
|
|
|
178
201
|
agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
|
|
179
202
|
if agent_id is None:
|
|
180
203
|
raise APIKeyVerificationError("Lucidic agent ID not specified. Make sure to either pass your agent ID into lai.init() or set the LUCIDIC_AGENT_ID environment variable.")
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
204
|
+
|
|
205
|
+
client = Client()
|
|
206
|
+
if client.session:
|
|
207
|
+
raise InvalidOperationError("[Lucidic] Session already in progress. Please call lai.end_session() or lai.reset_sdk() first.")
|
|
208
|
+
# if not yet initialized or still the NullClient -> create a real client when init is called
|
|
209
|
+
if not getattr(client, 'initialized', False):
|
|
210
|
+
client = Client(lucidic_api_key=lucidic_api_key, agent_id=agent_id)
|
|
211
|
+
|
|
212
|
+
# Handle auto_end with environment variable support
|
|
213
|
+
if auto_end is None:
|
|
214
|
+
auto_end = os.getenv("LUCIDIC_AUTO_END", "True").lower() == "true"
|
|
190
215
|
|
|
191
216
|
# Set up providers
|
|
192
217
|
_setup_providers(client, providers)
|
|
193
218
|
session_id = client.continue_session(session_id=session_id)
|
|
194
219
|
if masking_function:
|
|
195
220
|
client.masking_function = masking_function
|
|
221
|
+
|
|
222
|
+
# Set the auto_end flag on the client
|
|
223
|
+
client.auto_end = auto_end
|
|
224
|
+
|
|
196
225
|
logger.info(f"Session {session_id} continuing...")
|
|
197
226
|
return session_id # For consistency
|
|
198
227
|
|
|
@@ -249,9 +278,55 @@ def reset_sdk() -> None:
|
|
|
249
278
|
client = Client()
|
|
250
279
|
if not client.initialized:
|
|
251
280
|
return
|
|
281
|
+
|
|
282
|
+
# Shutdown OpenTelemetry if it was initialized
|
|
283
|
+
telemetry = LucidicTelemetry()
|
|
284
|
+
if telemetry.is_initialized():
|
|
285
|
+
telemetry.uninstrument_all()
|
|
286
|
+
|
|
252
287
|
client.clear()
|
|
253
288
|
|
|
254
289
|
|
|
290
|
+
def _cleanup_telemetry():
|
|
291
|
+
"""Cleanup function for OpenTelemetry shutdown"""
|
|
292
|
+
try:
|
|
293
|
+
telemetry = LucidicTelemetry()
|
|
294
|
+
if telemetry.is_initialized():
|
|
295
|
+
telemetry.uninstrument_all()
|
|
296
|
+
logger.info("OpenTelemetry instrumentation cleaned up")
|
|
297
|
+
except Exception as e:
|
|
298
|
+
logger.error(f"Error during telemetry cleanup: {e}")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _auto_end_session():
|
|
302
|
+
"""Automatically end session on exit if auto_end is enabled"""
|
|
303
|
+
try:
|
|
304
|
+
client = Client()
|
|
305
|
+
if hasattr(client, 'auto_end') and client.auto_end and client.session and not client.session.is_finished:
|
|
306
|
+
logger.info("Auto-ending active session on exit")
|
|
307
|
+
end_session()
|
|
308
|
+
except Exception as e:
|
|
309
|
+
logger.debug(f"Error during auto-end session: {e}")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _signal_handler(signum, frame):
|
|
313
|
+
"""Handle interruption signals"""
|
|
314
|
+
_auto_end_session()
|
|
315
|
+
_cleanup_telemetry()
|
|
316
|
+
# Re-raise the signal for default handling
|
|
317
|
+
signal.signal(signum, signal.SIG_DFL)
|
|
318
|
+
os.kill(os.getpid(), signum)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# Register cleanup functions (auto-end runs first due to LIFO order)
|
|
322
|
+
atexit.register(_cleanup_telemetry)
|
|
323
|
+
atexit.register(_auto_end_session)
|
|
324
|
+
|
|
325
|
+
# Register signal handlers for graceful shutdown
|
|
326
|
+
signal.signal(signal.SIGINT, _signal_handler)
|
|
327
|
+
signal.signal(signal.SIGTERM, _signal_handler)
|
|
328
|
+
|
|
329
|
+
|
|
255
330
|
def create_mass_sim(
|
|
256
331
|
mass_sim_name: str,
|
|
257
332
|
total_num_sessions: int,
|
lucidicai/client.py
CHANGED
|
@@ -10,7 +10,7 @@ from urllib3.util import Retry
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
from .errors import APIKeyVerificationError, InvalidOperationError, LucidicNotInitializedError
|
|
13
|
-
from .
|
|
13
|
+
from .telemetry.base_provider import BaseProvider
|
|
14
14
|
from .session import Session
|
|
15
15
|
from .singleton import singleton, clear_singletons
|
|
16
16
|
|
|
@@ -32,6 +32,7 @@ class Client:
|
|
|
32
32
|
self.api_key = lucidic_api_key
|
|
33
33
|
self.agent_id = agent_id
|
|
34
34
|
self.masking_function = None
|
|
35
|
+
self.auto_end = False # Default to False until explicitly set during init
|
|
35
36
|
self.request_session = requests.Session()
|
|
36
37
|
retry_cfg = Retry(
|
|
37
38
|
total=3, # 3 attempts in total
|
|
@@ -76,7 +77,8 @@ class Client:
|
|
|
76
77
|
task: Optional[str] = None,
|
|
77
78
|
rubrics: Optional[list] = None,
|
|
78
79
|
tags: Optional[list] = None,
|
|
79
|
-
production_monitoring: Optional[bool] = False
|
|
80
|
+
production_monitoring: Optional[bool] = False,
|
|
81
|
+
custom_session_id: Optional[str] = None,
|
|
80
82
|
) -> None:
|
|
81
83
|
self.session = Session(
|
|
82
84
|
agent_id=self.agent_id,
|
|
@@ -85,16 +87,20 @@ class Client:
|
|
|
85
87
|
task=task,
|
|
86
88
|
rubrics=rubrics,
|
|
87
89
|
tags=tags,
|
|
88
|
-
production_monitoring=production_monitoring
|
|
90
|
+
production_monitoring=production_monitoring,
|
|
91
|
+
custom_session_id=custom_session_id
|
|
89
92
|
)
|
|
90
93
|
self.initialized = True
|
|
91
94
|
return self.session.session_id
|
|
92
95
|
|
|
93
|
-
def continue_session(self, session_id: str)
|
|
96
|
+
def continue_session(self, session_id: str):
|
|
94
97
|
self.session = Session(
|
|
95
98
|
agent_id=self.agent_id,
|
|
96
99
|
session_id=session_id
|
|
97
100
|
)
|
|
101
|
+
if self.session.session_id != session_id:
|
|
102
|
+
# Custom session ID provided
|
|
103
|
+
self.session.custom_session_id = session_id
|
|
98
104
|
self.initialized = True
|
|
99
105
|
return self.session.session_id
|
|
100
106
|
|
lucidicai/decorators.py
ADDED
|
@@ -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/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
|