lucidicai 2.0.2__tar.gz → 2.1.1__tar.gz

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 (62) hide show
  1. {lucidicai-2.0.2 → lucidicai-2.1.1}/PKG-INFO +1 -1
  2. {lucidicai-2.0.2 → lucidicai-2.1.1}/README.md +3 -3
  3. lucidicai-2.1.1/lucidicai/__init__.py +409 -0
  4. lucidicai-2.1.1/lucidicai/api/__init__.py +1 -0
  5. lucidicai-2.1.1/lucidicai/api/client.py +218 -0
  6. lucidicai-2.1.1/lucidicai/api/resources/__init__.py +1 -0
  7. lucidicai-2.1.1/lucidicai/api/resources/dataset.py +192 -0
  8. lucidicai-2.1.1/lucidicai/api/resources/event.py +88 -0
  9. lucidicai-2.1.1/lucidicai/api/resources/session.py +126 -0
  10. lucidicai-2.1.1/lucidicai/core/__init__.py +1 -0
  11. lucidicai-2.1.1/lucidicai/core/config.py +223 -0
  12. {lucidicai-2.0.2/lucidicai → lucidicai-2.1.1/lucidicai/core}/errors.py +16 -16
  13. lucidicai-2.1.1/lucidicai/core/types.py +35 -0
  14. lucidicai-2.1.1/lucidicai/sdk/__init__.py +1 -0
  15. {lucidicai-2.0.2/lucidicai → lucidicai-2.1.1/lucidicai/sdk}/context.py +93 -6
  16. {lucidicai-2.0.2/lucidicai → lucidicai-2.1.1/lucidicai/sdk}/decorators.py +68 -27
  17. lucidicai-2.1.1/lucidicai/sdk/error_boundary.py +299 -0
  18. lucidicai-2.1.1/lucidicai/sdk/event.py +126 -0
  19. lucidicai-2.1.1/lucidicai/sdk/event_builder.py +304 -0
  20. lucidicai-2.1.1/lucidicai/sdk/features/__init__.py +1 -0
  21. lucidicai-2.1.1/lucidicai/sdk/features/dataset.py +605 -0
  22. {lucidicai-2.0.2/lucidicai → lucidicai-2.1.1/lucidicai/sdk/features}/feature_flag.py +93 -54
  23. lucidicai-2.1.1/lucidicai/sdk/init.py +361 -0
  24. lucidicai-2.1.1/lucidicai/sdk/shutdown_manager.py +302 -0
  25. lucidicai-2.1.1/lucidicai/telemetry/context_bridge.py +82 -0
  26. {lucidicai-2.0.2 → lucidicai-2.1.1}/lucidicai/telemetry/context_capture_processor.py +25 -9
  27. {lucidicai-2.0.2 → lucidicai-2.1.1}/lucidicai/telemetry/litellm_bridge.py +20 -24
  28. {lucidicai-2.0.2 → lucidicai-2.1.1}/lucidicai/telemetry/lucidic_exporter.py +99 -60
  29. lucidicai-2.1.1/lucidicai/telemetry/openai_patch.py +295 -0
  30. lucidicai-2.1.1/lucidicai/telemetry/openai_uninstrument.py +87 -0
  31. {lucidicai-2.0.2 → lucidicai-2.1.1}/lucidicai/telemetry/telemetry_init.py +16 -1
  32. lucidicai-2.1.1/lucidicai/utils/__init__.py +1 -0
  33. lucidicai-2.1.1/lucidicai/utils/images.py +337 -0
  34. lucidicai-2.1.1/lucidicai/utils/logger.py +168 -0
  35. lucidicai-2.1.1/lucidicai/utils/queue.py +393 -0
  36. {lucidicai-2.0.2 → lucidicai-2.1.1}/lucidicai.egg-info/PKG-INFO +1 -1
  37. lucidicai-2.1.1/lucidicai.egg-info/SOURCES.txt +45 -0
  38. {lucidicai-2.0.2 → lucidicai-2.1.1}/setup.py +1 -1
  39. lucidicai-2.0.2/lucidicai/__init__.py +0 -941
  40. lucidicai-2.0.2/lucidicai/client.py +0 -513
  41. lucidicai-2.0.2/lucidicai/constants.py +0 -29
  42. lucidicai-2.0.2/lucidicai/dataset.py +0 -114
  43. lucidicai-2.0.2/lucidicai/event.py +0 -53
  44. lucidicai-2.0.2/lucidicai/event_queue.py +0 -466
  45. lucidicai-2.0.2/lucidicai/image_upload.py +0 -111
  46. lucidicai-2.0.2/lucidicai/lru.py +0 -19
  47. lucidicai-2.0.2/lucidicai/session.py +0 -51
  48. lucidicai-2.0.2/lucidicai/singleton.py +0 -53
  49. lucidicai-2.0.2/lucidicai/streaming.py +0 -225
  50. lucidicai-2.0.2/lucidicai/telemetry/utils/image_storage.py +0 -45
  51. lucidicai-2.0.2/lucidicai/telemetry/utils/text_storage.py +0 -53
  52. lucidicai-2.0.2/lucidicai/telemetry/utils/universal_image_interceptor.py +0 -365
  53. lucidicai-2.0.2/lucidicai.egg-info/SOURCES.txt +0 -34
  54. {lucidicai-2.0.2 → lucidicai-2.1.1}/lucidicai/telemetry/__init__.py +0 -0
  55. {lucidicai-2.0.2 → lucidicai-2.1.1}/lucidicai/telemetry/extract.py +0 -0
  56. {lucidicai-2.0.2 → lucidicai-2.1.1}/lucidicai/telemetry/openai_agents_instrumentor.py +0 -0
  57. {lucidicai-2.0.2 → lucidicai-2.1.1}/lucidicai/telemetry/utils/__init__.py +0 -0
  58. {lucidicai-2.0.2/lucidicai → lucidicai-2.1.1/lucidicai/telemetry/utils}/model_pricing.py +0 -0
  59. {lucidicai-2.0.2 → lucidicai-2.1.1}/lucidicai.egg-info/dependency_links.txt +0 -0
  60. {lucidicai-2.0.2 → lucidicai-2.1.1}/lucidicai.egg-info/requires.txt +0 -0
  61. {lucidicai-2.0.2 → lucidicai-2.1.1}/lucidicai.egg-info/top_level.txt +0 -0
  62. {lucidicai-2.0.2 → lucidicai-2.1.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lucidicai
3
- Version: 2.0.2
3
+ Version: 2.1.1
4
4
  Summary: Lucidic AI Python SDK
5
5
  Author: Andy Liang
6
6
  Author-email: andy@lucidic.ai
@@ -83,7 +83,7 @@ lai.init(
83
83
  auto_end=True, # Optional: Auto-end session on exit (default: True)
84
84
  masking_function=my_mask_func, # Optional: Custom PII masking
85
85
  tags=["customer-support", "v1.2"], # Optional: Session tags
86
- rubrics=[...], # Optional: Evaluation criteria
86
+ evaluators=[...], # Optional: Evaluation criteria
87
87
  experiment_id="...", # Optional: Link to experiment
88
88
  capture_uncaught=True # Optional: Capture crash events (default: True)
89
89
  )
@@ -413,8 +413,8 @@ Create experiments to group and analyze multiple sessions:
413
413
  # Create an experiment
414
414
  experiment_id = lai.create_experiment(
415
415
  experiment_name="Prompt Optimization Test",
416
- pass_fail_rubrics=["response_quality", "latency"],
417
- score_rubrics=["coherence", "relevance"],
416
+ LLM_boolean_evaluators=["response_quality", "latency"],
417
+ LLM_numeric_evaluators=["coherence", "relevance"],
418
418
  description="Testing different prompt strategies",
419
419
  tags=["A/B-test", "prompts"]
420
420
  )
@@ -0,0 +1,409 @@
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
+ # Thread-local session management (advanced users)
18
+ set_thread_session,
19
+ clear_thread_session,
20
+ get_thread_session,
21
+ )
22
+
23
+ from .sdk.event import (
24
+ create_event as _create_event,
25
+ create_error_event as _create_error_event,
26
+ flush as _flush,
27
+ )
28
+
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
+ )
43
+
44
+ # Decorators
45
+ from .sdk.decorators import event, event as step # step is deprecated alias
46
+
47
+ # Error types
48
+ from .core.errors import (
49
+ LucidicError,
50
+ LucidicNotInitializedError,
51
+ APIKeyVerificationError,
52
+ InvalidOperationError,
53
+ PromptError,
54
+ FeatureFlagError,
55
+ )
56
+
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
+
104
+ # Use provided session_id or fall back to context
105
+ if not session_id:
106
+ session_id = get_session_id()
107
+ if not session_id:
108
+ return
109
+
110
+ # Flush events if requested
111
+ if wait_for_flush:
112
+ flush(timeout_seconds=5.0)
113
+
114
+ # End session via API
115
+ resources = get_resources()
116
+ if resources and 'sessions' in resources:
117
+ resources['sessions'].end_session(
118
+ session_id,
119
+ is_successful=is_successful,
120
+ session_eval=session_eval,
121
+ is_successful_reason=is_successful_reason,
122
+ session_eval_reason=session_eval_reason
123
+ )
124
+
125
+ # Clear session context
126
+ clear_active_session()
127
+
128
+
129
+ def _get_session():
130
+ """Get the current session object."""
131
+ from .sdk.init import get_session_id
132
+ return get_session_id()
133
+
134
+
135
+ def _create_experiment(
136
+ experiment_name,
137
+ LLM_boolean_evaluators=None,
138
+ LLM_numeric_evaluators=None,
139
+ description=None,
140
+ tags=None,
141
+ api_key=None,
142
+ agent_id=None,
143
+ ):
144
+ """Create a new experiment."""
145
+ from .sdk.init import get_http
146
+ from .core.config import SDKConfig, get_config
147
+
148
+ # Get or create HTTP client
149
+ http = get_http()
150
+ config = get_config()
151
+
152
+ if not http:
153
+ config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
154
+ from .api.client import HttpClient
155
+ http = HttpClient(config)
156
+
157
+ # Use provided agent_id or fall back to config
158
+ final_agent_id = agent_id or config.agent_id
159
+ if not final_agent_id:
160
+ raise ValueError("Agent ID is required for creating experiments")
161
+
162
+ evaluator_names = []
163
+ if LLM_boolean_evaluators:
164
+ evaluator_names.extend(LLM_boolean_evaluators)
165
+ if LLM_numeric_evaluators:
166
+ evaluator_names.extend(LLM_numeric_evaluators)
167
+
168
+ # Create experiment via API (matching TypeScript exactly)
169
+ response = http.post('createexperiment', {
170
+ 'agent_id': final_agent_id,
171
+ 'experiment_name': experiment_name,
172
+ 'description': description or '',
173
+ 'tags': tags or [],
174
+ 'evaluator_names': evaluator_names
175
+ })
176
+
177
+ return response.get('experiment_id')
178
+
179
+
180
+ def _get_prompt(
181
+ prompt_name,
182
+ variables=None,
183
+ cache_ttl=300,
184
+ label='production'
185
+ ):
186
+ """Get a prompt from the prompt database."""
187
+ from .sdk.init import get_http
188
+
189
+ http = get_http()
190
+ if not http:
191
+ return ""
192
+
193
+ # Get prompt from API
194
+ try:
195
+ response = http.get('getprompt', {
196
+ 'prompt_name': prompt_name,
197
+ 'label': label
198
+ })
199
+
200
+ # TypeScript SDK expects 'prompt_content' field
201
+ prompt = response.get('prompt_content', '')
202
+
203
+ # Replace variables if provided
204
+ if variables:
205
+ for key, value in variables.items():
206
+ prompt = prompt.replace(f"{{{key}}}", str(value))
207
+
208
+ return prompt
209
+ except Exception:
210
+ return ""
211
+
212
+
213
+ def _get_dataset(dataset_id, api_key=None, agent_id=None):
214
+ """Get a dataset by ID."""
215
+ from .sdk.features.dataset import get_dataset as __get_dataset
216
+ return __get_dataset(dataset_id, api_key, agent_id)
217
+
218
+
219
+ def _get_dataset_items(dataset_id, api_key=None, agent_id=None):
220
+ """Get dataset items."""
221
+ from .sdk.features.dataset import get_dataset_items as __get_dataset_items
222
+ return __get_dataset_items(dataset_id, api_key, agent_id)
223
+
224
+
225
+ def _list_datasets(api_key=None, agent_id=None):
226
+ """List all datasets."""
227
+ from .sdk.features.dataset import list_datasets as __list_datasets
228
+ return __list_datasets(api_key, agent_id)
229
+
230
+
231
+ def _create_dataset(name, description=None, tags=None, suggested_flag_config=None, api_key=None, agent_id=None):
232
+ """Create a new dataset."""
233
+ from .sdk.features.dataset import create_dataset as __create_dataset
234
+ return __create_dataset(name, description, tags, suggested_flag_config, api_key, agent_id)
235
+
236
+
237
+ def _update_dataset(dataset_id, name=None, description=None, tags=None, suggested_flag_config=None, api_key=None, agent_id=None):
238
+ """Update dataset metadata."""
239
+ from .sdk.features.dataset import update_dataset as __update_dataset
240
+ return __update_dataset(dataset_id, name, description, tags, suggested_flag_config, api_key, agent_id)
241
+
242
+
243
+ def _delete_dataset(dataset_id, api_key=None, agent_id=None):
244
+ """Delete a dataset."""
245
+ from .sdk.features.dataset import delete_dataset as __delete_dataset
246
+ return __delete_dataset(dataset_id, api_key, agent_id)
247
+
248
+
249
+ 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):
250
+ """Create a dataset item."""
251
+ from .sdk.features.dataset import create_dataset_item as __create_dataset_item
252
+ return __create_dataset_item(dataset_id, name, input_data, expected_output, description, tags, metadata, flag_overrides, api_key, agent_id)
253
+
254
+
255
+ def _get_dataset_item(dataset_id, item_id, api_key=None, agent_id=None):
256
+ """Get a specific dataset item."""
257
+ from .sdk.features.dataset import get_dataset_item as __get_dataset_item
258
+ return __get_dataset_item(dataset_id, item_id, api_key, agent_id)
259
+
260
+
261
+ 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):
262
+ """Update a dataset item."""
263
+ from .sdk.features.dataset import update_dataset_item as __update_dataset_item
264
+ return __update_dataset_item(dataset_id, item_id, name, input_data, expected_output, description, tags, metadata, flag_overrides, api_key, agent_id)
265
+
266
+
267
+ def _delete_dataset_item(dataset_id, item_id, api_key=None, agent_id=None):
268
+ """Delete a dataset item."""
269
+ from .sdk.features.dataset import delete_dataset_item as __delete_dataset_item
270
+ return __delete_dataset_item(dataset_id, item_id, api_key, agent_id)
271
+
272
+
273
+ def _list_dataset_item_sessions(dataset_id, item_id, api_key=None, agent_id=None):
274
+ """List all sessions for a dataset item."""
275
+ from .sdk.features.dataset import list_dataset_item_sessions as __list_dataset_item_sessions
276
+ return __list_dataset_item_sessions(dataset_id, item_id, api_key, agent_id)
277
+
278
+
279
+ # Feature flags
280
+ from .sdk.features.feature_flag import (
281
+ get_feature_flag,
282
+ get_bool_flag,
283
+ get_int_flag,
284
+ get_float_flag,
285
+ get_string_flag,
286
+ get_json_flag,
287
+ clear_feature_flag_cache,
288
+ )
289
+
290
+ # Error boundary utilities
291
+ is_silent_mode = error_boundary.is_silent_mode
292
+ get_error_history = error_boundary.get_error_history
293
+ clear_error_history = error_boundary.clear_error_history
294
+
295
+ # Version
296
+ __version__ = "2.1.1"
297
+
298
+ # Apply error boundary wrapping to all SDK functions
299
+ from .sdk.error_boundary import wrap_sdk_function
300
+
301
+ # Wrap main SDK functions
302
+ init = wrap_sdk_function(_init, "init")
303
+ get_session_id = wrap_sdk_function(_get_session_id, "init")
304
+ clear_state = wrap_sdk_function(_clear_state, "init")
305
+ create_event = wrap_sdk_function(_create_event, "event")
306
+ create_error_event = wrap_sdk_function(_create_error_event, "event")
307
+ flush = wrap_sdk_function(_flush, "event")
308
+
309
+ # Wrap session functions
310
+ update_session = wrap_sdk_function(_update_session, "session")
311
+ end_session = wrap_sdk_function(_end_session, "session")
312
+ get_session = wrap_sdk_function(_get_session, "session")
313
+
314
+ # Wrap feature functions
315
+ create_experiment = wrap_sdk_function(_create_experiment, "experiment")
316
+ get_prompt = wrap_sdk_function(_get_prompt, "prompt")
317
+
318
+ # Dataset management - complete CRUD
319
+ list_datasets = wrap_sdk_function(_list_datasets, "dataset")
320
+ create_dataset = wrap_sdk_function(_create_dataset, "dataset")
321
+ get_dataset = wrap_sdk_function(_get_dataset, "dataset")
322
+ update_dataset = wrap_sdk_function(_update_dataset, "dataset")
323
+ delete_dataset = wrap_sdk_function(_delete_dataset, "dataset")
324
+
325
+ # Dataset item management
326
+ create_dataset_item = wrap_sdk_function(_create_dataset_item, "dataset")
327
+ get_dataset_item = wrap_sdk_function(_get_dataset_item, "dataset")
328
+ update_dataset_item = wrap_sdk_function(_update_dataset_item, "dataset")
329
+ delete_dataset_item = wrap_sdk_function(_delete_dataset_item, "dataset")
330
+ get_dataset_items = wrap_sdk_function(_get_dataset_items, "dataset")
331
+ list_dataset_item_sessions = wrap_sdk_function(_list_dataset_item_sessions, "dataset")
332
+
333
+ # All exports
334
+ __all__ = [
335
+ # Main functions
336
+ 'init',
337
+ 'get_session_id',
338
+ 'clear_state',
339
+ 'update_session',
340
+ 'end_session',
341
+ 'get_session',
342
+ 'create_event',
343
+ 'create_error_event',
344
+ 'flush',
345
+
346
+ # Decorators
347
+ 'event',
348
+ 'step',
349
+
350
+ # Features
351
+ 'create_experiment',
352
+ 'get_prompt',
353
+
354
+ # Dataset management
355
+ 'list_datasets',
356
+ 'create_dataset',
357
+ 'get_dataset',
358
+ 'update_dataset',
359
+ 'delete_dataset',
360
+ 'create_dataset_item',
361
+ 'get_dataset_item',
362
+ 'update_dataset_item',
363
+ 'delete_dataset_item',
364
+ 'get_dataset_items',
365
+ 'list_dataset_item_sessions',
366
+
367
+ # Feature flags
368
+ 'get_feature_flag',
369
+ 'get_bool_flag',
370
+ 'get_int_flag',
371
+ 'get_float_flag',
372
+ 'get_string_flag',
373
+ 'get_json_flag',
374
+ 'clear_feature_flag_cache',
375
+
376
+ # Context management
377
+ 'set_active_session',
378
+ 'clear_active_session',
379
+ 'bind_session',
380
+ 'bind_session_async',
381
+ 'session',
382
+ 'session_async',
383
+ 'run_session',
384
+ 'run_in_session',
385
+ 'thread_worker_with_session',
386
+ 'current_session_id',
387
+ 'current_parent_event_id',
388
+
389
+ # Thread-local session management (advanced)
390
+ 'set_thread_session',
391
+ 'clear_thread_session',
392
+ 'get_thread_session',
393
+
394
+ # Error types
395
+ 'LucidicError',
396
+ 'LucidicNotInitializedError',
397
+ 'APIKeyVerificationError',
398
+ 'InvalidOperationError',
399
+ 'PromptError',
400
+ 'FeatureFlagError',
401
+
402
+ # Error boundary
403
+ 'is_silent_mode',
404
+ 'get_error_history',
405
+ 'clear_error_history',
406
+
407
+ # Version
408
+ '__version__',
409
+ ]
@@ -0,0 +1 @@
1
+ """API client and resources."""
@@ -0,0 +1,218 @@
1
+ """Pure HTTP client for Lucidic API communication.
2
+
3
+ This module contains only the HTTP client logic, separated from
4
+ session management and other concerns.
5
+ """
6
+ import json
7
+ from typing import Any, Dict, Optional
8
+ from urllib.parse import urlencode
9
+
10
+ import requests
11
+ from requests.adapters import HTTPAdapter
12
+ from urllib3.util import Retry
13
+
14
+ from ..core.config import SDKConfig, get_config
15
+ from ..core.errors import APIKeyVerificationError
16
+ from ..utils.logger import debug, info, warning, error, mask_sensitive, truncate_data
17
+
18
+
19
+ class HttpClient:
20
+ """HTTP client for API communication."""
21
+
22
+ def __init__(self, config: Optional[SDKConfig] = None):
23
+ """Initialize the HTTP client.
24
+
25
+ Args:
26
+ config: SDK configuration (uses global if not provided)
27
+ """
28
+ self.config = config or get_config()
29
+ self.base_url = self.config.network.base_url
30
+
31
+ # Create session with connection pooling
32
+ self.session = requests.Session()
33
+
34
+ # Configure retries
35
+ retry_cfg = Retry(
36
+ total=self.config.network.max_retries,
37
+ backoff_factor=self.config.network.backoff_factor,
38
+ status_forcelist=[502, 503, 504],
39
+ allowed_methods=["GET", "POST", "PUT", "DELETE"],
40
+ )
41
+
42
+ # Configure adapter with connection pooling
43
+ adapter = HTTPAdapter(
44
+ max_retries=retry_cfg,
45
+ pool_connections=self.config.network.connection_pool_size,
46
+ pool_maxsize=self.config.network.connection_pool_maxsize
47
+ )
48
+
49
+ self.session.mount("https://", adapter)
50
+ self.session.mount("http://", adapter)
51
+
52
+ # Set headers
53
+ self._update_headers()
54
+
55
+ # Verify API key if configured
56
+ if self.config.api_key:
57
+ self._verify_api_key()
58
+
59
+ def _update_headers(self) -> None:
60
+ """Update session headers with authentication."""
61
+ headers = {
62
+ "User-Agent": "lucidic-sdk/2.0",
63
+ "Content-Type": "application/json"
64
+ }
65
+
66
+ if self.config.api_key:
67
+ headers["Authorization"] = f"Api-Key {self.config.api_key}"
68
+
69
+ if self.config.agent_id:
70
+ headers["x-agent-id"] = self.config.agent_id
71
+
72
+ self.session.headers.update(headers)
73
+
74
+ def _verify_api_key(self) -> None:
75
+ """Verify the API key with the backend."""
76
+ debug("[HTTP] Verifying API key")
77
+ try:
78
+ response = self.get("verifyapikey")
79
+ # Backend returns 200 OK for valid key, check if we got a response
80
+ if response is None:
81
+ raise APIKeyVerificationError("No response from API")
82
+ info("[HTTP] API key verified successfully")
83
+ except APIKeyVerificationError:
84
+ raise
85
+ except requests.RequestException as e:
86
+ raise APIKeyVerificationError(f"Could not verify API key: {e}")
87
+
88
+ def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
89
+ """Make a GET request.
90
+
91
+ Args:
92
+ endpoint: API endpoint (without base URL)
93
+ params: Query parameters
94
+
95
+ Returns:
96
+ Response data as dictionary
97
+ """
98
+ return self.request("GET", endpoint, params=params)
99
+
100
+ def post(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
101
+ """Make a POST request.
102
+
103
+ Args:
104
+ endpoint: API endpoint (without base URL)
105
+ data: Request body data
106
+
107
+ Returns:
108
+ Response data as dictionary
109
+ """
110
+ # Add current_time to all POST requests like TypeScript SDK does
111
+ from datetime import datetime, timezone
112
+ if data is None:
113
+ data = {}
114
+ data["current_time"] = datetime.now(timezone.utc).isoformat()
115
+ return self.request("POST", endpoint, json=data)
116
+
117
+ def put(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
118
+ """Make a PUT request.
119
+
120
+ Args:
121
+ endpoint: API endpoint (without base URL)
122
+ data: Request body data
123
+
124
+ Returns:
125
+ Response data as dictionary
126
+ """
127
+ # Add current_time to all PUT requests like TypeScript SDK does
128
+ from datetime import datetime, timezone
129
+ if data is None:
130
+ data = {}
131
+ data["current_time"] = datetime.now(timezone.utc).isoformat()
132
+ return self.request("PUT", endpoint, json=data)
133
+
134
+ def delete(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
135
+ """Make a DELETE request.
136
+
137
+ Args:
138
+ endpoint: API endpoint (without base URL)
139
+ params: Query parameters
140
+
141
+ Returns:
142
+ Response data as dictionary
143
+ """
144
+ return self.request("DELETE", endpoint, params=params)
145
+
146
+ def request(
147
+ self,
148
+ method: str,
149
+ endpoint: str,
150
+ params: Optional[Dict[str, Any]] = None,
151
+ json: Optional[Dict[str, Any]] = None,
152
+ **kwargs
153
+ ) -> Dict[str, Any]:
154
+ """Make an HTTP request.
155
+
156
+ Args:
157
+ method: HTTP method
158
+ endpoint: API endpoint (without base URL)
159
+ params: Query parameters
160
+ json: Request body (for POST/PUT)
161
+ **kwargs: Additional arguments for requests
162
+
163
+ Returns:
164
+ Response data as dictionary
165
+
166
+ Raises:
167
+ requests.RequestException: On HTTP errors
168
+ """
169
+ url = f"{self.base_url}/{endpoint}"
170
+
171
+ # Log request details
172
+ debug(f"[HTTP] {method} {url}")
173
+ if params:
174
+ debug(f"[HTTP] Query params: {mask_sensitive(params)}")
175
+ if json:
176
+ debug(f"[HTTP] Request body: {truncate_data(mask_sensitive(json))}")
177
+
178
+ response = self.session.request(
179
+ method=method,
180
+ url=url,
181
+ params=params,
182
+ json=json,
183
+ timeout=self.config.network.timeout,
184
+ **kwargs
185
+ )
186
+
187
+ # Raise for HTTP errors with more detail
188
+ if not response.ok:
189
+ # Try to get error details from response
190
+ try:
191
+ error_data = response.json()
192
+ error_msg = error_data.get('detail', response.text)
193
+ except:
194
+ error_msg = response.text
195
+
196
+ error(f"[HTTP] Error {response.status_code}: {error_msg}")
197
+
198
+ response.raise_for_status()
199
+
200
+ # Parse JSON response
201
+ try:
202
+ data = response.json()
203
+ except ValueError:
204
+ # For empty responses (like verifyapikey), return success
205
+ if response.status_code == 200 and not response.text:
206
+ data = {"success": True}
207
+ else:
208
+ # Return text if not JSON
209
+ data = {"response": response.text}
210
+
211
+ debug(f"[HTTP] Response ({response.status_code}): {truncate_data(data)}")
212
+
213
+ return data
214
+
215
+ def close(self) -> None:
216
+ """Close the HTTP session."""
217
+ if self.session:
218
+ self.session.close()
@@ -0,0 +1 @@
1
+ """API resource modules."""