lucidicai 2.1.2__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lucidicai/__init__.py +32 -390
- lucidicai/api/client.py +260 -92
- lucidicai/api/resources/__init__.py +16 -1
- lucidicai/api/resources/dataset.py +422 -82
- lucidicai/api/resources/event.py +399 -27
- lucidicai/api/resources/experiment.py +108 -0
- lucidicai/api/resources/feature_flag.py +78 -0
- lucidicai/api/resources/prompt.py +84 -0
- lucidicai/api/resources/session.py +545 -38
- lucidicai/client.py +395 -480
- lucidicai/core/config.py +73 -48
- lucidicai/core/errors.py +3 -3
- lucidicai/sdk/bound_decorators.py +321 -0
- lucidicai/sdk/context.py +20 -2
- lucidicai/sdk/decorators.py +283 -74
- lucidicai/sdk/event.py +538 -36
- lucidicai/sdk/event_builder.py +2 -4
- lucidicai/sdk/features/dataset.py +408 -232
- lucidicai/sdk/features/feature_flag.py +344 -3
- lucidicai/sdk/init.py +50 -279
- lucidicai/sdk/session.py +502 -0
- lucidicai/sdk/shutdown_manager.py +103 -46
- lucidicai/session_obj.py +321 -0
- lucidicai/telemetry/context_capture_processor.py +13 -6
- lucidicai/telemetry/extract.py +60 -63
- lucidicai/telemetry/litellm_bridge.py +3 -44
- lucidicai/telemetry/lucidic_exporter.py +143 -131
- lucidicai/telemetry/openai_agents_instrumentor.py +2 -2
- lucidicai/telemetry/openai_patch.py +7 -6
- lucidicai/telemetry/telemetry_manager.py +183 -0
- lucidicai/telemetry/utils/model_pricing.py +21 -30
- lucidicai/telemetry/utils/provider.py +77 -0
- lucidicai/utils/images.py +30 -14
- lucidicai/utils/queue.py +2 -2
- lucidicai/utils/serialization.py +27 -0
- {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/METADATA +1 -1
- {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/RECORD +39 -30
- {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/WHEEL +0 -0
- {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -186,8 +186,8 @@ def get_feature_flag(
|
|
|
186
186
|
# Get HTTP client
|
|
187
187
|
http = get_http()
|
|
188
188
|
if not http:
|
|
189
|
-
from ..init import
|
|
190
|
-
|
|
189
|
+
from ..init import create_session
|
|
190
|
+
create_session(api_key=api_key, agent_id=agent_id)
|
|
191
191
|
http = get_http()
|
|
192
192
|
|
|
193
193
|
# check for active session
|
|
@@ -380,4 +380,345 @@ def get_json_flag(flag_name: str, default: Optional[dict] = None, **kwargs) -> d
|
|
|
380
380
|
def clear_feature_flag_cache():
|
|
381
381
|
"""Clear the feature flag cache."""
|
|
382
382
|
_flag_cache.clear()
|
|
383
|
-
logger.debug("Feature flag cache cleared")
|
|
383
|
+
logger.debug("Feature flag cache cleared")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ==================== Asynchronous Functions ====================
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# Async function overloads for type safety
|
|
390
|
+
@overload
|
|
391
|
+
async def aget_feature_flag(
|
|
392
|
+
flag_name: str,
|
|
393
|
+
default: Any = ...,
|
|
394
|
+
*,
|
|
395
|
+
return_missing: Literal[False] = False,
|
|
396
|
+
cache_ttl: Optional[int] = 300,
|
|
397
|
+
api_key: Optional[str] = None,
|
|
398
|
+
agent_id: Optional[str] = None,
|
|
399
|
+
) -> Any:
|
|
400
|
+
"""Get a single feature flag (asynchronous)."""
|
|
401
|
+
...
|
|
402
|
+
|
|
403
|
+
@overload
|
|
404
|
+
async def aget_feature_flag(
|
|
405
|
+
flag_name: str,
|
|
406
|
+
default: Any = ...,
|
|
407
|
+
*,
|
|
408
|
+
return_missing: Literal[True],
|
|
409
|
+
cache_ttl: Optional[int] = 300,
|
|
410
|
+
api_key: Optional[str] = None,
|
|
411
|
+
agent_id: Optional[str] = None,
|
|
412
|
+
) -> Tuple[Any, List[str]]:
|
|
413
|
+
"""Get a single feature flag with missing info (asynchronous)."""
|
|
414
|
+
...
|
|
415
|
+
|
|
416
|
+
@overload
|
|
417
|
+
async def aget_feature_flag(
|
|
418
|
+
flag_name: List[str],
|
|
419
|
+
defaults: Optional[Dict[str, Any]] = None,
|
|
420
|
+
*,
|
|
421
|
+
return_missing: Literal[False] = False,
|
|
422
|
+
cache_ttl: Optional[int] = 300,
|
|
423
|
+
api_key: Optional[str] = None,
|
|
424
|
+
agent_id: Optional[str] = None,
|
|
425
|
+
) -> Dict[str, Any]:
|
|
426
|
+
"""Get multiple feature flags (asynchronous)."""
|
|
427
|
+
...
|
|
428
|
+
|
|
429
|
+
@overload
|
|
430
|
+
async def aget_feature_flag(
|
|
431
|
+
flag_name: List[str],
|
|
432
|
+
defaults: Optional[Dict[str, Any]] = None,
|
|
433
|
+
*,
|
|
434
|
+
return_missing: Literal[True],
|
|
435
|
+
cache_ttl: Optional[int] = 300,
|
|
436
|
+
api_key: Optional[str] = None,
|
|
437
|
+
agent_id: Optional[str] = None,
|
|
438
|
+
) -> Tuple[Dict[str, Any], List[str]]:
|
|
439
|
+
"""Get multiple feature flags with missing info (asynchronous)."""
|
|
440
|
+
...
|
|
441
|
+
|
|
442
|
+
async def aget_feature_flag(
|
|
443
|
+
flag_name: Union[str, List[str]],
|
|
444
|
+
default_or_defaults: Any = MISSING,
|
|
445
|
+
*,
|
|
446
|
+
return_missing: bool = False,
|
|
447
|
+
cache_ttl: Optional[int] = 300,
|
|
448
|
+
api_key: Optional[str] = None,
|
|
449
|
+
agent_id: Optional[str] = None,
|
|
450
|
+
) -> Union[Any, Tuple[Any, List[str]], Dict[str, Any], Tuple[Dict[str, Any], List[str]]]:
|
|
451
|
+
"""
|
|
452
|
+
Get feature flag(s) from backend (asynchronous). Raises FeatureFlagError on failure unless default provided.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
flag_name: Single flag name (str) or list of flag names
|
|
456
|
+
default_or_defaults:
|
|
457
|
+
- If flag_name is str: default value for that flag (optional)
|
|
458
|
+
- If flag_name is List[str]: dict of defaults {flag_name: default_value}
|
|
459
|
+
cache_ttl: Cache time-to-live in seconds (0 to disable, -1 for forever)
|
|
460
|
+
api_key: Optional API key
|
|
461
|
+
agent_id: Optional agent ID
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
- If flag_name is str: The flag value (or tuple with missing list if return_missing=True)
|
|
465
|
+
- If flag_name is List[str]: Dict mapping flag_name -> value (or tuple with missing list if return_missing=True)
|
|
466
|
+
|
|
467
|
+
Raises:
|
|
468
|
+
FeatureFlagError: If fetch fails and no default provided
|
|
469
|
+
APIKeyVerificationError: If credentials missing
|
|
470
|
+
|
|
471
|
+
Examples:
|
|
472
|
+
# Single flag with default
|
|
473
|
+
retries = await lai.aget_feature_flag("max_retries", default=3)
|
|
474
|
+
|
|
475
|
+
# Single flag without default (can raise)
|
|
476
|
+
retries = await lai.aget_feature_flag("max_retries")
|
|
477
|
+
|
|
478
|
+
# Multiple flags
|
|
479
|
+
flags = await lai.aget_feature_flag(
|
|
480
|
+
["max_retries", "timeout"],
|
|
481
|
+
defaults={"max_retries": 3}
|
|
482
|
+
)
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
load_dotenv()
|
|
486
|
+
|
|
487
|
+
# Determine if single or batch
|
|
488
|
+
is_single = isinstance(flag_name, str)
|
|
489
|
+
flag_names = [flag_name] if is_single else flag_name
|
|
490
|
+
|
|
491
|
+
# Parse defaults
|
|
492
|
+
if is_single:
|
|
493
|
+
has_default = default_or_defaults is not MISSING
|
|
494
|
+
defaults = {flag_name: default_or_defaults} if has_default else {}
|
|
495
|
+
else:
|
|
496
|
+
defaults = default_or_defaults if default_or_defaults not in (None, MISSING) else {}
|
|
497
|
+
|
|
498
|
+
# Track missing flags
|
|
499
|
+
missing_flags = []
|
|
500
|
+
|
|
501
|
+
# Check cache first
|
|
502
|
+
uncached_flags = []
|
|
503
|
+
cached_results = {}
|
|
504
|
+
|
|
505
|
+
if cache_ttl != 0:
|
|
506
|
+
for name in flag_names:
|
|
507
|
+
cache_key = f"{agent_id}:{name}"
|
|
508
|
+
cached_value = _flag_cache.get(cache_key)
|
|
509
|
+
if cached_value is not None:
|
|
510
|
+
cached_results[name] = cached_value
|
|
511
|
+
else:
|
|
512
|
+
uncached_flags.append(name)
|
|
513
|
+
else:
|
|
514
|
+
uncached_flags = flag_names
|
|
515
|
+
|
|
516
|
+
# Fetch uncached flags if needed
|
|
517
|
+
if uncached_flags:
|
|
518
|
+
# Get credentials
|
|
519
|
+
if api_key is None:
|
|
520
|
+
api_key = os.getenv("LUCIDIC_API_KEY", None)
|
|
521
|
+
if api_key is None:
|
|
522
|
+
raise APIKeyVerificationError(
|
|
523
|
+
"Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
if agent_id is None:
|
|
527
|
+
agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
|
|
528
|
+
if agent_id is None:
|
|
529
|
+
raise APIKeyVerificationError(
|
|
530
|
+
"Lucidic agent ID not specified. Make sure to either pass your agent ID or set the LUCIDIC_AGENT_ID environment variable."
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Get HTTP client
|
|
534
|
+
http = get_http()
|
|
535
|
+
if not http:
|
|
536
|
+
from ..init import create_session
|
|
537
|
+
create_session(api_key=api_key, agent_id=agent_id)
|
|
538
|
+
http = get_http()
|
|
539
|
+
|
|
540
|
+
# check for active session
|
|
541
|
+
from ..init import get_session_id
|
|
542
|
+
session_id = get_session_id()
|
|
543
|
+
|
|
544
|
+
try:
|
|
545
|
+
if len(uncached_flags) == 1:
|
|
546
|
+
# Single flag evaluation
|
|
547
|
+
if session_id:
|
|
548
|
+
# Use session-based evaluation for consistency
|
|
549
|
+
response = await http.apost('evaluatefeatureflag', {
|
|
550
|
+
'session_id': session_id,
|
|
551
|
+
'flag_name': uncached_flags[0],
|
|
552
|
+
'context': {},
|
|
553
|
+
'default': defaults.get(uncached_flags[0])
|
|
554
|
+
})
|
|
555
|
+
else:
|
|
556
|
+
# Use stateless evaluation as fallback
|
|
557
|
+
response = await http.apost('evaluatefeatureflagstateless', {
|
|
558
|
+
'agent_id': agent_id,
|
|
559
|
+
'flag_name': uncached_flags[0],
|
|
560
|
+
'context': {},
|
|
561
|
+
'default': defaults.get(uncached_flags[0])
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
# Extract value from response
|
|
565
|
+
if 'value' in response:
|
|
566
|
+
value = response['value']
|
|
567
|
+
cached_results[uncached_flags[0]] = value
|
|
568
|
+
|
|
569
|
+
# Cache the result
|
|
570
|
+
if cache_ttl != 0:
|
|
571
|
+
cache_key = f"{agent_id}:{uncached_flags[0]}"
|
|
572
|
+
_flag_cache.set(cache_key, value, ttl=cache_ttl if cache_ttl > 0 else None)
|
|
573
|
+
elif 'error' in response:
|
|
574
|
+
# Flag not found or error
|
|
575
|
+
logger.warning(f"Feature flag error: {response['error']}")
|
|
576
|
+
missing_flags.append(uncached_flags[0])
|
|
577
|
+
|
|
578
|
+
else:
|
|
579
|
+
# Batch evaluation
|
|
580
|
+
if session_id:
|
|
581
|
+
# Use session-based batch evaluation
|
|
582
|
+
response = await http.apost('evaluatebatchfeatureflags', {
|
|
583
|
+
'session_id': session_id,
|
|
584
|
+
'flag_names': uncached_flags,
|
|
585
|
+
'context': {},
|
|
586
|
+
'defaults': {k: v for k, v in defaults.items() if k in uncached_flags}
|
|
587
|
+
})
|
|
588
|
+
else:
|
|
589
|
+
# Use stateless batch evaluation
|
|
590
|
+
response = await http.apost('evaluatebatchfeatureflagsstateless', {
|
|
591
|
+
'agent_id': agent_id,
|
|
592
|
+
'flag_names': uncached_flags,
|
|
593
|
+
'context': {},
|
|
594
|
+
'defaults': {k: v for k, v in defaults.items() if k in uncached_flags}
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
# Process batch response
|
|
598
|
+
if 'flags' in response:
|
|
599
|
+
for name in uncached_flags:
|
|
600
|
+
flag_data = response['flags'].get(name)
|
|
601
|
+
if flag_data and 'value' in flag_data:
|
|
602
|
+
value = flag_data['value']
|
|
603
|
+
cached_results[name] = value
|
|
604
|
+
|
|
605
|
+
# Cache it
|
|
606
|
+
if cache_ttl != 0:
|
|
607
|
+
cache_key = f"{agent_id}:{name}"
|
|
608
|
+
_flag_cache.set(cache_key, value, ttl=cache_ttl if cache_ttl > 0 else None)
|
|
609
|
+
else:
|
|
610
|
+
missing_flags.append(name)
|
|
611
|
+
|
|
612
|
+
except Exception as e:
|
|
613
|
+
# HTTP client raises on errors, fall back to defaults
|
|
614
|
+
logger.error(f"Failed to fetch feature flags: {e}")
|
|
615
|
+
|
|
616
|
+
# Use defaults for all uncached flags
|
|
617
|
+
for name in uncached_flags:
|
|
618
|
+
if name in defaults:
|
|
619
|
+
cached_results[name] = defaults[name]
|
|
620
|
+
else:
|
|
621
|
+
missing_flags.append(name)
|
|
622
|
+
if is_single and not return_missing:
|
|
623
|
+
raise FeatureFlagError(f"'{name}': {e}") from e
|
|
624
|
+
|
|
625
|
+
# Build final result
|
|
626
|
+
result = {}
|
|
627
|
+
for name in flag_names:
|
|
628
|
+
if name in cached_results:
|
|
629
|
+
result[name] = cached_results[name]
|
|
630
|
+
elif name in defaults:
|
|
631
|
+
result[name] = defaults[name]
|
|
632
|
+
else:
|
|
633
|
+
# No value and no default
|
|
634
|
+
missing_flags.append(name)
|
|
635
|
+
if is_single and not return_missing:
|
|
636
|
+
raise FeatureFlagError(f"'{name}' not found and no default provided")
|
|
637
|
+
else:
|
|
638
|
+
result[name] = None
|
|
639
|
+
|
|
640
|
+
# Return based on input type and return_missing flag
|
|
641
|
+
if return_missing:
|
|
642
|
+
return (result[flag_names[0]] if is_single else result, missing_flags)
|
|
643
|
+
else:
|
|
644
|
+
return result[flag_names[0]] if is_single else result
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
# Async typed convenience functions
|
|
648
|
+
async def aget_bool_flag(flag_name: str, default: Optional[bool] = None, **kwargs) -> bool:
|
|
649
|
+
"""
|
|
650
|
+
Get a boolean feature flag with type validation (asynchronous).
|
|
651
|
+
|
|
652
|
+
Raises:
|
|
653
|
+
FeatureFlagError: If fetch fails and no default provided
|
|
654
|
+
TypeError: If flag value is not a boolean
|
|
655
|
+
"""
|
|
656
|
+
value = await aget_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
|
|
657
|
+
if not isinstance(value, bool):
|
|
658
|
+
if default is not None:
|
|
659
|
+
logger.warning(f"Feature flag '{flag_name}' is not a boolean, using default")
|
|
660
|
+
return default
|
|
661
|
+
raise TypeError(f"Feature flag '{flag_name}' expected boolean, got {type(value).__name__}")
|
|
662
|
+
return value
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
async def aget_int_flag(flag_name: str, default: Optional[int] = None, **kwargs) -> int:
|
|
666
|
+
"""
|
|
667
|
+
Get an integer feature flag with type validation (asynchronous).
|
|
668
|
+
|
|
669
|
+
Raises:
|
|
670
|
+
FeatureFlagError: If fetch fails and no default provided
|
|
671
|
+
TypeError: If flag value is not an integer
|
|
672
|
+
"""
|
|
673
|
+
value = await aget_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
|
|
674
|
+
if not isinstance(value, int) or isinstance(value, bool): # bool is subclass of int
|
|
675
|
+
if default is not None:
|
|
676
|
+
logger.warning(f"Feature flag '{flag_name}' is not an integer, using default")
|
|
677
|
+
return default
|
|
678
|
+
raise TypeError(f"Feature flag '{flag_name}' expected integer, got {type(value).__name__}")
|
|
679
|
+
return value
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
async def aget_float_flag(flag_name: str, default: Optional[float] = None, **kwargs) -> float:
|
|
683
|
+
"""
|
|
684
|
+
Get a float feature flag with type validation (asynchronous).
|
|
685
|
+
|
|
686
|
+
Raises:
|
|
687
|
+
FeatureFlagError: If fetch fails and no default provided
|
|
688
|
+
TypeError: If flag value is not a float
|
|
689
|
+
"""
|
|
690
|
+
value = await aget_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
|
|
691
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
692
|
+
if default is not None:
|
|
693
|
+
logger.warning(f"Feature flag '{flag_name}' is not a float, using default")
|
|
694
|
+
return default
|
|
695
|
+
raise TypeError(f"Feature flag '{flag_name}' expected float, got {type(value).__name__}")
|
|
696
|
+
return float(value)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
async def aget_string_flag(flag_name: str, default: Optional[str] = None, **kwargs) -> str:
|
|
700
|
+
"""
|
|
701
|
+
Get a string feature flag with type validation (asynchronous).
|
|
702
|
+
|
|
703
|
+
Raises:
|
|
704
|
+
FeatureFlagError: If fetch fails and no default provided
|
|
705
|
+
TypeError: If flag value is not a string
|
|
706
|
+
"""
|
|
707
|
+
value = await aget_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
|
|
708
|
+
if not isinstance(value, str):
|
|
709
|
+
if default is not None:
|
|
710
|
+
logger.warning(f"Feature flag '{flag_name}' is not a string, using default")
|
|
711
|
+
return default
|
|
712
|
+
raise TypeError(f"Feature flag '{flag_name}' expected string, got {type(value).__name__}")
|
|
713
|
+
return value
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
async def aget_json_flag(flag_name: str, default: Optional[dict] = None, **kwargs) -> dict:
|
|
717
|
+
"""
|
|
718
|
+
Get a JSON object feature flag (asynchronous).
|
|
719
|
+
|
|
720
|
+
Raises:
|
|
721
|
+
FeatureFlagError: If fetch fails and no default provided
|
|
722
|
+
"""
|
|
723
|
+
value = await aget_feature_flag(flag_name, default=default if default is not None else MISSING, **kwargs)
|
|
724
|
+
return value
|