lucidicai 2.0.2__py3-none-any.whl → 2.1.1__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 (38) hide show
  1. lucidicai/__init__.py +367 -899
  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/core/__init__.py +1 -0
  9. lucidicai/core/config.py +223 -0
  10. lucidicai/core/errors.py +60 -0
  11. lucidicai/core/types.py +35 -0
  12. lucidicai/sdk/__init__.py +1 -0
  13. lucidicai/sdk/context.py +231 -0
  14. lucidicai/sdk/decorators.py +187 -0
  15. lucidicai/sdk/error_boundary.py +299 -0
  16. lucidicai/sdk/event.py +126 -0
  17. lucidicai/sdk/event_builder.py +304 -0
  18. lucidicai/sdk/features/__init__.py +1 -0
  19. lucidicai/sdk/features/dataset.py +605 -0
  20. lucidicai/sdk/features/feature_flag.py +383 -0
  21. lucidicai/sdk/init.py +361 -0
  22. lucidicai/sdk/shutdown_manager.py +302 -0
  23. lucidicai/telemetry/context_bridge.py +82 -0
  24. lucidicai/telemetry/context_capture_processor.py +25 -9
  25. lucidicai/telemetry/litellm_bridge.py +20 -24
  26. lucidicai/telemetry/lucidic_exporter.py +99 -60
  27. lucidicai/telemetry/openai_patch.py +295 -0
  28. lucidicai/telemetry/openai_uninstrument.py +87 -0
  29. lucidicai/telemetry/telemetry_init.py +16 -1
  30. lucidicai/telemetry/utils/model_pricing.py +278 -0
  31. lucidicai/utils/__init__.py +1 -0
  32. lucidicai/utils/images.py +337 -0
  33. lucidicai/utils/logger.py +168 -0
  34. lucidicai/utils/queue.py +393 -0
  35. {lucidicai-2.0.2.dist-info → lucidicai-2.1.1.dist-info}/METADATA +1 -1
  36. {lucidicai-2.0.2.dist-info → lucidicai-2.1.1.dist-info}/RECORD +38 -9
  37. {lucidicai-2.0.2.dist-info → lucidicai-2.1.1.dist-info}/WHEEL +0 -0
  38. {lucidicai-2.0.2.dist-info → lucidicai-2.1.1.dist-info}/top_level.txt +0 -0
@@ -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."""
@@ -0,0 +1,192 @@
1
+ """Dataset resource API operations."""
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from ..client import HttpClient
5
+
6
+
7
+ class DatasetResource:
8
+ """Handle dataset-related API operations."""
9
+
10
+ def __init__(self, http: HttpClient):
11
+ """Initialize dataset resource.
12
+
13
+ Args:
14
+ http: HTTP client instance
15
+ """
16
+ self.http = http
17
+
18
+ def list_datasets(self, agent_id=None):
19
+ """List all datasets for agent.
20
+
21
+ Args:
22
+ agent_id: Optional agent ID to filter by
23
+
24
+ Returns:
25
+ Dictionary with num_datasets and datasets list
26
+ """
27
+ params = {}
28
+ if agent_id:
29
+ params["agent_id"] = agent_id
30
+ return self.http.get("sdk/datasets", params)
31
+
32
+ def create_dataset(self, name, description=None, tags=None,
33
+ suggested_flag_config=None, agent_id=None):
34
+ """Create new dataset.
35
+
36
+ Args:
37
+ name: Dataset name (must be unique per agent)
38
+ description: Optional description
39
+ tags: Optional list of tags
40
+ suggested_flag_config: Optional flag configuration
41
+ agent_id: Optional agent ID
42
+
43
+ Returns:
44
+ Dictionary with dataset_id
45
+ """
46
+ data = {"name": name}
47
+ if description is not None:
48
+ data["description"] = description
49
+ if tags is not None:
50
+ data["tags"] = tags
51
+ if suggested_flag_config is not None:
52
+ data["suggested_flag_config"] = suggested_flag_config
53
+ if agent_id is not None:
54
+ data["agent_id"] = agent_id
55
+ return self.http.post("sdk/datasets/create", data)
56
+
57
+ def get_dataset(self, dataset_id):
58
+ """Get dataset with all items - uses existing endpoint.
59
+
60
+ Args:
61
+ dataset_id: Dataset UUID
62
+
63
+ Returns:
64
+ Full dataset data including all items
65
+ """
66
+ return self.http.get("getdataset", {"dataset_id": dataset_id})
67
+
68
+ def update_dataset(self, dataset_id, **kwargs):
69
+ """Update dataset metadata.
70
+
71
+ Args:
72
+ dataset_id: Dataset UUID
73
+ **kwargs: Fields to update (name, description, tags, suggested_flag_config)
74
+
75
+ Returns:
76
+ Updated dataset data
77
+ """
78
+ data = {"dataset_id": dataset_id}
79
+ data.update(kwargs)
80
+ return self.http.put("sdk/datasets/update", data)
81
+
82
+ def delete_dataset(self, dataset_id):
83
+ """Delete dataset and all items.
84
+
85
+ Args:
86
+ dataset_id: Dataset UUID
87
+
88
+ Returns:
89
+ Success message
90
+ """
91
+ return self.http.delete("sdk/datasets/delete", {"dataset_id": dataset_id})
92
+
93
+ def create_item(self, dataset_id, name, input_data,
94
+ expected_output=None, description=None,
95
+ tags=None, metadata=None, flag_overrides=None):
96
+ """Create dataset item.
97
+
98
+ Args:
99
+ dataset_id: Dataset UUID
100
+ name: Item name
101
+ input_data: Input data dictionary
102
+ expected_output: Optional expected output
103
+ description: Optional description
104
+ tags: Optional list of tags
105
+ metadata: Optional metadata dictionary
106
+ flag_overrides: Optional flag overrides
107
+
108
+ Returns:
109
+ Dictionary with datasetitem_id
110
+ """
111
+ data = {
112
+ "dataset_id": dataset_id,
113
+ "name": name,
114
+ "input": input_data
115
+ }
116
+
117
+ # Add optional fields if provided
118
+ if expected_output is not None:
119
+ data["expected_output"] = expected_output
120
+ if description is not None:
121
+ data["description"] = description
122
+ if tags is not None:
123
+ data["tags"] = tags
124
+ if metadata is not None:
125
+ data["metadata"] = metadata
126
+ if flag_overrides is not None:
127
+ data["flag_overrides"] = flag_overrides
128
+
129
+ return self.http.post("sdk/datasets/items/create", data)
130
+
131
+ def get_item(self, dataset_id, item_id):
132
+ """Get specific dataset item.
133
+
134
+ Args:
135
+ dataset_id: Dataset UUID
136
+ item_id: Item UUID
137
+
138
+ Returns:
139
+ Dataset item data
140
+ """
141
+ return self.http.get("sdk/datasets/items/get", {
142
+ "dataset_id": dataset_id,
143
+ "datasetitem_id": item_id
144
+ })
145
+
146
+ def update_item(self, dataset_id, item_id, **kwargs):
147
+ """Update dataset item.
148
+
149
+ Args:
150
+ dataset_id: Dataset UUID
151
+ item_id: Item UUID
152
+ **kwargs: Fields to update
153
+
154
+ Returns:
155
+ Updated item data
156
+ """
157
+ data = {
158
+ "dataset_id": dataset_id,
159
+ "datasetitem_id": item_id
160
+ }
161
+ data.update(kwargs)
162
+ return self.http.put("sdk/datasets/items/update", data)
163
+
164
+ def delete_item(self, dataset_id, item_id):
165
+ """Delete dataset item.
166
+
167
+ Args:
168
+ dataset_id: Dataset UUID
169
+ item_id: Item UUID
170
+
171
+ Returns:
172
+ Success message
173
+ """
174
+ return self.http.delete("sdk/datasets/items/delete", {
175
+ "dataset_id": dataset_id,
176
+ "datasetitem_id": item_id
177
+ })
178
+
179
+ def list_item_sessions(self, dataset_id, item_id):
180
+ """List all sessions for a dataset item.
181
+
182
+ Args:
183
+ dataset_id: Dataset UUID
184
+ item_id: Item UUID
185
+
186
+ Returns:
187
+ Dictionary with num_sessions and sessions list
188
+ """
189
+ return self.http.get("sdk/datasets/items/sessions", {
190
+ "dataset_id": dataset_id,
191
+ "datasetitem_id": item_id
192
+ })
@@ -0,0 +1,88 @@
1
+ """Event resource API operations."""
2
+ from typing import Any, Dict, Optional
3
+ from datetime import datetime
4
+
5
+ from ..client import HttpClient
6
+
7
+
8
+ class EventResource:
9
+ """Handle event-related API operations."""
10
+
11
+ def __init__(self, http: HttpClient):
12
+ """Initialize event resource.
13
+
14
+ Args:
15
+ http: HTTP client instance
16
+ """
17
+ self.http = http
18
+
19
+ def create_event(self, params: Dict[str, Any]) -> Dict[str, Any]:
20
+ """Create a new event.
21
+
22
+ Args:
23
+ params: Event parameters including:
24
+ - client_event_id: Client-generated event ID
25
+ - session_id: Session ID
26
+ - type: Event type
27
+ - occurred_at: When the event occurred
28
+ - payload: Event payload
29
+ - etc.
30
+
31
+ Returns:
32
+ API response with optional blob_url for large payloads
33
+ """
34
+ return self.http.post("events", params)
35
+
36
+ def get_event(self, event_id: str) -> Dict[str, Any]:
37
+ """Get an event by ID.
38
+
39
+ Args:
40
+ event_id: Event ID
41
+
42
+ Returns:
43
+ Event data
44
+ """
45
+ return self.http.get(f"events/{event_id}")
46
+
47
+ def update_event(self, event_id: str, updates: Dict[str, Any]) -> Dict[str, Any]:
48
+ """Update an existing event.
49
+
50
+ Args:
51
+ event_id: Event ID
52
+ updates: Fields to update
53
+
54
+ Returns:
55
+ Updated event data
56
+ """
57
+ return self.http.put(f"events/{event_id}", updates)
58
+
59
+ def list_events(
60
+ self,
61
+ session_id: Optional[str] = None,
62
+ event_type: Optional[str] = None,
63
+ limit: int = 100,
64
+ offset: int = 0
65
+ ) -> Dict[str, Any]:
66
+ """List events with optional filters.
67
+
68
+ Args:
69
+ session_id: Filter by session ID
70
+ event_type: Filter by event type
71
+ limit: Maximum number of events to return
72
+ offset: Pagination offset
73
+
74
+ Returns:
75
+ List of events and pagination info
76
+ """
77
+ params = {
78
+ "limit": limit,
79
+ "offset": offset
80
+ }
81
+
82
+ if session_id:
83
+ params["session_id"] = session_id
84
+
85
+ if event_type:
86
+ params["type"] = event_type
87
+
88
+ return self.http.get("events", params)
@@ -0,0 +1,126 @@
1
+ """Session resource API operations."""
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from ..client import HttpClient
5
+
6
+
7
+ class SessionResource:
8
+ """Handle session-related API operations."""
9
+
10
+ def __init__(self, http: HttpClient):
11
+ """Initialize session resource.
12
+
13
+ Args:
14
+ http: HTTP client instance
15
+ """
16
+ self.http = http
17
+
18
+ def create_session(self, params: Dict[str, Any]) -> Dict[str, Any]:
19
+ """Create a new session.
20
+
21
+ Args:
22
+ params: Session parameters including:
23
+ - session_name: Name of the session
24
+ - agent_id: Agent ID
25
+ - task: Optional task description
26
+ - tags: Optional tags
27
+ - etc.
28
+
29
+ Returns:
30
+ Created session data with session_id
31
+ """
32
+ return self.http.post("initsession", params)
33
+
34
+ def get_session(self, session_id: str) -> Dict[str, Any]:
35
+ """Get a session by ID.
36
+
37
+ Args:
38
+ session_id: Session ID
39
+
40
+ Returns:
41
+ Session data
42
+ """
43
+ return self.http.get(f"sessions/{session_id}")
44
+
45
+ def update_session(self, session_id: str, updates: Dict[str, Any]) -> Dict[str, Any]:
46
+ """Update an existing session.
47
+
48
+ Args:
49
+ session_id: Session ID
50
+ updates: Fields to update (task, is_finished, etc.)
51
+
52
+ Returns:
53
+ Updated session data
54
+ """
55
+ # Add session_id to the updates payload
56
+ updates["session_id"] = session_id
57
+ return self.http.put("updatesession", updates)
58
+
59
+ def end_session(
60
+ self,
61
+ session_id: str,
62
+ is_successful: Optional[bool] = None,
63
+ is_successful_reason: Optional[str] = None,
64
+ session_eval: Optional[float] = None,
65
+ session_eval_reason: Optional[str] = None
66
+ ) -> Dict[str, Any]:
67
+ """End a session.
68
+
69
+ Args:
70
+ session_id: Session ID
71
+ is_successful: Whether session was successful
72
+ is_successful_reason: Reason for success or failure
73
+ session_eval: Session evaluation score
74
+ session_eval_reason: Reason for evaluation
75
+
76
+ Returns:
77
+ Final session data
78
+ """
79
+ updates = {
80
+ "is_finished": True
81
+ }
82
+
83
+ if is_successful is not None:
84
+ updates["is_successful"] = is_successful
85
+
86
+ if session_eval is not None:
87
+ updates["session_eval"] = session_eval
88
+
89
+ if session_eval_reason is not None:
90
+ updates["session_eval_reason"] = session_eval_reason
91
+
92
+ if is_successful_reason is not None:
93
+ updates["is_successful_reason"] = is_successful_reason
94
+
95
+ return self.update_session(session_id, updates)
96
+
97
+ def list_sessions(
98
+ self,
99
+ agent_id: Optional[str] = None,
100
+ experiment_id: Optional[str] = None,
101
+ limit: int = 100,
102
+ offset: int = 0
103
+ ) -> Dict[str, Any]:
104
+ """List sessions with optional filters.
105
+
106
+ Args:
107
+ agent_id: Filter by agent ID
108
+ experiment_id: Filter by experiment ID
109
+ limit: Maximum number of sessions
110
+ offset: Pagination offset
111
+
112
+ Returns:
113
+ List of sessions and pagination info
114
+ """
115
+ params = {
116
+ "limit": limit,
117
+ "offset": offset
118
+ }
119
+
120
+ if agent_id:
121
+ params["agent_id"] = agent_id
122
+
123
+ if experiment_id:
124
+ params["experiment_id"] = experiment_id
125
+
126
+ return self.http.get("sessions", params)
@@ -0,0 +1 @@
1
+ """Core utilities and configuration."""