lucidicai 2.1.1__tar.gz → 2.1.3__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.
- {lucidicai-2.1.1 → lucidicai-2.1.3}/PKG-INFO +1 -1
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/__init__.py +5 -1
- lucidicai-2.1.3/lucidicai/api/client.py +357 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/sdk/context.py +42 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/sdk/features/dataset.py +23 -237
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/sdk/init.py +74 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/telemetry/lucidic_exporter.py +13 -0
- lucidicai-2.1.3/lucidicai/telemetry/openai_patch.py +425 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/telemetry/telemetry_init.py +3 -3
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/utils/images.py +3 -3
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/utils/queue.py +40 -8
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai.egg-info/PKG-INFO +1 -1
- {lucidicai-2.1.1 → lucidicai-2.1.3}/setup.py +1 -1
- lucidicai-2.1.1/lucidicai/api/client.py +0 -218
- lucidicai-2.1.1/lucidicai/telemetry/openai_patch.py +0 -295
- {lucidicai-2.1.1 → lucidicai-2.1.3}/README.md +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/api/__init__.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/api/resources/__init__.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/api/resources/dataset.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/api/resources/event.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/api/resources/session.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/core/__init__.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/core/config.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/core/errors.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/core/types.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/sdk/__init__.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/sdk/decorators.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/sdk/error_boundary.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/sdk/event.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/sdk/event_builder.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/sdk/features/__init__.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/sdk/features/feature_flag.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/sdk/shutdown_manager.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/telemetry/__init__.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/telemetry/context_bridge.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/telemetry/context_capture_processor.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/telemetry/extract.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/telemetry/litellm_bridge.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/telemetry/openai_agents_instrumentor.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/telemetry/openai_uninstrument.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/telemetry/utils/__init__.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/telemetry/utils/model_pricing.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/utils/__init__.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai/utils/logger.py +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai.egg-info/SOURCES.txt +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai.egg-info/dependency_links.txt +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai.egg-info/requires.txt +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/lucidicai.egg-info/top_level.txt +0 -0
- {lucidicai-2.1.1 → lucidicai-2.1.3}/setup.cfg +0 -0
|
@@ -100,6 +100,7 @@ def _end_session(
|
|
|
100
100
|
):
|
|
101
101
|
"""End the current session."""
|
|
102
102
|
from .sdk.init import get_resources, get_session_id, get_event_queue
|
|
103
|
+
from .sdk.shutdown_manager import get_shutdown_manager
|
|
103
104
|
|
|
104
105
|
# Use provided session_id or fall back to context
|
|
105
106
|
if not session_id:
|
|
@@ -125,6 +126,9 @@ def _end_session(
|
|
|
125
126
|
# Clear session context
|
|
126
127
|
clear_active_session()
|
|
127
128
|
|
|
129
|
+
# unregister from shutdown manager
|
|
130
|
+
get_shutdown_manager().unregister_session(session_id)
|
|
131
|
+
|
|
128
132
|
|
|
129
133
|
def _get_session():
|
|
130
134
|
"""Get the current session object."""
|
|
@@ -293,7 +297,7 @@ get_error_history = error_boundary.get_error_history
|
|
|
293
297
|
clear_error_history = error_boundary.clear_error_history
|
|
294
298
|
|
|
295
299
|
# Version
|
|
296
|
-
__version__ = "2.1.
|
|
300
|
+
__version__ = "2.1.3"
|
|
297
301
|
|
|
298
302
|
# Apply error boundary wrapping to all SDK functions
|
|
299
303
|
from .sdk.error_boundary import wrap_sdk_function
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Pure HTTP client for Lucidic API communication.
|
|
2
|
+
|
|
3
|
+
This module contains only the HTTP client logic using httpx,
|
|
4
|
+
supporting both synchronous and asynchronous operations.
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from ..core.config import SDKConfig, get_config
|
|
13
|
+
from ..core.errors import APIKeyVerificationError
|
|
14
|
+
from ..utils.logger import debug, info, warning, error, mask_sensitive, truncate_data
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HttpClient:
|
|
18
|
+
"""HTTP client for API communication with sync and async support."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: Optional[SDKConfig] = None):
|
|
21
|
+
"""Initialize the HTTP client.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
config: SDK configuration (uses global if not provided)
|
|
25
|
+
"""
|
|
26
|
+
self.config = config or get_config()
|
|
27
|
+
self.base_url = self.config.network.base_url
|
|
28
|
+
|
|
29
|
+
# Build default headers
|
|
30
|
+
self._headers = self._build_headers()
|
|
31
|
+
|
|
32
|
+
# Transport configuration for connection pooling and retries
|
|
33
|
+
self._transport_kwargs = {
|
|
34
|
+
"retries": self.config.network.max_retries,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Connection limits for pooling
|
|
38
|
+
self._limits = httpx.Limits(
|
|
39
|
+
max_connections=self.config.network.connection_pool_maxsize,
|
|
40
|
+
max_keepalive_connections=self.config.network.connection_pool_size,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Lazy-initialized clients
|
|
44
|
+
self._sync_client: Optional[httpx.Client] = None
|
|
45
|
+
self._async_client: Optional[httpx.AsyncClient] = None
|
|
46
|
+
|
|
47
|
+
def _build_headers(self) -> Dict[str, str]:
|
|
48
|
+
"""Build default headers for requests."""
|
|
49
|
+
headers = {
|
|
50
|
+
"User-Agent": "lucidic-sdk/2.0",
|
|
51
|
+
"Content-Type": "application/json"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if self.config.api_key:
|
|
55
|
+
headers["Authorization"] = f"Api-Key {self.config.api_key}"
|
|
56
|
+
|
|
57
|
+
if self.config.agent_id:
|
|
58
|
+
headers["x-agent-id"] = self.config.agent_id
|
|
59
|
+
|
|
60
|
+
return headers
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def sync_client(self) -> httpx.Client:
|
|
64
|
+
"""Get or create the synchronous HTTP client."""
|
|
65
|
+
if self._sync_client is None or self._sync_client.is_closed:
|
|
66
|
+
transport = httpx.HTTPTransport(**self._transport_kwargs)
|
|
67
|
+
self._sync_client = httpx.Client(
|
|
68
|
+
base_url=self.base_url,
|
|
69
|
+
headers=self._headers,
|
|
70
|
+
timeout=httpx.Timeout(self.config.network.timeout),
|
|
71
|
+
limits=self._limits,
|
|
72
|
+
transport=transport,
|
|
73
|
+
)
|
|
74
|
+
return self._sync_client
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
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:
|
|
80
|
+
transport = httpx.AsyncHTTPTransport(**self._transport_kwargs)
|
|
81
|
+
self._async_client = httpx.AsyncClient(
|
|
82
|
+
base_url=self.base_url,
|
|
83
|
+
headers=self._headers,
|
|
84
|
+
timeout=httpx.Timeout(self.config.network.timeout),
|
|
85
|
+
limits=self._limits,
|
|
86
|
+
transport=transport,
|
|
87
|
+
)
|
|
88
|
+
return self._async_client
|
|
89
|
+
|
|
90
|
+
def _add_timestamp(self, data: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
91
|
+
"""Add current_time to request data."""
|
|
92
|
+
if data is None:
|
|
93
|
+
data = {}
|
|
94
|
+
data["current_time"] = datetime.now(timezone.utc).isoformat()
|
|
95
|
+
return data
|
|
96
|
+
|
|
97
|
+
def _handle_response(self, response: httpx.Response) -> Dict[str, Any]:
|
|
98
|
+
"""Handle HTTP response and parse JSON.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
response: httpx Response object
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Response data as dictionary
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
APIKeyVerificationError: On 401 Unauthorized responses
|
|
108
|
+
httpx.HTTPStatusError: On other HTTP errors
|
|
109
|
+
"""
|
|
110
|
+
# Log and raise for HTTP errors
|
|
111
|
+
if not response.is_success:
|
|
112
|
+
try:
|
|
113
|
+
error_data = response.json()
|
|
114
|
+
error_msg = error_data.get('detail', response.text)
|
|
115
|
+
except Exception:
|
|
116
|
+
error_msg = response.text
|
|
117
|
+
|
|
118
|
+
error(f"[HTTP] Error {response.status_code}: {error_msg}")
|
|
119
|
+
|
|
120
|
+
# Raise specific error for authentication/authorization failures
|
|
121
|
+
if response.status_code in (401, 403):
|
|
122
|
+
raise APIKeyVerificationError(f"Authentication failed: {error_msg}")
|
|
123
|
+
|
|
124
|
+
response.raise_for_status()
|
|
125
|
+
|
|
126
|
+
# Parse JSON response
|
|
127
|
+
try:
|
|
128
|
+
data = response.json()
|
|
129
|
+
except ValueError:
|
|
130
|
+
# For empty responses (like verifyapikey), return success
|
|
131
|
+
if response.status_code == 200 and not response.text:
|
|
132
|
+
data = {"success": True}
|
|
133
|
+
else:
|
|
134
|
+
# Return text if not JSON
|
|
135
|
+
data = {"response": response.text}
|
|
136
|
+
|
|
137
|
+
debug(f"[HTTP] Response ({response.status_code}): {truncate_data(data)}")
|
|
138
|
+
|
|
139
|
+
return data
|
|
140
|
+
|
|
141
|
+
# ==================== Synchronous Methods ====================
|
|
142
|
+
|
|
143
|
+
def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
144
|
+
"""Make a synchronous GET request.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
endpoint: API endpoint (without base URL)
|
|
148
|
+
params: Query parameters
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Response data as dictionary
|
|
152
|
+
"""
|
|
153
|
+
return self.request("GET", endpoint, params=params)
|
|
154
|
+
|
|
155
|
+
def post(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
156
|
+
"""Make a synchronous POST request.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
endpoint: API endpoint (without base URL)
|
|
160
|
+
data: Request body data
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Response data as dictionary
|
|
164
|
+
"""
|
|
165
|
+
data = self._add_timestamp(data)
|
|
166
|
+
return self.request("POST", endpoint, json=data)
|
|
167
|
+
|
|
168
|
+
def put(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
169
|
+
"""Make a synchronous PUT request.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
endpoint: API endpoint (without base URL)
|
|
173
|
+
data: Request body data
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Response data as dictionary
|
|
177
|
+
"""
|
|
178
|
+
data = self._add_timestamp(data)
|
|
179
|
+
return self.request("PUT", endpoint, json=data)
|
|
180
|
+
|
|
181
|
+
def delete(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
182
|
+
"""Make a synchronous DELETE request.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
endpoint: API endpoint (without base URL)
|
|
186
|
+
params: Query parameters
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Response data as dictionary
|
|
190
|
+
"""
|
|
191
|
+
return self.request("DELETE", endpoint, params=params)
|
|
192
|
+
|
|
193
|
+
def request(
|
|
194
|
+
self,
|
|
195
|
+
method: str,
|
|
196
|
+
endpoint: str,
|
|
197
|
+
params: Optional[Dict[str, Any]] = None,
|
|
198
|
+
json: Optional[Dict[str, Any]] = None,
|
|
199
|
+
**kwargs
|
|
200
|
+
) -> Dict[str, Any]:
|
|
201
|
+
"""Make a synchronous HTTP request.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
method: HTTP method
|
|
205
|
+
endpoint: API endpoint (without base URL)
|
|
206
|
+
params: Query parameters
|
|
207
|
+
json: Request body (for POST/PUT)
|
|
208
|
+
**kwargs: Additional arguments for httpx
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Response data as dictionary
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
httpx.HTTPError: On HTTP errors
|
|
215
|
+
"""
|
|
216
|
+
url = f"/{endpoint}"
|
|
217
|
+
|
|
218
|
+
# Log request details
|
|
219
|
+
debug(f"[HTTP] {method} {self.base_url}{url}")
|
|
220
|
+
if params:
|
|
221
|
+
debug(f"[HTTP] Query params: {mask_sensitive(params)}")
|
|
222
|
+
if json:
|
|
223
|
+
debug(f"[HTTP] Request body: {truncate_data(mask_sensitive(json))}")
|
|
224
|
+
|
|
225
|
+
response = self.sync_client.request(
|
|
226
|
+
method=method,
|
|
227
|
+
url=url,
|
|
228
|
+
params=params,
|
|
229
|
+
json=json,
|
|
230
|
+
**kwargs
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return self._handle_response(response)
|
|
234
|
+
|
|
235
|
+
# ==================== Asynchronous Methods ====================
|
|
236
|
+
|
|
237
|
+
async def aget(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
238
|
+
"""Make an asynchronous GET request.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
endpoint: API endpoint (without base URL)
|
|
242
|
+
params: Query parameters
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Response data as dictionary
|
|
246
|
+
"""
|
|
247
|
+
return await self.arequest("GET", endpoint, params=params)
|
|
248
|
+
|
|
249
|
+
async def apost(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
250
|
+
"""Make an asynchronous POST request.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
endpoint: API endpoint (without base URL)
|
|
254
|
+
data: Request body data
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Response data as dictionary
|
|
258
|
+
"""
|
|
259
|
+
data = self._add_timestamp(data)
|
|
260
|
+
return await self.arequest("POST", endpoint, json=data)
|
|
261
|
+
|
|
262
|
+
async def aput(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
263
|
+
"""Make an asynchronous PUT request.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
endpoint: API endpoint (without base URL)
|
|
267
|
+
data: Request body data
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Response data as dictionary
|
|
271
|
+
"""
|
|
272
|
+
data = self._add_timestamp(data)
|
|
273
|
+
return await self.arequest("PUT", endpoint, json=data)
|
|
274
|
+
|
|
275
|
+
async def adelete(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
276
|
+
"""Make an asynchronous DELETE request.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
endpoint: API endpoint (without base URL)
|
|
280
|
+
params: Query parameters
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Response data as dictionary
|
|
284
|
+
"""
|
|
285
|
+
return await self.arequest("DELETE", endpoint, params=params)
|
|
286
|
+
|
|
287
|
+
async def arequest(
|
|
288
|
+
self,
|
|
289
|
+
method: str,
|
|
290
|
+
endpoint: str,
|
|
291
|
+
params: Optional[Dict[str, Any]] = None,
|
|
292
|
+
json: Optional[Dict[str, Any]] = None,
|
|
293
|
+
**kwargs
|
|
294
|
+
) -> Dict[str, Any]:
|
|
295
|
+
"""Make an asynchronous HTTP request.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
method: HTTP method
|
|
299
|
+
endpoint: API endpoint (without base URL)
|
|
300
|
+
params: Query parameters
|
|
301
|
+
json: Request body (for POST/PUT)
|
|
302
|
+
**kwargs: Additional arguments for httpx
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Response data as dictionary
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
httpx.HTTPError: On HTTP errors
|
|
309
|
+
"""
|
|
310
|
+
url = f"/{endpoint}"
|
|
311
|
+
|
|
312
|
+
# Log request details
|
|
313
|
+
debug(f"[HTTP] {method} {self.base_url}{url}")
|
|
314
|
+
if params:
|
|
315
|
+
debug(f"[HTTP] Query params: {mask_sensitive(params)}")
|
|
316
|
+
if json:
|
|
317
|
+
debug(f"[HTTP] Request body: {truncate_data(mask_sensitive(json))}")
|
|
318
|
+
|
|
319
|
+
response = await self.async_client.request(
|
|
320
|
+
method=method,
|
|
321
|
+
url=url,
|
|
322
|
+
params=params,
|
|
323
|
+
json=json,
|
|
324
|
+
**kwargs
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return self._handle_response(response)
|
|
328
|
+
|
|
329
|
+
# ==================== Lifecycle Methods ====================
|
|
330
|
+
|
|
331
|
+
def close(self) -> None:
|
|
332
|
+
"""Close the synchronous HTTP client."""
|
|
333
|
+
if self._sync_client is not None and not self._sync_client.is_closed:
|
|
334
|
+
self._sync_client.close()
|
|
335
|
+
self._sync_client = None
|
|
336
|
+
|
|
337
|
+
async def aclose(self) -> None:
|
|
338
|
+
"""Close the asynchronous HTTP client."""
|
|
339
|
+
if self._async_client is not None and not self._async_client.is_closed:
|
|
340
|
+
await self._async_client.aclose()
|
|
341
|
+
self._async_client = None
|
|
342
|
+
|
|
343
|
+
def __enter__(self) -> "HttpClient":
|
|
344
|
+
"""Context manager entry for sync client."""
|
|
345
|
+
return self
|
|
346
|
+
|
|
347
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
348
|
+
"""Context manager exit for sync client."""
|
|
349
|
+
self.close()
|
|
350
|
+
|
|
351
|
+
async def __aenter__(self) -> "HttpClient":
|
|
352
|
+
"""Async context manager entry."""
|
|
353
|
+
return self
|
|
354
|
+
|
|
355
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
356
|
+
"""Async context manager exit."""
|
|
357
|
+
await self.aclose()
|
|
@@ -151,6 +151,27 @@ def session(**init_params) -> Iterator[None]:
|
|
|
151
151
|
clear_thread_session()
|
|
152
152
|
current_session_id.reset(token)
|
|
153
153
|
try:
|
|
154
|
+
# Force flush OpenTelemetry spans before ending session
|
|
155
|
+
from .init import get_tracer_provider
|
|
156
|
+
from ..utils.logger import debug, info
|
|
157
|
+
import time
|
|
158
|
+
|
|
159
|
+
tracer_provider = get_tracer_provider()
|
|
160
|
+
if tracer_provider:
|
|
161
|
+
debug(f"[Session] Force flushing OpenTelemetry spans for session {session_id}")
|
|
162
|
+
try:
|
|
163
|
+
# Force flush with 5 second timeout to ensure all spans are exported
|
|
164
|
+
flush_result = tracer_provider.force_flush(timeout_millis=5000)
|
|
165
|
+
debug(f"[Session] Tracer provider force_flush returned: {flush_result}")
|
|
166
|
+
|
|
167
|
+
# Give a small additional delay to ensure the exporter processes everything
|
|
168
|
+
# This is necessary because force_flush on the provider flushes the processors,
|
|
169
|
+
# but the exporter might still be processing the spans
|
|
170
|
+
time.sleep(0.5)
|
|
171
|
+
debug(f"[Session] Successfully flushed spans for session {session_id}")
|
|
172
|
+
except Exception as e:
|
|
173
|
+
debug(f"[Session] Error flushing spans: {e}")
|
|
174
|
+
|
|
154
175
|
# Pass session_id explicitly to avoid context issues
|
|
155
176
|
lai.end_session(session_id=session_id)
|
|
156
177
|
except Exception:
|
|
@@ -184,6 +205,27 @@ async def session_async(**init_params) -> AsyncIterator[None]:
|
|
|
184
205
|
clear_task_session()
|
|
185
206
|
current_session_id.reset(token)
|
|
186
207
|
try:
|
|
208
|
+
# Force flush OpenTelemetry spans before ending session
|
|
209
|
+
from .init import get_tracer_provider
|
|
210
|
+
from ..utils.logger import debug, info
|
|
211
|
+
import asyncio
|
|
212
|
+
|
|
213
|
+
tracer_provider = get_tracer_provider()
|
|
214
|
+
if tracer_provider:
|
|
215
|
+
debug(f"[Session] Force flushing OpenTelemetry spans for async session {session_id}")
|
|
216
|
+
try:
|
|
217
|
+
# Force flush with 5 second timeout to ensure all spans are exported
|
|
218
|
+
flush_result = tracer_provider.force_flush(timeout_millis=5000)
|
|
219
|
+
debug(f"[Session] Tracer provider force_flush returned: {flush_result}")
|
|
220
|
+
|
|
221
|
+
# Give a small additional delay to ensure the exporter processes everything
|
|
222
|
+
# This is necessary because force_flush on the provider flushes the processors,
|
|
223
|
+
# but the exporter might still be processing the spans
|
|
224
|
+
await asyncio.sleep(0.5)
|
|
225
|
+
debug(f"[Session] Successfully flushed spans for async session {session_id}")
|
|
226
|
+
except Exception as e:
|
|
227
|
+
debug(f"[Session] Error flushing spans: {e}")
|
|
228
|
+
|
|
187
229
|
# Pass session_id explicitly to avoid context issues in async
|
|
188
230
|
lai.end_session(session_id=session_id)
|
|
189
231
|
except Exception:
|