Skip to content

Commit

Permalink
13009290: Create RankProcessor class to Azure Personalizer client lib…
Browse files Browse the repository at this point in the history
…rary for .NET for multi slot (Azure#7)

* 13009290: Create RankProcessor class to Azure Personalizer client
              library for .NET for multi slot

* Address comments
  • Loading branch information
johnhuang01 committed Feb 1, 2022
1 parent 062871e commit 7161b99
Show file tree
Hide file tree
Showing 12 changed files with 1,488 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage("Usage", "AZC0016:Invalid ServiceVersion member name.", Justification = "Generated code: https://github.com/Azure/autorest.csharp/issues/1524", Scope = "type", Target = "~T:Azure.AI.Personalizer.PersonalizerClientOptions.ServiceVersion")]
[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "<Pending>", Scope = "member", Target = "~P:Azure.AI.Personalizer.DecisionContextDocument.SlotJson")]
[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "<Pending>", Scope = "member", Target = "~P:Azure.AI.Personalizer.PersonalizerSlotOptions.Features")]
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,30 @@ public DecisionContext(IEnumerable<object> contextFeatures, List<PersonalizerRan
{
List<string> actionFeatures = action.Features.Select(f => JsonSerializer.Serialize(f)).ToList();
return new DecisionContextDocument(action.Id, actionFeatures);
return new DecisionContextDocument(action.Id, actionFeatures, null, null);
}).ToArray();
}

/// <summary> Initializes a new instance of DecisionContext. </summary>
/// <param name="rankRequest"> Personalizer multi-slot rank options </param>
/// <param name="slotIdToFeatures"> A map from slot id to its features </param>
public DecisionContext(PersonalizerRankMultiSlotOptions rankRequest, Dictionary<string, IList<object>> slotIdToFeatures)
{
this.ContextFeatures = rankRequest.ContextFeatures.Select(f => JsonSerializer.Serialize(f)).ToList();

this.Documents = rankRequest.Actions
.Select(action =>
{
List<string> actionFeatures = action.Features.Select(f => JsonSerializer.Serialize(f)).ToList();
return new DecisionContextDocument(action.Id, actionFeatures, null, null);
}).ToArray();
this.Slots = rankRequest.Slots?
.Select(
slot => new DecisionContextDocument(null, null, slot.Id, serializeFeatures(slotIdToFeatures[slot.Id]))
).ToArray();
}

/// <summary> Properties from url </summary>
[JsonPropertyName("FromUrl")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
Expand All @@ -45,5 +65,16 @@ public DecisionContext(IEnumerable<object> contextFeatures, List<PersonalizerRan
[JsonPropertyName("_slots")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public DecisionContextDocument[] Slots { get; set; }

private static List<string> serializeFeatures(IList<object> features)
{
List<string> result = new List<string>();
foreach (object feature in features)
{
result.Add(JsonSerializer.Serialize(feature));
}

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,41 @@ public class DecisionContextDocument
{
/// <summary> Initializes a new instance of DecisionContextDocument. </summary>
/// <param name="id"> Id of the decision context document </param>
/// <param name="Json"> The json features </param>
public DecisionContextDocument(string id, List<string> Json)
/// <param name="json"> The json features </param>
/// <param name="slotId"> The slot Id </param>
/// <param name="slotJson"> The slot json features </param>
public DecisionContextDocument(string id, List<string> json, string slotId, List<string> slotJson)
{
ID = id;
JSON = Json;
JSON = json;
SlotId = slotId;
SlotJson = slotJson;
}
/// <summary>
/// Supply _tag for online evaluation
/// </summary>
[JsonPropertyName("_tag")]

/// <summary>
/// Supply _tag for online evaluation
/// </summary>
[JsonPropertyName("_tag")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string ID
{
get;
set;
get { return this?.Marginal?.ID; }
set
{
this.Marginal = value == null ? null : new DecisionContextDocumentId
{
ID = value
};
}
}

/// <summary>
/// Provide feature for marginal feature based on document id.
/// </summary>
[JsonPropertyName("i")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public DecisionContextDocumentId Marginal { get; set; }

/// <summary>
/// Provide source set feature.
/// </summary>
Expand Down Expand Up @@ -63,6 +81,6 @@ public string ID
[JsonPropertyName("sj")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(JsonRawStringListConverter))]
public List<string> SlotJson { get; }
public List<string> SlotJson { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.AI.Personalizer
{
/// <summary> The Decision Context Document. </summary>
public class DecisionContextDocumentId
{
/// <summary>
/// Required for --marginal
/// </summary>
[JsonPropertyName("constant")]
public int Constant { get; set; } = 1;

/// <summary>
/// Included for offline analysis.
/// </summary>
[JsonPropertyName("id")]
public string ID { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,14 @@ public virtual async Task<Response<PersonalizerMultiSlotRankResult>> RankMultiSl
scope.Start();
try
{
return await MultiSlotRestClient.RankAsync(options, cancellationToken).ConfigureAwait(false);
if (_isLocalInference)
{
return _rankProcessor.Rank(options);
}
else
{
return await MultiSlotRestClient.RankAsync(options, cancellationToken).ConfigureAwait(false);
}
}
catch (Exception e)
{
Expand Down Expand Up @@ -316,7 +323,14 @@ public virtual Response<PersonalizerMultiSlotRankResult> RankMultiSlot(Personali
scope.Start();
try
{
return MultiSlotRestClient.Rank(options, cancellationToken);
if (_isLocalInference)
{
return _rankProcessor.Rank(options);
}
else
{
return MultiSlotRestClient.Rank(options, cancellationToken);
}
}
catch (Exception e)
{
Expand Down Expand Up @@ -545,8 +559,8 @@ internal Configuration GetConfigurationForLiveModel(string authType, string auth
//ToDo: TASK 13057958 Working on changes to support token authentication in RLClient
//config["http.token.key"] = authValue;
}
config["interaction.http.api.host"] = stringEndpoint+"personalizer/v1.1-preview.1/logs/interactions";
config["observation.http.api.host"] = stringEndpoint+"personalizer/v1.1-preview.1/logs/observations";
config["interaction.http.api.host"] = stringEndpoint + "personalizer/v1.1-preview.1/logs/interactions";
config["observation.http.api.host"] = stringEndpoint + "personalizer/v1.1-preview.1/logs/observations";
//ToDo: TASK 13057958 Working on changes to support model api in RL.Net
config["model.blob.uri"] = stringEndpoint + "personalizer/v1.1-preview.1/model";
config["vw.commandline"] = _personalizerPolicy.Arguments;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public partial class PersonalizerSlotOptions
/// List of dictionaries containing slot features.
/// Need to be JSON serializable. https://docs.microsoft.com/azure/cognitive-services/personalizer/concepts-features.
/// </summary>
public IList<object> Features { get; }
public IList<object> Features { get; set; }

/// <summary>
/// Initializes a new instance of the RankRequest class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using Rl.Net;

namespace Azure.AI.Personalizer
Expand Down Expand Up @@ -68,5 +68,37 @@ public Response<PersonalizerRankResult> Rank(PersonalizerRankOptions options)

return Response.FromValue(value, default);
}

/// <summary> Submit a Personalizer rank request. Receives a context and a list of actions. Returns which of the provided actions should be used by your application, in rewardActionId. </summary>
/// <param name="options"> A Personalizer multi-slot Rank request. </param>
public Response<PersonalizerMultiSlotRankResult> Rank(PersonalizerRankMultiSlotOptions options)
{
string eventId = options.EventId;
if (String.IsNullOrEmpty(eventId))
{
eventId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}

Dictionary<string, int> actionIdToActionIndex = RlObjectConverter.GetActionIdToIndexMapping(options.Actions);
Dictionary<string, IList<object>> slotIdToFeatures = new Dictionary<string, IList<object>>();
foreach (var slot in options.Slots)
{
slotIdToFeatures.Add(slot.Id, RlObjectConverter.GetIncludedActionsForSlot(slot, actionIdToActionIndex));
}

// Convert options to the compatible parameter for ChooseRank
DecisionContext decisionContext = new DecisionContext(options, slotIdToFeatures);
var contextJson = JsonSerializer.Serialize(decisionContext);
ActionFlags flags = options.DeferActivation == true ? ActionFlags.Deferred : ActionFlags.Default;
int[] baselineActions = RlObjectConverter.ExtractBaselineActionsFromRankRequest(options);

// Call ChooseRank of local RL.Net
MultiSlotResponseDetailed multiSlotResponse = _liveModel.RequestMultiSlotDecisionDetailed(eventId, contextJson, flags, baselineActions);

// Convert response to PersonalizerRankResult
var value = RlObjectConverter.GenerateMultiSlotRankResponse(options.Actions, multiSlotResponse, eventId);

return Response.FromValue(value, default);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Rl.Net;
using System.Collections.Generic;
using System.Linq;
using Azure.Core;

namespace Azure.AI.Personalizer
{
Expand Down Expand Up @@ -87,5 +88,54 @@ public static string ConvertToContextJson(IEnumerable<object> contextFeatures, L

return personalizerRankResult;
}

public static PersonalizerMultiSlotRankResult GenerateMultiSlotRankResponse(IList<PersonalizerRankableAction> actions, MultiSlotResponseDetailed multiSlotResponse, string eventId)
{
Dictionary<long, string> actionIndexToActionId = actions
.Select((action, index) => new { action, index = (long)index })
.ToDictionary(obj => obj.index, obj => obj.action.Id);

List<PersonalizerSlotResult> slots = multiSlotResponse
.Select(slotRanking => new PersonalizerSlotResult(slotRanking.SlotId, actionIndexToActionId[slotRanking.ChosenAction]))
.ToList();

return new PersonalizerMultiSlotRankResult(slots, eventId);
}

public static int[] ExtractBaselineActionsFromRankRequest(PersonalizerRankMultiSlotOptions request)
{
Dictionary<string, int> actionIdToIndex = GetActionIdToIndexMapping(request.Actions);
return request.Slots
.Select(slot => actionIdToIndex[slot.BaselineAction]).ToArray();
}

public static Dictionary<string, int> GetActionIdToIndexMapping(IList<PersonalizerRankableAction> actions)
{
return actions
.Select((action, index) => new { action, index })
.ToDictionary(obj => obj.action.Id, obj => obj.index);
}

public static IList<object> GetIncludedActionsForSlot(PersonalizerSlotOptions slot, Dictionary<string, int> actionIdToActionIndex)
{
IList<object> res = new ChangeTrackingList<object>();
if (slot.Features != null)
{
foreach (object feature in slot.Features)
{
res.Add(feature);
}
}
if (slot.ExcludedActions != null)
{
List<int> excludeActionIndices = slot.ExcludedActions.Select(id => actionIdToActionIndex[id]).ToList();
var allActionIndices = new HashSet<int>(actionIdToActionIndex.Values);
List<int> includedActionIndices = allActionIndices.Except(excludeActionIndices).ToList();
var includedActions = (new { _inc = includedActionIndices });
res.Add(includedActions);
}

return res;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Generic;
using System.Threading.Tasks;
using Azure.Core.TestFramework;
using NUnit.Framework;

namespace Azure.AI.Personalizer.Tests
Expand All @@ -13,7 +14,7 @@ public MultiSlotTests(bool isAsync) : base(isAsync)
{
}

private static IList<PersonalizerRankableAction> actions = new List<PersonalizerRankableAction>()
public static IList<PersonalizerRankableAction> actions = new List<PersonalizerRankableAction>()
{
new PersonalizerRankableAction(
id: "NewsArticle",
Expand Down Expand Up @@ -57,13 +58,13 @@ public MultiSlotTests(bool isAsync) : base(isAsync)
excludedActions: new List<string>() { "EntertainmentArticle" }
);

private static IList<PersonalizerSlotOptions> slots = new List<PersonalizerSlotOptions>()
public static IList<PersonalizerSlotOptions> slots = new List<PersonalizerSlotOptions>()
{
slot1,
slot2
};

private static IList<object> contextFeatures = new List<object>()
public static IList<object> contextFeatures = new List<object>()
{
new { User = new { ProfileType = "AnonymousUser", LatLong = "47.6,-122.1"} },
new { Environment = new { DayOfMonth = "28", MonthOfYear = "8", Weather = "Sunny"} },
Expand All @@ -75,6 +76,18 @@ public MultiSlotTests(bool isAsync) : base(isAsync)
public async Task MultiSlotTest()
{
PersonalizerClient client = await GetPersonalizerClientAsync(isSingleSlot: false);
await MultiSlotTestInner(client);
}

[Test]
public async Task MultiSlotLocalInferenceTest()
{
PersonalizerClient client = await GetPersonalizerClientAsync(isSingleSlot: false, isLocalInference: true);
await MultiSlotTestInner(client);
}

private async Task MultiSlotTestInner(PersonalizerClient client)
{
await RankMultiSlotNullParameters(client);
await RankMultiSlotNoOptions(client);
await RankMultiSlot(client);
Expand Down

0 comments on commit 7161b99

Please sign in to comment.