lucidicai 2.1.3__py3-none-any.whl → 3.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 +32 -390
  2. lucidicai/api/client.py +31 -2
  3. lucidicai/api/resources/__init__.py +16 -1
  4. lucidicai/api/resources/dataset.py +422 -82
  5. lucidicai/api/resources/evals.py +209 -0
  6. lucidicai/api/resources/event.py +399 -27
  7. lucidicai/api/resources/experiment.py +108 -0
  8. lucidicai/api/resources/feature_flag.py +78 -0
  9. lucidicai/api/resources/prompt.py +84 -0
  10. lucidicai/api/resources/session.py +545 -38
  11. lucidicai/client.py +408 -480
  12. lucidicai/core/config.py +73 -48
  13. lucidicai/core/errors.py +3 -3
  14. lucidicai/sdk/bound_decorators.py +321 -0
  15. lucidicai/sdk/context.py +20 -2
  16. lucidicai/sdk/decorators.py +283 -74
  17. lucidicai/sdk/event.py +538 -36
  18. lucidicai/sdk/event_builder.py +2 -4
  19. lucidicai/sdk/features/dataset.py +391 -1
  20. lucidicai/sdk/features/feature_flag.py +344 -3
  21. lucidicai/sdk/init.py +49 -347
  22. lucidicai/sdk/session.py +502 -0
  23. lucidicai/sdk/shutdown_manager.py +103 -46
  24. lucidicai/session_obj.py +321 -0
  25. lucidicai/telemetry/context_capture_processor.py +13 -6
  26. lucidicai/telemetry/extract.py +60 -63
  27. lucidicai/telemetry/litellm_bridge.py +3 -44
  28. lucidicai/telemetry/lucidic_exporter.py +143 -131
  29. lucidicai/telemetry/openai_agents_instrumentor.py +2 -2
  30. lucidicai/telemetry/openai_patch.py +7 -6
  31. lucidicai/telemetry/telemetry_manager.py +183 -0
  32. lucidicai/telemetry/utils/model_pricing.py +21 -30
  33. lucidicai/telemetry/utils/provider.py +77 -0
  34. lucidicai/utils/images.py +27 -11
  35. lucidicai/utils/serialization.py +27 -0
  36. {lucidicai-2.1.3.dist-info → lucidicai-3.1.0.dist-info}/METADATA +1 -1
  37. {lucidicai-2.1.3.dist-info → lucidicai-3.1.0.dist-info}/RECORD +39 -29
  38. {lucidicai-2.1.3.dist-info → lucidicai-3.1.0.dist-info}/WHEEL +0 -0
  39. {lucidicai-2.1.3.dist-info → lucidicai-3.1.0.dist-info}/top_level.txt +0 -0
lucidicai/__init__.py CHANGED
@@ -1,48 +1,28 @@
1
- """Lucidic AI SDK - Clean Export-Only Entry Point
1
+ """Lucidic AI SDK - Instance-based client for AI observability.
2
2
 
3
- This file only contains exports, with all logic moved to appropriate modules.
4
- """
3
+ This SDK provides observability for AI applications, tracking workflows,
4
+ costs, and performance across multiple LLM providers.
5
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
6
+ Example:
7
+ from lucidicai import LucidicAI
11
8
 
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
- # Thread-local session management (advanced users)
18
- set_thread_session,
19
- clear_thread_session,
20
- get_thread_session,
21
- )
9
+ client = LucidicAI(api_key="...", agent_id="...", providers=["openai"])
22
10
 
23
- from .sdk.event import (
24
- create_event as _create_event,
25
- create_error_event as _create_error_event,
26
- flush as _flush,
27
- )
11
+ with client.create_session(session_name="My Session") as session:
12
+ @client.event
13
+ def my_function():
14
+ # LLM calls are automatically tracked
15
+ pass
16
+ my_function()
28
17
 
29
- # Context management exports
30
- from .sdk.context import (
31
- set_active_session,
32
- clear_active_session,
33
- bind_session,
34
- bind_session_async,
35
- session,
36
- session_async,
37
- run_session,
38
- run_in_session,
39
- thread_worker_with_session, # Thread isolation helper
40
- current_session_id,
41
- current_parent_event_id,
42
- )
18
+ client.close()
19
+ """
20
+
21
+ # Main client class
22
+ from .client import LucidicAI
43
23
 
44
- # Decorators
45
- from .sdk.decorators import event, event as step # step is deprecated alias
24
+ # Session object
25
+ from .session_obj import Session
46
26
 
47
27
  # Error types
48
28
  from .core.errors import (
@@ -54,360 +34,22 @@ from .core.errors import (
54
34
  FeatureFlagError,
55
35
  )
56
36
 
57
- # Import functions that need to be implemented
58
- def _update_session(
59
- task=None,
60
- session_eval=None,
61
- session_eval_reason=None,
62
- is_successful=None,
63
- is_successful_reason=None,
64
- session_id=None # Accept explicit session_id
65
- ):
66
- """Update the current session."""
67
- from .sdk.init import get_resources, get_session_id
68
-
69
- # Use provided session_id or fall back to context
70
- if not session_id:
71
- session_id = get_session_id()
72
- if not session_id:
73
- return
74
-
75
- resources = get_resources()
76
- if resources and 'sessions' in resources:
77
- updates = {}
78
- if task is not None:
79
- updates['task'] = task
80
- if session_eval is not None:
81
- updates['session_eval'] = session_eval
82
- if session_eval_reason is not None:
83
- updates['session_eval_reason'] = session_eval_reason
84
- if is_successful is not None:
85
- updates['is_successful'] = is_successful
86
- if is_successful_reason is not None:
87
- updates['is_successful_reason'] = is_successful_reason
88
-
89
- if updates:
90
- resources['sessions'].update_session(session_id, updates)
91
-
92
-
93
- def _end_session(
94
- session_eval=None,
95
- session_eval_reason=None,
96
- is_successful=None,
97
- is_successful_reason=None,
98
- wait_for_flush=True,
99
- session_id=None # Accept explicit session_id
100
- ):
101
- """End the current session."""
102
- from .sdk.init import get_resources, get_session_id, get_event_queue
103
- from .sdk.shutdown_manager import get_shutdown_manager
104
-
105
- # Use provided session_id or fall back to context
106
- if not session_id:
107
- session_id = get_session_id()
108
- if not session_id:
109
- return
110
-
111
- # Flush events if requested
112
- if wait_for_flush:
113
- flush(timeout_seconds=5.0)
114
-
115
- # End session via API
116
- resources = get_resources()
117
- if resources and 'sessions' in resources:
118
- resources['sessions'].end_session(
119
- session_id,
120
- is_successful=is_successful,
121
- session_eval=session_eval,
122
- is_successful_reason=is_successful_reason,
123
- session_eval_reason=session_eval_reason
124
- )
125
-
126
- # Clear session context
127
- clear_active_session()
128
-
129
- # unregister from shutdown manager
130
- get_shutdown_manager().unregister_session(session_id)
131
-
132
-
133
- def _get_session():
134
- """Get the current session object."""
135
- from .sdk.init import get_session_id
136
- return get_session_id()
137
-
138
-
139
- def _create_experiment(
140
- experiment_name,
141
- LLM_boolean_evaluators=None,
142
- LLM_numeric_evaluators=None,
143
- description=None,
144
- tags=None,
145
- api_key=None,
146
- agent_id=None,
147
- ):
148
- """Create a new experiment."""
149
- from .sdk.init import get_http
150
- from .core.config import SDKConfig, get_config
151
-
152
- # Get or create HTTP client
153
- http = get_http()
154
- config = get_config()
155
-
156
- if not http:
157
- config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
158
- from .api.client import HttpClient
159
- http = HttpClient(config)
160
-
161
- # Use provided agent_id or fall back to config
162
- final_agent_id = agent_id or config.agent_id
163
- if not final_agent_id:
164
- raise ValueError("Agent ID is required for creating experiments")
165
-
166
- evaluator_names = []
167
- if LLM_boolean_evaluators:
168
- evaluator_names.extend(LLM_boolean_evaluators)
169
- if LLM_numeric_evaluators:
170
- evaluator_names.extend(LLM_numeric_evaluators)
171
-
172
- # Create experiment via API (matching TypeScript exactly)
173
- response = http.post('createexperiment', {
174
- 'agent_id': final_agent_id,
175
- 'experiment_name': experiment_name,
176
- 'description': description or '',
177
- 'tags': tags or [],
178
- 'evaluator_names': evaluator_names
179
- })
180
-
181
- return response.get('experiment_id')
182
-
183
-
184
- def _get_prompt(
185
- prompt_name,
186
- variables=None,
187
- cache_ttl=300,
188
- label='production'
189
- ):
190
- """Get a prompt from the prompt database."""
191
- from .sdk.init import get_http
192
-
193
- http = get_http()
194
- if not http:
195
- return ""
196
-
197
- # Get prompt from API
198
- try:
199
- response = http.get('getprompt', {
200
- 'prompt_name': prompt_name,
201
- 'label': label
202
- })
203
-
204
- # TypeScript SDK expects 'prompt_content' field
205
- prompt = response.get('prompt_content', '')
206
-
207
- # Replace variables if provided
208
- if variables:
209
- for key, value in variables.items():
210
- prompt = prompt.replace(f"{{{key}}}", str(value))
211
-
212
- return prompt
213
- except Exception:
214
- return ""
215
-
216
-
217
- def _get_dataset(dataset_id, api_key=None, agent_id=None):
218
- """Get a dataset by ID."""
219
- from .sdk.features.dataset import get_dataset as __get_dataset
220
- return __get_dataset(dataset_id, api_key, agent_id)
221
-
222
-
223
- def _get_dataset_items(dataset_id, api_key=None, agent_id=None):
224
- """Get dataset items."""
225
- from .sdk.features.dataset import get_dataset_items as __get_dataset_items
226
- return __get_dataset_items(dataset_id, api_key, agent_id)
227
-
228
-
229
- def _list_datasets(api_key=None, agent_id=None):
230
- """List all datasets."""
231
- from .sdk.features.dataset import list_datasets as __list_datasets
232
- return __list_datasets(api_key, agent_id)
233
-
234
-
235
- def _create_dataset(name, description=None, tags=None, suggested_flag_config=None, api_key=None, agent_id=None):
236
- """Create a new dataset."""
237
- from .sdk.features.dataset import create_dataset as __create_dataset
238
- return __create_dataset(name, description, tags, suggested_flag_config, api_key, agent_id)
239
-
240
-
241
- def _update_dataset(dataset_id, name=None, description=None, tags=None, suggested_flag_config=None, api_key=None, agent_id=None):
242
- """Update dataset metadata."""
243
- from .sdk.features.dataset import update_dataset as __update_dataset
244
- return __update_dataset(dataset_id, name, description, tags, suggested_flag_config, api_key, agent_id)
245
-
246
-
247
- def _delete_dataset(dataset_id, api_key=None, agent_id=None):
248
- """Delete a dataset."""
249
- from .sdk.features.dataset import delete_dataset as __delete_dataset
250
- return __delete_dataset(dataset_id, api_key, agent_id)
251
-
252
-
253
- 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):
254
- """Create a dataset item."""
255
- from .sdk.features.dataset import create_dataset_item as __create_dataset_item
256
- return __create_dataset_item(dataset_id, name, input_data, expected_output, description, tags, metadata, flag_overrides, api_key, agent_id)
257
-
258
-
259
- def _get_dataset_item(dataset_id, item_id, api_key=None, agent_id=None):
260
- """Get a specific dataset item."""
261
- from .sdk.features.dataset import get_dataset_item as __get_dataset_item
262
- return __get_dataset_item(dataset_id, item_id, api_key, agent_id)
263
-
264
-
265
- 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):
266
- """Update a dataset item."""
267
- from .sdk.features.dataset import update_dataset_item as __update_dataset_item
268
- return __update_dataset_item(dataset_id, item_id, name, input_data, expected_output, description, tags, metadata, flag_overrides, api_key, agent_id)
269
-
270
-
271
- def _delete_dataset_item(dataset_id, item_id, api_key=None, agent_id=None):
272
- """Delete a dataset item."""
273
- from .sdk.features.dataset import delete_dataset_item as __delete_dataset_item
274
- return __delete_dataset_item(dataset_id, item_id, api_key, agent_id)
275
-
276
-
277
- def _list_dataset_item_sessions(dataset_id, item_id, api_key=None, agent_id=None):
278
- """List all sessions for a dataset item."""
279
- from .sdk.features.dataset import list_dataset_item_sessions as __list_dataset_item_sessions
280
- return __list_dataset_item_sessions(dataset_id, item_id, api_key, agent_id)
281
-
282
-
283
- # Feature flags
284
- from .sdk.features.feature_flag import (
285
- get_feature_flag,
286
- get_bool_flag,
287
- get_int_flag,
288
- get_float_flag,
289
- get_string_flag,
290
- get_json_flag,
291
- clear_feature_flag_cache,
292
- )
293
-
294
- # Error boundary utilities
295
- is_silent_mode = error_boundary.is_silent_mode
296
- get_error_history = error_boundary.get_error_history
297
- clear_error_history = error_boundary.clear_error_history
298
-
299
37
  # Version
300
- __version__ = "2.1.3"
301
-
302
- # Apply error boundary wrapping to all SDK functions
303
- from .sdk.error_boundary import wrap_sdk_function
304
-
305
- # Wrap main SDK functions
306
- init = wrap_sdk_function(_init, "init")
307
- get_session_id = wrap_sdk_function(_get_session_id, "init")
308
- clear_state = wrap_sdk_function(_clear_state, "init")
309
- create_event = wrap_sdk_function(_create_event, "event")
310
- create_error_event = wrap_sdk_function(_create_error_event, "event")
311
- flush = wrap_sdk_function(_flush, "event")
312
-
313
- # Wrap session functions
314
- update_session = wrap_sdk_function(_update_session, "session")
315
- end_session = wrap_sdk_function(_end_session, "session")
316
- get_session = wrap_sdk_function(_get_session, "session")
317
-
318
- # Wrap feature functions
319
- create_experiment = wrap_sdk_function(_create_experiment, "experiment")
320
- get_prompt = wrap_sdk_function(_get_prompt, "prompt")
321
-
322
- # Dataset management - complete CRUD
323
- list_datasets = wrap_sdk_function(_list_datasets, "dataset")
324
- create_dataset = wrap_sdk_function(_create_dataset, "dataset")
325
- get_dataset = wrap_sdk_function(_get_dataset, "dataset")
326
- update_dataset = wrap_sdk_function(_update_dataset, "dataset")
327
- delete_dataset = wrap_sdk_function(_delete_dataset, "dataset")
328
-
329
- # Dataset item management
330
- create_dataset_item = wrap_sdk_function(_create_dataset_item, "dataset")
331
- get_dataset_item = wrap_sdk_function(_get_dataset_item, "dataset")
332
- update_dataset_item = wrap_sdk_function(_update_dataset_item, "dataset")
333
- delete_dataset_item = wrap_sdk_function(_delete_dataset_item, "dataset")
334
- get_dataset_items = wrap_sdk_function(_get_dataset_items, "dataset")
335
- list_dataset_item_sessions = wrap_sdk_function(_list_dataset_item_sessions, "dataset")
38
+ __version__ = "3.1.0"
336
39
 
337
40
  # All exports
338
41
  __all__ = [
339
- # Main functions
340
- 'init',
341
- 'get_session_id',
342
- 'clear_state',
343
- 'update_session',
344
- 'end_session',
345
- 'get_session',
346
- 'create_event',
347
- 'create_error_event',
348
- 'flush',
349
-
350
- # Decorators
351
- 'event',
352
- 'step',
353
-
354
- # Features
355
- 'create_experiment',
356
- 'get_prompt',
357
-
358
- # Dataset management
359
- 'list_datasets',
360
- 'create_dataset',
361
- 'get_dataset',
362
- 'update_dataset',
363
- 'delete_dataset',
364
- 'create_dataset_item',
365
- 'get_dataset_item',
366
- 'update_dataset_item',
367
- 'delete_dataset_item',
368
- 'get_dataset_items',
369
- 'list_dataset_item_sessions',
370
-
371
- # Feature flags
372
- 'get_feature_flag',
373
- 'get_bool_flag',
374
- 'get_int_flag',
375
- 'get_float_flag',
376
- 'get_string_flag',
377
- 'get_json_flag',
378
- 'clear_feature_flag_cache',
379
-
380
- # Context management
381
- 'set_active_session',
382
- 'clear_active_session',
383
- 'bind_session',
384
- 'bind_session_async',
385
- 'session',
386
- 'session_async',
387
- 'run_session',
388
- 'run_in_session',
389
- 'thread_worker_with_session',
390
- 'current_session_id',
391
- 'current_parent_event_id',
392
-
393
- # Thread-local session management (advanced)
394
- 'set_thread_session',
395
- 'clear_thread_session',
396
- 'get_thread_session',
397
-
42
+ # Main client
43
+ "LucidicAI",
44
+ # Session object
45
+ "Session",
398
46
  # Error types
399
- 'LucidicError',
400
- 'LucidicNotInitializedError',
401
- 'APIKeyVerificationError',
402
- 'InvalidOperationError',
403
- 'PromptError',
404
- 'FeatureFlagError',
405
-
406
- # Error boundary
407
- 'is_silent_mode',
408
- 'get_error_history',
409
- 'clear_error_history',
410
-
47
+ "LucidicError",
48
+ "LucidicNotInitializedError",
49
+ "APIKeyVerificationError",
50
+ "InvalidOperationError",
51
+ "PromptError",
52
+ "FeatureFlagError",
411
53
  # Version
412
- '__version__',
413
- ]
54
+ "__version__",
55
+ ]
lucidicai/api/client.py CHANGED
@@ -43,6 +43,7 @@ class HttpClient:
43
43
  # Lazy-initialized clients
44
44
  self._sync_client: Optional[httpx.Client] = None
45
45
  self._async_client: Optional[httpx.AsyncClient] = None
46
+ self._async_client_loop: Optional[asyncio.AbstractEventLoop] = None
46
47
 
47
48
  def _build_headers(self) -> Dict[str, str]:
48
49
  """Build default headers for requests."""
@@ -75,8 +76,34 @@ class HttpClient:
75
76
 
76
77
  @property
77
78
  def async_client(self) -> httpx.AsyncClient:
78
- """Get or create the asynchronous HTTP client."""
79
- if self._async_client is None or self._async_client.is_closed:
79
+ """Get or create the asynchronous HTTP client.
80
+
81
+ The client is recreated if the event loop has changed, since
82
+ httpx.AsyncClient is tied to a specific event loop.
83
+ """
84
+ # Check if we need to recreate the client
85
+ current_loop = None
86
+ try:
87
+ current_loop = asyncio.get_running_loop()
88
+ except RuntimeError:
89
+ pass # No running loop
90
+
91
+ # Recreate client if: no client, client closed, or event loop changed
92
+ needs_new_client = (
93
+ self._async_client is None or
94
+ self._async_client.is_closed or
95
+ (current_loop is not None and self._async_client_loop is not current_loop)
96
+ )
97
+
98
+ if needs_new_client:
99
+ # Close old client if it exists and isn't already closed
100
+ if self._async_client is not None and not self._async_client.is_closed:
101
+ try:
102
+ # Can't await in a property, so we just let it be garbage collected
103
+ pass
104
+ except Exception:
105
+ pass
106
+
80
107
  transport = httpx.AsyncHTTPTransport(**self._transport_kwargs)
81
108
  self._async_client = httpx.AsyncClient(
82
109
  base_url=self.base_url,
@@ -85,6 +112,8 @@ class HttpClient:
85
112
  limits=self._limits,
86
113
  transport=transport,
87
114
  )
115
+ self._async_client_loop = current_loop
116
+
88
117
  return self._async_client
89
118
 
90
119
  def _add_timestamp(self, data: Optional[Dict[str, Any]]) -> Dict[str, Any]:
@@ -1 +1,16 @@
1
- """API resource modules."""
1
+ """API resource modules."""
2
+ from .session import SessionResource
3
+ from .event import EventResource
4
+ from .dataset import DatasetResource
5
+ from .experiment import ExperimentResource
6
+ from .prompt import PromptResource
7
+ from .feature_flag import FeatureFlagResource
8
+
9
+ __all__ = [
10
+ "SessionResource",
11
+ "EventResource",
12
+ "DatasetResource",
13
+ "ExperimentResource",
14
+ "PromptResource",
15
+ "FeatureFlagResource",
16
+ ]