|
| 1 | +--- |
| 2 | +title: Azure Function Rule concepts for Azure Communication Services |
| 3 | +titleSuffix: An Azure Communication Services concept document |
| 4 | +description: Learn about the Azure Communication Services Job Router Azure Function Rule concepts. |
| 5 | +author: rsarkar |
| 6 | +manager: bo.gao |
| 7 | +services: azure-communication-services |
| 8 | + |
| 9 | +ms.author: rsarkar |
| 10 | +ms.date: 02/23/2022 |
| 11 | +ms.topic: conceptual |
| 12 | +ms.service: azure-communication-services |
| 13 | +--- |
| 14 | + |
| 15 | +# Azure function rule concepts |
| 16 | + |
| 17 | +[!INCLUDE [Private Preview Disclaimer](../../includes/private-preview-include-section.md)] |
| 18 | + |
| 19 | +As part of customer extensibility model, Azure Communication Services Job Router supports Azure Function Rule Engine. It gives you the ability to bring your own Azure function. With Azure function Rule, you can incorporate custom and complex logic into the process of routing. |
| 20 | + |
| 21 | +A couple of examples are given below to showcase the flexibility that Azure Function Rule provides. |
| 22 | + |
| 23 | +## Scenario: Custom scoring rule in best worker distribution mode |
| 24 | + |
| 25 | +We want to distribute offers among their workers associated with a queue. The workers will be given a score based on their labels and skill set. The worker with the highest score should get the first offer (_BestWorker Distribution Mode_). |
| 26 | + |
| 27 | +:::image type="content" source="../media/router/best-worker-distribution-mode-problem-statement.png" alt-text="Diagram showing Best Worker Distribution Mode problem statement" lightbox="../media/router/best-worker-distribution-mode-problem-statement.png"::: |
| 28 | + |
| 29 | +### Situation |
| 30 | + |
| 31 | +- A job has been created and classified. |
| 32 | + - Job has the following **labels** associated with it |
| 33 | + - ["CommunicationType"] = "Chat" |
| 34 | + - ["IssueType"] = "XboxSupport" |
| 35 | + - ["Language"] = "en" |
| 36 | + - ["HighPriority"] = true |
| 37 | + - ["SubIssueType"] = "ConsoleMalfunction" |
| 38 | + - ["ConsoleType"] = "XBOX_SERIES_X" |
| 39 | + - ["Model"] = "XBOX_SERIES_X_1TB" |
| 40 | + - Job has the following **WorkerSelectors** associated with it |
| 41 | + - ["English"] >= 7 |
| 42 | + - ["ChatSupport"] = true |
| 43 | + - ["XboxSupport"] = true |
| 44 | +- Job currently is in a state of '**Queued**'; enqueued in *Xbox Hardware Support Queue* waiting to be matched to a worker. |
| 45 | +- Multiple workers become available simultaneously. |
| 46 | + - **Worker 1** has been created with the following **labels** |
| 47 | + - ["HighPrioritySupport"] = true |
| 48 | + - ["HardwareSupport"] = true |
| 49 | + - ["Support_XBOX_SERIES_X"] = true |
| 50 | + - ["English"] = 10 |
| 51 | + - ["ChatSupport"] = true |
| 52 | + - ["XboxSupport"] = true |
| 53 | + - **Worker 2** has been created with the following **labels** |
| 54 | + - ["HighPrioritySupport"] = true |
| 55 | + - ["HardwareSupport"] = true |
| 56 | + - ["Support_XBOX_SERIES_X"] = true |
| 57 | + - ["Support_XBOX_SERIES_S"] = true |
| 58 | + - ["English"] = 8 |
| 59 | + - ["ChatSupport"] = true |
| 60 | + - ["XboxSupport"] = true |
| 61 | + - **Worker 3** has been created with the following **labels** |
| 62 | + - ["HighPrioritySupport"] = false |
| 63 | + - ["HardwareSupport"] = true |
| 64 | + - ["Support_XBOX"] = true |
| 65 | + - ["English"] = 7 |
| 66 | + - ["ChatSupport"] = true |
| 67 | + - ["XboxSupport"] = true |
| 68 | + |
| 69 | +### Expectation |
| 70 | + |
| 71 | +We would like the following behavior when scoring workers to select which worker gets the first offer. |
| 72 | + |
| 73 | +:::image type="content" source="../media/router/best-worker-distribution-mode-scoring-rule.png" alt-text="Decision flow diagram for scoring worker" lightbox="../media/router/best-worker-distribution-mode-scoring-rule.png"::: |
| 74 | + |
| 75 | +The decision flow (as shown above) is as follows: |
| 76 | + |
| 77 | +- If a job is **NOT HighPriority**: |
| 78 | + - Workers with label: **["Support_XBOX"] = true**; get a score of *100* |
| 79 | + - Otherwise, get a score of *1* |
| 80 | + |
| 81 | +- If a job is **HighPriority**: |
| 82 | + - Workers with label: **["HighPrioritySupport"] = false**; get a score of *1* |
| 83 | + - Otherwise, if **["HighPrioritySupport"] = true**: |
| 84 | + - Does Worker specialize in console type -> Does worker have label: **["Support_<**jobLabels.ConsoleType**>"] = true**? If true, worker gets score of *200* |
| 85 | + - Otherwise, get a score of *100* |
| 86 | + |
| 87 | +### Creating an Azure function |
| 88 | + |
| 89 | +Before moving on any further in the process, let us first define an Azure function that scores worker. |
| 90 | +> [!NOTE] |
| 91 | +> The following Azure function is using Javascript. For more information, please refer to [Quickstart: Create a JavaScript function in Azure using Visual Studio Code](../../../azure-functions/create-first-function-vs-code-node.md) |
| 92 | +
|
| 93 | +Sample input for **Worker 1** |
| 94 | + |
| 95 | +```json |
| 96 | +{ |
| 97 | + "job": { |
| 98 | + "CommunicationType": "Chat", |
| 99 | + "IssueType": "XboxSupport", |
| 100 | + "Language": "en", |
| 101 | + "HighPriority": true, |
| 102 | + "SubIssueType": "ConsoleMalfunction", |
| 103 | + "ConsoleType": "XBOX_SERIES_X", |
| 104 | + "Model": "XBOX_SERIES_X_1TB" |
| 105 | + }, |
| 106 | + "selectors": [ |
| 107 | + { |
| 108 | + "key": "English", |
| 109 | + "operator": "GreaterThanEqual", |
| 110 | + "value": 7, |
| 111 | + "ttl": null |
| 112 | + }, |
| 113 | + { |
| 114 | + "key": "ChatSupport", |
| 115 | + "operator": "Equal", |
| 116 | + "value": true, |
| 117 | + "ttl": null |
| 118 | + }, |
| 119 | + { |
| 120 | + "key": "XboxSupport", |
| 121 | + "operator": "Equal", |
| 122 | + "value": true, |
| 123 | + "ttl": null |
| 124 | + } |
| 125 | + ], |
| 126 | + "worker": { |
| 127 | + "Id": "e3a3f2f9-3582-4bfe-9c5a-aa57831a0f88", |
| 128 | + "HighPrioritySupport": true, |
| 129 | + "HardwareSupport": true, |
| 130 | + "Support_XBOX_SERIES_X": true, |
| 131 | + "English": 10, |
| 132 | + "ChatSupport": true, |
| 133 | + "XboxSupport": true |
| 134 | + } |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +Sample implementation: |
| 139 | + |
| 140 | +```javascript |
| 141 | +module.exports = async function (context, req) { |
| 142 | + context.log('Best Worker Distribution Mode using Azure Function'); |
| 143 | + |
| 144 | + let score = 0; |
| 145 | + const jobLabels = req.body.job; |
| 146 | + const workerLabels = req.body.worker; |
| 147 | + |
| 148 | + const isHighPriority = !!jobLabels["HighPriority"]; |
| 149 | + context.log('Job is high priority? Status: ' + isHighPriority); |
| 150 | + |
| 151 | + if(!isHighPriority) { |
| 152 | + const isGenericXboxSupportWorker = !!workerLabels["Support_XBOX"]; |
| 153 | + context.log('Worker provides general xbox support? Status: ' + isGenericXboxSupportWorker); |
| 154 | + |
| 155 | + score = isGenericXboxSupportWorker ? 100 : 1; |
| 156 | + |
| 157 | + } else { |
| 158 | + const workerSupportsHighPriorityJob = !!workerLabels["HighPrioritySupport"]; |
| 159 | + context.log('Worker provides high priority support? Status: ' + workerSupportsHighPriorityJob); |
| 160 | + |
| 161 | + if(!workerSupportsHighPriorityJob) { |
| 162 | + score = 1; |
| 163 | + } else { |
| 164 | + const key = `Support_${jobLabels["ConsoleType"]}`; |
| 165 | + |
| 166 | + const workerSpecializeInConsoleType = !!workerLabels[key]; |
| 167 | + context.log(`Worker specializes in consoleType: ${jobLabels["ConsoleType"]} ? Status: ${workerSpecializeInConsoleType}`); |
| 168 | + |
| 169 | + score = workerSpecializeInConsoleType ? 200 : 100; |
| 170 | + } |
| 171 | + } |
| 172 | + context.log('Final score of worker: ' + score); |
| 173 | + |
| 174 | + context.res = { |
| 175 | + // status: 200, /* Defaults to 200 */ |
| 176 | + body: score |
| 177 | + }; |
| 178 | +} |
| 179 | +``` |
| 180 | + |
| 181 | +Output for **Worker 1** |
| 182 | + |
| 183 | +```markdown |
| 184 | +200 |
| 185 | +``` |
| 186 | + |
| 187 | +With the aforementioned implementation, for the given job we'll get the following scores for workers: |
| 188 | + |
| 189 | +| Worker | Score | |
| 190 | +|--------|-------| |
| 191 | +| Worker 1 | 200 | |
| 192 | +| Worker 2 | 200 | |
| 193 | +| Worker 3 | 1 | |
| 194 | + |
| 195 | +### Distribute offers based on best worker mode |
| 196 | + |
| 197 | +Now that the Azure function app is ready, let us create an instance of **BestWorkerDistribution** mode using Router SDK. |
| 198 | + |
| 199 | +```csharp |
| 200 | + // ----- initialize router client |
| 201 | + // Setup Distribution Policy |
| 202 | + var bestWorkerDistributionMode = new BestWorkerMode( |
| 203 | + scoringRule: new AzureFunctionRule( |
| 204 | + functionAppUrl: "<insert function url>"); |
| 205 | + |
| 206 | + var distributionPolicy = await client.SetDistributionPolicyAsync( |
| 207 | + id: "BestWorkerDistributionMode", |
| 208 | + mode: bestWorkerDistributionMode, |
| 209 | + name: "XBox hardware support distribution", |
| 210 | + offerTTL: TimeSpan.FromMinutes(5)); |
| 211 | + |
| 212 | + // Setup Queue |
| 213 | + var queue = await client.SetQueueAsync( |
| 214 | + id: "XBox_Hardware_Support_Q", |
| 215 | + distributionPolicyId: distributionPolicy.Value.Id, |
| 216 | + name: "XBox Hardware Support Queue"); |
| 217 | + |
| 218 | + // Setup Channel |
| 219 | + var channel = await client.SetChannelAsync("Xbox_Chat_Channel"); |
| 220 | + |
| 221 | + // Create workers |
| 222 | +
|
| 223 | + var worker1Labels = new LabelCollection() |
| 224 | + { |
| 225 | + ["HighPrioritySupport"] = true, |
| 226 | + ["HardwareSupport"] = true, |
| 227 | + ["Support_XBOX_SERIES_X"] = true, |
| 228 | + ["English"] = 10, |
| 229 | + ["ChatSupport"] = true, |
| 230 | + ["XboxSupport"] = true |
| 231 | + }; |
| 232 | + var worker1 = await client.RegisterWorkerAsync( |
| 233 | + id: "Worker_1", |
| 234 | + totalCapacity: 100, |
| 235 | + queueIds: new[] {queue.Value.Id}, |
| 236 | + labels: worker1Labels, |
| 237 | + channelConfigurations: new[] {new ChannelConfiguration(channel.Value.Id, 10)}); |
| 238 | + |
| 239 | + var worker2Labels = new LabelCollection() |
| 240 | + { |
| 241 | + ["HighPrioritySupport"] = true, |
| 242 | + ["HardwareSupport"] = true, |
| 243 | + ["Support_XBOX_SERIES_X"] = true, |
| 244 | + ["Support_XBOX_SERIES_S"] = true, |
| 245 | + ["English"] = 8, |
| 246 | + ["ChatSupport"] = true, |
| 247 | + ["XboxSupport"] = true |
| 248 | + }; |
| 249 | + var worker2 = await client.RegisterWorkerAsync( |
| 250 | + id: "Worker_2", |
| 251 | + totalCapacity: 100, |
| 252 | + queueIds: new[] { queue.Value.Id }, |
| 253 | + labels: worker2Labels, |
| 254 | + channelConfigurations: new[] { new ChannelConfiguration(channel.Value.Id, 10) }); |
| 255 | + |
| 256 | + var worker3Labels = new LabelCollection() |
| 257 | + { |
| 258 | + ["HighPrioritySupport"] = false, |
| 259 | + ["HardwareSupport"] = true, |
| 260 | + ["Support_XBOX"] = true, |
| 261 | + ["English"] = 7, |
| 262 | + ["ChatSupport"] = true, |
| 263 | + ["XboxSupport"] = true |
| 264 | + }; |
| 265 | + var worker3 = await client.RegisterWorkerAsync( |
| 266 | + id: "Worker_3", |
| 267 | + totalCapacity: 100, |
| 268 | + queueIds: new[] { queue.Value.Id }, |
| 269 | + labels: worker3Labels, |
| 270 | + channelConfigurations: new[] { new ChannelConfiguration(channel.Value.Id, 10) }); |
| 271 | + |
| 272 | + // Create Job |
| 273 | + var jobLabels = new LabelCollection() |
| 274 | + { |
| 275 | + ["CommunicationType"] = "Chat", |
| 276 | + ["IssueType"] = "XboxSupport", |
| 277 | + ["Language"] = "en", |
| 278 | + ["HighPriority"] = true, |
| 279 | + ["SubIssueType"] = "ConsoleMalfunction", |
| 280 | + ["ConsoleType"] = "XBOX_SERIES_X", |
| 281 | + ["Model"] = "XBOX_SERIES_X_1TB" |
| 282 | + }; |
| 283 | + var workerSelectors = new List<LabelSelector>() |
| 284 | + { |
| 285 | + new LabelSelector("English", LabelOperator.GreaterThanEqual, 7), |
| 286 | + new LabelSelector("ChatSupport", LabelOperator.Equal, true), |
| 287 | + new LabelSelector("XboxSupport", LabelOperator.Equal, true) |
| 288 | + }; |
| 289 | + var job = await client.CreateJobAsync( |
| 290 | + channelId: channel.Value.Id, |
| 291 | + queueId: queue.Value.Id, |
| 292 | + priority: 100, |
| 293 | + channelReference: "ChatChannel", |
| 294 | + labels: jobLabels, |
| 295 | + workerSelectors: workerSelectors); |
| 296 | + |
| 297 | + var getJob = await client.GetJobAsync(job.Value.Id); |
| 298 | + Console.WriteLine(getJob.Value.Assignments.Select(assignment => assignment.Value.WorkerId).First()); |
| 299 | +``` |
| 300 | + |
| 301 | +Output |
| 302 | + |
| 303 | +```markdown |
| 304 | +Worker_1 // or Worker_2 |
| 305 | +
|
| 306 | +Since both workers, Worker_1 and Worker_2, get the same score of 200, |
| 307 | +the worker who has been idle the longest will get the first offer. |
| 308 | +``` |
| 309 | + |
| 310 | +## Next steps |
| 311 | + |
| 312 | +- [Router Rule concepts](router-rule-concepts.md) |
0 commit comments