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.
Files changed (39) hide show
  1. lucidicai/__init__.py +32 -390
  2. lucidicai/api/client.py +260 -92
  3. lucidicai/api/resources/__init__.py +16 -1
  4. lucidicai/api/resources/dataset.py +422 -82
  5. lucidicai/api/resources/event.py +399 -27
  6. lucidicai/api/resources/experiment.py +108 -0
  7. lucidicai/api/resources/feature_flag.py +78 -0
  8. lucidicai/api/resources/prompt.py +84 -0
  9. lucidicai/api/resources/session.py +545 -38
  10. lucidicai/client.py +395 -480
  11. lucidicai/core/config.py +73 -48
  12. lucidicai/core/errors.py +3 -3
  13. lucidicai/sdk/bound_decorators.py +321 -0
  14. lucidicai/sdk/context.py +20 -2
  15. lucidicai/sdk/decorators.py +283 -74
  16. lucidicai/sdk/event.py +538 -36
  17. lucidicai/sdk/event_builder.py +2 -4
  18. lucidicai/sdk/features/dataset.py +408 -232
  19. lucidicai/sdk/features/feature_flag.py +344 -3
  20. lucidicai/sdk/init.py +50 -279
  21. lucidicai/sdk/session.py +502 -0
  22. lucidicai/sdk/shutdown_manager.py +103 -46
  23. lucidicai/session_obj.py +321 -0
  24. lucidicai/telemetry/context_capture_processor.py +13 -6
  25. lucidicai/telemetry/extract.py +60 -63
  26. lucidicai/telemetry/litellm_bridge.py +3 -44
  27. lucidicai/telemetry/lucidic_exporter.py +143 -131
  28. lucidicai/telemetry/openai_agents_instrumentor.py +2 -2
  29. lucidicai/telemetry/openai_patch.py +7 -6
  30. lucidicai/telemetry/telemetry_manager.py +183 -0
  31. lucidicai/telemetry/utils/model_pricing.py +21 -30
  32. lucidicai/telemetry/utils/provider.py +77 -0
  33. lucidicai/utils/images.py +30 -14
  34. lucidicai/utils/queue.py +2 -2
  35. lucidicai/utils/serialization.py +27 -0
  36. {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/METADATA +1 -1
  37. {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/RECORD +39 -30
  38. {lucidicai-2.1.2.dist-info → lucidicai-3.0.0.dist-info}/WHEEL +0 -0
  39. {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 init
190
- init(api_key=api_key, agent_id=agent_id)
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