|
11 | 11 | import cloud.eppo.logging.BanditAssignment; |
12 | 12 | import cloud.eppo.logging.BanditLogger; |
13 | 13 | import cloud.eppo.ufc.dto.*; |
14 | | -import cloud.eppo.ufc.dto.adapters.EppoModule; |
15 | 14 | import com.fasterxml.jackson.databind.JsonNode; |
16 | | -import com.fasterxml.jackson.databind.ObjectMapper; |
17 | 15 | import java.util.HashMap; |
18 | 16 | import java.util.Map; |
19 | 17 | import java.util.Timer; |
|
26 | 24 |
|
27 | 25 | public class BaseEppoClient { |
28 | 26 | private static final Logger log = LoggerFactory.getLogger(BaseEppoClient.class); |
29 | | - private final ObjectMapper mapper = |
30 | | - new ObjectMapper() |
31 | | - .registerModule(EppoModule.eppoModule()); // TODO: is this the best place for this? |
32 | 27 |
|
33 | 28 | protected final ConfigurationRequestor requestor; |
34 | 29 |
|
@@ -598,68 +593,151 @@ public BanditResult getBanditAction( |
598 | 593 | DiscriminableAttributes subjectAttributes, |
599 | 594 | Actions actions, |
600 | 595 | String defaultValue) { |
601 | | - BanditResult result = new BanditResult(defaultValue, null); |
| 596 | + try { |
| 597 | + AssignmentDetails<String> details = |
| 598 | + getBanditActionDetails(flagKey, subjectKey, subjectAttributes, actions, defaultValue); |
| 599 | + return new BanditResult(details.getVariation(), details.getAction()); |
| 600 | + } catch (Exception e) { |
| 601 | + return throwIfNotGraceful(e, new BanditResult(defaultValue, null)); |
| 602 | + } |
| 603 | + } |
| 604 | + |
| 605 | + /** |
| 606 | + * Returns bandit action assignment with detailed evaluation information including flag evaluation |
| 607 | + * details and bandit action selection. |
| 608 | + */ |
| 609 | + public AssignmentDetails<String> getBanditActionDetails( |
| 610 | + String flagKey, |
| 611 | + String subjectKey, |
| 612 | + DiscriminableAttributes subjectAttributes, |
| 613 | + Actions actions, |
| 614 | + String defaultValue) { |
602 | 615 | final Configuration config = getConfiguration(); |
603 | 616 | try { |
604 | | - String assignedVariation = |
605 | | - getStringAssignment( |
| 617 | + // Get detailed flag assignment |
| 618 | + AssignmentDetails<String> flagDetails = |
| 619 | + getStringAssignmentDetails( |
606 | 620 | flagKey, subjectKey, subjectAttributes.getAllAttributes(), defaultValue); |
607 | 621 |
|
608 | | - // Update result to reflect that we've been assigned a variation |
609 | | - result = new BanditResult(assignedVariation, null); |
| 622 | + String assignedVariation = flagDetails.getVariation(); |
| 623 | + String assignedAction = null; |
610 | 624 |
|
| 625 | + // If we got a variation, check for bandit |
611 | 626 | String banditKey = config.banditKeyForVariation(flagKey, assignedVariation); |
612 | | - if (banditKey != null && !actions.isEmpty()) { |
613 | | - BanditParameters banditParameters = config.getBanditParameters(banditKey); |
614 | | - BanditEvaluationResult banditResult = |
615 | | - BanditEvaluator.evaluateBandit( |
616 | | - flagKey, subjectKey, subjectAttributes, actions, banditParameters.getModelData()); |
617 | | - |
618 | | - // Update result to reflect that we've been assigned an action |
619 | | - result = new BanditResult(assignedVariation, banditResult.getActionKey()); |
620 | | - |
621 | | - if (banditLogger != null) { |
622 | | - try { |
623 | | - BanditAssignment banditAssignment = |
624 | | - new BanditAssignment( |
625 | | - flagKey, |
626 | | - banditKey, |
627 | | - subjectKey, |
628 | | - banditResult.getActionKey(), |
629 | | - banditResult.getActionWeight(), |
630 | | - banditResult.getOptimalityGap(), |
631 | | - banditParameters.getModelVersion(), |
632 | | - subjectAttributes.getNumericAttributes(), |
633 | | - subjectAttributes.getCategoricalAttributes(), |
634 | | - banditResult.getActionAttributes().getNumericAttributes(), |
635 | | - banditResult.getActionAttributes().getCategoricalAttributes(), |
636 | | - buildLogMetaData(config.isConfigObfuscated())); |
637 | | - |
638 | | - // Log, only if there is no cache hit. |
639 | | - boolean logBanditAssignment = true; |
640 | | - AssignmentCacheEntry cacheEntry = |
641 | | - AssignmentCacheEntry.fromBanditAssignment(banditAssignment); |
642 | | - if (banditAssignmentCache != null) { |
643 | | - if (banditAssignmentCache.hasEntry(cacheEntry)) { |
644 | | - logBanditAssignment = false; |
645 | | - } |
646 | | - } |
647 | 627 |
|
648 | | - if (logBanditAssignment) { |
649 | | - banditLogger.logBanditAssignment(banditAssignment); |
| 628 | + // If variation is a bandit but no actions supplied, return variation with null action |
| 629 | + // This matches Python/JS SDK behavior: "if no actions are given, return the variation with no |
| 630 | + // action" |
| 631 | + if (banditKey != null && actions.isEmpty()) { |
| 632 | + EvaluationDetails noActionsDetails = |
| 633 | + EvaluationDetails.builder(flagDetails.getEvaluationDetails()) |
| 634 | + .flagEvaluationCode(FlagEvaluationCode.NO_ACTIONS_SUPPLIED_FOR_BANDIT) |
| 635 | + .flagEvaluationDescription("No actions supplied for bandit evaluation") |
| 636 | + .banditKey(banditKey) |
| 637 | + .banditAction(null) |
| 638 | + .build(); |
| 639 | + return new AssignmentDetails<>(assignedVariation, null, noActionsDetails); |
| 640 | + } |
650 | 641 |
|
| 642 | + if (banditKey != null) { |
| 643 | + try { |
| 644 | + BanditParameters banditParameters = config.getBanditParameters(banditKey); |
| 645 | + if (banditParameters == null) { |
| 646 | + throw new RuntimeException("Bandit parameters not found for bandit key: " + banditKey); |
| 647 | + } |
| 648 | + BanditEvaluationResult banditResult = |
| 649 | + BanditEvaluator.evaluateBandit( |
| 650 | + flagKey, subjectKey, subjectAttributes, actions, banditParameters.getModelData()); |
| 651 | + |
| 652 | + assignedAction = banditResult.getActionKey(); |
| 653 | + |
| 654 | + // Log bandit assignment if needed |
| 655 | + if (banditLogger != null) { |
| 656 | + try { |
| 657 | + BanditAssignment banditAssignment = |
| 658 | + new BanditAssignment( |
| 659 | + flagKey, |
| 660 | + banditKey, |
| 661 | + subjectKey, |
| 662 | + banditResult.getActionKey(), |
| 663 | + banditResult.getActionWeight(), |
| 664 | + banditResult.getOptimalityGap(), |
| 665 | + banditParameters.getModelVersion(), |
| 666 | + subjectAttributes.getNumericAttributes(), |
| 667 | + subjectAttributes.getCategoricalAttributes(), |
| 668 | + banditResult.getActionAttributes().getNumericAttributes(), |
| 669 | + banditResult.getActionAttributes().getCategoricalAttributes(), |
| 670 | + buildLogMetaData(config.isConfigObfuscated())); |
| 671 | + |
| 672 | + boolean logBanditAssignment = true; |
| 673 | + AssignmentCacheEntry cacheEntry = |
| 674 | + AssignmentCacheEntry.fromBanditAssignment(banditAssignment); |
651 | 675 | if (banditAssignmentCache != null) { |
652 | | - banditAssignmentCache.put(cacheEntry); |
| 676 | + if (banditAssignmentCache.hasEntry(cacheEntry)) { |
| 677 | + logBanditAssignment = false; |
| 678 | + } |
653 | 679 | } |
| 680 | + |
| 681 | + if (logBanditAssignment) { |
| 682 | + banditLogger.logBanditAssignment(banditAssignment); |
| 683 | + if (banditAssignmentCache != null) { |
| 684 | + banditAssignmentCache.put(cacheEntry); |
| 685 | + } |
| 686 | + } |
| 687 | + } catch (Exception e) { |
| 688 | + log.warn("Error logging bandit assignment: {}", e.getMessage(), e); |
654 | 689 | } |
655 | | - } catch (Exception e) { |
656 | | - log.warn("Error logging bandit assignment: {}", e.getMessage(), e); |
657 | 690 | } |
| 691 | + |
| 692 | + // Update evaluation details to include bandit information |
| 693 | + EvaluationDetails updatedDetails = |
| 694 | + EvaluationDetails.builder(flagDetails.getEvaluationDetails()) |
| 695 | + .banditKey(banditKey) |
| 696 | + .banditAction(assignedAction) |
| 697 | + .build(); |
| 698 | + |
| 699 | + return new AssignmentDetails<>(assignedVariation, assignedAction, updatedDetails); |
| 700 | + } catch (Exception banditError) { |
| 701 | + // Bandit evaluation failed - respect graceful mode setting |
| 702 | + log.warn( |
| 703 | + "Bandit evaluation failed for flag {}: {}", |
| 704 | + flagKey, |
| 705 | + banditError.getMessage(), |
| 706 | + banditError); |
| 707 | + |
| 708 | + // If graceful mode is off, throw the exception |
| 709 | + if (!isGracefulMode) { |
| 710 | + throw new RuntimeException(banditError); |
| 711 | + } |
| 712 | + |
| 713 | + // In graceful mode, return flag details with BANDIT_ERROR code |
| 714 | + EvaluationDetails banditErrorDetails = |
| 715 | + EvaluationDetails.builder(flagDetails.getEvaluationDetails()) |
| 716 | + .flagEvaluationCode(FlagEvaluationCode.BANDIT_ERROR) |
| 717 | + .flagEvaluationDescription( |
| 718 | + "Bandit evaluation failed: " + banditError.getMessage()) |
| 719 | + .banditKey(banditKey) |
| 720 | + .banditAction(null) |
| 721 | + .build(); |
| 722 | + return new AssignmentDetails<>(assignedVariation, null, banditErrorDetails); |
658 | 723 | } |
659 | 724 | } |
660 | | - return result; |
| 725 | + |
| 726 | + // No bandit - return flag details as-is |
| 727 | + return flagDetails; |
661 | 728 | } catch (Exception e) { |
662 | | - return throwIfNotGraceful(e, result); |
| 729 | + AssignmentDetails<String> errorDetails = |
| 730 | + new AssignmentDetails<>( |
| 731 | + defaultValue, |
| 732 | + null, |
| 733 | + EvaluationDetails.buildDefault( |
| 734 | + config.getEnvironmentName(), |
| 735 | + config.getConfigFetchedAt(), |
| 736 | + config.getConfigPublishedAt(), |
| 737 | + FlagEvaluationCode.ASSIGNMENT_ERROR, |
| 738 | + e.getMessage(), |
| 739 | + EppoValue.valueOf(defaultValue))); |
| 740 | + return throwIfNotGraceful(e, errorDetails); |
663 | 741 | } |
664 | 742 | } |
665 | 743 |
|
|
0 commit comments