Skip to content

Commit 01c0372

Browse files
committed
details methods for bandits
1 parent a8c6ead commit 01c0372

File tree

2 files changed

+348
-60
lines changed

2 files changed

+348
-60
lines changed

src/main/java/cloud/eppo/BaseEppoClient.java

Lines changed: 130 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@
1111
import cloud.eppo.logging.BanditAssignment;
1212
import cloud.eppo.logging.BanditLogger;
1313
import cloud.eppo.ufc.dto.*;
14-
import cloud.eppo.ufc.dto.adapters.EppoModule;
1514
import com.fasterxml.jackson.databind.JsonNode;
16-
import com.fasterxml.jackson.databind.ObjectMapper;
1715
import java.util.HashMap;
1816
import java.util.Map;
1917
import java.util.Timer;
@@ -26,9 +24,6 @@
2624

2725
public class BaseEppoClient {
2826
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?
3227

3328
protected final ConfigurationRequestor requestor;
3429

@@ -598,68 +593,151 @@ public BanditResult getBanditAction(
598593
DiscriminableAttributes subjectAttributes,
599594
Actions actions,
600595
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) {
602615
final Configuration config = getConfiguration();
603616
try {
604-
String assignedVariation =
605-
getStringAssignment(
617+
// Get detailed flag assignment
618+
AssignmentDetails<String> flagDetails =
619+
getStringAssignmentDetails(
606620
flagKey, subjectKey, subjectAttributes.getAllAttributes(), defaultValue);
607621

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;
610624

625+
// If we got a variation, check for bandit
611626
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-
}
647627

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+
}
650641

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);
651675
if (banditAssignmentCache != null) {
652-
banditAssignmentCache.put(cacheEntry);
676+
if (banditAssignmentCache.hasEntry(cacheEntry)) {
677+
logBanditAssignment = false;
678+
}
653679
}
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);
654689
}
655-
} catch (Exception e) {
656-
log.warn("Error logging bandit assignment: {}", e.getMessage(), e);
657690
}
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);
658723
}
659724
}
660-
return result;
725+
726+
// No bandit - return flag details as-is
727+
return flagDetails;
661728
} 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);
663741
}
664742
}
665743

0 commit comments

Comments
 (0)