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.
- lucidicai/__init__.py +111 -21
- lucidicai/client.py +22 -5
- lucidicai/decorators.py +357 -0
- lucidicai/event.py +2 -2
- lucidicai/image_upload.py +24 -1
- lucidicai/providers/anthropic_handler.py +0 -7
- lucidicai/providers/image_storage.py +45 -0
- lucidicai/providers/langchain.py +0 -78
- 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/openai_handler.py +1 -56
- 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 +2 -19
- lucidicai/providers/text_storage.py +53 -0
- lucidicai/providers/universal_image_interceptor.py +276 -0
- lucidicai/session.py +17 -4
- lucidicai/step.py +4 -4
- lucidicai/streaming.py +2 -3
- 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.15.dist-info → lucidicai-1.2.17.dist-info}/METADATA +1 -1
- lucidicai-1.2.17.dist-info/RECORD +49 -0
- lucidicai-1.2.15.dist-info/RECORD +0 -25
- {lucidicai-1.2.15.dist-info → lucidicai-1.2.17.dist-info}/WHEEL +0 -0
- {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(
|
|
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,11 +99,8 @@ __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
|
|
|
@@ -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 .
|
|
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)
|
|
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>"
|
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/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
|