lucidicai 2.0.1__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. lucidicai/__init__.py +351 -876
  2. lucidicai/api/__init__.py +1 -0
  3. lucidicai/api/client.py +218 -0
  4. lucidicai/api/resources/__init__.py +1 -0
  5. lucidicai/api/resources/dataset.py +192 -0
  6. lucidicai/api/resources/event.py +88 -0
  7. lucidicai/api/resources/session.py +126 -0
  8. lucidicai/client.py +4 -1
  9. lucidicai/core/__init__.py +1 -0
  10. lucidicai/core/config.py +223 -0
  11. lucidicai/core/errors.py +60 -0
  12. lucidicai/core/types.py +35 -0
  13. lucidicai/dataset.py +2 -0
  14. lucidicai/errors.py +6 -0
  15. lucidicai/feature_flag.py +8 -0
  16. lucidicai/sdk/__init__.py +1 -0
  17. lucidicai/sdk/context.py +144 -0
  18. lucidicai/sdk/decorators.py +187 -0
  19. lucidicai/sdk/error_boundary.py +299 -0
  20. lucidicai/sdk/event.py +122 -0
  21. lucidicai/sdk/event_builder.py +304 -0
  22. lucidicai/sdk/features/__init__.py +1 -0
  23. lucidicai/sdk/features/dataset.py +605 -0
  24. lucidicai/sdk/features/feature_flag.py +383 -0
  25. lucidicai/sdk/init.py +271 -0
  26. lucidicai/sdk/shutdown_manager.py +302 -0
  27. lucidicai/telemetry/context_bridge.py +82 -0
  28. lucidicai/telemetry/context_capture_processor.py +25 -9
  29. lucidicai/telemetry/litellm_bridge.py +18 -24
  30. lucidicai/telemetry/lucidic_exporter.py +51 -36
  31. lucidicai/telemetry/utils/model_pricing.py +278 -0
  32. lucidicai/utils/__init__.py +1 -0
  33. lucidicai/utils/images.py +337 -0
  34. lucidicai/utils/logger.py +168 -0
  35. lucidicai/utils/queue.py +393 -0
  36. {lucidicai-2.0.1.dist-info → lucidicai-2.1.0.dist-info}/METADATA +1 -1
  37. {lucidicai-2.0.1.dist-info → lucidicai-2.1.0.dist-info}/RECORD +39 -12
  38. {lucidicai-2.0.1.dist-info → lucidicai-2.1.0.dist-info}/WHEEL +0 -0
  39. {lucidicai-2.0.1.dist-info → lucidicai-2.1.0.dist-info}/top_level.txt +0 -0
lucidicai/__init__.py CHANGED
@@ -1,917 +1,392 @@
1
- import atexit
2
- import logging
3
- import os
4
- import signal
5
- import sys
6
- import traceback
7
- import threading
8
- from typing import List, Literal, Optional
9
-
10
- from dotenv import load_dotenv
11
-
12
- from .client import Client
13
- from .errors import APIKeyVerificationError, InvalidOperationError, LucidicNotInitializedError, PromptError
14
- from .event import Event
15
- from .session import Session
16
- from .singleton import clear_singletons
17
-
18
- # Import decorators
19
- from .decorators import event
20
- from .context import (
1
+ """Lucidic AI SDK - Clean Export-Only Entry Point
2
+
3
+ This file only contains exports, with all logic moved to appropriate modules.
4
+ """
5
+
6
+ # Import core modules
7
+ from .sdk import init as init_module
8
+ from .sdk import event as event_module
9
+ from .sdk import error_boundary
10
+ from .core.config import get_config
11
+
12
+ # Import raw functions
13
+ from .sdk.init import (
14
+ init as _init,
15
+ get_session_id as _get_session_id,
16
+ clear_state as _clear_state,
17
+ )
18
+
19
+ from .sdk.event import (
20
+ create_event as _create_event,
21
+ create_error_event as _create_error_event,
22
+ flush as _flush,
23
+ )
24
+
25
+ # Context management exports
26
+ from .sdk.context import (
21
27
  set_active_session,
28
+ clear_active_session,
22
29
  bind_session,
23
30
  bind_session_async,
24
- clear_active_session,
25
- current_session_id,
26
31
  session,
27
32
  session_async,
28
33
  run_session,
29
34
  run_in_session,
35
+ current_session_id,
36
+ current_parent_event_id,
30
37
  )
31
38
 
32
- ProviderType = Literal[
33
- "openai",
34
- "anthropic",
35
- "langchain",
36
- "pydantic_ai",
37
- "openai_agents",
38
- "litellm",
39
- "bedrock",
40
- "aws_bedrock",
41
- "amazon_bedrock",
42
- "google",
43
- "google_generativeai",
44
- "vertexai",
45
- "vertex_ai",
46
- "cohere",
47
- "groq",
48
- ]
49
-
50
- # Configure logging
51
- logger = logging.getLogger("Lucidic")
52
- if not logger.handlers:
53
- handler = logging.StreamHandler()
54
- formatter = logging.Formatter('[Lucidic] %(message)s')
55
- handler.setFormatter(formatter)
56
- logger.addHandler(handler)
57
- logger.setLevel(logging.INFO)
58
-
59
-
60
- # Crash/exit capture configuration
61
- MAX_ERROR_DESCRIPTION_LENGTH = 16384
62
- _crash_handlers_installed = False
63
- _original_sys_excepthook = None
64
- _original_threading_excepthook = None
65
- _shutdown_lock = threading.Lock()
66
- _is_shutting_down = False
67
-
68
-
69
- def _mask_and_truncate(text: Optional[str]) -> Optional[str]:
70
- """Apply masking and truncate to a safe length. Best effort; never raises."""
71
- if text is None:
72
- return text
73
- try:
74
- masked = Client().mask(text)
75
- except Exception:
76
- masked = text
77
- if masked is None:
78
- return masked
79
- return masked[:MAX_ERROR_DESCRIPTION_LENGTH]
80
-
81
-
82
- def _post_fatal_event(exit_code: int, description: str, extra: Optional[dict] = None) -> None:
83
- """Best-effort creation of a final Lucidic event on fatal paths.
84
-
85
- - Idempotent using a process-wide shutdown flag to avoid duplicates when
86
- multiple hooks fire (signal + excepthook).
87
- - Swallows all exceptions to avoid interfering with shutdown.
88
- """
89
- global _is_shutting_down
90
- with _shutdown_lock:
91
- if _is_shutting_down:
92
- return
93
- _is_shutting_down = True
94
- try:
95
- client = Client()
96
- session = getattr(client, 'session', None)
97
- if not session or getattr(session, 'is_finished', False):
98
- return
99
- arguments = {"exit_code": exit_code}
100
- if extra:
101
- try:
102
- arguments.update(extra)
103
- except Exception:
104
- pass
105
-
106
- # Create a single immutable event describing the crash
107
- session.create_event(
108
- type="error_traceback",
109
- error=_mask_and_truncate(description),
110
- traceback="",
111
- metadata={"exit_code": exit_code, **({} if not extra else extra)},
112
- )
113
- except Exception:
114
- # Never raise during shutdown
115
- pass
116
-
117
-
118
- def _install_crash_handlers() -> None:
119
- """Install global uncaught exception handlers (idempotent)."""
120
- global _crash_handlers_installed, _original_sys_excepthook, _original_threading_excepthook
121
- if _crash_handlers_installed:
122
- return
123
-
124
- _original_sys_excepthook = sys.excepthook
125
-
126
- def _sys_hook(exc_type, exc, tb):
127
- try:
128
- trace_str = ''.join(traceback.format_exception(exc_type, exc, tb))
129
- except Exception:
130
- trace_str = f"Uncaught exception: {getattr(exc_type, '__name__', str(exc_type))}: {exc}"
131
-
132
- # Emit final event and end the session as unsuccessful
133
- _post_fatal_event(1, trace_str, {
134
- "exception_type": getattr(exc_type, "__name__", str(exc_type)),
135
- "exception_message": str(exc),
136
- "thread_name": threading.current_thread().name,
137
- })
138
-
139
- # Follow proper shutdown sequence to prevent broken pipes
140
- try:
141
- client = Client()
142
-
143
- # 1. Flush OpenTelemetry spans first
144
- if hasattr(client, '_tracer_provider'):
145
- try:
146
- client._tracer_provider.force_flush(timeout_millis=5000)
147
- except Exception:
148
- pass
149
-
150
- # 2. Flush and shutdown EventQueue (with active sessions cleared)
151
- if hasattr(client, "_event_queue"):
152
- try:
153
- # Clear active sessions to allow shutdown
154
- client._event_queue._active_sessions.clear()
155
- client._event_queue.force_flush()
156
- client._event_queue.shutdown(timeout=5.0)
157
- except Exception:
158
- pass
159
-
160
- # 3. Shutdown TracerProvider after EventQueue
161
- if hasattr(client, '_tracer_provider'):
162
- try:
163
- client._tracer_provider.shutdown()
164
- except Exception:
165
- pass
166
-
167
- # 4. Mark client as shutting down to prevent new requests
168
- client._shutdown = True
169
-
170
- # 5. Prevent auto_end double work
171
- try:
172
- client.auto_end = False
173
- except Exception:
174
- pass
175
-
176
- # 6. End session explicitly as unsuccessful
177
- end_session()
178
-
179
- except Exception:
180
- pass
181
-
182
- # Chain to original to preserve default printing/behavior
183
- try:
184
- _original_sys_excepthook(exc_type, exc, tb)
185
- except Exception:
186
- # Avoid recursion/errors in fatal path
187
- pass
188
-
189
- sys.excepthook = _sys_hook
190
-
191
- # For Python 3.8+, only treat main-thread exceptions as fatal (process-exiting)
192
- if hasattr(threading, 'excepthook'):
193
- _original_threading_excepthook = threading.excepthook
194
-
195
- def _thread_hook(args):
196
- try:
197
- if args.thread is threading.main_thread():
198
- # For main thread exceptions, use full shutdown sequence
199
- _sys_hook(args.exc_type, args.exc_value, args.exc_traceback)
200
- else:
201
- # For non-main threads, just flush spans without full shutdown
202
- try:
203
- client = Client()
204
- # Flush any pending spans from this thread
205
- if hasattr(client, '_tracer_provider'):
206
- client._tracer_provider.force_flush(timeout_millis=1000)
207
- # Force flush events but don't shutdown
208
- if hasattr(client, "_event_queue"):
209
- client._event_queue.force_flush()
210
- except Exception:
211
- pass
212
- except Exception:
213
- pass
214
- try:
215
- _original_threading_excepthook(args)
216
- except Exception:
217
- pass
218
-
219
- threading.excepthook = _thread_hook
220
-
221
- _crash_handlers_installed = True
39
+ # Decorators
40
+ from .sdk.decorators import event, event as step # step is deprecated alias
41
+
42
+ # Error types
43
+ from .core.errors import (
44
+ LucidicError,
45
+ LucidicNotInitializedError,
46
+ APIKeyVerificationError,
47
+ InvalidOperationError,
48
+ PromptError,
49
+ FeatureFlagError,
50
+ )
222
51
 
223
- __all__ = [
224
- 'Session',
225
- 'Event',
226
- 'init',
227
- 'create_experiment',
228
- 'create_event',
229
- 'end_session',
230
- 'get_prompt',
231
- 'get_session',
232
- 'ProviderType',
233
- 'APIKeyVerificationError',
234
- 'LucidicNotInitializedError',
235
- 'PromptError',
236
- 'InvalidOperationError',
237
- 'event',
238
- 'set_active_session',
239
- 'bind_session',
240
- 'bind_session_async',
241
- 'clear_active_session',
242
- 'session',
243
- 'session_async',
244
- 'run_session',
245
- 'run_in_session',
246
- ]
247
-
248
-
249
- def init(
250
- session_name: Optional[str] = None,
251
- session_id: Optional[str] = None,
252
- api_key: Optional[str] = None,
253
- agent_id: Optional[str] = None,
254
- task: Optional[str] = None,
255
- providers: Optional[List[ProviderType]] = [],
256
- production_monitoring: Optional[bool] = False,
257
- experiment_id: Optional[str] = None,
258
- rubrics: Optional[list] = None,
259
- tags: Optional[list] = None,
260
- masking_function = None,
261
- auto_end: Optional[bool] = True,
262
- capture_uncaught: Optional[bool] = True,
263
- ) -> str:
264
- """
265
- Initialize the Lucidic client.
52
+ # Import functions that need to be implemented
53
+ def _update_session(
54
+ task=None,
55
+ session_eval=None,
56
+ session_eval_reason=None,
57
+ is_successful=None,
58
+ is_successful_reason=None
59
+ ):
60
+ """Update the current session."""
61
+ from .sdk.init import get_resources, get_session_id
266
62
 
267
- Args:
268
- session_name: The display name of the session.
269
- session_id: Custom ID of the session. If not provided, a random ID will be generated.
270
- api_key: API key for authentication. If not provided, will use the LUCIDIC_API_KEY environment variable.
271
- agent_id: Agent ID. If not provided, will use the LUCIDIC_AGENT_ID environment variable.
272
- task: Task description.
273
- providers: List of provider types ("openai", "anthropic", "langchain", "pydantic_ai").
274
- experiment_id: Optional experiment ID, if session is to be part of an experiment.
275
- rubrics: Optional rubrics for evaluation, list of strings.
276
- tags: Optional tags for the session, list of strings.
277
- masking_function: Optional function to mask sensitive data.
278
- auto_end: If True, automatically end the session on process exit. Defaults to True.
63
+ session_id = get_session_id()
64
+ if not session_id:
65
+ return
279
66
 
280
- Raises:
281
- InvalidOperationError: If the client is already initialized.
282
- APIKeyVerificationError: If the API key is invalid.
283
- """
284
-
285
- load_dotenv()
286
-
287
- if os.getenv("LUCIDIC_DEBUG", "False").lower() == "true":
288
- logger.setLevel(logging.DEBUG)
67
+ resources = get_resources()
68
+ if resources and 'sessions' in resources:
69
+ updates = {}
70
+ if task is not None:
71
+ updates['task'] = task
72
+ if session_eval is not None:
73
+ updates['session_eval'] = session_eval
74
+ if session_eval_reason is not None:
75
+ updates['session_eval_reason'] = session_eval_reason
76
+ if is_successful is not None:
77
+ updates['is_successful'] = is_successful
78
+ if is_successful_reason is not None:
79
+ updates['is_successful_reason'] = is_successful_reason
80
+
81
+ if updates:
82
+ resources['sessions'].update_session(session_id, updates)
83
+
84
+
85
+ def _end_session(
86
+ session_eval=None,
87
+ session_eval_reason=None,
88
+ is_successful=None,
89
+ is_successful_reason=None,
90
+ wait_for_flush=True
91
+ ):
92
+ """End the current session."""
93
+ from .sdk.init import get_resources, get_session_id, get_event_queue
289
94
 
290
- # get current client which will be NullClient if never lai is never initialized
291
- client = Client()
292
- # if not yet initialized or still the NullClient -> creaet a real client when init is called
293
- if not getattr(client, 'initialized', False):
294
- if api_key is None:
295
- api_key = os.getenv("LUCIDIC_API_KEY", None)
296
- if api_key is None:
297
- raise APIKeyVerificationError("Make sure to either pass your API key into lai.init() or set the LUCIDIC_API_KEY environment variable.")
298
- if agent_id is None:
299
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
300
- if agent_id is None:
301
- 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.")
302
- client = Client(api_key=api_key, agent_id=agent_id)
303
- else:
304
- # Already initialized, this is a re-init
305
- api_key = api_key or os.getenv("LUCIDIC_API_KEY", None)
306
- agent_id = agent_id or os.getenv("LUCIDIC_AGENT_ID", None)
307
- client.agent_id = agent_id
308
- if api_key is not None and agent_id is not None and (api_key != client.api_key or agent_id != client.agent_id):
309
- client.set_api_key(api_key)
310
- client.agent_id = agent_id
311
-
95
+ session_id = get_session_id()
96
+ if not session_id:
97
+ return
312
98
 
313
- # Handle auto_end with environment variable support
314
- if auto_end is None:
315
- auto_end = os.getenv("LUCIDIC_AUTO_END", "True").lower() == "true"
99
+ # Flush events if requested
100
+ if wait_for_flush:
101
+ flush(timeout_seconds=5.0)
316
102
 
317
- # Set up providers
318
- # Use the client's singleton telemetry initialization
319
- if providers:
320
- success = client.initialize_telemetry(providers)
321
- if not success:
322
- logger.warning("[Telemetry] Failed to initialize telemetry for some providers")
323
- real_session_id = client.init_session(
324
- session_name=session_name,
325
- task=task,
326
- rubrics=rubrics,
327
- tags=tags,
328
- production_monitoring=production_monitoring,
329
- session_id=session_id,
330
- experiment_id=experiment_id,
331
- )
332
- if masking_function:
333
- client.masking_function = masking_function
103
+ # End session via API
104
+ resources = get_resources()
105
+ if resources and 'sessions' in resources:
106
+ resources['sessions'].end_session(
107
+ session_id,
108
+ is_successful=is_successful,
109
+ session_eval=session_eval,
110
+ is_successful_reason=is_successful_reason,
111
+ session_eval_reason=session_eval_reason
112
+ )
334
113
 
335
- # Set the auto_end flag on the client
336
- client.auto_end = auto_end
337
- # Bind this session id to the current execution context for async-safety
338
- try:
339
- set_active_session(real_session_id)
340
- except Exception:
341
- pass
342
- # Install crash handlers unless explicitly disabled
343
- try:
344
- if capture_uncaught:
345
- _install_crash_handlers()
346
- # Also install error event handler for uncaught exceptions
347
- try:
348
- from .errors import install_error_handler
349
- install_error_handler()
350
- except Exception:
351
- pass
352
- except Exception:
353
- pass
114
+ # Clear session context
115
+ clear_active_session()
116
+
117
+
118
+ def _get_session():
119
+ """Get the current session object."""
120
+ from .sdk.init import get_session_id
121
+ return get_session_id()
122
+
123
+
124
+ def _create_experiment(
125
+ experiment_name,
126
+ LLM_boolean_evaluators=None,
127
+ LLM_numeric_evaluators=None,
128
+ description=None,
129
+ tags=None,
130
+ api_key=None,
131
+ agent_id=None,
132
+ ):
133
+ """Create a new experiment."""
134
+ from .sdk.init import get_http
135
+ from .core.config import SDKConfig, get_config
354
136
 
355
- logger.info("Session initialized successfully")
356
- return real_session_id
357
-
358
-
359
- def update_session(
360
- task: Optional[str] = None,
361
- session_eval: Optional[float] = None,
362
- session_eval_reason: Optional[str] = None,
363
- is_successful: Optional[bool] = None,
364
- is_successful_reason: Optional[str] = None
365
- ) -> None:
366
- """
367
- Update the current session.
137
+ # Get or create HTTP client
138
+ http = get_http()
139
+ config = get_config()
368
140
 
369
- Args:
370
- task: Task description.
371
- session_eval: Session evaluation.
372
- session_eval_reason: Session evaluation reason.
373
- is_successful: Whether the session was successful.
374
- is_successful_reason: Session success reason.
375
- """
376
- # Prefer context-bound session over global active session
377
- client = Client()
378
- target_sid = None
379
- try:
380
- target_sid = current_session_id.get(None)
381
- except Exception:
382
- target_sid = None
383
- if not target_sid and client.session:
384
- target_sid = client.session.session_id
385
- if not target_sid:
386
- return
387
- # Use ephemeral session facade to avoid mutating global state
388
- session = client.session if (client.session and client.session.session_id == target_sid) else Session(agent_id=client.agent_id, session_id=target_sid)
389
- session.update_session(**locals())
390
-
391
-
392
- def end_session(
393
- session_eval: Optional[float] = None,
394
- session_eval_reason: Optional[str] = None,
395
- is_successful: Optional[bool] = None,
396
- is_successful_reason: Optional[str] = None,
397
- wait_for_flush: bool = True
398
- ) -> None:
399
- """
400
- End the current session.
141
+ if not http:
142
+ config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
143
+ from .api.client import HttpClient
144
+ http = HttpClient(config)
401
145
 
402
- Args:
403
- session_eval: Session evaluation.
404
- session_eval_reason: Session evaluation reason.
405
- is_successful: Whether the session was successful.
406
- is_successful_reason: Session success reason.
407
- wait_for_flush: Whether to block until event queue is empty (default True).
408
- Set to False during signal handling to prevent hangs.
409
- """
410
- client = Client()
411
- # Prefer context-bound session id
412
- target_sid = None
413
- try:
414
- target_sid = current_session_id.get(None)
415
- except Exception:
416
- target_sid = None
417
- if not target_sid and client.session:
418
- target_sid = client.session.session_id
419
- if not target_sid:
420
- return
421
-
422
- # If ending the globally active session, perform cleanup
423
- if client.session and client.session.session_id == target_sid:
424
- # Best-effort: wait for LiteLLM callbacks to flush before ending
425
- try:
426
- import litellm
427
- cbs = getattr(litellm, 'callbacks', None)
428
- if cbs:
429
- for cb in cbs:
430
- try:
431
- if hasattr(cb, 'wait_for_pending_callbacks'):
432
- cb.wait_for_pending_callbacks(timeout=1)
433
- except Exception:
434
- pass
435
- except Exception:
436
- pass
437
- # CRITICAL: Flush OpenTelemetry spans FIRST (blocking)
438
- # This ensures all spans are converted to events before we flush the event queue
439
- try:
440
- if hasattr(client, '_tracer_provider') and client._tracer_provider:
441
- logger.debug("[Session] Flushing OpenTelemetry spans before session end...")
442
- # Force flush with generous timeout to ensure all spans are exported
443
- # The BatchSpanProcessor now exports every 100ms, so this should be quick
444
- success = client._tracer_provider.force_flush(timeout_millis=10000) # 10 second timeout
445
- if not success:
446
- logger.warning("[Session] OpenTelemetry flush timed out - some spans may be lost")
447
- else:
448
- logger.debug("[Session] OpenTelemetry spans flushed successfully")
449
- except Exception as e:
450
- logger.debug(f"[Session] Failed to flush telemetry spans: {e}")
451
-
452
- # THEN flush event queue (which now contains events from flushed spans)
453
- try:
454
- if hasattr(client, '_event_queue'):
455
- logger.debug("[Session] Flushing event queue...")
456
- client._event_queue.force_flush(timeout_seconds=10.0)
457
-
458
- # Wait for queue to be completely empty (only if blocking)
459
- if wait_for_flush:
460
- import time
461
- wait_start = time.time()
462
- max_wait = 10.0 # seconds - timeout for blob uploads
463
- while not client._event_queue.is_empty():
464
- if time.time() - wait_start > max_wait:
465
- logger.warning(f"[Session] EventQueue not empty after {max_wait}s timeout")
466
- break
467
- time.sleep(0.1)
468
-
469
- if client._event_queue.is_empty():
470
- logger.debug("[Session] EventQueue confirmed empty")
471
- else:
472
- logger.debug("[Session] Non-blocking mode - skipping wait for empty queue")
473
- except Exception as e:
474
- logger.debug(f"[Session] Failed to flush event queue: {e}")
475
-
476
- # Mark session as inactive FIRST (prevents race conditions)
477
- client.mark_session_inactive(target_sid)
478
-
479
- # Send only expected fields to update endpoint
480
- update_kwargs = {
481
- "is_finished": True,
482
- "session_eval": session_eval,
483
- "session_eval_reason": session_eval_reason,
484
- "is_successful": is_successful,
485
- "is_successful_reason": is_successful_reason,
486
- }
487
- try:
488
- client.session.update_session(**update_kwargs)
489
- except Exception as e:
490
- logger.warning(f"[Session] Failed to update session: {e}")
491
-
492
- # Clear only the global session reference, not the singleton
493
- # This preserves the client and event queue for other threads
494
- client.session = None
495
- logger.debug(f"[Session] Ended global session {target_sid}")
496
- # DO NOT shutdown event queue - other threads may be using it
497
- # DO NOT call client.clear() - preserve singleton for other threads
498
- return
499
-
500
- # Otherwise, end the specified session id without clearing global state
501
- # First flush telemetry and event queue for non-global sessions too
502
- try:
503
- if hasattr(client, '_tracer_provider') and client._tracer_provider:
504
- logger.debug(f"[Session] Flushing OpenTelemetry spans for session {target_sid[:8]}...")
505
- success = client._tracer_provider.force_flush(timeout_millis=10000)
506
- if not success:
507
- logger.warning("[Session] OpenTelemetry flush timed out")
508
- except Exception as e:
509
- logger.debug(f"[Session] Failed to flush telemetry spans: {e}")
146
+ # Use provided agent_id or fall back to config
147
+ final_agent_id = agent_id or config.agent_id
148
+ if not final_agent_id:
149
+ raise ValueError("Agent ID is required for creating experiments")
510
150
 
511
- # Flush and wait for event queue to empty
512
- try:
513
- if hasattr(client, '_event_queue'):
514
- logger.debug(f"[Session] Flushing event queue for session {target_sid[:8]}...")
515
- client._event_queue.force_flush(timeout_seconds=10.0)
516
-
517
- # Wait for queue to be completely empty (only if blocking)
518
- if wait_for_flush:
519
- import time
520
- wait_start = time.time()
521
- max_wait = 10.0 # seconds - timeout for blob uploads
522
- while not client._event_queue.is_empty():
523
- if time.time() - wait_start > max_wait:
524
- logger.warning(f"[Session] EventQueue not empty after {max_wait}s timeout")
525
- break
526
- time.sleep(0.1)
527
-
528
- if client._event_queue.is_empty():
529
- logger.debug(f"[Session] EventQueue confirmed empty for session {target_sid[:8]}")
530
- else:
531
- logger.debug(f"[Session] Non-blocking mode - skipping wait for session {target_sid[:8]}")
532
- except Exception as e:
533
- logger.debug(f"[Session] Failed to flush event queue: {e}")
151
+ evaluator_names = []
152
+ if LLM_boolean_evaluators:
153
+ evaluator_names.extend(LLM_boolean_evaluators)
154
+ if LLM_numeric_evaluators:
155
+ evaluator_names.extend(LLM_numeric_evaluators)
534
156
 
535
- # CRITICAL: Mark session as inactive FIRST for ALL sessions
536
- client.mark_session_inactive(target_sid)
157
+ # Create experiment via API (matching TypeScript exactly)
158
+ response = http.post('createexperiment', {
159
+ 'agent_id': final_agent_id,
160
+ 'experiment_name': experiment_name,
161
+ 'description': description or '',
162
+ 'tags': tags or [],
163
+ 'evaluator_names': evaluator_names
164
+ })
537
165
 
538
- temp = Session(agent_id=client.agent_id, session_id=target_sid)
539
- update_kwargs = {
540
- "is_finished": True,
541
- "session_eval": session_eval,
542
- "session_eval_reason": session_eval_reason,
543
- "is_successful": is_successful,
544
- "is_successful_reason": is_successful_reason,
545
- }
546
- try:
547
- temp.update_session(**update_kwargs)
548
- except Exception as e:
549
- logger.warning(f"[Session] Failed to update session: {e}")
166
+ return response.get('experiment_id')
550
167
 
551
168
 
552
- def flush(timeout_seconds: float = 2.0) -> bool:
553
- """
554
- Manually flush all pending telemetry data.
169
+ def _get_prompt(
170
+ prompt_name,
171
+ variables=None,
172
+ cache_ttl=300,
173
+ label='production'
174
+ ):
175
+ """Get a prompt from the prompt database."""
176
+ from .sdk.init import get_http
555
177
 
556
- Flushes both OpenTelemetry spans and queued events to ensure
557
- all telemetry data is sent to the backend. This is called
558
- automatically on process exit but can be called manually
559
- for explicit control.
178
+ http = get_http()
179
+ if not http:
180
+ return ""
560
181
 
561
- Args:
562
- timeout_seconds: Maximum time to wait for flush
563
-
564
- Returns:
565
- True if all flushes succeeded, False otherwise
566
-
567
- Example:
568
- ```python
569
- import lucidicai as lai
570
-
571
- # ... your code using Lucidic ...
572
-
573
- # Manually flush before critical operation
574
- lai.flush()
575
- ```
576
- """
182
+ # Get prompt from API
577
183
  try:
578
- client = Client()
579
- success = True
184
+ response = http.get('getprompt', {
185
+ 'prompt_name': prompt_name,
186
+ 'label': label
187
+ })
580
188
 
581
- # Flush OpenTelemetry spans first
582
- if hasattr(client, 'flush_telemetry'):
583
- span_success = client.flush_telemetry(timeout_seconds)
584
- success = success and span_success
189
+ # TypeScript SDK expects 'prompt_content' field
190
+ prompt = response.get('prompt_content', '')
585
191
 
586
- # Then flush event queue
587
- if hasattr(client, '_event_queue'):
588
- client._event_queue.force_flush(timeout_seconds)
589
-
590
- logger.debug(f"[Flush] Manual flush completed (success={success})")
591
- return success
592
- except Exception as e:
593
- logger.error(f"Failed to flush telemetry: {e}")
594
- return False
192
+ # Replace variables if provided
193
+ if variables:
194
+ for key, value in variables.items():
195
+ prompt = prompt.replace(f"{{{key}}}", str(value))
196
+
197
+ return prompt
198
+ except Exception:
199
+ return ""
595
200
 
596
201
 
597
- def _auto_end_session():
598
- """Automatically end session on exit if auto_end is enabled"""
599
- try:
600
- client = Client()
601
- if hasattr(client, 'auto_end') and client.auto_end and client.session and not client.session.is_finished:
602
- logger.info("Auto-ending active session on exit")
603
- client.auto_end = False # To avoid repeating auto-end on exit
604
-
605
- # Flush telemetry
606
- if hasattr(client, '_tracer_provider'):
607
- client._tracer_provider.force_flush(timeout_millis=5000)
608
-
609
- # Force flush event queue before ending session
610
- if hasattr(client, '_event_queue'):
611
- if logger.isEnabledFor(logging.DEBUG):
612
- logger.debug("[Shutdown] Flushing event queue before session end")
613
- client._event_queue.force_flush(timeout_seconds=5.0)
614
-
615
- # Use non-blocking mode during shutdown to prevent hangs
616
- # The actual wait for queue empty happens in _cleanup_singleton_on_exit
617
- end_session(wait_for_flush=False)
618
-
619
- except Exception as e:
620
- logger.debug(f"Error during auto-end session: {e}")
202
+ def _get_dataset(dataset_id, api_key=None, agent_id=None):
203
+ """Get a dataset by ID."""
204
+ from .sdk.features.dataset import get_dataset as __get_dataset
205
+ return __get_dataset(dataset_id, api_key, agent_id)
621
206
 
622
207
 
623
- def _cleanup_singleton_on_exit():
624
- """
625
- Clean up singleton resources only on process exit.
626
-
627
- CRITICAL ORDER:
628
- 1. Flush OpenTelemetry spans (blocking) - ensures spans become events
629
- 2. Flush EventQueue - sends all events including those from spans
630
- 3. Close HTTP session - graceful TCP FIN prevents broken pipes
631
- 4. Clear singletons - final cleanup
632
-
633
- This order is essential to prevent lost events and broken connections.
634
- """
635
- try:
636
- client = Client()
637
-
638
- # 1. FIRST: Flush OpenTelemetry spans (blocking until exported)
639
- # This is the critical fix - we must flush spans before events
640
- if hasattr(client, '_tracer_provider') and client._tracer_provider:
641
- try:
642
- # Small delay to ensure spans have reached the processor
643
- import time
644
- time.sleep(0.1) # 100ms to let spans reach BatchSpanProcessor
645
-
646
- logger.debug("[Exit] Flushing OpenTelemetry spans...")
647
- # force_flush() blocks until all spans are exported or timeout
648
- success = client._tracer_provider.force_flush(timeout_millis=3000)
649
- if success:
650
- logger.debug("[Exit] OpenTelemetry spans flushed successfully")
651
- else:
652
- logger.warning("[Exit] OpenTelemetry flush timed out - some spans may be lost")
653
-
654
- # DON'T shutdown TracerProvider yet - wait until after EventQueue
655
- # This prevents losing spans that are still being processed
656
- except Exception as e:
657
- logger.debug(f"[Exit] Telemetry cleanup error: {e}")
658
-
659
- # 2. SECOND: Flush and shutdown EventQueue
660
- # Now it contains all events from the flushed spans
661
- if hasattr(client, '_event_queue'):
662
- try:
663
- logger.debug("[Exit] Flushing event queue...")
664
- client._event_queue.force_flush(timeout_seconds=2.0)
665
-
666
- # Wait for queue to be completely empty before proceeding
667
- import time
668
- max_wait = 5.0 # seconds
669
- start_time = time.time()
670
- while not client._event_queue.is_empty():
671
- if time.time() - start_time > max_wait:
672
- logger.warning("[Exit] EventQueue not empty after timeout")
673
- break
674
- time.sleep(0.01) # Small sleep to avoid busy waiting
675
-
676
- if client._event_queue.is_empty():
677
- logger.debug("[Exit] EventQueue is empty, proceeding with shutdown")
678
-
679
- # Clear any stale active sessions (threads may have died without cleanup)
680
- if hasattr(client, '_active_sessions'):
681
- with client._active_sessions_lock:
682
- if client._active_sessions:
683
- logger.debug(f"[Exit] Clearing {len(client._active_sessions)} remaining active sessions")
684
- client._active_sessions.clear()
685
-
686
- # Now shutdown EventQueue
687
- client._event_queue.shutdown()
688
- logger.debug("[Exit] Event queue shutdown complete")
689
- except Exception as e:
690
- logger.debug(f"[Exit] Event queue cleanup error: {e}")
691
-
692
- # 3. THIRD: Shutdown TracerProvider after EventQueue is done
693
- # This ensures all spans can be exported before shutdown
694
- if hasattr(client, '_tracer_provider') and client._tracer_provider:
695
- try:
696
- logger.debug("[Exit] Shutting down TracerProvider...")
697
- client._tracer_provider.shutdown()
698
- logger.debug("[Exit] TracerProvider shutdown complete")
699
- except Exception as e:
700
- logger.debug(f"[Exit] TracerProvider shutdown error: {e}")
701
-
702
- # 4. FOURTH: Close HTTP session ONLY after everything else
703
- # This prevents broken pipes by ensuring all events are sent first
704
- if hasattr(client, 'request_session'):
705
- try:
706
- # Mark client as shutting down to prevent new requests
707
- client._shutdown = True
708
- logger.debug("[Exit] Closing HTTP session (queue empty, worker stopped)")
709
- client.request_session.close()
710
- logger.debug("[Exit] HTTP session closed gracefully")
711
- except Exception as e:
712
- logger.debug(f"[Exit] HTTP session cleanup error: {e}")
713
-
714
- # 5. FINALLY: Clear singletons
715
- # Safe to destroy now that all data is flushed
716
- clear_singletons()
717
- logger.debug("[Exit] Singleton cleanup complete")
718
-
719
- except Exception as e:
720
- # Silent fail on exit to avoid disrupting process termination
721
- if logger.isEnabledFor(logging.DEBUG):
722
- logger.debug(f"[Exit] Cleanup error: {e}")
208
+ def _get_dataset_items(dataset_id, api_key=None, agent_id=None):
209
+ """Get dataset items."""
210
+ from .sdk.features.dataset import get_dataset_items as __get_dataset_items
211
+ return __get_dataset_items(dataset_id, api_key, agent_id)
723
212
 
724
213
 
725
- def _signal_handler(signum, frame):
726
- """Handle interruption signals with better queue flushing."""
727
- # Best-effort final event for signal exits
728
- try:
729
- try:
730
- name = signal.Signals(signum).name
731
- except Exception:
732
- name = str(signum)
733
- try:
734
- stack_str = ''.join(traceback.format_stack(frame)) if frame else ''
735
- except Exception:
736
- stack_str = ''
737
- desc = _mask_and_truncate(f"Received signal {name}\n{stack_str}")
738
- _post_fatal_event(128 + signum, desc, {"signal": name, "signum": signum})
739
- except Exception:
740
- pass
214
+ def _list_datasets(api_key=None, agent_id=None):
215
+ """List all datasets."""
216
+ from .sdk.features.dataset import list_datasets as __list_datasets
217
+ return __list_datasets(api_key, agent_id)
218
+
219
+
220
+ def _create_dataset(name, description=None, tags=None, suggested_flag_config=None, api_key=None, agent_id=None):
221
+ """Create a new dataset."""
222
+ from .sdk.features.dataset import create_dataset as __create_dataset
223
+ return __create_dataset(name, description, tags, suggested_flag_config, api_key, agent_id)
224
+
225
+
226
+ def _update_dataset(dataset_id, name=None, description=None, tags=None, suggested_flag_config=None, api_key=None, agent_id=None):
227
+ """Update dataset metadata."""
228
+ from .sdk.features.dataset import update_dataset as __update_dataset
229
+ return __update_dataset(dataset_id, name, description, tags, suggested_flag_config, api_key, agent_id)
230
+
231
+
232
+ def _delete_dataset(dataset_id, api_key=None, agent_id=None):
233
+ """Delete a dataset."""
234
+ from .sdk.features.dataset import delete_dataset as __delete_dataset
235
+ return __delete_dataset(dataset_id, api_key, agent_id)
236
+
237
+
238
+ def _create_dataset_item(dataset_id, name, input_data, expected_output=None, description=None, tags=None, metadata=None, flag_overrides=None, api_key=None, agent_id=None):
239
+ """Create a dataset item."""
240
+ from .sdk.features.dataset import create_dataset_item as __create_dataset_item
241
+ return __create_dataset_item(dataset_id, name, input_data, expected_output, description, tags, metadata, flag_overrides, api_key, agent_id)
242
+
243
+
244
+ def _get_dataset_item(dataset_id, item_id, api_key=None, agent_id=None):
245
+ """Get a specific dataset item."""
246
+ from .sdk.features.dataset import get_dataset_item as __get_dataset_item
247
+ return __get_dataset_item(dataset_id, item_id, api_key, agent_id)
248
+
249
+
250
+ def _update_dataset_item(dataset_id, item_id, name=None, input_data=None, expected_output=None, description=None, tags=None, metadata=None, flag_overrides=None, api_key=None, agent_id=None):
251
+ """Update a dataset item."""
252
+ from .sdk.features.dataset import update_dataset_item as __update_dataset_item
253
+ return __update_dataset_item(dataset_id, item_id, name, input_data, expected_output, description, tags, metadata, flag_overrides, api_key, agent_id)
254
+
255
+
256
+ def _delete_dataset_item(dataset_id, item_id, api_key=None, agent_id=None):
257
+ """Delete a dataset item."""
258
+ from .sdk.features.dataset import delete_dataset_item as __delete_dataset_item
259
+ return __delete_dataset_item(dataset_id, item_id, api_key, agent_id)
260
+
261
+
262
+ def _list_dataset_item_sessions(dataset_id, item_id, api_key=None, agent_id=None):
263
+ """List all sessions for a dataset item."""
264
+ from .sdk.features.dataset import list_dataset_item_sessions as __list_dataset_item_sessions
265
+ return __list_dataset_item_sessions(dataset_id, item_id, api_key, agent_id)
266
+
267
+
268
+ # Feature flags
269
+ from .sdk.features.feature_flag import (
270
+ get_feature_flag,
271
+ get_bool_flag,
272
+ get_int_flag,
273
+ get_float_flag,
274
+ get_string_flag,
275
+ get_json_flag,
276
+ clear_feature_flag_cache,
277
+ )
278
+
279
+ # Error boundary utilities
280
+ is_silent_mode = error_boundary.is_silent_mode
281
+ get_error_history = error_boundary.get_error_history
282
+ clear_error_history = error_boundary.clear_error_history
283
+
284
+ # Version
285
+ __version__ = "2.0.0"
286
+
287
+ # Apply error boundary wrapping to all SDK functions
288
+ from .sdk.error_boundary import wrap_sdk_function
289
+
290
+ # Wrap main SDK functions
291
+ init = wrap_sdk_function(_init, "init")
292
+ get_session_id = wrap_sdk_function(_get_session_id, "init")
293
+ clear_state = wrap_sdk_function(_clear_state, "init")
294
+ create_event = wrap_sdk_function(_create_event, "event")
295
+ create_error_event = wrap_sdk_function(_create_error_event, "event")
296
+ flush = wrap_sdk_function(_flush, "event")
297
+
298
+ # Wrap session functions
299
+ update_session = wrap_sdk_function(_update_session, "session")
300
+ end_session = wrap_sdk_function(_end_session, "session")
301
+ get_session = wrap_sdk_function(_get_session, "session")
302
+
303
+ # Wrap feature functions
304
+ create_experiment = wrap_sdk_function(_create_experiment, "experiment")
305
+ get_prompt = wrap_sdk_function(_get_prompt, "prompt")
306
+
307
+ # Dataset management - complete CRUD
308
+ list_datasets = wrap_sdk_function(_list_datasets, "dataset")
309
+ create_dataset = wrap_sdk_function(_create_dataset, "dataset")
310
+ get_dataset = wrap_sdk_function(_get_dataset, "dataset")
311
+ update_dataset = wrap_sdk_function(_update_dataset, "dataset")
312
+ delete_dataset = wrap_sdk_function(_delete_dataset, "dataset")
313
+
314
+ # Dataset item management
315
+ create_dataset_item = wrap_sdk_function(_create_dataset_item, "dataset")
316
+ get_dataset_item = wrap_sdk_function(_get_dataset_item, "dataset")
317
+ update_dataset_item = wrap_sdk_function(_update_dataset_item, "dataset")
318
+ delete_dataset_item = wrap_sdk_function(_delete_dataset_item, "dataset")
319
+ get_dataset_items = wrap_sdk_function(_get_dataset_items, "dataset")
320
+ list_dataset_item_sessions = wrap_sdk_function(_list_dataset_item_sessions, "dataset")
321
+
322
+ # All exports
323
+ __all__ = [
324
+ # Main functions
325
+ 'init',
326
+ 'get_session_id',
327
+ 'clear_state',
328
+ 'update_session',
329
+ 'end_session',
330
+ 'get_session',
331
+ 'create_event',
332
+ 'create_error_event',
333
+ 'flush',
741
334
 
742
- # Proper shutdown sequence matching atexit handler
743
- try:
744
- client = Client()
745
-
746
- # 1. FIRST: Flush OpenTelemetry spans
747
- if hasattr(client, '_tracer_provider') and client._tracer_provider:
748
- try:
749
- logger.debug(f"[Signal] Flushing OpenTelemetry spans on signal {signum}")
750
- client._tracer_provider.force_flush(timeout_millis=2000) # Shorter timeout for signals
751
- except Exception:
752
- pass
753
-
754
- # 2. SECOND: Flush and shutdown EventQueue
755
- if hasattr(client, "_event_queue"):
756
- logger.debug(f"[Signal] Flushing event queue on signal {signum}")
757
- client._event_queue.force_flush(timeout_seconds=2.0)
758
-
759
- # Clear active sessions to allow shutdown
760
- if hasattr(client, '_active_sessions'):
761
- with client._active_sessions_lock:
762
- client._active_sessions.clear()
763
-
764
- client._event_queue.shutdown()
765
-
766
- # 3. THIRD: Shutdown TracerProvider after EventQueue
767
- if hasattr(client, '_tracer_provider') and client._tracer_provider:
768
- logger.debug(f"[Signal] Shutting down TracerProvider on signal {signum}")
769
- try:
770
- client._tracer_provider.shutdown()
771
- except Exception:
772
- pass
773
-
774
- # 4. Mark client as shutting down
775
- client._shutdown = True
776
-
777
- except Exception:
778
- pass
335
+ # Decorators
336
+ 'event',
337
+ 'step',
779
338
 
780
- logger.debug(f"[Signal] Auto-ending session on signal {signum}")
781
- _auto_end_session()
782
- # Re-raise the signal for default handling
783
- signal.signal(signum, signal.SIG_DFL)
784
- os.kill(os.getpid(), signum)
785
-
786
-
787
- # Register cleanup functions
788
- atexit.register(_cleanup_singleton_on_exit) # Clean up singleton resources on exit
789
- atexit.register(_auto_end_session) # Auto-end session if enabled
790
-
791
- # Register signal handlers for graceful shutdown
792
- signal.signal(signal.SIGINT, _signal_handler)
793
- signal.signal(signal.SIGTERM, _signal_handler)
794
-
795
-
796
- def create_experiment(
797
- experiment_name: str,
798
- pass_fail_rubrics: Optional[list] = None,
799
- score_rubrics: Optional[list] = None,
800
- description: Optional[str] = None,
801
- tags: Optional[list] = None,
802
- api_key: Optional[str] = None,
803
- agent_id: Optional[str] = None,
804
- ) -> str:
805
- """
806
- Create a new experiment for grouping and analyzing sessions.
807
-
808
- Args:
809
- experiment_name: Name of the experiment (required)
810
- pass_fail_rubrics: List of pass/fail rubric names to associate
811
- description: Description of the experiment
812
- task: Task description.
813
- tags: List of tags for categorization
814
- score_rubrics: List of score rubric names to associate
815
- api_key: API key (uses env if not provided)
816
- agent_id: Agent ID (uses env if not provided)
817
-
818
- Returns:
819
- experiment_id: UUID of the created experiment
820
-
821
- Raises:
822
- APIKeyVerificationError: If API key is invalid or missing
823
- InvalidOperationError: If experiment creation fails
824
- ValueError: If name is empty
825
- """
826
-
827
- # validation
828
- if not experiment_name:
829
- raise ValueError("Experiment name is required")
830
-
831
- if api_key is None:
832
- api_key = os.getenv("LUCIDIC_API_KEY", None)
833
- if api_key is None:
834
- raise APIKeyVerificationError("Make sure to either pass your API key into create_experiment() or set the LUCIDIC_API_KEY environment variable.")
835
- if agent_id is None:
836
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
837
- if agent_id is None:
838
- raise APIKeyVerificationError("Lucidic agent ID not specified. Make sure to either pass your agent ID into create_experiment() or set the LUCIDIC_AGENT_ID environment variable.")
839
-
840
- # combine rubrics into single list
841
- rubric_names = (pass_fail_rubrics or []) + (score_rubrics or [])
842
-
843
- # get current client which will be NullClient if never lai.init() is never called
844
- client = Client()
845
- # if not yet initialized or still the NullClient -> create a real client when init is called
846
- if not getattr(client, 'initialized', False):
847
- client = Client(api_key=api_key, agent_id=agent_id)
848
- else:
849
- # Already initialized, this is a re-init
850
- if api_key is not None and agent_id is not None and (api_key != client.api_key or agent_id != client.agent_id):
851
- client.set_api_key(api_key)
852
- client.agent_id = agent_id
853
-
854
- # create experiment
855
- experiment_id = client.create_experiment(experiment_name=experiment_name, rubric_names=rubric_names, description=description, tags=tags)
856
- logger.info(f"Created experiment with ID: {experiment_id}")
857
-
858
- return experiment_id
859
-
860
-
861
- def create_event(
862
- type: str = "generic",
863
- **kwargs
864
- ) -> str:
865
- client = Client()
866
- if not client.session:
867
- return
868
- return client.session.create_event(type=type, **kwargs)
869
-
339
+ # Features
340
+ 'create_experiment',
341
+ 'get_prompt',
870
342
 
871
- def get_prompt(
872
- prompt_name: str,
873
- variables: Optional[dict] = None,
874
- cache_ttl: Optional[int] = 300,
875
- label: Optional[str] = 'production'
876
- ) -> str:
877
- """
878
- Get a prompt from the prompt database.
343
+ # Dataset management
344
+ 'list_datasets',
345
+ 'create_dataset',
346
+ 'get_dataset',
347
+ 'update_dataset',
348
+ 'delete_dataset',
349
+ 'create_dataset_item',
350
+ 'get_dataset_item',
351
+ 'update_dataset_item',
352
+ 'delete_dataset_item',
353
+ 'get_dataset_items',
354
+ 'list_dataset_item_sessions',
355
+
356
+ # Feature flags
357
+ 'get_feature_flag',
358
+ 'get_bool_flag',
359
+ 'get_int_flag',
360
+ 'get_float_flag',
361
+ 'get_string_flag',
362
+ 'get_json_flag',
363
+ 'clear_feature_flag_cache',
879
364
 
880
- Args:
881
- prompt_name: Name of the prompt.
882
- variables: {{Variables}} to replace in the prompt, supplied as a dictionary.
883
- cache_ttl: Time-to-live for the prompt in the cache in seconds (default: 300). Set to -1 to cache forever. Set to 0 to disable caching.
884
- label: Optional label for the prompt.
365
+ # Context management
366
+ 'set_active_session',
367
+ 'clear_active_session',
368
+ 'bind_session',
369
+ 'bind_session_async',
370
+ 'session',
371
+ 'session_async',
372
+ 'run_session',
373
+ 'run_in_session',
374
+ 'current_session_id',
375
+ 'current_parent_event_id',
885
376
 
886
- Returns:
887
- str: The prompt.
888
- """
889
- client = Client()
890
- if not client.session:
891
- return ""
892
- prompt = client.get_prompt(prompt_name, cache_ttl, label)
893
- if variables:
894
- for key, val in variables.items():
895
- index = prompt.find("{{" + key +"}}")
896
- if index == -1:
897
- raise PromptError("Supplied variable not found in prompt")
898
- prompt = prompt.replace("{{" + key +"}}", str(val))
899
- if "{{" in prompt and "}}" in prompt and prompt.find("{{") < prompt.find("}}"):
900
- logger.warning("Unreplaced variable(s) left in prompt. Please check your prompt.")
901
- return prompt
902
-
903
-
904
- def get_session():
905
- """Get the current session object
377
+ # Error types
378
+ 'LucidicError',
379
+ 'LucidicNotInitializedError',
380
+ 'APIKeyVerificationError',
381
+ 'InvalidOperationError',
382
+ 'PromptError',
383
+ 'FeatureFlagError',
906
384
 
907
- Returns:
908
- Session: The current session object, or None if no session exists
909
- """
910
- try:
911
- client = Client()
912
- return client.session
913
- except (LucidicNotInitializedError, AttributeError) as e:
914
- logger.debug(f"No active session: {str(e)}")
915
- return None
916
-
917
-
385
+ # Error boundary
386
+ 'is_silent_mode',
387
+ 'get_error_history',
388
+ 'clear_error_history',
389
+
390
+ # Version
391
+ '__version__',
392
+ ]