lucidicai 2.0.1__tar.gz → 2.0.2__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.0.1 → lucidicai-2.0.2}/PKG-INFO +1 -1
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/__init__.py +24 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/client.py +4 -1
- lucidicai-2.0.2/lucidicai/dataset.py +114 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/errors.py +6 -0
- lucidicai-2.0.2/lucidicai/feature_flag.py +344 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai.egg-info/PKG-INFO +1 -1
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai.egg-info/SOURCES.txt +2 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/setup.py +1 -1
- {lucidicai-2.0.1 → lucidicai-2.0.2}/README.md +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/constants.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/context.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/decorators.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/event.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/event_queue.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/image_upload.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/lru.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/model_pricing.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/session.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/singleton.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/streaming.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/telemetry/__init__.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/telemetry/context_capture_processor.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/telemetry/extract.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/telemetry/litellm_bridge.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/telemetry/lucidic_exporter.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/telemetry/openai_agents_instrumentor.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/telemetry/telemetry_init.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/telemetry/utils/__init__.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/telemetry/utils/image_storage.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/telemetry/utils/text_storage.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/telemetry/utils/universal_image_interceptor.py +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai.egg-info/dependency_links.txt +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai.egg-info/requires.txt +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai.egg-info/top_level.txt +0 -0
- {lucidicai-2.0.1 → lucidicai-2.0.2}/setup.cfg +0 -0
|
@@ -28,6 +28,17 @@ from .context import (
|
|
|
28
28
|
run_session,
|
|
29
29
|
run_in_session,
|
|
30
30
|
)
|
|
31
|
+
from .dataset import get_dataset, get_dataset_items
|
|
32
|
+
from .feature_flag import (
|
|
33
|
+
get_feature_flag,
|
|
34
|
+
get_bool_flag,
|
|
35
|
+
get_int_flag,
|
|
36
|
+
get_float_flag,
|
|
37
|
+
get_string_flag,
|
|
38
|
+
get_json_flag,
|
|
39
|
+
clear_feature_flag_cache,
|
|
40
|
+
FeatureFlagError
|
|
41
|
+
)
|
|
31
42
|
|
|
32
43
|
ProviderType = Literal[
|
|
33
44
|
"openai",
|
|
@@ -229,6 +240,16 @@ __all__ = [
|
|
|
229
240
|
'end_session',
|
|
230
241
|
'get_prompt',
|
|
231
242
|
'get_session',
|
|
243
|
+
'get_dataset',
|
|
244
|
+
'get_dataset_items',
|
|
245
|
+
'get_feature_flag',
|
|
246
|
+
'get_bool_flag',
|
|
247
|
+
'get_int_flag',
|
|
248
|
+
'get_float_flag',
|
|
249
|
+
'get_string_flag',
|
|
250
|
+
'get_json_flag',
|
|
251
|
+
'clear_feature_flag_cache',
|
|
252
|
+
'FeatureFlagError',
|
|
232
253
|
'ProviderType',
|
|
233
254
|
'APIKeyVerificationError',
|
|
234
255
|
'LucidicNotInitializedError',
|
|
@@ -257,6 +278,7 @@ def init(
|
|
|
257
278
|
experiment_id: Optional[str] = None,
|
|
258
279
|
rubrics: Optional[list] = None,
|
|
259
280
|
tags: Optional[list] = None,
|
|
281
|
+
dataset_item_id: Optional[str] = None,
|
|
260
282
|
masking_function = None,
|
|
261
283
|
auto_end: Optional[bool] = True,
|
|
262
284
|
capture_uncaught: Optional[bool] = True,
|
|
@@ -274,6 +296,7 @@ def init(
|
|
|
274
296
|
experiment_id: Optional experiment ID, if session is to be part of an experiment.
|
|
275
297
|
rubrics: Optional rubrics for evaluation, list of strings.
|
|
276
298
|
tags: Optional tags for the session, list of strings.
|
|
299
|
+
dataset_item_id: Optional dataset item ID to link session to a dataset item.
|
|
277
300
|
masking_function: Optional function to mask sensitive data.
|
|
278
301
|
auto_end: If True, automatically end the session on process exit. Defaults to True.
|
|
279
302
|
|
|
@@ -328,6 +351,7 @@ def init(
|
|
|
328
351
|
production_monitoring=production_monitoring,
|
|
329
352
|
session_id=session_id,
|
|
330
353
|
experiment_id=experiment_id,
|
|
354
|
+
dataset_item_id=dataset_item_id,
|
|
331
355
|
)
|
|
332
356
|
if masking_function:
|
|
333
357
|
client.masking_function = masking_function
|
|
@@ -98,6 +98,7 @@ class Client:
|
|
|
98
98
|
production_monitoring: Optional[bool] = False,
|
|
99
99
|
session_id: Optional[str] = None,
|
|
100
100
|
experiment_id: Optional[str] = None,
|
|
101
|
+
dataset_item_id: Optional[str] = None,
|
|
101
102
|
) -> None:
|
|
102
103
|
if session_id:
|
|
103
104
|
# Check if it's a known session ID, maybe custom and maybe real
|
|
@@ -122,7 +123,9 @@ class Client:
|
|
|
122
123
|
"experiment_id": experiment_id,
|
|
123
124
|
"rubrics": rubrics,
|
|
124
125
|
"tags": tags,
|
|
125
|
-
"session_id": session_id
|
|
126
|
+
"session_id": session_id,
|
|
127
|
+
"dataset_item_id": dataset_item_id,
|
|
128
|
+
"production_monitoring": production_monitoring
|
|
126
129
|
}
|
|
127
130
|
data = self.make_request('initsession', 'POST', request_data)
|
|
128
131
|
real_session_id = data["session_id"]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional, Dict, List, Any
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
from .client import Client
|
|
7
|
+
from .errors import APIKeyVerificationError
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("Lucidic")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_dataset(
|
|
13
|
+
dataset_id: str,
|
|
14
|
+
api_key: Optional[str] = None,
|
|
15
|
+
agent_id: Optional[str] = None,
|
|
16
|
+
) -> Dict[str, Any]:
|
|
17
|
+
"""
|
|
18
|
+
Get a dataset by ID with all its items.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
dataset_id: The ID of the dataset to retrieve (required).
|
|
22
|
+
api_key: API key for authentication. If not provided, will use the LUCIDIC_API_KEY environment variable.
|
|
23
|
+
agent_id: Agent ID. If not provided, will use the LUCIDIC_AGENT_ID environment variable.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A dictionary containing the dataset information including:
|
|
27
|
+
- dataset_id: The dataset ID
|
|
28
|
+
- name: Dataset name
|
|
29
|
+
- description: Dataset description
|
|
30
|
+
- tags: List of tags
|
|
31
|
+
- created_at: Creation timestamp
|
|
32
|
+
- updated_at: Last update timestamp
|
|
33
|
+
- num_items: Number of items in the dataset
|
|
34
|
+
- items: List of dataset items
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
APIKeyVerificationError: If API key or agent ID is missing or invalid.
|
|
38
|
+
ValueError: If dataset_id is not provided.
|
|
39
|
+
"""
|
|
40
|
+
return # no op for now
|
|
41
|
+
load_dotenv()
|
|
42
|
+
|
|
43
|
+
# Validation
|
|
44
|
+
if not dataset_id:
|
|
45
|
+
raise ValueError("Dataset ID is required")
|
|
46
|
+
|
|
47
|
+
# Get credentials
|
|
48
|
+
if api_key is None:
|
|
49
|
+
api_key = os.getenv("LUCIDIC_API_KEY", None)
|
|
50
|
+
if api_key is None:
|
|
51
|
+
raise APIKeyVerificationError(
|
|
52
|
+
"Make sure to either pass your API key into get_dataset() or set the LUCIDIC_API_KEY environment variable."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if agent_id is None:
|
|
56
|
+
agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
|
|
57
|
+
if agent_id is None:
|
|
58
|
+
raise APIKeyVerificationError(
|
|
59
|
+
"Lucidic agent ID not specified. Make sure to either pass your agent ID into get_dataset() or set the LUCIDIC_AGENT_ID environment variable."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Get current client or create a new one
|
|
63
|
+
client = Client()
|
|
64
|
+
# If not yet initialized or still the NullClient -> create a real client
|
|
65
|
+
if not getattr(client, 'initialized', False):
|
|
66
|
+
client = Client(api_key=api_key, agent_id=agent_id)
|
|
67
|
+
else:
|
|
68
|
+
# Already initialized, check if we need to update credentials
|
|
69
|
+
if api_key is not None and agent_id is not None and (api_key != client.api_key or agent_id != client.agent_id):
|
|
70
|
+
client.set_api_key(api_key)
|
|
71
|
+
client.agent_id = agent_id
|
|
72
|
+
|
|
73
|
+
# Make request to get dataset
|
|
74
|
+
response = client.make_request(
|
|
75
|
+
'getdataset',
|
|
76
|
+
'GET',
|
|
77
|
+
{'dataset_id': dataset_id}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
logger.info(f"Retrieved dataset {dataset_id} with {response.get('num_items', 0)} items")
|
|
81
|
+
return response
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_dataset_items(
|
|
85
|
+
dataset_id: str,
|
|
86
|
+
api_key: Optional[str] = None,
|
|
87
|
+
agent_id: Optional[str] = None,
|
|
88
|
+
) -> List[Dict[str, Any]]:
|
|
89
|
+
"""
|
|
90
|
+
Convenience function to get just the items from a dataset.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
dataset_id: The ID of the dataset to retrieve items from (required).
|
|
94
|
+
api_key: API key for authentication. If not provided, will use the LUCIDIC_API_KEY environment variable.
|
|
95
|
+
agent_id: Agent ID. If not provided, will use the LUCIDIC_AGENT_ID environment variable.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
A list of dataset items, where each item contains:
|
|
99
|
+
- dataset_item_id: The item ID
|
|
100
|
+
- name: Item name
|
|
101
|
+
- description: Item description
|
|
102
|
+
- tags: List of tags
|
|
103
|
+
- input: Input data for the item
|
|
104
|
+
- expected_output: Expected output data
|
|
105
|
+
- metadata: Additional metadata
|
|
106
|
+
- created_at: Creation timestamp
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
APIKeyVerificationError: If API key or agent ID is missing or invalid.
|
|
110
|
+
ValueError: If dataset_id is not provided.
|
|
111
|
+
"""
|
|
112
|
+
return # no op for now
|
|
113
|
+
dataset = get_dataset(dataset_id, api_key, agent_id)
|
|
114
|
+
return dataset.get('items', [])
|
|
@@ -23,6 +23,12 @@ class InvalidOperationError(Exception):
|
|
|
23
23
|
super().__init__(f"An invalid Lucidic operation was attempted: {message}")
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
class FeatureFlagError(Exception):
|
|
27
|
+
"""Exception for feature flag fetch failures"""
|
|
28
|
+
def __init__(self, message: str):
|
|
29
|
+
super().__init__(f"Failed to fetch feature flag: {message}")
|
|
30
|
+
|
|
31
|
+
|
|
26
32
|
def install_error_handler():
|
|
27
33
|
"""Install global handler to create ERROR_TRACEBACK events for uncaught exceptions."""
|
|
28
34
|
from .client import Client
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from typing import Union, List, Dict, Any, Optional, overload, Tuple, Literal
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
from .client import Client
|
|
8
|
+
from .errors import APIKeyVerificationError, FeatureFlagError
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("Lucidic")
|
|
11
|
+
|
|
12
|
+
# Cache implementation
|
|
13
|
+
class FeatureFlagCache:
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self._cache: Dict[str, tuple[Any, float]] = {}
|
|
16
|
+
self._default_ttl = 300 # 5 minutes
|
|
17
|
+
|
|
18
|
+
def get(self, key: str) -> Optional[Any]:
|
|
19
|
+
if key in self._cache:
|
|
20
|
+
value, expiry = self._cache[key]
|
|
21
|
+
if time.time() < expiry:
|
|
22
|
+
return value
|
|
23
|
+
else:
|
|
24
|
+
del self._cache[key]
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def set(self, key: str, value: Any, ttl: int = None):
|
|
28
|
+
if ttl is None:
|
|
29
|
+
ttl = self._default_ttl
|
|
30
|
+
if ttl > 0:
|
|
31
|
+
self._cache[key] = (value, time.time() + ttl)
|
|
32
|
+
|
|
33
|
+
def clear(self):
|
|
34
|
+
self._cache.clear()
|
|
35
|
+
|
|
36
|
+
# Global cache instance
|
|
37
|
+
_flag_cache = FeatureFlagCache()
|
|
38
|
+
|
|
39
|
+
# Sentinel value to distinguish None from missing
|
|
40
|
+
MISSING = object()
|
|
41
|
+
|
|
42
|
+
# Function overloads for type safety
|
|
43
|
+
@overload
|
|
44
|
+
def get_feature_flag(
|
|
45
|
+
flag_name: str,
|
|
46
|
+
default: Any = ...,
|
|
47
|
+
*,
|
|
48
|
+
return_missing: Literal[False] = False,
|
|
49
|
+
cache_ttl: Optional[int] = 300,
|
|
50
|
+
api_key: Optional[str] = None,
|
|
51
|
+
agent_id: Optional[str] = None,
|
|
52
|
+
) -> Any:
|
|
53
|
+
"""Get a single feature flag."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
@overload
|
|
57
|
+
def get_feature_flag(
|
|
58
|
+
flag_name: str,
|
|
59
|
+
default: Any = ...,
|
|
60
|
+
*,
|
|
61
|
+
return_missing: Literal[True],
|
|
62
|
+
cache_ttl: Optional[int] = 300,
|
|
63
|
+
api_key: Optional[str] = None,
|
|
64
|
+
agent_id: Optional[str] = None,
|
|
65
|
+
) -> Tuple[Any, List[str]]:
|
|
66
|
+
"""Get a single feature flag with missing info."""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
@overload
|
|
70
|
+
def get_feature_flag(
|
|
71
|
+
flag_name: List[str],
|
|
72
|
+
defaults: Optional[Dict[str, Any]] = None,
|
|
73
|
+
*,
|
|
74
|
+
return_missing: Literal[False] = False,
|
|
75
|
+
cache_ttl: Optional[int] = 300,
|
|
76
|
+
api_key: Optional[str] = None,
|
|
77
|
+
agent_id: Optional[str] = None,
|
|
78
|
+
) -> Dict[str, Any]:
|
|
79
|
+
"""Get multiple feature flags."""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
@overload
|
|
83
|
+
def get_feature_flag(
|
|
84
|
+
flag_name: List[str],
|
|
85
|
+
defaults: Optional[Dict[str, Any]] = None,
|
|
86
|
+
*,
|
|
87
|
+
return_missing: Literal[True],
|
|
88
|
+
cache_ttl: Optional[int] = 300,
|
|
89
|
+
api_key: Optional[str] = None,
|
|
90
|
+
agent_id: Optional[str] = None,
|
|
91
|
+
) -> Tuple[Dict[str, Any], List[str]]:
|
|
92
|
+
"""Get multiple feature flags with missing info."""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
def get_feature_flag(
|
|
96
|
+
flag_name: Union[str, List[str]],
|
|
97
|
+
default_or_defaults: Any = MISSING,
|
|
98
|
+
*,
|
|
99
|
+
return_missing: bool = False,
|
|
100
|
+
cache_ttl: Optional[int] = 300,
|
|
101
|
+
api_key: Optional[str] = None,
|
|
102
|
+
agent_id: Optional[str] = None,
|
|
103
|
+
) -> Union[Any, Tuple[Any, List[str]], Dict[str, Any], Tuple[Dict[str, Any], List[str]]]:
|
|
104
|
+
"""
|
|
105
|
+
Get feature flag(s) from backend. Raises FeatureFlagError on failure unless default provided.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
flag_name: Single flag name (str) or list of flag names
|
|
109
|
+
default_or_defaults:
|
|
110
|
+
- If flag_name is str: default value for that flag (optional)
|
|
111
|
+
- If flag_name is List[str]: dict of defaults {flag_name: default_value}
|
|
112
|
+
cache_ttl: Cache time-to-live in seconds (0 to disable, -1 for forever)
|
|
113
|
+
api_key: Optional API key
|
|
114
|
+
agent_id: Optional agent ID
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
- If flag_name is str: The flag value (or tuple with missing list if return_missing=True)
|
|
118
|
+
- If flag_name is List[str]: Dict mapping flag_name -> value (or tuple with missing list if return_missing=True)
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
FeatureFlagError: If fetch fails and no default provided
|
|
122
|
+
APIKeyVerificationError: If credentials missing
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
# Single flag with default
|
|
126
|
+
retries = lai.get_feature_flag("max_retries", default=3)
|
|
127
|
+
|
|
128
|
+
# Single flag without default (can raise)
|
|
129
|
+
retries = lai.get_feature_flag("max_retries")
|
|
130
|
+
|
|
131
|
+
# Multiple flags
|
|
132
|
+
flags = lai.get_feature_flag(
|
|
133
|
+
["max_retries", "timeout"],
|
|
134
|
+
defaults={"max_retries": 3}
|
|
135
|
+
)
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
return # no op for now
|
|
139
|
+
|
|
140
|
+
load_dotenv()
|
|
141
|
+
|
|
142
|
+
# Determine if single or batch
|
|
143
|
+
is_single = isinstance(flag_name, str)
|
|
144
|
+
flag_names = [flag_name] if is_single else flag_name
|
|
145
|
+
|
|
146
|
+
# Parse defaults
|
|
147
|
+
if is_single:
|
|
148
|
+
has_default = default_or_defaults is not MISSING
|
|
149
|
+
defaults = {flag_name: default_or_defaults} if has_default else {}
|
|
150
|
+
else:
|
|
151
|
+
defaults = default_or_defaults if default_or_defaults not in (None, MISSING) else {}
|
|
152
|
+
|
|
153
|
+
# Track missing flags
|
|
154
|
+
missing_flags = []
|
|
155
|
+
|
|
156
|
+
# Check cache first
|
|
157
|
+
uncached_flags = []
|
|
158
|
+
cached_results = {}
|
|
159
|
+
|
|
160
|
+
if cache_ttl != 0:
|
|
161
|
+
for name in flag_names:
|
|
162
|
+
cache_key = f"{agent_id}:{name}"
|
|
163
|
+
cached_value = _flag_cache.get(cache_key)
|
|
164
|
+
if cached_value is not None:
|
|
165
|
+
cached_results[name] = cached_value
|
|
166
|
+
else:
|
|
167
|
+
uncached_flags.append(name)
|
|
168
|
+
else:
|
|
169
|
+
uncached_flags = flag_names
|
|
170
|
+
|
|
171
|
+
# Fetch uncached flags if needed
|
|
172
|
+
if uncached_flags:
|
|
173
|
+
# Get credentials
|
|
174
|
+
if api_key is None:
|
|
175
|
+
api_key = os.getenv("LUCIDIC_API_KEY", None)
|
|
176
|
+
if api_key is None:
|
|
177
|
+
raise APIKeyVerificationError(
|
|
178
|
+
"Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if agent_id is None:
|
|
182
|
+
agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
|
|
183
|
+
if agent_id is None:
|
|
184
|
+
raise APIKeyVerificationError(
|
|
185
|
+
"Lucidic agent ID not specified. Make sure to either pass your agent ID or set the LUCIDIC_AGENT_ID environment variable."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Get client
|
|
189
|
+
client = Client()
|
|
190
|
+
if not getattr(client, 'initialized', False):
|
|
191
|
+
client = Client(api_key=api_key, agent_id=agent_id)
|
|
192
|
+
else:
|
|
193
|
+
if api_key != client.api_key or agent_id != client.agent_id:
|
|
194
|
+
client.set_api_key(api_key)
|
|
195
|
+
client.agent_id = agent_id
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
# Make batch API call
|
|
199
|
+
response = client.make_request(
|
|
200
|
+
'getfeatureflags',
|
|
201
|
+
'POST',
|
|
202
|
+
{'flag_names': uncached_flags}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Process response and update cache
|
|
206
|
+
for name in uncached_flags:
|
|
207
|
+
if name in response['flags']:
|
|
208
|
+
if response['flags'][name]['found']:
|
|
209
|
+
value = response['flags'][name]['value']
|
|
210
|
+
cached_results[name] = value
|
|
211
|
+
|
|
212
|
+
# Cache the value
|
|
213
|
+
if cache_ttl != 0:
|
|
214
|
+
cache_key = f"{agent_id}:{name}"
|
|
215
|
+
_flag_cache.set(cache_key, value, ttl=cache_ttl if cache_ttl > 0 else None)
|
|
216
|
+
else:
|
|
217
|
+
# Flag not found on server
|
|
218
|
+
missing_flags.append(name)
|
|
219
|
+
logger.warning(f"Feature flag '{name}' not found on server")
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
# Log the error
|
|
223
|
+
logger.error(f"Failed to fetch feature flags: {e}")
|
|
224
|
+
|
|
225
|
+
# Check if we have defaults for missing flags
|
|
226
|
+
for name in uncached_flags:
|
|
227
|
+
if name not in cached_results:
|
|
228
|
+
if name in defaults:
|
|
229
|
+
cached_results[name] = defaults[name]
|
|
230
|
+
elif is_single and not return_missing:
|
|
231
|
+
# Single flag without default and not returning missing - raise error
|
|
232
|
+
raise FeatureFlagError(f"'{name}': {e}") from e
|
|
233
|
+
|
|
234
|
+
# Build final result
|
|
235
|
+
result = {}
|
|
236
|
+
for name in flag_names:
|
|
237
|
+
if name in cached_results:
|
|
238
|
+
result[name] = cached_results[name]
|
|
239
|
+
elif name in defaults:
|
|
240
|
+
result[name] = defaults[name]
|
|
241
|
+
else:
|
|
242
|
+
# No value and no default
|
|
243
|
+
missing_flags.append(name)
|
|
244
|
+
if is_single and not return_missing:
|
|
245
|
+
raise FeatureFlagError(f"'{name}' not found and no default provided")
|
|
246
|
+
else:
|
|
247
|
+
result[name] = None
|
|
248
|
+
|
|
249
|
+
# Return based on input type and return_missing flag
|
|
250
|
+
if return_missing:
|
|
251
|
+
return (result[flag_names[0]] if is_single else result, missing_flags)
|
|
252
|
+
else:
|
|
253
|
+
return result[flag_names[0]] if is_single else result
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# Typed convenience functions
|
|
257
|
+
def get_bool_flag(flag_name: str, default: Optional[bool] = None, **kwargs) -> bool:
|
|
258
|
+
"""
|
|
259
|
+
Get a boolean feature flag with type validation.
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
FeatureFlagError: If fetch fails and no default provided
|
|
263
|
+
TypeError: If flag value is not a boolean
|
|
264
|
+
"""
|
|
265
|
+
return # no op for now
|
|
266
|
+
value = get_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
|
|
267
|
+
if not isinstance(value, bool):
|
|
268
|
+
if default is not None:
|
|
269
|
+
logger.warning(f"Feature flag '{flag_name}' is not a boolean, using default")
|
|
270
|
+
return default
|
|
271
|
+
raise TypeError(f"Feature flag '{flag_name}' expected boolean, got {type(value).__name__}")
|
|
272
|
+
return value
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def get_int_flag(flag_name: str, default: Optional[int] = None, **kwargs) -> int:
|
|
276
|
+
"""
|
|
277
|
+
Get an integer feature flag with type validation.
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
FeatureFlagError: If fetch fails and no default provided
|
|
281
|
+
TypeError: If flag value is not an integer
|
|
282
|
+
"""
|
|
283
|
+
return # no op for now
|
|
284
|
+
value = get_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
|
|
285
|
+
if not isinstance(value, int) or isinstance(value, bool): # bool is subclass of int
|
|
286
|
+
if default is not None:
|
|
287
|
+
logger.warning(f"Feature flag '{flag_name}' is not an integer, using default")
|
|
288
|
+
return default
|
|
289
|
+
raise TypeError(f"Feature flag '{flag_name}' expected integer, got {type(value).__name__}")
|
|
290
|
+
return value
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def get_float_flag(flag_name: str, default: Optional[float] = None, **kwargs) -> float:
|
|
294
|
+
"""
|
|
295
|
+
Get a float feature flag with type validation.
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
FeatureFlagError: If fetch fails and no default provided
|
|
299
|
+
TypeError: If flag value is not a float
|
|
300
|
+
"""
|
|
301
|
+
return # no op for now
|
|
302
|
+
value = get_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
|
|
303
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
304
|
+
if default is not None:
|
|
305
|
+
logger.warning(f"Feature flag '{flag_name}' is not a float, using default")
|
|
306
|
+
return default
|
|
307
|
+
raise TypeError(f"Feature flag '{flag_name}' expected float, got {type(value).__name__}")
|
|
308
|
+
return float(value)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def get_string_flag(flag_name: str, default: Optional[str] = None, **kwargs) -> str:
|
|
312
|
+
"""
|
|
313
|
+
Get a string feature flag with type validation.
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
FeatureFlagError: If fetch fails and no default provided
|
|
317
|
+
TypeError: If flag value is not a string
|
|
318
|
+
"""
|
|
319
|
+
return # no op for now
|
|
320
|
+
value = get_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
|
|
321
|
+
if not isinstance(value, str):
|
|
322
|
+
if default is not None:
|
|
323
|
+
logger.warning(f"Feature flag '{flag_name}' is not a string, using default")
|
|
324
|
+
return default
|
|
325
|
+
raise TypeError(f"Feature flag '{flag_name}' expected string, got {type(value).__name__}")
|
|
326
|
+
return value
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def get_json_flag(flag_name: str, default: Optional[dict] = None, **kwargs) -> dict:
|
|
330
|
+
"""
|
|
331
|
+
Get a JSON object feature flag.
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
FeatureFlagError: If fetch fails and no default provided
|
|
335
|
+
"""
|
|
336
|
+
return # no op for now
|
|
337
|
+
value = get_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
|
|
338
|
+
return value
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def clear_feature_flag_cache():
|
|
342
|
+
"""Clear the feature flag cache."""
|
|
343
|
+
_flag_cache.clear()
|
|
344
|
+
logger.debug("Feature flag cache cleared")
|
|
@@ -4,10 +4,12 @@ lucidicai/__init__.py
|
|
|
4
4
|
lucidicai/client.py
|
|
5
5
|
lucidicai/constants.py
|
|
6
6
|
lucidicai/context.py
|
|
7
|
+
lucidicai/dataset.py
|
|
7
8
|
lucidicai/decorators.py
|
|
8
9
|
lucidicai/errors.py
|
|
9
10
|
lucidicai/event.py
|
|
10
11
|
lucidicai/event_queue.py
|
|
12
|
+
lucidicai/feature_flag.py
|
|
11
13
|
lucidicai/image_upload.py
|
|
12
14
|
lucidicai/lru.py
|
|
13
15
|
lucidicai/model_pricing.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lucidicai-2.0.1 → lucidicai-2.0.2}/lucidicai/telemetry/utils/universal_image_interceptor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|