This commit is contained in:
Chris McConnell 2018-07-13 19:02:53 -07:00
Родитель 980b1fbd08 f843585174
Коммит 81ecdc0514
179 изменённых файлов: 5475 добавлений и 2357 удалений

Просмотреть файл

@ -66,6 +66,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Builder.Integ
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Tests", "tests\Microsoft.Bot.Builder.Tests\Microsoft.Bot.Builder.Tests.csproj", "{35F9B8A8-1974-4795-930B-5E4980EE85E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Builder.Transcripts.Tests", "tests\Microsoft.Bot.Builder.Transcripts.Tests\Microsoft.Bot.Builder.Transcripts.Tests.csproj", "{71698D71-4C6F-40D5-8BDE-2587514CA21C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug - NuGet Packages|Any CPU = Debug - NuGet Packages|Any CPU
@ -272,6 +274,14 @@ Global
{35F9B8A8-1974-4795-930B-5E4980EE85E5}.Documentation|Any CPU.Build.0 = Debug|Any CPU
{35F9B8A8-1974-4795-930B-5E4980EE85E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35F9B8A8-1974-4795-930B-5E4980EE85E5}.Release|Any CPU.Build.0 = Release|Any CPU
{71698D71-4C6F-40D5-8BDE-2587514CA21C}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU
{71698D71-4C6F-40D5-8BDE-2587514CA21C}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU
{71698D71-4C6F-40D5-8BDE-2587514CA21C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71698D71-4C6F-40D5-8BDE-2587514CA21C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71698D71-4C6F-40D5-8BDE-2587514CA21C}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU
{71698D71-4C6F-40D5-8BDE-2587514CA21C}.Documentation|Any CPU.Build.0 = Debug|Any CPU
{71698D71-4C6F-40D5-8BDE-2587514CA21C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71698D71-4C6F-40D5-8BDE-2587514CA21C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -307,6 +317,7 @@ Global
{62C4DC83-3B07-45D7-856E-224FFB368E5F} = {0A0E26B0-7A46-4F1A-8BFE-9A763FDF6CF8}
{2614D290-1345-4A41-BE90-F85F817CEADE} = {0A0E26B0-7A46-4F1A-8BFE-9A763FDF6CF8}
{35F9B8A8-1974-4795-930B-5E4980EE85E5} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{71698D71-4C6F-40D5-8BDE-2587514CA21C} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16}

102
doc/UserAgents.md Normal file
Просмотреть файл

@ -0,0 +1,102 @@
# User Agents in Bot Framework
## Problem Being Solved
The Bot Framework team needs to know more about the shape of Bots that are running. The shape of bots includes data such as the host operating system, the middleware components that are being used, and the Cognitive Services that are integrated into the Bot.
This data is needed to effectivly prioritize features investments, as the process today is largley guesswork.
Data today around usage is implied from Nuget / NPM package downloads and GitHub activity.
## High Level Design.
This is a solved problem in the Web + Browser space. Our solution is to copy what modern browsers do for User Agent strings so that accurate data can be extracted.
User-Agents are defined in [RFC 2616](https://tools.ietf.org/html/rfc2616#section-14.43). The key text is extracted here:
> The User-Agent request-header field contains information about the
user agent originating the request. This is for statistical purposes,
the tracing of protocol violations, and automated recognition of user
agents for the sake of tailoring responses to avoid particular user
agent limitations. User agents SHOULD include this field with
requests. The field can contain multiple product tokens (section 3.8)
and comments identifying the agent and any subproducts which form a
significant part of the user agent. By convention, the product tokens
are listed in order of their significance for identifying the
application.
```
User-Agent: CERN-LineMode/2.15 libwww/2.17b3
```
Chrome User-Agent String:
```
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/537.
```
Edge User-Agent String:
```
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; ServiceUI 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134
```
Internet Explorer User-Agent String:
```
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko
```
Chrome Extensions modify the User-Agent string through code:
```js
chrome.webRequest.onBeforeSendHeaders.addListener(
function(details) {
for (var i = 0; i < details.requestHeaders.length; ++i) {
if (details.requestHeaders[i].name === 'User-Agent') {
details.requestHeaders[i].value = details.requestHeaders[i].value + ' OurUAToken/1.0';
}
break;
}
return { requestHeaders: details.requestHeaders };
});
```
## Current User Agent String
V4 BotBuilder SDK does the following:
```
"user-agent": "Microsoft-BotFramework/3.1 (BotBuilder .Net/4.0.0.0)"
```
## Proposed Bot User Agent
Add the Following:
1. SDK Version. The Bot Framework SDK Version MUST be added to the user-agent string.
2. SDK Host. The SDK Host, .Net Core, JVM, Node version, MUST be added to the user-agent string.
3. Operating System. The OS that hosts the Bot SHOULD be added to the user-agent string.
4. Azure Bot Service Version (if applicable)
5. Active Middleware Components. A RFC 2616 compatable list of Middleware SHOULD be added. The format is:
```
(Middleware1/version; Middleware2/version; Middleware3/version)
```
6. Active Storage Components. The storage providers being used SHOULD be added to the user-agent string.
```
(CosmosDBStorageProvider/version; AzureTables/version;)
```
7. LUIS. The usage of LUIS SHOULD be added to the user-agent string.
```
(LUIS/APIversion)
```
8. QnA. The usage of QnA Maker SHOULD be added to the user-agent string.
```
(QnAMaker/APIVersion)
```
9. Classic. Usage of the "Classic / Compatability" layers SHOULD be added to the user-agent string.
```
classic/version
```
A "Fully Realized" user-agent string may look like:
```
User-Agent: BotBuilder/4.0.0.0 (netcore2; abs/2.65 Windows NT 10.0; Win64; x64) Middleware(TSLogger; CatchExceptions; PII) Storage(CosmosDB) LUIS(v2.0) Qna(v3.0)
```
## Implementation
Implementation is platform specific.
For static (build-time) strings, attributes SHOULD be put on Middleware to define the names that are used by the User Agent strings, and the BotAdapter can scan for those attributes. C#, TS, JS, Java, and Python all support this.
For more dynamic strings (O/S, LUIS API Version, QnA Maker API Verion), relevant libraries will need to look for a shared list of "Active" components anb update accordingly.

Просмотреть файл

@ -0,0 +1,282 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text.RegularExpressions;
namespace Microsoft.Bot.Builder.Ai.LUIS
{
/// <summary>
/// This class represents LUIS BuiltIn entities.
/// </summary>
public static partial class BuiltIn
{
/// <summary>
/// Strongly typed LUIS builtin_datetime.
/// </summary>
public static partial class DateTime
{
public enum DayPart
{
/// <summary>
/// Represents the morning.
/// </summary>
[Description("morning")]
MO,
/// <summary>
/// Represents mid-day.
/// </summary>
[Description("midday")]
MI,
/// <summary>
/// Represents the afternoon.
/// </summary>
[Description("afternoon")]
AF,
/// <summary>
/// Represents the evening.
/// </summary>
[Description("evening")]
EV,
/// <summary>
/// Represents the night.
/// </summary>
[Description("night")]
NI,
}
public enum Reference
{
/// <summary>
/// Represents the past.
/// </summary>
[Description("past")]
PAST_REF,
/// <summary>
/// Represents the present.
/// </summary>
[Description("present")]
PRESENT_REF,
/// <summary>
/// Represents the future.
/// </summary>
[Description("future")]
FUTURE_REF,
}
[Serializable]
public sealed class DateTimeResolution : Resolution, IEquatable<DateTimeResolution>
{
public const RegexOptions Options = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.IgnorePatternWhitespace;
public const string PatternDate =
@"
(?:
(?<year>X+|\d+)
(?:
-
(?<weekM>W)?
(?<month>X+|\d+)
(?:
-
(?<weekD>W)?
(?<day>X+|\d+)
)?
)?
)
";
public const string PatternTime =
@"
(?:
T
(?:
(?<part>MO|MI|AF|EV|NI)
|
(?<hour>X+|\d+)
(?:
:
(?<minute>X+|\d+)
(?:
:
(?<second>X+|\d+)
)?
)?
)
)
";
public static readonly string Pattern = $"^({PatternDate}{PatternTime} | {PatternDate} | {PatternTime})$";
public static readonly Regex Regex = new Regex(Pattern, Options);
private static readonly IReadOnlyDictionary<string, Reference> ReferenceByText
= new Dictionary<string, Reference>(StringComparer.OrdinalIgnoreCase)
{
{ "PAST_REF", DateTime.Reference.PAST_REF },
{ "PRESENT_REF", DateTime.Reference.PRESENT_REF },
{ "FUTURE_REF", DateTime.Reference.FUTURE_REF },
};
public DateTimeResolution(
Reference? reference = null,
int? year = null,
int? month = null,
int? day = null,
int? week = null,
DayOfWeek? dayOfWeek = null,
DayPart? dayPart = null,
int? hour = null,
int? minute = null,
int? second = null)
{
this.Reference = reference;
this.Year = year;
this.Month = month;
this.Day = day;
this.Week = week;
this.DayOfWeek = dayOfWeek;
this.DayPart = dayPart;
this.Hour = hour;
this.Minute = minute;
this.Second = second;
}
public DateTimeResolution(System.DateTime dateTime)
{
this.Year = dateTime.Year;
this.Month = dateTime.Month;
this.Day = dateTime.Day;
this.Hour = dateTime.Hour;
this.Minute = dateTime.Minute;
this.Second = dateTime.Second;
}
public Reference? Reference { get; }
public int? Year { get; }
public int? Month { get; }
public int? Day { get; }
public int? Week { get; }
public DayOfWeek? DayOfWeek { get; }
public DayPart? DayPart { get; }
public int? Hour { get; }
public int? Minute { get; }
public int? Second { get; }
public static bool TryParse(string text, out DateTimeResolution resolution)
{
if (ReferenceByText.TryGetValue(text, out var reference))
{
resolution = new DateTimeResolution(reference);
return true;
}
var match = Regex.Match(text);
if (match.Success)
{
var groups = match.Groups;
var weekM = groups["weekM"].Success;
var weekD = weekM || groups["weekD"].Success;
resolution = new DateTimeResolution(
year: ParseIntOrNull(groups["year"]),
week: weekM ? ParseIntOrNull(groups["month"]) : null,
month: !weekM ? ParseIntOrNull(groups["month"]) : null,
dayOfWeek: weekD ? (DayOfWeek?)ParseIntOrNull(groups["day"]) : null,
day: !weekD ? ParseIntOrNull(groups["day"]) : null,
dayPart: ParseEnumOrNull<DayPart>(groups["part"]),
hour: ParseIntOrNull(groups["hour"]),
minute: ParseIntOrNull(groups["minute"]),
second: ParseIntOrNull(groups["second"]));
return true;
}
resolution = null;
return false;
}
public bool Equals(DateTimeResolution other) => other != null
&& this.Reference == other.Reference
&& this.Year == other.Year
&& this.Month == other.Month
&& this.Day == other.Day
&& this.Week == other.Week
&& this.DayOfWeek == other.DayOfWeek
&& this.DayPart == other.DayPart
&& this.Hour == other.Hour
&& this.Minute == other.Minute
&& this.Second == other.Second;
public override bool Equals(object other) => this.Equals(other as DateTimeResolution);
public override int GetHashCode() => throw new NotImplementedException();
}
private static int? ParseIntOrNull(Group group)
{
if (group.Success)
{
var text = group.Value;
if (int.TryParse(text, out var number))
{
return number;
}
else if (text.Length > 0)
{
for (var index = 0; index < text.Length; ++index)
{
switch (text[index])
{
case 'X':
case 'x':
continue;
default:
throw new NotImplementedException();
}
}
// -1 means some variable X rather than missing "null" or specified constant value
return -1;
}
}
return null;
}
private static T? ParseEnumOrNull<T>(Group group)
where T : struct
{
if (group.Success)
{
var text = group.Value;
if (Enum.TryParse<T>(text, out var result))
{
return result;
}
}
return null;
}
public sealed class DurationResolution
{
}
}
}
}

Просмотреть файл

@ -0,0 +1,60 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
using System;
namespace Microsoft.Bot.Builder.Ai.LUIS
{
/// <summary>
/// A mockable interface for the LUIS model.
/// </summary>
public interface ILuisModel
{
/// <summary>
/// Gets the GUID for the LUIS model.
/// </summary>
/// <value>
/// The GUID for the LUIS model.
/// </value>
string ModelID { get; }
/// <summary>
/// Gets the subscription key for LUIS.
/// </summary>
/// <value>
/// The subscription key for LUIS.
/// </value>
string SubscriptionKey { get; }
/// <summary>
/// Gets base URI for LUIS calls.
/// </summary>
/// <value>
/// Base URI for LUIS calls.
/// </value>
Uri UriBase { get; }
/// <summary>
/// Gets version of the LUIS API to call.
/// </summary>
/// <value>
/// Version of the LUIS API to call.
/// </value>
LuisApiVersion ApiVersion { get; }
/// <summary>
/// Gets threshold for top scoring intent.
/// </summary>
/// <value>
/// Threshold for top scoring intent.
/// </value>
double Threshold { get; }
/// <summary>
/// Modify a Luis request to specify query parameters like spelling or logging.
/// </summary>
/// <param name="request">Request so far.</param>
/// <returns>Modified request.</returns>
LuisRequest ModifyRequest(LuisRequest request);
}
}

Просмотреть файл

@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
namespace Microsoft.Bot.Builder.Ai.LUIS
{
/// <summary>
/// Interface containing optional parameters for a LUIS request.
/// </summary>
public interface ILuisOptions
{
/// <summary>
/// Gets or sets a value indicating whether if logging of queries to LUIS is allowed.
/// </summary>
/// <value>
/// Indicates if logging of queries to LUIS is allowed.
/// </value>
bool? Log { get; set; }
/// <summary>
/// Gets or sets a value indicating whether if spell checking is enabled.
/// </summary>
/// <value>
/// Indicates if spell checking is enabled.</placeholder>
/// </value>
bool? SpellCheck { get; set; }
/// <summary>
/// Gets or sets a value indicating whether if the staging endpoint is used..
/// </summary>
/// <value>
/// Indicates if the staging endpoint is used.
/// </value>
bool? Staging { get; set; }
/// <summary>
/// Gets or sets the time zone offset.
/// </summary>
/// <value>
/// The time zone offset.
/// </value>
double? TimezoneOffset { get; set; }
/// <summary>
/// Gets or sets the verbose flag.
/// </summary>
/// <value>
/// The verbose flag.
/// </value>
bool? Verbose { get; set; }
/// <summary>
/// Gets or sets the Bing Spell Check subscription key.
/// </summary>
/// <value>
/// The Bing Spell Check subscription key.
/// </value>
string BingSpellCheckSubscriptionKey { get; set; }
}
/// <summary>
/// LUIS extension methods.
/// </summary>
public static partial class Extensions
{
public static void Apply(this ILuisOptions source, ILuisOptions target)
{
if (source.Log.HasValue)
{
target.Log = source.Log.Value;
}
if (source.SpellCheck.HasValue)
{
target.SpellCheck = source.SpellCheck.Value;
}
if (source.Staging.HasValue)
{
target.Staging = source.Staging.Value;
}
if (source.TimezoneOffset.HasValue)
{
target.TimezoneOffset = source.TimezoneOffset.Value;
}
if (source.Verbose.HasValue)
{
target.Verbose = source.Verbose.Value;
}
if (!string.IsNullOrWhiteSpace(source.BingSpellCheckSubscriptionKey))
{
target.BingSpellCheckSubscriptionKey = source.BingSpellCheckSubscriptionKey;
}
}
}
}

Просмотреть файл

@ -0,0 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Cognitive.LUIS.Models;
namespace Microsoft.Bot.Builder.Ai.LUIS
{
/// <summary>
/// A mockable interface for the LUIS service.
/// </summary>
public interface ILuisService
{
/// <summary>
/// Modify the incoming LUIS request.
/// </summary>
/// <param name="request">Request so far.</param>
/// <returns>Modified request.</returns>
LuisRequest ModifyRequest(LuisRequest request);
/// <summary>
/// Build the query uri for the <see cref="LuisRequest"/>.
/// </summary>
/// <param name="luisRequest">The luis request text.</param>
/// <returns>The query uri.</returns>
Uri BuildUri(LuisRequest luisRequest);
/// <summary>
/// Query the LUIS service using this uri.
/// </summary>
/// <param name="uri">The query uri.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The LUIS result.</returns>
Task<LuisResult> QueryAsync(Uri uri, CancellationToken token);
}
}

Просмотреть файл

@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Microsoft.Bot.Builder.Ai.LUIS
{
public interface IResolutionParser
{
bool TryParse(IDictionary<string, object> properties, out Resolution resolution);
}
}

Просмотреть файл

@ -0,0 +1,210 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Cognitive.LUIS.Models;
using Microsoft.Rest;
using Newtonsoft.Json;
namespace Microsoft.Bot.Builder.Ai.LUIS
{
/// <summary>
/// Object that contains all the possible parameters to build Luis request.
/// </summary>
public sealed class LuisRequest : ILuisOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="LuisRequest"/> class.
/// </summary>
public LuisRequest()
: this(string.Empty)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LuisRequest"/> class.
/// </summary>
/// <param name="query"> The text query.</param>
public LuisRequest(string query)
{
this.Query = query;
this.Log = true;
}
/// <summary>
/// Gets or sets the text query.
/// </summary>
/// <value>
/// The text query.
/// </value>
public string Query { get; set; }
/// <summary>
/// Gets or sets if logging of queries to LUIS is allowed.
/// </summary>
/// <value>
/// Indicates if logging of queries to LUIS is allowed.
/// </value>
public bool? Log { get; set; }
/// <summary>
/// Gets or sets if spell checking is enabled.
/// </summary>
/// <value>
/// Indicates if spell checking is enabled.</placeholder>
/// </value>
public bool? SpellCheck { get; set; }
/// <summary>
/// Gets or sets if the staging endpoint is used.
/// </summary>
/// <value>
/// If the staging endpoint is used.
/// </value>
public bool? Staging { get; set; }
/// <summary>
/// Gets or sets the time zone offset.
/// </summary>
/// <value>
/// The time zone offset.
/// </value>
public double? TimezoneOffset { get; set; }
/// <summary>
/// Gets or sets the verbose flag.
/// </summary>
/// <value>
/// The verbose flag.
/// </value>
public bool? Verbose { get; set; }
/// <summary>
/// Gets or sets the Bing Spell Check subscription key.
/// </summary>
/// <value>
/// The Bing Spell Check subscription key.
/// </value>
public string BingSpellCheckSubscriptionKey { get; set; }
/// <summary>
/// Gets or sets any extra query parameters for the URL.
/// </summary>
/// <value>
/// Extra query parameters for the URL.
/// </value>
public string ExtraParameters { get; set; }
/// <summary>
/// Gets or sets the context id.
/// </summary>
/// <value>
/// The context id.
/// </value>
[Obsolete("Action binding in LUIS should be replaced with code.")]
public string ContextId { get; set; }
/// <summary>
/// Gets or sets force setting the parameter when using action binding.
/// </summary>
/// <value>
/// Force setting the parameter when using action binding.
/// </value>
[Obsolete("Action binding in LUIS should be replaced with code.")]
public string ForceSet { get; set; }
/// <summary>
/// Build the Uri for issuing the request for the specified Luis model.
/// </summary>
/// <param name="model"> The Luis model.</param>
/// <returns> The request Uri.</returns>
public Uri BuildUri(ILuisModel model)
{
if (model.ModelID == null)
{
throw new ValidationException(ValidationRules.CannotBeNull, "id");
}
if (model.SubscriptionKey == null)
{
throw new ValidationException(ValidationRules.CannotBeNull, "subscriptionKey");
}
var queryParameters = new List<string>
{
$"subscription-key={Uri.EscapeDataString(model.SubscriptionKey)}",
$"q={Uri.EscapeDataString(Query)}",
};
UriBuilder builder;
var id = Uri.EscapeDataString(model.ModelID);
switch (model.ApiVersion)
{
#pragma warning disable CS0612
case LuisApiVersion.V1:
builder = new UriBuilder(model.UriBase);
queryParameters.Add($"id={id}");
break;
#pragma warning restore CS0612
case LuisApiVersion.V2:
// v2.0 have the model as path parameter
builder = new UriBuilder(new Uri(model.UriBase, id));
break;
default:
throw new ArgumentException($"{model.ApiVersion} is not a valid Luis api version.");
}
if (Log != null)
{
queryParameters.Add($"log={Uri.EscapeDataString(Convert.ToString(Log))}");
}
if (SpellCheck != null)
{
queryParameters.Add($"spellCheck={Uri.EscapeDataString(Convert.ToString(SpellCheck))}");
}
if (Staging != null)
{
queryParameters.Add($"staging={Uri.EscapeDataString(Convert.ToString(Staging))}");
}
if (TimezoneOffset != null)
{
queryParameters.Add($"timezoneOffset={Uri.EscapeDataString(Convert.ToString(TimezoneOffset))}");
}
if (Verbose != null)
{
queryParameters.Add($"verbose={Uri.EscapeDataString(Convert.ToString(Verbose))}");
}
if (!string.IsNullOrWhiteSpace(BingSpellCheckSubscriptionKey))
{
queryParameters.Add($"bing-spell-check-subscription-key={Uri.EscapeDataString(BingSpellCheckSubscriptionKey)}");
}
#pragma warning disable CS0618
if (ContextId != null)
{
queryParameters.Add($"contextId={Uri.EscapeDataString(ContextId)}");
}
if (ForceSet != null)
{
queryParameters.Add($"forceSet={Uri.EscapeDataString(ForceSet)}");
}
#pragma warning restore CS0618
if (ExtraParameters != null)
{
queryParameters.Add(ExtraParameters);
}
builder.Query = string.Join("&", queryParameters);
return builder.Uri;
}
}
}

Просмотреть файл

@ -0,0 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Microsoft.Bot.Builder.Ai.LUIS
{
public sealed class ResolutionParser : IResolutionParser
{
bool IResolutionParser.TryParse(IDictionary<string, object> properties, out Resolution resolution)
{
if (properties != null)
{
if (properties.TryGetValue("resolution_type", out var value) && value is string)
{
switch (value as string)
{
case "builtin.datetime.date":
if (properties.TryGetValue("date", out value) && value is string)
{
if (BuiltIn.DateTime.DateTimeResolution.TryParse(value as string, out var dateTime))
{
resolution = dateTime;
return true;
}
}
break;
case "builtin.datetime.time":
case "builtin.datetime.set":
if (properties.TryGetValue("time", out value) && value is string)
{
if (BuiltIn.DateTime.DateTimeResolution.TryParse(value as string, out var dateTime))
{
resolution = dateTime;
return true;
}
}
break;
}
}
}
resolution = null;
return false;
}
}
}

Просмотреть файл

@ -17,6 +17,22 @@ namespace Microsoft.Bot.Builder.Ai.Luis
/// </remarks>
public class DateTimeSpec
{
public DateTimeSpec(string type, IEnumerable<string> expressions)
{
if (string.IsNullOrWhiteSpace(type))
{
throw new ArgumentNullException(nameof(type));
}
if (expressions == null)
{
throw new ArgumentNullException(nameof(expressions));
}
Type = type;
Expressions = expressions.ToList();
}
/// <summary>
/// Initializes a new instance of the <see cref="DateTimeSpec"/> class.
/// </summary>
@ -52,12 +68,18 @@ namespace Microsoft.Bot.Builder.Ai.Luis
/// <item>set -- a recurrence like "every monday".</item>
/// </list>
/// </remarks>
/// <value>
/// The type of expression.
/// </value>
[JsonProperty("type")]
public string Type { get; }
/// <summary>
/// Gets Timex expressions.
/// </summary>
/// <value>
/// Timex expressions.
/// </value>
[JsonProperty("timex")]
public IReadOnlyList<string> Expressions { get; }

Просмотреть файл

@ -13,30 +13,45 @@ namespace Microsoft.Bot.Builder.Ai.Luis
/// <summary>
/// Gets or sets 0-based index in the analyzed text for where entity starts.
/// </summary>
/// <value>
/// 0-based index in the analyzed text for where entity starts.
/// </value>
[JsonProperty("startIndex")]
public int StartIndex { get; set; }
/// <summary>
/// Gets or sets 0-based index of the first character beyond the recognized entity.
/// </summary>
/// <value>
/// 0-based index of the first character beyond the recognized entity.
/// </value>
[JsonProperty("endIndex")]
public int EndIndex { get; set; }
/// <summary>
/// Gets or sets word broken and normalized text for the entity.
/// </summary>
/// <value>
/// Word broken and normalized text for the entity.
/// </value>
[JsonProperty("text")]
public string Text { get; set; }
/// <summary>
/// Gets or sets optional confidence in the recognition.
/// </summary>
/// <value>
/// Optional confidence in the recognition.
/// </value>
[JsonProperty("score")]
public double? Score { get; set; }
/// <summary>
/// Gets or sets any extra properties.
/// </summary>
/// <value>
/// Any extra properties.
/// </value>
[JsonExtensionData(ReadData = true, WriteData = true)]
public IDictionary<string, object> Properties { get; set; }
}

Просмотреть файл

@ -11,14 +11,20 @@ namespace Microsoft.Bot.Builder.Ai.Luis
public class IntentData
{
/// <summary>
/// Gets or sets confidence in intent classification.
/// Gets or sets the confidence in intent classification.
/// </summary>
/// <value>
/// Confidence in intent classification.
/// </value>
[JsonProperty("score")]
public double Score { get; set; }
/// <summary>
/// Gets or sets any extra properties.
/// </summary>
/// <value>
/// Any extra properties.
/// </value>
[JsonExtensionData(ReadData = true, WriteData = true)]
public IDictionary<string, object> Properties { get; set; }
}

Просмотреть файл

@ -44,11 +44,11 @@ namespace Microsoft.Bot.Builder.Ai.Luis
}
/// <inheritdoc />
public async Task<RecognizerResult> Recognize(string utterance, CancellationToken ct)
public async Task<RecognizerResult> RecognizeAsync(string utterance, CancellationToken ct)
=> await RecognizeInternal(utterance, ct).ConfigureAwait(false);
/// <inheritdoc />
public async Task<T> Recognize<T>(string utterance, CancellationToken ct)
public async Task<T> RecognizeAsync<T>(string utterance, CancellationToken ct)
where T : IRecognizerConvert, new()
{
var result = new T();
@ -125,6 +125,7 @@ namespace Microsoft.Bot.Builder.Ai.Luis
private static JToken ExtractEntityValue(EntityModel entity)
{
if (!entity.AdditionalProperties.TryGetValue("resolution", out dynamic resolution))
{
{
return entity.Entity;
}

Просмотреть файл

@ -54,13 +54,19 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug - NuGet Packages|AnyCPU'">
<CodeAnalysisRuleSet>Microsoft.Bot.Builder.Ai.LUIS.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>Microsoft.Bot.Builder.Ai.LUIS.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.CognitiveServices.Language.LUIS.Runtime" Version="0.9.0-preview" />
<PackageReference Include="AsyncUsageAnalyzers" Version="1.0.0-alpha003" PrivateAssets="all" />
<PackageReference Include="Microsoft.Bot.Builder" Condition=" '$(PackageVersion)' != '' " Version="$(PackageVersion)" />
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.11" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta008" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta008" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -1,76 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Microsoft Managed Recommended Rules" Description="These rules focus on the most critical problems in your code, including potential security holes, application crashes, and other important logic and design errors. It is recommended to include this rule set in any custom rule set you create for your projects." ToolsVersion="10.0">
<Localization ResourceAssembly="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.dll" ResourceBaseName="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.Localized">
<Name Resource="MinimumRecommendedRules_Name" />
<Description Resource="MinimumRecommendedRules_Description" />
</Localization>
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
<Rule Id="CA1001" Action="Warning" />
<Rule Id="CA1009" Action="Warning" />
<Rule Id="CA1016" Action="Warning" />
<Rule Id="CA1033" Action="Warning" />
<Rule Id="CA1049" Action="Warning" />
<Rule Id="CA1060" Action="Warning" />
<Rule Id="CA1061" Action="Warning" />
<Rule Id="CA1063" Action="Warning" />
<Rule Id="CA1065" Action="Warning" />
<Rule Id="CA1301" Action="Warning" />
<Rule Id="CA1400" Action="Warning" />
<Rule Id="CA1401" Action="Warning" />
<Rule Id="CA1403" Action="Warning" />
<Rule Id="CA1404" Action="Warning" />
<Rule Id="CA1405" Action="Warning" />
<Rule Id="CA1410" Action="Warning" />
<Rule Id="CA1415" Action="Warning" />
<Rule Id="CA1821" Action="Warning" />
<Rule Id="CA1900" Action="Warning" />
<Rule Id="CA1901" Action="Warning" />
<Rule Id="CA2002" Action="Warning" />
<Rule Id="CA2100" Action="Warning" />
<Rule Id="CA2101" Action="Warning" />
<Rule Id="CA2108" Action="Warning" />
<Rule Id="CA2111" Action="Warning" />
<Rule Id="CA2112" Action="Warning" />
<Rule Id="CA2114" Action="Warning" />
<Rule Id="CA2116" Action="Warning" />
<Rule Id="CA2117" Action="Warning" />
<Rule Id="CA2122" Action="Warning" />
<Rule Id="CA2123" Action="Warning" />
<Rule Id="CA2124" Action="Warning" />
<Rule Id="CA2126" Action="Warning" />
<Rule Id="CA2131" Action="Warning" />
<Rule Id="CA2132" Action="Warning" />
<Rule Id="CA2133" Action="Warning" />
<Rule Id="CA2134" Action="Warning" />
<Rule Id="CA2137" Action="Warning" />
<Rule Id="CA2138" Action="Warning" />
<Rule Id="CA2140" Action="Warning" />
<Rule Id="CA2141" Action="Warning" />
<Rule Id="CA2146" Action="Warning" />
<Rule Id="CA2147" Action="Warning" />
<Rule Id="CA2149" Action="Warning" />
<Rule Id="CA2200" Action="Warning" />
<Rule Id="CA2202" Action="Warning" />
<Rule Id="CA2207" Action="Warning" />
<Rule Id="CA2212" Action="Warning" />
<Rule Id="CA2213" Action="Warning" />
<Rule Id="CA2214" Action="Warning" />
<Rule Id="CA2216" Action="Warning" />
<Rule Id="CA2220" Action="Warning" />
<Rule Id="CA2229" Action="Warning" />
<Rule Id="CA2231" Action="Warning" />
<Rule Id="CA2232" Action="Warning" />
<Rule Id="CA2235" Action="Warning" />
<Rule Id="CA2236" Action="Warning" />
<Rule Id="CA2237" Action="Warning" />
<Rule Id="CA2238" Action="Warning" />
<Rule Id="CA2240" Action="Warning" />
<Rule Id="CA2241" Action="Warning" />
<Rule Id="CA2242" Action="Warning" />
<RuleSet Name="Rules for StyleCop.Analyzers" Description="Code analysis rules for StyleCop.Analyzers.csproj." ToolsVersion="14.0">
<Rules AnalyzerId="AsyncUsageAnalyzers" RuleNamespace="AsyncUsageAnalyzers">
<Rule Id="UseConfigureAwait" Action="Warning" />
<Rule Id="AvoidAsyncVoid" Action="Warning" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Features" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Features">
<Rule Id="IDE0003" Action="None" />
</Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<Rule Id="SA1633" Action="None" />
<Rule Id="SA1200" Action="None" />
<Rule Id="SA1101" Action="None" />
<!-- local calls prefixed with this -->
<Rule Id="SA1200" Action="None" />
<Rule Id="SA1305" Action="Warning" />
<Rule Id="SA1309" Action="None" />
<Rule Id="SA1412" Action="Warning" />
<Rule Id="SA1600" Action="None" />
<Rule Id="SA1609" Action="Warning" />
<Rule Id="SA1633" Action="None" />
</Rules>
</RuleSet>

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using Newtonsoft.Json;
namespace Microsoft.Bot.Builder.Ai.QnA
{
[Serializable]
public class Metadata
{
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
[JsonProperty(PropertyName = "value")]
public string Value { get; set; }
}
}

Просмотреть файл

@ -45,10 +45,16 @@
<DocumentationFile>bin\$(Configuration)\netstandard2.0\Microsoft.Bot.Builder.Ai.QnA.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>Microsoft.Bot.Builder.Ai.QnA.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AsyncUsageAnalyzers" Version="1.0.0-alpha003" PrivateAssets="all" />
<PackageReference Include="Microsoft.Bot.Builder" Condition=" '$(PackageVersion)' == '' " Version="4.0.0-local" />
<PackageReference Include="Microsoft.Bot.Builder" Condition=" '$(PackageVersion)' != '' " Version="$(PackageVersion)" />
<PackageReference Include="SourceLink.Create.CommandLine" Version="2.8.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta008" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for StyleCop.Analyzers" Description="Code analysis rules for StyleCop.Analyzers.csproj." ToolsVersion="14.0">
<Rules AnalyzerId="AsyncUsageAnalyzers" RuleNamespace="AsyncUsageAnalyzers">
<Rule Id="UseConfigureAwait" Action="Warning" />
<Rule Id="AvoidAsyncVoid" Action="Warning" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Features" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Features">
<Rule Id="IDE0003" Action="None" />
</Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<Rule Id="SA1101" Action="None" />
<!-- local calls prefixed with this -->
<Rule Id="SA1200" Action="None" />
<Rule Id="SA1305" Action="Warning" />
<Rule Id="SA1309" Action="None" />
<Rule Id="SA1412" Action="Warning" />
<Rule Id="SA1600" Action="None" />
<Rule Id="SA1609" Action="Warning" />
<Rule Id="SA1633" Action="None" />
</Rules>
</RuleSet>

Просмотреть файл

@ -21,7 +21,7 @@ namespace Microsoft.Bot.Builder.Ai.QnA
private readonly QnAMakerOptions _options;
/// <summary>
/// Creates a new <see cref="QnAMaker"/> instance.
/// Initializes a new instance of the <see cref="QnAMaker"/> class.
/// </summary>
/// <param name="endpoint">The endpoint of the knowledge base to query.</param>
/// <param name="options">The options for the QnA Maker knowledge base.</param>
@ -37,10 +37,12 @@ namespace Microsoft.Bot.Builder.Ai.QnA
{
throw new ArgumentException(nameof(endpoint.KnowledgeBaseId));
}
if (string.IsNullOrEmpty(endpoint.Host))
{
throw new ArgumentException(nameof(endpoint.Host));
}
if (string.IsNullOrEmpty(endpoint.EndpointKey))
{
throw new ArgumentException(nameof(endpoint.EndpointKey));
@ -52,14 +54,17 @@ namespace Microsoft.Bot.Builder.Ai.QnA
{
_options.ScoreThreshold = 0.3F;
}
if (_options.Top == 0)
{
_options.Top = 1;
}
if (_options.ScoreThreshold < 0 || _options.ScoreThreshold > 1)
{
throw new ArgumentOutOfRangeException(nameof(_options.ScoreThreshold), "Score threshold should be a value between 0 and 1");
}
if (_options.Top < 1)
{
throw new ArgumentOutOfRangeException(nameof(_options.Top), "Top should be an integer greater than 0");
@ -69,6 +74,7 @@ namespace Microsoft.Bot.Builder.Ai.QnA
{
_options.StrictFilters = new Metadata[] { };
}
if (_options.MetadataBoost == null)
{
_options.MetadataBoost = new Metadata[] { };
@ -86,17 +92,18 @@ namespace Microsoft.Bot.Builder.Ai.QnA
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
string jsonRequest = JsonConvert.SerializeObject(new
{
question,
top = _options.Top,
strictFilters = _options.StrictFilters,
metadataBoost = _options.MetadataBoost
}, Formatting.None);
var jsonRequest = JsonConvert.SerializeObject(
new
{
question,
top = _options.Top,
strictFilters = _options.StrictFilters,
metadataBoost = _options.MetadataBoost,
}, Formatting.None);
request.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, "application/json");
bool isLegacyProtocol = (_endpoint.Host.EndsWith("v2.0") || _endpoint.Host.EndsWith("v3.0"));
var isLegacyProtocol = _endpoint.Host.EndsWith("v2.0") || _endpoint.Host.EndsWith("v3.0");
if (isLegacyProtocol)
{
@ -121,6 +128,7 @@ namespace Microsoft.Bot.Builder.Ai.QnA
{
answer.Score = answer.Score / 100;
}
return results.Answers.Where(answer => answer.Score > _options.ScoreThreshold).ToArray();
}
@ -129,11 +137,9 @@ namespace Microsoft.Bot.Builder.Ai.QnA
// The old version of the protocol returns the id in a field called qnaId the
// following classes and helper function translate this old structure
private QueryResults ConvertLegacyResults(InternalQueryResults legacyResults)
private QueryResults ConvertLegacyResults(InternalQueryResults legacyResults) => new QueryResults
{
return new QueryResults
{
Answers = legacyResults.Answers
Answers = legacyResults.Answers
.Select(answer => new QueryResult
{
// The old version of the protocol returns the "id" in a field called "qnaId"
@ -142,11 +148,10 @@ namespace Microsoft.Bot.Builder.Ai.QnA
Metadata = answer.Metadata,
Score = answer.Score,
Source = answer.Source,
Questions = answer.Questions
Questions = answer.Questions,
})
.ToArray()
};
}
.ToArray(),
};
private class InternalQueryResult : QueryResult
{
@ -157,125 +162,11 @@ namespace Microsoft.Bot.Builder.Ai.QnA
private class InternalQueryResults
{
/// <summary>
/// The answers for a user query,
/// Gets or sets the answers for a user query,
/// sorted in decreasing order of ranking score.
/// </summary>
[JsonProperty("answers")]
public InternalQueryResult[] Answers { get; set; }
}
}
/// <summary>
/// Defines an endpoint used to connect to a QnA Maker Knowledge base.
/// </summary>
public class QnAMakerEndpoint
{
/// <summary>
/// The knowledge base ID.
/// </summary>
public string KnowledgeBaseId { get; set; }
/// <summary>
/// The endpoint key for the knowledge base.
/// </summary>
public string EndpointKey { get; set; }
/// <summary>
/// The host path. For example "https://westus.api.cognitive.microsoft.com/qnamaker/v2.0"
/// </summary>
public string Host { get; set; }
}
/// <summary>
/// Defines options for the QnA Maker knowledge base.
/// </summary>
public class QnAMakerOptions
{
public QnAMakerOptions()
{
ScoreThreshold = 0.3f;
}
/// <summary>
/// The minimum score threshold, used to filter returned results.
/// </summary>
/// <remarks>Scores are normalized to the range of 0.0 to 1.0
/// before filtering.</remarks>
public float ScoreThreshold { get; set; }
/// <summary>
/// The number of ranked results you want in the output.
/// </summary>
public int Top { get; set; }
public Metadata[] StrictFilters { get; set; }
public Metadata[] MetadataBoost { get; set; }
}
[Serializable]
public class Metadata
{
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
[JsonProperty(PropertyName = "value")]
public string Value { get; set; }
}
/// <summary>
/// Represents an individual result from a knowledge base query.
/// </summary>
public class QueryResult
{
/// <summary>
/// The list of questions indexed in the QnA Service for the given answer.
/// </summary>
[JsonProperty("questions")]
public string[] Questions { get; set; }
/// <summary>
/// The answer text.
/// </summary>
[JsonProperty("answer")]
public string Answer { get; set; }
/// <summary>
/// The answer's score, from 0.0 (least confidence) to
/// 1.0 (greatest confidence).
/// </summary>
[JsonProperty("score")]
public float Score { get; set; }
/// <summary>
/// Metadata that is associated with the answer
/// </summary>
[JsonProperty(PropertyName = "metadata")]
public Metadata[] Metadata { get; set; }
/// <summary>
/// The source from which the QnA was extracted
/// </summary>
[JsonProperty(PropertyName = "source")]
public string Source { get; set; }
/// <summary>
/// The index of the answer in the knowledge base. V3 uses
/// 'qnaId', V4 uses 'id'.
/// </summary>
[JsonProperty(PropertyName = "id")]
public int Id { get; set; }
}
/// <summary>
/// Contains answers for a user query.
/// </summary>
public class QueryResults
{
/// <summary>
/// The answers for a user query,
/// sorted in decreasing order of ranking score.
/// </summary>
[JsonProperty("answers")]
public QueryResult[] Answers { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Microsoft.Bot.Builder.Ai.QnA
{
/// <summary>
/// Defines an endpoint used to connect to a QnA Maker Knowledge base.
/// </summary>
public class QnAMakerEndpoint
{
/// <summary>
/// Gets or sets the knowledge base ID.
/// </summary>
/// <value>
/// The knowledge base ID.
/// </value>
public string KnowledgeBaseId { get; set; }
/// <summary>
/// Gets or sets the endpoint key for the knowledge base.
/// </summary>
/// <value>
/// The endpoint key for the knowledge base.
/// </value>
public string EndpointKey { get; set; }
/// <summary>
/// Gets or sets the host path. For example "https://westus.api.cognitive.microsoft.com/qnamaker/v2.0".
/// </summary>
/// <value>
/// The host path.
/// </value>
public string Host { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Microsoft.Bot.Builder.Ai.QnA
{
/// <summary>
/// Defines options for the QnA Maker knowledge base.
/// </summary>
public class QnAMakerOptions
{
public QnAMakerOptions()
{
ScoreThreshold = 0.3f;
}
/// <summary>
/// Gets or sets the minimum score threshold, used to filter returned results.
/// </summary>
/// <remarks>Scores are normalized to the range of 0.0 to 1.0
/// before filtering.</remarks>
/// <value>
/// The minimum score threshold, used to filter returned results.
/// </value>
public float ScoreThreshold { get; set; }
/// <summary>
/// Gets or sets the number of ranked results you want in the output.
/// </summary>
/// <value>
/// The number of ranked results you want in the output.
/// </value>
public int Top { get; set; }
public Metadata[] StrictFilters { get; set; }
public Metadata[] MetadataBoost { get; set; }
}
}

Просмотреть файл

@ -7,49 +7,72 @@ using Newtonsoft.Json;
namespace Microsoft.Bot.Builder.Ai.QnA
{
/// <summary>
/// This class represents all the trace info that we collect from the QnAMaker Middleware
/// This class represents all the trace info that we collect from the QnAMaker Middleware.
/// </summary>
public class QnAMakerTraceInfo
{
/// <summary>
/// Message which instigated the query to QnAMaker
/// Gets or sets message which instigated the query to QnAMaker.
/// </summary>
/// <value>
/// Message which instigated the query to QnAMaker.
/// </value>
[JsonProperty("message")]
public IMessageActivity Message { set; get; }
public IMessageActivity Message { get; set; }
/// <summary>
/// Results that QnAMaker returned
/// Gets or sets results that QnAMaker returned.
/// </summary>
/// <value>
/// Results that QnAMaker returned.
/// </value>
[JsonProperty("queryResults")]
public QueryResult[] QueryResults { set; get; }
public QueryResult[] QueryResults { get; set; }
/// <summary>
/// ID of the Knowledgebase that is being used
/// Gets or sets iD of the Knowledgebase that is being used.
/// </summary>
/// <value>
/// ID of the Knowledgebase that is being used.
/// </value>
[JsonProperty("knowledgeBaseId")]
public string KnowledgeBaseId { get; set; }
/// <summary>
/// Questions with a match of less than the score threshold are not returned
/// Gets or sets the minimum score threshold, used to filter returned results.
/// </summary>
/// <remarks>Scores are normalized to the range of 0.0 to 1.0
/// before filtering.</remarks>
/// <value>
/// The minimum score threshold, used to filter returned results.
/// </value>
[JsonProperty("scoreThreshold")]
public float ScoreThreshold { get; set; }
/// <summary>
/// Number of ranked results that are asked to be returned
/// Gets or sets number of ranked results that are asked to be returned.
/// </summary>
/// <value>
/// Number of ranked results that are asked to be returned.
/// </value>
[JsonProperty("top")]
public int Top { get; set; }
/// <summary>
///
/// Gets or sets the filters used to return answers that have the specified metadata.
/// </summary>
/// <value>
/// The filters used to return answers that have the specified metadata.
/// </value>
[JsonProperty("strictFilters")]
public Metadata[] StrictFilters { get; set; }
/// <summary>
/// Miscellaneous data
/// Gets or sets miscellaneous data to boost answers.
/// </summary>
/// <value>
/// Miscellaneous data to boost answers.
/// </value>
[JsonProperty("metadataBoost")]
public Metadata[] MetadataBoost { get; set; }
}

Просмотреть файл

@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Newtonsoft.Json;
namespace Microsoft.Bot.Builder.Ai.QnA
{
/// <summary>
/// Represents an individual result from a knowledge base query.
/// </summary>
public class QueryResult
{
/// <summary>
/// Gets or sets the list of questions indexed in the QnA Service for the given answer.
/// </summary>
/// <value>
/// The list of questions indexed in the QnA Service for the given answer.
/// </value>
[JsonProperty("questions")]
public string[] Questions { get; set; }
/// <summary>
/// Gets or sets the answer text.
/// </summary>
/// <value>
/// The answer text.
/// </value>
[JsonProperty("answer")]
public string Answer { get; set; }
/// <summary>
/// Gets or sets the answer's score, from 0.0 (least confidence) to
/// 1.0 (greatest confidence).
/// </summary>
/// <value>
/// The answer's score, from 0.0 (least confidence) to
/// 1.0 (greatest confidence).
/// </value>
[JsonProperty("score")]
public float Score { get; set; }
/// <summary>
/// Gets or sets metadata that is associated with the answer.
/// </summary>
/// <value>
/// Metadata that is associated with the answer.
/// </value>
[JsonProperty(PropertyName = "metadata")]
public Metadata[] Metadata { get; set; }
/// <summary>
/// Gets or sets the source from which the QnA was extracted.
/// </summary>
/// <value>
/// The source from which the QnA was extracted.
/// </value>
[JsonProperty(PropertyName = "source")]
public string Source { get; set; }
/// <summary>
/// Gets or sets the index of the answer in the knowledge base. V3 uses
/// 'qnaId', V4 uses 'id'.
/// </summary>
/// <value>
/// The index of the answer in the knowledge base. V3 uses
/// 'qnaId', V4 uses 'id'.
/// </value>
[JsonProperty(PropertyName = "id")]
public int Id { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Newtonsoft.Json;
namespace Microsoft.Bot.Builder.Ai.QnA
{
/// <summary>
/// Contains answers for a user query.
/// </summary>
public class QueryResults
{
/// <summary>
/// Gets or sets the answers for a user query,
/// sorted in decreasing order of ranking score.
/// </summary>
/// <value>
/// The answers for a user query,
/// sorted in decreasing order of ranking score.
/// </value>
[JsonProperty("answers")]
public QueryResult[] Answers { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
namespace Microsoft.Bot.Builder.Ai.Translation
{
/// <summary>
/// Wraps an HTTP client with which to obtain an access token.
/// </summary>
internal class AzureAuthToken
{
// Name of header used to pass the subscription key to the token service
private const string OcpApimSubscriptionKeyHeader = "Ocp-Apim-Subscription-Key";
private static readonly HttpClient DefaultHttpClient = new HttpClient();
// URL of the token service
private static readonly Uri ServiceUrl = new Uri("https://api.cognitive.microsoft.com/sts/v1.0/issueToken");
// After obtaining a valid token, this class will cache it for this duration.
// Use a duration of 5 minutes, which is less than the actual token lifetime of 10 minutes.
private static readonly TimeSpan TokenCacheDuration = new TimeSpan(0, 5, 0);
private HttpClient _httpClient = null;
// Cache the value of the last valid token obtained from the token service.
private string _storedTokenValue = string.Empty;
// When the last valid token was obtained.
private DateTime _storedTokenTime = DateTime.MinValue;
/// <summary>
/// Initializes a new instance of the <see cref="AzureAuthToken"/> class.
/// </summary>
/// <param name="key">Subscription key to use to get an authentication token.</param>
/// <param name="client">An alternate HTTP client to use.</param>
internal AzureAuthToken(string key, HttpClient client = null)
{
_httpClient = client ?? DefaultHttpClient;
if (string.IsNullOrEmpty(key))
{
throw new ArgumentNullException(nameof(key), "A subscription key is required");
}
this.SubscriptionKey = key;
this.RequestStatusCode = HttpStatusCode.InternalServerError;
}
// Gets the subscription key.
internal string SubscriptionKey { get; }
// Gets the HTTP status code for the most recent request to the token service.
internal HttpStatusCode RequestStatusCode { get; private set; }
/// <summary>
/// Gets a token for the specified subscription.
/// </summary>
/// <returns>The encoded JWT token prefixed with the string "Bearer ".</returns>
/// <remarks>
/// This method uses a cache to limit the number of request to the token service.
/// A fresh token can be re-used during its lifetime of 10 minutes. After a successful
/// request to the token service, this method caches the access token. Subsequent
/// invocations of the method return the cached token for the next 5 minutes. After
/// 5 minutes, a new token is fetched from the token service and the cache is updated.
/// </remarks>
internal async Task<string> GetAccessTokenAsync()
{
if (string.IsNullOrWhiteSpace(this.SubscriptionKey))
{
return string.Empty;
}
// Re-use the cached token if there is one.
if ((DateTime.Now - _storedTokenTime) < TokenCacheDuration)
{
return _storedTokenValue;
}
using (var request = new HttpRequestMessage())
{
request.Method = HttpMethod.Post;
request.RequestUri = ServiceUrl;
request.Content = new StringContent(string.Empty);
request.Headers.TryAddWithoutValidation(OcpApimSubscriptionKeyHeader, this.SubscriptionKey);
using (var response = await _httpClient.SendAsync(request))
{
this.RequestStatusCode = response.StatusCode;
response.EnsureSuccessStatusCode();
var token = await response.Content.ReadAsStringAsync();
_storedTokenTime = DateTime.Now;
_storedTokenValue = "Bearer " + token;
return _storedTokenValue;
}
}
}
}
}

Просмотреть файл

@ -1,32 +1,30 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Microsoft.Bot.Builder.Ai.Translation.PostProcessor;
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder.Ai.Translation
{
/// <summary>
/// A Custom dictionary used to store all the configured user language dictionaries
/// which in turn will be used in <see cref="CustomDictionaryPostProcessor"/> to overwrite the machine translation output for specific vocab
/// with the specific translation in the provided dictionary,
/// the <see cref="CustomDictionary"/> contains an internal dictionary of dictionaries inexed by language id,
/// which in turn will be used in <see cref="CustomDictionaryPostProcessor"/> to overwrite the machine translation output for specific vocab
/// with the specific translation in the provided dictionary,
/// the <see cref="CustomDictionary"/> contains an internal dictionary of dictionaries indexed by language id,
/// for ex of how this interal state would look like :
/// [
/// "en", ["court", "courtyard"]
/// "it", ["camera", "bedroom"]
/// ]
/// as per the last example, the outer dictionary contains all the user configured custom dictionaries indexed by the language id,
/// as per the last example, the outer dictionary contains all the user configured custom dictionaries indexed by the language id,
/// and each internal dictionary contains the <see cref="KeyValuePair{String, String}"/> of this specific language.
/// </summary>
public class CustomDictionary
{
private readonly Dictionary<string, Dictionary<string, string>> _userCustomDictionaries;
/// <summary>
/// Constructs a new <see cref="CustomDictionary"/> object and initializes the internal dictionary variable.
private readonly Dictionary<string, Dictionary<string, string>> _userCustomDictionaries;
/// <summary>
/// Initializes a new instance of the <see cref="CustomDictionary"/> class and initializes the internal dictionary variable.
/// </summary>
public CustomDictionary()
{
@ -36,8 +34,8 @@ namespace Microsoft.Bot.Builder.Ai.Translation
/// <summary>
/// Adds new custom language dictionary for the set of configured dictionaries.
/// </summary>
/// <param name="languageId"></param>
/// <param name="dictionary"></param>
/// <param name="languageId">The language id to use as the key of the new custom language dictionary.</param>
/// <param name="dictionary">The dictionary containing the <see cref="KeyValuePair{String, String}"/> of the specific language.</param>
public void AddNewLanguageDictionary(string languageId, Dictionary<string, string> dictionary)
{
if (string.IsNullOrWhiteSpace(languageId))
@ -61,8 +59,8 @@ namespace Microsoft.Bot.Builder.Ai.Translation
/// <summary>
/// Get a specific language dictionary using it's key (language id).
/// </summary>
/// <param name="languageId"></param>
/// <returns>A <see cref="Dictionary{String, String}"/> that matches the provided language id</returns>
/// <param name="languageId">The id of the language dictionary to get.</param>
/// <returns>A <see cref="Dictionary{String, String}"/> that matches the provided language id.</returns>
public Dictionary<string, string> GetLanguageDictionary(string languageId)
{
if (string.IsNullOrWhiteSpace(languageId))
@ -81,10 +79,7 @@ namespace Microsoft.Bot.Builder.Ai.Translation
/// <summary>
/// Check if the <see cref="CustomDictionary"/> object is empty or not.
/// </summary>
/// <returns></returns>
public Boolean IsEmpty()
{
return _userCustomDictionaries.Count == 0;
}
/// <returns><c>true</c> if user custom dictionary is empty; otherwise, <c>false</c>.</returns>
public bool IsEmpty() => _userCustomDictionaries.Count == 0;
}
}

Просмотреть файл

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Microsoft.Bot.Builder.Ai.Translation
{
/// <summary>
/// DateAndTimeLocaleFormat Class used to store date format and time format
/// for different locales.
/// </summary>
internal class DateAndTimeLocaleFormat
{
public string TimeFormat { get; set; }
public string DateFormat { get; set; }
}
}

Просмотреть файл

@ -1,17 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.Bot.Builder.Ai.Translation
{
public interface ILocaleConverter
{
bool IsLocaleAvailable(string locale);
string Convert(string message, string fromLocale, string toLocale);
string[] GetAvailableLocales();
}
}

Просмотреть файл

@ -8,146 +8,9 @@ using System.Linq;
namespace Microsoft.Bot.Builder.Ai.Translation
{
public class LanguageMap
{
public static LanguageMap Global = new LanguageMap();
Dictionary<string, string> codesToNames = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
Dictionary<string, string> namesToCode = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
Dictionary<string, string[]> namesToNames = new Dictionary<string, string[]>(StringComparer.InvariantCultureIgnoreCase);
public LanguageMap()
{
Add("ar", "Arabic");
Add("bs-Latn", "Bosnian");
Add("bg", "Bulgarian");
Add("ca", "Catalan");
Add("zh-CHS", "Chinese Simplified");
Add("zh-CHT", "Chinese Traditional");
namesToNames.Add("Chinese", new string[] { "Simplified", "Traditional" });
Add("hr", "Croatian");
Add("cs", "Czech");
Add("da", "Danish");
Add("nl", "Dutch");
Add("en", "English");
Add("et", "Estonian");
Add("fi", "Finnish");
Add("fr", "French");
Add("de", "German");
Add("el", "Greek");
Add("ht", "Haitian Creole");
namesToCode.Add("Creole", "ht");
Add("he", "Hebrew");
Add("hi", "Hindi");
Add("mww", "Hmong Daw");
namesToCode.Add("Hmong", "mww");
namesToCode.Add("Daw", "mww");
Add("hu", "Hungarian");
Add("id", "Indonesian");
Add("it", "Italian");
Add("ja", "Japanese");
Add("sw", "Kiswahili");
Add("ko", "Korean");
Add("tlh", "Klingon");
Add("lv", "Latvian");
Add("lt", "Lithuanian");
Add("ms", "Malay");
Add("mt", "Maltese");
Add("no", "Norwegian");
Add("fa", "Persian");
Add("pl", "Polish");
Add("pt", "Portuguese");
Add("otq", "Querétaro Otomi");
namesToCode.Add("Querétaro", "otq");
namesToCode.Add("Otomi", "otq");
Add("ro", "Romanian");
Add("ru", "Russian");
Add("sr-Cyrl", "Serbian Cyrillic");
Add("sr-Latn", "Serbian Latin");
namesToNames.Add("Serbian", new string[] { "Cyrillic", "Latin" });
Add("sk", "Slovak");
Add("sl", "Slovenian");
Add("es", "Spanish");
Add("sv", "Swedish");
Add("th", "Thai");
Add("tr", "Turkish");
Add("uk", "Ukrainian");
Add("ur", "Urdu");
Add("vi", "Vietnamese");
Add("cy", "Welsh");
Add("yua", "Yucatec Maya");
namesToCode.Add("yucatec", "yua");
namesToCode.Add("maya", "yua");
}
private void Add(string code, string name)
{
codesToNames.Add(code, name);
namesToCode.Add(name, code);
}
public string GetCodeForInput(IEnumerable<string> values)
{
string code;
foreach (var value in values)
{
if (this.NamesToCodes.TryGetValue(value, out code))
return code;
if (this.NamesToNames.ContainsKey(value))
{
foreach (var name in this.NamesToNames[value])
{
var secondVal = values.Where(val => String.Compare(val, name, true) == 0).FirstOrDefault();
if (secondVal != null)
{
if (TryCompound(value, secondVal, out code))
return code;
}
}
if (TryCompound(value, this.NamesToNames[value].FirstOrDefault(), out code))
return code;
}
if (this.NamesToCodes.Values.Contains(value))
return value;
}
return null;
}
public string GetCodeOrFallback(string code)
{
if (string.IsNullOrWhiteSpace(code))
return "en";
code = code.Trim();
if (CodesToNames.ContainsKey(code))
return code;
return "en";
}
private bool TryCompound(string firstVal, string secondVal, out string code)
{
string compound = $"{firstVal} {secondVal}";
if (this.NamesToCodes.TryGetValue(compound, out code))
return true;
compound = $"{secondVal} {firstVal}";
if (this.NamesToCodes.TryGetValue(compound, out code))
return true;
return false;
}
public Dictionary<string, string> CodesToNames { get { return codesToNames; } }
public Dictionary<string, string> NamesToCodes { get { return namesToCode; } }
public Dictionary<string, string[]> NamesToNames { get { return namesToNames; } }
{
/// <summary>
/// Names for languages in all languages
/// Names for languages in all languages.
/// </summary>
public static readonly string[] Names =
{
@ -576,6 +439,172 @@ namespace Microsoft.Bot.Builder.Ai.Translation
"slofeneg", "sbaeneg", "swedeg", "twrceg", "wrceineg",
"wrdw", "fietnameg", "cymraeg", "xokiko'ob catalan", "chino xíiw",
"ingles", "frances", "japones", "káastelan", "maaya yucateco",
};
};
private static LanguageMap _global;
public LanguageMap()
{
Add("ar", "Arabic");
Add("bs-Latn", "Bosnian");
Add("bg", "Bulgarian");
Add("ca", "Catalan");
Add("zh-CHS", "Chinese Simplified");
Add("zh-CHT", "Chinese Traditional");
NamesToNames.Add("Chinese", new string[] { "Simplified", "Traditional" });
Add("hr", "Croatian");
Add("cs", "Czech");
Add("da", "Danish");
Add("nl", "Dutch");
Add("en", "English");
Add("et", "Estonian");
Add("fi", "Finnish");
Add("fr", "French");
Add("de", "German");
Add("el", "Greek");
Add("ht", "Haitian Creole");
NamesToCodes.Add("Creole", "ht");
Add("he", "Hebrew");
Add("hi", "Hindi");
Add("mww", "Hmong Daw");
NamesToCodes.Add("Hmong", "mww");
NamesToCodes.Add("Daw", "mww");
Add("hu", "Hungarian");
Add("id", "Indonesian");
Add("it", "Italian");
Add("ja", "Japanese");
Add("sw", "Kiswahili");
Add("ko", "Korean");
Add("tlh", "Klingon");
Add("lv", "Latvian");
Add("lt", "Lithuanian");
Add("ms", "Malay");
Add("mt", "Maltese");
Add("no", "Norwegian");
Add("fa", "Persian");
Add("pl", "Polish");
Add("pt", "Portuguese");
Add("otq", "Querétaro Otomi");
NamesToCodes.Add("Querétaro", "otq");
NamesToCodes.Add("Otomi", "otq");
Add("ro", "Romanian");
Add("ru", "Russian");
Add("sr-Cyrl", "Serbian Cyrillic");
Add("sr-Latn", "Serbian Latin");
NamesToNames.Add("Serbian", new string[] { "Cyrillic", "Latin" });
Add("sk", "Slovak");
Add("sl", "Slovenian");
Add("es", "Spanish");
Add("sv", "Swedish");
Add("th", "Thai");
Add("tr", "Turkish");
Add("uk", "Ukrainian");
Add("ur", "Urdu");
Add("vi", "Vietnamese");
Add("cy", "Welsh");
Add("yua", "Yucatec Maya");
NamesToCodes.Add("yucatec", "yua");
NamesToCodes.Add("maya", "yua");
}
public static LanguageMap Global
{
get
{
if (_global == null)
{
_global = new LanguageMap();
}
return _global;
}
}
public Dictionary<string, string> CodesToNames { get; } = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
public Dictionary<string, string> NamesToCodes { get; } = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
public Dictionary<string, string[]> NamesToNames { get; } = new Dictionary<string, string[]>(StringComparer.InvariantCultureIgnoreCase);
public string GetCodeForInput(IEnumerable<string> values)
{
foreach (var value in values)
{
if (this.NamesToCodes.TryGetValue(value, out var code))
{
return code;
}
if (this.NamesToNames.ContainsKey(value))
{
foreach (var name in this.NamesToNames[value])
{
var secondVal = values.Where(val => string.Compare(val, name, true) == 0).FirstOrDefault();
if (secondVal != null)
{
if (TryCompound(value, secondVal, out code))
{
return code;
}
}
}
if (TryCompound(value, this.NamesToNames[value].FirstOrDefault(), out code))
{
return code;
}
}
if (this.NamesToCodes.Values.Contains(value))
{
return value;
}
}
return null;
}
public string GetCodeOrFallback(string code)
{
if (string.IsNullOrWhiteSpace(code))
{
return "en";
}
code = code.Trim();
if (CodesToNames.ContainsKey(code))
{
return code;
}
return "en";
}
private void Add(string code, string name)
{
CodesToNames.Add(code, name);
NamesToCodes.Add(name, code);
}
private bool TryCompound(string firstVal, string secondVal, out string code)
{
var compound = $"{firstVal} {secondVal}";
if (this.NamesToCodes.TryGetValue(compound, out code))
{
return true;
}
compound = $"{secondVal} {firstVal}";
if (this.NamesToCodes.TryGetValue(compound, out code))
{
return true;
}
return false;
}
}
}

Просмотреть файл

@ -6,95 +6,43 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Recognizers.Text;
using Microsoft.Recognizers.Text.DateTime;
namespace Microsoft.Bot.Builder.Ai.Translation
{
/// <summary>
/// DateAndTimeLocaleFormat Class used to store date format and time format
/// for different locales.
/// </summary>
internal class DateAndTimeLocaleFormat
{
public string TimeFormat { get; set; }
public string DateFormat { get; set; }
}
/// <summary>
/// TextAndDateTime Class used to store text and date time object
/// from Microsoft Recognizer recognition result.
/// </summary>
internal class TextAndDateTime
{
public string Text { get; set; }
public DateTime dateTime { get; set; }
public string type { get; set; }
public bool range { get; set; }
public DateTime endDateTime { get; set; }
}
/// <summary>
/// Locale Converter Class Converts dates and times
/// Locale Converter Class Converts dates and times
/// between different locales.
/// </summary>
public class LocaleConverter : ILocaleConverter
{
private static readonly ConcurrentDictionary<string, DateAndTimeLocaleFormat> _mapLocaleToFunction = new ConcurrentDictionary<string, DateAndTimeLocaleFormat>();
private static LocaleConverter _localeConverter;
private LocaleConverter()
{
InitLocales();
}
public static LocaleConverter Converter
{
get
{
if (_localeConverter == null)
{
_localeConverter = new LocaleConverter();
}
return _localeConverter;
}
}
private LocaleConverter()
{
InitLocales();
}
/// <summary>
/// Init different locales format,
/// Supporting English, French, Deutsche and Chinese Locales.
/// </summary>
private void InitLocales()
{
if (_mapLocaleToFunction.Count > 0)
return;
var supportedLocales = new string[]
{
"en-us", "en-za", "en-ie", "en-gb", "en-ca", "fr-ca", "zh-cn", "zh-sg", "zh-hk", "zh-mo", "zh-tw",
"en-au", "fr-be", "fr-ch", "fr-fr", "fr-lu", "fr-mc", "de-at", "de-ch", "de-de", "de-lu", "de-li",
"es-es"
};
foreach (string locale in supportedLocales)
{
CultureInfo cultureInfo = new CultureInfo(locale);
var dateTimeInfo = new DateAndTimeLocaleFormat()
{
DateFormat = $"{{0:{cultureInfo.DateTimeFormat.ShortDatePattern}}}",
TimeFormat = $"{{0:{cultureInfo.DateTimeFormat.ShortTimePattern}}}"
};
_mapLocaleToFunction[locale] = dateTimeInfo;
}
}
}
/// <summary>
/// Check if a specific locale is available.
/// </summary>
/// <param name="locale">input locale that we need to check if available</param>
/// <param name="locale">input locale that we need to check if available.</param>
/// <returns>true if the locale is found, otherwise false.</returns>
public bool IsLocaleAvailable(string locale)
{
@ -102,92 +50,90 @@ namespace Microsoft.Bot.Builder.Ai.Translation
return _mapLocaleToFunction.ContainsKey(locale);
}
private static void AssertValidLocale(string locale)
{
if (string.IsNullOrWhiteSpace(locale))
throw new ArgumentNullException(nameof(locale));
}
/// <summary>
/// Extract date and time from a sentence using Microsoft Recognizer
/// Convert a message from locale to another locale.
/// </summary>
/// <param name="message">input user message</param>
/// <param name="fromLocale">Source Locale</param>
/// <returns></returns>
private List<TextAndDateTime> ExtractDate(string message, string fromLocale)
/// <param name="message"> input user message.</param>
/// <param name="fromLocale">Source Locale.</param>
/// <param name="toLocale">Target Locale.</param>
/// <returns>The message converted to the target locale.</returns>
public string Convert(string message, string fromLocale, string toLocale)
{
List<TextAndDateTime> fndDates = new List<TextAndDateTime>();
//extract culture name.
var cultureName = FindCulture(fromLocale);
var results = DateTimeRecognizer.RecognizeDateTime(message, cultureName);
//looping on each result and extracting found date objects from input utterance
foreach (ModelResult result in results)
if (string.IsNullOrEmpty(message))
{
var resolutionValues = (IList<Dictionary<string, string>>)result.Resolution["values"];
string type = result.TypeName.Replace("datetimeV2.", "");
DateTime moment; ;
string momentType;
DateTime momentEnd;
TextAndDateTime curDateTimeText;
if (type.Contains("range"))
throw new ArgumentException("Empty message");
}
var dates = ExtractDate(message, fromLocale);
if (!IsLocaleAvailable(toLocale))
{
throw new InvalidOperationException($"Unsupported from locale: {toLocale}");
}
var processedMessage = message;
foreach (var date in dates)
{
if (date.Range)
{
if (type.Contains("date") && type.Contains("time"))
if (date.Type == "time")
{
momentType = "datetime";
var timeRange = $"{string.Format(_mapLocaleToFunction[toLocale].TimeFormat, date.DateTime)} - {string.Format(_mapLocaleToFunction[toLocale].TimeFormat, date.EndDateTime)}";
processedMessage = Regex.Replace(processedMessage, $"\\b{date.Text}\\b", timeRange, RegexOptions.Singleline | RegexOptions.IgnoreCase);
}
else if (type.Contains("date"))
else if (date.Type == "date")
{
momentType = "date";
var dateRange = $"{string.Format(_mapLocaleToFunction[toLocale].DateFormat, date.DateTime)} - {string.Format(_mapLocaleToFunction[toLocale].DateFormat, date.EndDateTime)}";
processedMessage = Regex.Replace(processedMessage, $"\\b{date.Text}\\b", dateRange, RegexOptions.Singleline | RegexOptions.IgnoreCase);
}
else
{
momentType = "time";
}
moment = DateTime.Parse(resolutionValues.First()["start"]);
momentEnd = DateTime.Parse(resolutionValues.First()["end"]);
curDateTimeText = new TextAndDateTime
{
dateTime = moment,
Text = result.Text,
type = momentType,
range = true,
endDateTime = momentEnd
};
var convertedStartDate = string.Format(_mapLocaleToFunction[toLocale].DateFormat, date.DateTime);
var convertedStartTime = string.Format(_mapLocaleToFunction[toLocale].TimeFormat, date.DateTime);
var convertedEndDate = string.Format(_mapLocaleToFunction[toLocale].DateFormat, date.EndDateTime);
var convertedEndTime = string.Format(_mapLocaleToFunction[toLocale].TimeFormat, date.EndDateTime);
processedMessage = Regex.Replace(processedMessage, $"\\b{date.Text}\\b", $"{convertedStartDate} {convertedStartTime} - {convertedEndDate} {convertedEndTime}", RegexOptions.Singleline | RegexOptions.IgnoreCase);
}
}
else
{
if (type.Contains("date") && type.Contains("time"))
if (date.Type == "time")
{
momentType = "datetime";
processedMessage = Regex.Replace(processedMessage, $"\\b{date.Text}\\b", string.Format(_mapLocaleToFunction[toLocale].TimeFormat, date.DateTime), RegexOptions.Singleline | RegexOptions.IgnoreCase);
}
else if (type.Contains("date"))
else if (date.Type == "date")
{
momentType = "date";
processedMessage = Regex.Replace(processedMessage, $"\\b{date.Text}\\b", string.Format(_mapLocaleToFunction[toLocale].DateFormat, date.DateTime), RegexOptions.Singleline | RegexOptions.IgnoreCase);
}
else
{
momentType = "time";
var convertedDate = string.Format(_mapLocaleToFunction[toLocale].DateFormat, date.DateTime);
var convertedTime = string.Format(_mapLocaleToFunction[toLocale].TimeFormat, date.DateTime);
processedMessage = Regex.Replace(processedMessage, $"\\b{date.Text}\\b", $"{convertedDate} {convertedTime}", RegexOptions.Singleline | RegexOptions.IgnoreCase);
}
moment = resolutionValues.Select(v => DateTime.Parse(v["value"])).FirstOrDefault();
curDateTimeText = new TextAndDateTime
{
dateTime = moment,
Text = result.Text,
type = momentType,
range = false,
};
}
fndDates.Add(curDateTimeText);
}
return fndDates;
return processedMessage;
}
/// <summary>
/// Get all available locales.
/// </summary>
/// <returns>The available locales.</returns>
public string[] GetAvailableLocales() => _mapLocaleToFunction.Keys.ToArray();
private static void AssertValidLocale(string locale)
{
if (string.IsNullOrWhiteSpace(locale))
{
throw new ArgumentNullException(nameof(locale));
}
}
private static string FindCulture(string fromLocale)
{
string culture = fromLocale.Split('-')[0];
var culture = fromLocale.Split('-')[0];
if (fromLocale.StartsWith("fr"))
{
return Culture.French;
@ -214,81 +160,117 @@ namespace Microsoft.Bot.Builder.Ai.Translation
}
else
{
throw (new InvalidOperationException($"Unsupported from locale: {fromLocale}"));
throw new InvalidOperationException($"Unsupported from locale: {fromLocale}");
}
}
/// <summary>
/// Convert a message from locale to another locale
/// Init different locales format,
/// Supporting English, French, Deutsche and Chinese Locales.
/// </summary>
/// <param name="message"> input user message</param>
/// <param name="fromLocale">Source Locale</param>
/// <param name="toLocale">Target Locale</param>
/// <returns></returns>
public string Convert(string message, string fromLocale, string toLocale)
private void InitLocales()
{
if (string.IsNullOrEmpty(message))
if (_mapLocaleToFunction.Count > 0)
{
return;
}
var supportedLocales = new string[]
{
throw (new ArgumentException("Empty message"));
}
List<TextAndDateTime> dates = ExtractDate(message, fromLocale);
if (!IsLocaleAvailable(toLocale))
"en-us", "en-za", "en-ie", "en-gb", "en-ca", "fr-ca", "zh-cn", "zh-sg", "zh-hk", "zh-mo", "zh-tw",
"en-au", "fr-be", "fr-ch", "fr-fr", "fr-lu", "fr-mc", "de-at", "de-ch", "de-de", "de-lu", "de-li",
"es-es",
};
foreach (var locale in supportedLocales)
{
throw (new InvalidOperationException($"Unsupported from locale: {toLocale}"));
}
string processedMessage = message;
foreach (TextAndDateTime date in dates)
{
if (date.range)
var cultureInfo = new CultureInfo(locale);
var dateTimeInfo = new DateAndTimeLocaleFormat()
{
if (date.type == "time")
DateFormat = $"{{0:{cultureInfo.DateTimeFormat.ShortDatePattern}}}",
TimeFormat = $"{{0:{cultureInfo.DateTimeFormat.ShortTimePattern}}}",
};
_mapLocaleToFunction[locale] = dateTimeInfo;
}
}
/// <summary>
/// Extract date and time from a sentence using Microsoft Recognizer.
/// </summary>
/// <param name="message">input user message.</param>
/// <param name="fromLocale">Source Locale.</param>
/// <returns>A <see cref="List{T}"/> of <see cref="TextAndDateTime"/> recognized from a sentence.</returns>
private List<TextAndDateTime> ExtractDate(string message, string fromLocale)
{
var fndDates = new List<TextAndDateTime>();
// extract culture name.
var cultureName = FindCulture(fromLocale);
var results = DateTimeRecognizer.RecognizeDateTime(message, cultureName);
// looping on each result and extracting found date objects from input utterance
foreach (var result in results)
{
var resolutionValues = (IList<Dictionary<string, string>>)result.Resolution["values"];
var type = result.TypeName.Replace("datetimeV2.", string.Empty);
DateTime moment;
string momentType;
DateTime momentEnd;
TextAndDateTime curDateTimeText;
if (type.Contains("range"))
{
if (type.Contains("date") && type.Contains("time"))
{
var timeRange = $"{String.Format(_mapLocaleToFunction[toLocale].TimeFormat, date.dateTime)} - {String.Format(_mapLocaleToFunction[toLocale].TimeFormat, date.endDateTime)}";
processedMessage = Regex.Replace(processedMessage, $"\\b{date.Text}\\b", timeRange, RegexOptions.Singleline | RegexOptions.IgnoreCase);
momentType = "datetime";
}
else if (date.type == "date")
else if (type.Contains("date"))
{
var dateRange = $"{String.Format(_mapLocaleToFunction[toLocale].DateFormat, date.dateTime)} - {String.Format(_mapLocaleToFunction[toLocale].DateFormat, date.endDateTime)}";
processedMessage = Regex.Replace(processedMessage, $"\\b{date.Text}\\b", dateRange, RegexOptions.Singleline | RegexOptions.IgnoreCase);
momentType = "date";
}
else
{
var convertedStartDate = String.Format(_mapLocaleToFunction[toLocale].DateFormat, date.dateTime);
var convertedStartTime = String.Format(_mapLocaleToFunction[toLocale].TimeFormat, date.dateTime);
var convertedEndDate = String.Format(_mapLocaleToFunction[toLocale].DateFormat, date.endDateTime);
var convertedEndTime = String.Format(_mapLocaleToFunction[toLocale].TimeFormat, date.endDateTime);
processedMessage = Regex.Replace(processedMessage, $"\\b{date.Text}\\b", $"{convertedStartDate} {convertedStartTime} - {convertedEndDate} {convertedEndTime}", RegexOptions.Singleline | RegexOptions.IgnoreCase);
momentType = "time";
}
moment = DateTime.Parse(resolutionValues.First()["start"]);
momentEnd = DateTime.Parse(resolutionValues.First()["end"]);
curDateTimeText = new TextAndDateTime
{
DateTime = moment,
Text = result.Text,
Type = momentType,
Range = true,
EndDateTime = momentEnd,
};
}
else
{
if (date.type == "time")
if (type.Contains("date") && type.Contains("time"))
{
processedMessage = Regex.Replace(processedMessage, $"\\b{date.Text}\\b", String.Format(_mapLocaleToFunction[toLocale].TimeFormat, date.dateTime), RegexOptions.Singleline | RegexOptions.IgnoreCase);
momentType = "datetime";
}
else if (date.type == "date")
else if (type.Contains("date"))
{
processedMessage = Regex.Replace(processedMessage, $"\\b{date.Text}\\b", String.Format(_mapLocaleToFunction[toLocale].DateFormat, date.dateTime), RegexOptions.Singleline | RegexOptions.IgnoreCase);
momentType = "date";
}
else
{
var convertedDate = String.Format(_mapLocaleToFunction[toLocale].DateFormat, date.dateTime);
var convertedTime = String.Format(_mapLocaleToFunction[toLocale].TimeFormat, date.dateTime);
processedMessage = Regex.Replace(processedMessage, $"\\b{date.Text}\\b", $"{convertedDate} {convertedTime}", RegexOptions.Singleline | RegexOptions.IgnoreCase);
momentType = "time";
}
moment = resolutionValues.Select(v => DateTime.Parse(v["value"])).FirstOrDefault();
curDateTimeText = new TextAndDateTime
{
DateTime = moment,
Text = result.Text,
Type = momentType,
Range = false,
};
}
fndDates.Add(curDateTimeText);
}
return processedMessage;
return fndDates;
}
/// <summary>
/// Get all available locales
/// </summary>
/// <returns></returns>
public string[] GetAvailableLocales()
{
return _mapLocaleToFunction.Keys.ToArray();
}
}
}
}
}

Просмотреть файл

@ -1,83 +1,94 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Microsoft.Bot.Schema;
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Schema;
namespace Microsoft.Bot.Builder.Ai.Translation
{
/// <summary>
/// Middleware to convert messages between different locales specified
/// Middleware that translates from the input locale to a specified locale.
/// </summary>
public class LocaleConverterMiddleware : IMiddleware
{
private readonly ILocaleConverter _localeConverter;
private readonly ILocaleConverter _localeConverter;
private readonly string _toLocale;
private readonly Func<ITurnContext, string> _getUserLocale;
private readonly Func<ITurnContext, Task<bool>> _setUserLocale;
/// <summary>
/// Constructor for developer defined detection of user messages
private readonly Func<ITurnContext, Task<bool>> _setUserLocale;
/// <summary>
/// Initializes a new instance of the <see cref="LocaleConverterMiddleware"/> class.
/// </summary>
/// <param name="getUserLocale">Delegate for getting the user locale</param>
/// <param name="checkUserLocaleChanged">Delegate that returns true if the locale was changed (implements logic to change locale by intercepting the message)</param>
/// <param name="toLocale">Target Locale</param>
/// <param name="localeConverter">An ILocaleConverter instance</param>
/// <param name="getUserLocale">Delegate for getting the user locale.</param>
/// <param name="checkUserLocaleChanged">Delegate that returns true if the locale was
/// changed (implements logic to change locale by intercepting the message).</param>
/// <param name="toLocale">The target locale.</param>
/// <param name="localeConverter">The locale converter to use.</param>
/// <exception cref="ArgumentNullException">Thrown when any of the parameters is null.</exception>
public LocaleConverterMiddleware(Func<ITurnContext, string> getUserLocale, Func<ITurnContext, Task<bool>> checkUserLocaleChanged, string toLocale, ILocaleConverter localeConverter)
{
_localeConverter = localeConverter ?? throw new ArgumentNullException(nameof(localeConverter));
if (string.IsNullOrEmpty(toLocale))
throw new ArgumentNullException(nameof(toLocale));
else if( !localeConverter.IsLocaleAvailable(toLocale))
throw new ArgumentNullException("The locale " +nameof(toLocale)+" is unavailable");
if (string.IsNullOrEmpty(toLocale))
{
throw new ArgumentNullException(nameof(toLocale));
}
else if (!localeConverter.IsLocaleAvailable(toLocale))
{
throw new ArgumentNullException("The locale " + nameof(toLocale) + " is unavailable");
}
_toLocale = toLocale;
_getUserLocale = getUserLocale ?? throw new ArgumentNullException(nameof(getUserLocale));
_getUserLocale = getUserLocale ?? throw new ArgumentNullException(nameof(getUserLocale));
_setUserLocale = checkUserLocaleChanged ?? throw new ArgumentNullException(nameof(checkUserLocaleChanged));
}
/// <summary>
/// Incoming activity
/// Processess an incoming activity.
/// </summary>
/// <param name="context"></param>
/// <param name="next"></param>
/// <returns></returns>
public async Task OnTurn(ITurnContext context, NextDelegate next, CancellationToken cancellationToken)
/// <param name="context">The context object for this turn.</param>
/// <param name="next">The delegate to call to continue the bot middleware pipeline.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>This middleware converts the text of incoming message activities to the target locale
/// on the leading edge of the middleware pipeline.</remarks>
public async Task OnTurnAsync(ITurnContext context, NextDelegate next, CancellationToken cancellationToken)
{
if (context.Activity.Type == ActivityTypes.Message)
{
IMessageActivity message = context.Activity.AsMessageActivity();
var message = context.Activity.AsMessageActivity();
if (message != null)
{
if (!String.IsNullOrWhiteSpace(message.Text))
if (!string.IsNullOrWhiteSpace(message.Text))
{
bool localeChanged = await _setUserLocale(context);
var localeChanged = await _setUserLocale(context).ConfigureAwait(false);
if (!localeChanged)
{
string fromLocale = _getUserLocale(context);
var fromLocale = _getUserLocale(context);
ConvertLocaleMessage(context, fromLocale);
}
else {
else
{
// skip routing in case of user changed the locale
return ;
return;
}
}
}
}
await next(cancellationToken).ConfigureAwait(false);
}
private void ConvertLocaleMessage(ITurnContext context,string fromLocale)
private void ConvertLocaleMessage(ITurnContext context, string fromLocale)
{
IMessageActivity message = context.Activity.AsMessageActivity();
var message = context.Activity.AsMessageActivity();
if (message != null)
{
if (_localeConverter.IsLocaleAvailable(fromLocale) && fromLocale != _toLocale)
{
string localeConvertedText = _localeConverter.Convert(message.Text, fromLocale, _toLocale);
var localeConvertedText = _localeConverter.Convert(message.Text, fromLocale, _toLocale);
message.Text = localeConvertedText;
}
}

Просмотреть файл

@ -8,7 +8,7 @@ namespace Microsoft.Bot.Builder.Ai.Translation
/// </summary>
public class MessagesProvider
{
//error messages
// error messages
public const string ExistingDictionaryErrorMessage = "A language dictionary with the same language id already exists";
public const string NonExistentDictionaryErrorMessage = "No dictionary found that matches the provided language id";
public const string IncorrectAlignmentFormatErrorMessage = "Incorrect alignment format, please use the following format for each alignment entry : [starting_source_index]:[ending_source_index]-[starting_translated_index]:[ending_translated_index] ";
@ -17,7 +17,7 @@ namespace Microsoft.Bot.Builder.Ai.Translation
public const string NotFoundTokenInSourceArray = "Token not found in the specified array, check the alignment information";
public const string EmptyPatternsErrorMessage = "Patterns dictionary can't be empty";
//information messages
// information messages
public const string MissingTranslatorEnvironmentVariablesMessage = "Missing Translator Environment variables - Skipping test";
}
}

Просмотреть файл

@ -38,18 +38,24 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Documentation|AnyCPU'">
<DocumentationFile>bin\$(Configuration)\netstandard2.0\Microsoft.Bot.Builder.Ai.Translation.xml</DocumentationFile>
<DocumentationFile>bin\$(Configuration)\netstandard2.0\Microsoft.Bot.Builder.Ai.Translation.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DocumentationFile>bin\$(Configuration)\netstandard2.0\Microsoft.Bot.Builder.Ai.Translation.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>Microsoft.Bot.Builder.Ai.Translation.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AsyncUsageAnalyzers" Version="1.0.0-alpha003" PrivateAssets="all" />
<PackageReference Include="Microsoft.Bot.Builder" Condition=" '$(PackageVersion)' == '' " Version="4.0.0-local" />
<PackageReference Include="Microsoft.Bot.Builder" Condition=" '$(PackageVersion)' != '' " Version="$(PackageVersion)" />
<PackageReference Include="Microsoft.Recognizers.Text.DateTime" Version="1.0.8.2" />
<PackageReference Include="SourceLink.Create.CommandLine" Version="2.8.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta008" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for StyleCop.Analyzers" Description="Code analysis rules for StyleCop.Analyzers.csproj." ToolsVersion="14.0">
<Rules AnalyzerId="AsyncUsageAnalyzers" RuleNamespace="AsyncUsageAnalyzers">
<Rule Id="UseConfigureAwait" Action="Warning" />
<Rule Id="AvoidAsyncVoid" Action="Warning" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Features" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Features">
<Rule Id="IDE0003" Action="None" />
</Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<Rule Id="SA1101" Action="None" />
<!-- local calls prefixed with this -->
<Rule Id="SA1200" Action="None" />
<Rule Id="SA1305" Action="Warning" />
<Rule Id="SA1309" Action="None" />
<Rule Id="SA1412" Action="Warning" />
<Rule Id="SA1600" Action="None" />
<Rule Id="SA1609" Action="Warning" />
<Rule Id="SA1633" Action="None" />
</Rules>
</RuleSet>

Просмотреть файл

@ -2,22 +2,20 @@
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
{
/// <summary>
/// Custom dictionary post processor is used to forcibly translate certain vocab from a provided user dictinary.
/// Custom dictionary post processor is used to forcibly translate certain vocab from a provided user dictionary.
/// </summary>
public class CustomDictionaryPostProcessor : IPostProcessor
{
private readonly CustomDictionary _userCustomDictionaries;
/// <summary>
/// Constructor a new <see cref="CustomDictionaryPostProcessor"/> object.
private readonly CustomDictionary _userCustomDictionaries;
/// <summary>
/// Initializes a new instance of the <see cref="CustomDictionaryPostProcessor"/> class.
/// </summary>
/// <param name="userCustomDictionaries">A <see cref="CustomDictionary"/> object that stores all the different languages dictionaries keyed by language id</param>
/// <param name="userCustomDictionaries">A <see cref="CustomDictionary"/> object that stores all the different languages dictionaries keyed by language id.</param>
public CustomDictionaryPostProcessor(CustomDictionary userCustomDictionaries)
{
this._userCustomDictionaries = userCustomDictionaries ?? throw new ArgumentNullException(nameof(userCustomDictionaries));
@ -28,7 +26,7 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
/// </summary>
/// <param name="translatedDocument">Translated document.</param>
/// <param name="languageId">Current source language id.</param>
/// <returns>A <see cref="PostProcessedDocument"/> stores the original translated document state and the newly post processed message</returns>
/// <returns>A <see cref="PostProcessedDocument"/> stores the original translated document state and the newly post processed message.</returns>
public PostProcessedDocument Process(TranslatedDocument translatedDocument, string languageId)
{
// Check if provided custom dictionary for this language is not empty
@ -36,19 +34,21 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
{
string processedResult;
var languageDictionary = _userCustomDictionaries.GetLanguageDictionary(languageId);
// Loop for all the original message tokens, and check if any of these tokens exists in the user custom dictionary,
// to forcibly overwrite this token's translation with the user provided translation
for (int i = 0; i < translatedDocument.SourceTokens.Length; i++)
for (var i = 0; i < translatedDocument.SourceTokens.Length; i++)
{
if (languageDictionary.ContainsKey(translatedDocument.SourceTokens[i]))
{
// If a token of the original source message/phrase found in the user dictionary ,
// If a token of the original source message/phrase found in the user dictionary,
// replace it's equivalent translated token with the user provided translation
// the equivalent translated token can be found using the alignment map in the translated document
translatedDocument.TranslatedTokens[translatedDocument.IndexedAlignment[i]] = languageDictionary[translatedDocument.SourceTokens[i]];
}
}
// Finally return PostProcessedDocument object that holds the orignal TRanslatedDocument and a string that joins all the translated tokens together
// Finally return PostProcessedDocument object that holds the orignal TRanslatedDocument and a string that joins all the translated tokens together
processedResult = PostProcessingUtilities.Join(" ", translatedDocument.TranslatedTokens);
return new PostProcessedDocument(translatedDocument, processedResult);
}

Просмотреть файл

@ -4,7 +4,7 @@
namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
{
/// <summary>
/// Abstraction for post processor
/// Abstraction for post processor.
/// </summary>
public interface IPostProcessor
{
@ -14,7 +14,7 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
/// </summary>
/// <param name="translatedDocument">Translated document.</param>
/// <param name="languageId">Current source language id.</param>
/// <returns><see cref="PostProcessedDocument"/> that holds the original document and the newly post processed message/phrease</returns>
/// <returns><see cref="PostProcessedDocument"/> that holds the original document and the newly post processed message/phrease.</returns>
PostProcessedDocument Process(TranslatedDocument translatedDocument, string languageId);
}
}

Просмотреть файл

@ -3,8 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
@ -16,12 +14,12 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
/// </summary>
public class PatternsPostProcessor : IPostProcessor
{
private readonly Dictionary<string, HashSet<string>> _processedPatterns;
/// <summary>
/// Constructor that indexes input template for the source language.
private readonly Dictionary<string, HashSet<string>> _processedPatterns;
/// <summary>
/// Initializes a new instance of the <see cref="PatternsPostProcessor"/> class that indexes input template for the source language.
/// </summary>
/// <param name="patterns">No translate patterns for different languages.</param>
/// <param name="patterns">No translate patterns for different languages.</param>
public PatternsPostProcessor(Dictionary<string, List<string>> patterns)
{
if (patterns == null)
@ -35,17 +33,19 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
}
_processedPatterns = new Dictionary<string, HashSet<string>>();
foreach (KeyValuePair<string, List<string>> item in patterns)
foreach (var item in patterns)
{
_processedPatterns.Add(item.Key, new HashSet<string>());
foreach (string pattern in item.Value)
foreach (var pattern in item.Value)
{
string processedLine = pattern.Trim();
//if (the pattern doesn't follow this format (pattern), add the braces around the pattern
var processedLine = pattern.Trim();
// if (the pattern doesn't follow this format (pattern), add the braces around the pattern
if (!Regex.IsMatch(pattern, "(\\(.+\\))"))
{
processedLine = '(' + processedLine + ')';
}
_processedPatterns[item.Key].Add(processedLine);
}
}
@ -59,24 +59,24 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
/// <returns>A <see cref="PostProcessedDocument"/> stores the original translated document state and the newly post processed message.</returns>
public PostProcessedDocument Process(TranslatedDocument translatedDocument, string languageId)
{
//validate function arguments for null and incorrect format
// validate function arguments for null and incorrect format
ValidateParameters(translatedDocument);
//flag to indicate if the source message contains number , will used for
bool containsNum = Regex.IsMatch(translatedDocument.SourceMessage, @"\d");
// flag to indicate if the source message contains number , will used for
var containsNum = Regex.IsMatch(translatedDocument.SourceMessage, @"\d");
//output variable declaration
// output variable declaration
string processedResult;
//temporary pattern is used to contain two set of patterns :
// temporary pattern is used to contain two set of patterns :
// - the post processed patterns that was configured by the user ie : _processedPatterns and
// - the liternal no translate pattern ie : translatedDocument.LiteranlNoTranslatePhrases , which takes the following regx "<literal>(.*)</literal>" , so the following code checks if this pattern exists in the translated document object to be added to the no translate list
// - ex : translatedDocument.sourceMessage = I like my friend <literal>happy</literal> , the literal tag here specifies that the word "happy" shouldn't be translated
HashSet<string> temporaryPatterns = _processedPatterns[languageId];
var temporaryPatterns = _processedPatterns[languageId];
if (translatedDocument.LiteranlNoTranslatePhrases != null && translatedDocument.LiteranlNoTranslatePhrases.Count > 0)
{
temporaryPatterns.UnionWith((translatedDocument.LiteranlNoTranslatePhrases));
temporaryPatterns.UnionWith(translatedDocument.LiteranlNoTranslatePhrases);
}
if (temporaryPatterns.Count == 0 && !containsNum)
@ -89,86 +89,90 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
processedResult = translatedDocument.TargetMessage;
}
//loop for all the patterns and substitute each no translate pattern match with the original source words
// loop for all the patterns and substitute each no translate pattern match with the original source words
//ex : assuming the pattern = "mon nom est (.+)"
//and the phrase = "mon nom est l'etat"
//the original translator output for this phrase would be "My name is the state",
//after applying the patterns post processor , the output would be : "My name is l'etat"
foreach (string pattern in temporaryPatterns)
// ex : assuming the pattern = "mon nom est (.+)"
// and the phrase = "mon nom est l'etat"
// the original translator output for this phrase would be "My name is the state",
// after applying the patterns post processor , the output would be : "My name is l'etat"
foreach (var pattern in temporaryPatterns)
{
if (Regex.IsMatch(translatedDocument.SourceMessage, pattern, RegexOptions.Singleline | RegexOptions.IgnoreCase))
{
SubstituteNoTranslatePattern(translatedDocument, pattern);
}
}
SubstituteNumericPattern(translatedDocument);
processedResult = PostProcessingUtilities.Join(" ", translatedDocument.TranslatedTokens);
return new PostProcessedDocument(translatedDocument, processedResult);
}
/// <summary>
/// Substitutes matched no translate pattern with the original token
/// Substitutes matched no translate pattern with the original token.
/// </summary>
/// <param name="translatedDocument">Translated document.</param>
/// <param name="pattern">The no translate pattern.</param>
private void SubstituteNoTranslatePattern(TranslatedDocument translatedDocument, string pattern)
{
//get the matched no translate pattern
Match matchNoTranslate = Regex.Match(translatedDocument.SourceMessage, pattern, RegexOptions.Singleline | RegexOptions.IgnoreCase);
//calculate the boundaries of the pattern match
//ex : "mon nom est l'etat
// get the matched no translate pattern
var matchNoTranslate = Regex.Match(translatedDocument.SourceMessage, pattern, RegexOptions.Singleline | RegexOptions.IgnoreCase);
// calculate the boundaries of the pattern match
// ex : "mon nom est l'etat
// start index = 12
//length = 6
int noTranslateStartChrIndex = matchNoTranslate.Groups[1].Index;
//the length of the matched pattern without spaces , which will be used in determining the translated tokens that will be replaced by their original values
int noTranslateMatchLength = matchNoTranslate.Groups[1].Value.Replace(" ", "").Length;
int wrdIndx = 0;
int chrIndx = 0;
int newChrLengthFromMatch = 0;
int srcIndex = -1;
int newNoTranslateArrayLength = 1;
// length = 6
var noTranslateStartChrIndex = matchNoTranslate.Groups[1].Index;
// the length of the matched pattern without spaces , which will be used in determining the translated tokens that will be replaced by their original values
var noTranslateMatchLength = matchNoTranslate.Groups[1].Value.Replace(" ", string.Empty).Length;
var wrdIndx = 0;
var chrIndx = 0;
var newChrLengthFromMatch = 0;
var srcIndex = -1;
var newNoTranslateArrayLength = 1;
var sourceMessageCharacters = translatedDocument.SourceMessage.ToCharArray();
foreach (string wrd in translatedDocument.SourceTokens)
foreach (var wrd in translatedDocument.SourceTokens)
{
//if the beginning of the current word equals the beginning of the matched no trasnalate word, then assign the current word index to srcIndex
// if the beginning of the current word equals the beginning of the matched no trasnalate word, then assign the current word index to srcIndex
if (chrIndx == noTranslateStartChrIndex)
{
srcIndex = wrdIndx;
}
//the following code block does the folowing :
//- checks if a match wsa found
//- checks if this match length equals the starting matching token length, if yes then this is the only token to process,
//otherwise continue the loop and add the next token to the list of tokens to be processed
//ex : "mon nom est l'etat"
//tokens = {"mon", "nom", "est", "l'", "etat"}
//when the loop reaches the token "l'" then srcIndex will = 3, but we don't want to consider only the token "l'" as the no translate token,
//instead we want to match the whole "l'etat" string regardless how many tokens it contains ie regardless that "l'etat" is actually composed of 2 tokens "l'" and "etat"
//so what these condition is trying to do is make the necessary checks that we got all the matched pattern not just a part of it's tokens!
// the following code block does the folowing :
// - checks if a match wsa found
// - checks if this match length equals the starting matching token length, if yes then this is the only token to process,
// otherwise continue the loop and add the next token to the list of tokens to be processed
// ex : "mon nom est l'etat"
// tokens = {"mon", "nom", "est", "l'", "etat"}
// when the loop reaches the token "l'" then srcIndex will = 3, but we don't want to consider only the token "l'" as the no translate token,
// instead we want to match the whole "l'etat" string regardless how many tokens it contains ie regardless that "l'etat" is actually composed of 2 tokens "l'" and "etat"
// so what these condition is trying to do is make the necessary checks that we got all the matched pattern not just a part of it's tokens!
//checks if match was found or not, because srcIndex value changes only in case a match was found !
// checks if match was found or not, because srcIndex value changes only in case a match was found !
if (srcIndex != -1)
{
//checks if we found all the tokens that matches the pattern
if (newChrLengthFromMatch + translatedDocument.SourceTokens[wrdIndx].Length >= noTranslateMatchLength)
break;
//if the previous condition fails it means that the next token is also matched in the pattern, so we increase the size of the no translate words array by 1
// checks if we found all the tokens that matches the pattern
if (newChrLengthFromMatch + translatedDocument.SourceTokens[wrdIndx].Length >= noTranslateMatchLength)
{
break;
}
// if the previous condition fails it means that the next token is also matched in the pattern, so we increase the size of the no translate words array by 1
newNoTranslateArrayLength += 1;
//increment newChrLengthFromMatch with the found word size
// increment newChrLengthFromMatch with the found word size
newChrLengthFromMatch += translatedDocument.SourceTokens[wrdIndx].Length;
}
// the following block of code is used to calculate the next token starting index which could have two cases
//the first case is that the current token is followed by a space in this case we increment the next chrIndx by 1 to get the next character after the space
//the second case is that the token is followed by the next token without spaces , in this case we calculate chrIndx as chrIndx += wrd.Length without incrementing
//assumption : The provided sourceMessage and sourceMessageCharacters doesn't contain any consecutive white spaces,
//in our use case this handling is done using the translator output itself using the following line of code in PreprocessMessage function :
//textToTranslate = Regex.Replace(textToTranslate, @"\s+", " ");//used to remove multiple spaces in input user message
// the first case is that the current token is followed by a space in this case we increment the next chrIndx by 1 to get the next character after the space
// the second case is that the token is followed by the next token without spaces , in this case we calculate chrIndx as chrIndx += wrd.Length without incrementing
// assumption : The provided sourceMessage and sourceMessageCharacters doesn't contain any consecutive white spaces,
// in our use case this handling is done using the translator output itself using the following line of code in PreprocessMessage function:
// textToTranslate = Regex.Replace(textToTranslate, @"\s+", " ");//used to remove multiple spaces in input user message
if (chrIndx + wrd.Length < sourceMessageCharacters.Length && sourceMessageCharacters[chrIndx + wrd.Length] == ' ')
{
chrIndx += wrd.Length + 1;
@ -177,18 +181,22 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
{
chrIndx += wrd.Length;
}
wrdIndx++;
}
//if the loop ends and srcIndex then no match was found
if (srcIndex == -1)
return;
//add the no translate words to a new array
string[] wrdNoTranslate = new string[newNoTranslateArrayLength];
// if the loop ends and srcIndex then no match was found
if (srcIndex == -1)
{
return;
}
// add the no translate words to a new array
var wrdNoTranslate = new string[newNoTranslateArrayLength];
Array.Copy(translatedDocument.SourceTokens, srcIndex, wrdNoTranslate, 0, newNoTranslateArrayLength);
//loop for each of the no translate words and replace it's translation with it's origin
foreach (string srcWrd in wrdNoTranslate)
// loop for each of the no translate words and replace it's translation with it's origin
foreach (var srcWrd in wrdNoTranslate)
{
translatedDocument.TranslatedTokens = PostProcessingUtilities.KeepSourceWordInTranslation(translatedDocument.IndexedAlignment, translatedDocument.SourceTokens, translatedDocument.TranslatedTokens, srcIndex);
srcIndex++;
@ -201,10 +209,10 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
/// <param name="translatedDocument">Translated document.</param>
private void SubstituteNumericPattern(TranslatedDocument translatedDocument)
{
MatchCollection numericMatches = Regex.Matches(translatedDocument.SourceMessage, @"\d+", RegexOptions.Singleline);
var numericMatches = Regex.Matches(translatedDocument.SourceMessage, @"\d+", RegexOptions.Singleline);
foreach (Match numericMatch in numericMatches)
{
int srcIndex = Array.FindIndex(translatedDocument.SourceTokens, row => row == numericMatch.Groups[0].Value);
var srcIndex = Array.FindIndex(translatedDocument.SourceTokens, row => row == numericMatch.Groups[0].Value);
translatedDocument.TranslatedTokens = PostProcessingUtilities.KeepSourceWordInTranslation(translatedDocument.IndexedAlignment, translatedDocument.SourceTokens, translatedDocument.TranslatedTokens, srcIndex);
}
}
@ -212,7 +220,7 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
/// <summary>
/// Validate <see cref="TranslatedDocument"/> object main parameters for null values.
/// </summary>
/// <param name="translatedDocument"></param>
/// <param name="translatedDocument">The document to validate.</param>
private void ValidateParameters(TranslatedDocument translatedDocument)
{
if (translatedDocument == null)

Просмотреть файл

@ -7,21 +7,20 @@ namespace Microsoft.Bot.Builder.Ai.Translation
/// A class to store the state of the translated document before and after the post processing.
/// </summary>
public class PostProcessedDocument
{
private TranslatedDocument translatedDocument;
private string postProcessedMessage;
/// <summary>
/// Constructor that initializes a post processed document object using the two states.
{
/// <summary>
/// Initializes a new instance of the <see cref="PostProcessedDocument"/> class using the two states.
/// </summary>
/// <param name="translatedDocument">Translated object to be post processed</param>
/// <param name="postProcessedMessage">The result message/translation after the post processing</param>
/// <param name="translatedDocument">Translated object to be post processed.</param>
/// <param name="postProcessedMessage">The result message/translation after the post processing.</param>
public PostProcessedDocument(TranslatedDocument translatedDocument, string postProcessedMessage)
{
this.translatedDocument = translatedDocument;
this.postProcessedMessage = postProcessedMessage;
this.TranslatedDocument = translatedDocument;
this.PostProcessedMessage = postProcessedMessage;
}
public TranslatedDocument TranslatedDocument { get => translatedDocument; set => translatedDocument = value; }
public string PostProcessedMessage { get => postProcessedMessage; set => postProcessedMessage = value; }
public TranslatedDocument TranslatedDocument { get; set; }
public string PostProcessedMessage { get; set; }
}
}

Просмотреть файл

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
@ -14,12 +13,12 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
/// <summary>
/// Helper to join words to sentence.
/// </summary>
/// <param name="delimiter">String delimiter used to join words.</param>
/// <param name="words">String Array of words to be joined.</param>
/// <param name="delimiter">String delimiter used to join words.</param>
/// <param name="words">String Array of words to be joined.</param>
/// <returns>A <see cref="string"/> joined sentence.</returns>
public static string Join(string delimiter, string[] words)
{
//null checking parameters
// null checking parameters
if (delimiter == null)
{
throw new ArgumentNullException(nameof(delimiter));
@ -30,17 +29,18 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
throw new ArgumentNullException(nameof(words));
}
//join the tokens using the specified delimiter
//ex : words = {"I", "didn", "'t", "see", "you"}
// join the tokens using the specified delimiter
// ex : words = {"I", "didn", "'t", "see", "you"}
// delimiter = " "
string sentence = string.Join(delimiter, words);
//sentence = I didn 't see you
var sentence = string.Join(delimiter, words);
// sentence = I didn 't see you
//if found this punctuation pattern, overwrite it with only apostrophe, ie : remove the delimited around the apostrophe
// if found this punctuation pattern, overwrite it with only apostrophe, ie : remove the delimited around the apostrophe
sentence = Regex.Replace(sentence, "[ ]?'[ ]?", "'");
//sentence = I didn't see you
//the space delimiter is removed from the sentence
// sentence = I didn't see you
// the space delimiter is removed from the sentence
return sentence.Trim();
}
@ -49,9 +49,8 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
/// </summary>
/// <param name="sentence">String containing sentence to be splitted.</param>
/// <param name="alignments">Alignment information from translator output.</param>
/// <param name="isSourceSentence">Flag to indicate if the sentence sent is a source sentence or translated sentence</param>
/// <param name="isSourceSentence">Flag to indicate if the sentence sent is a source sentence or translated <paramref name="sentence"/>.</param>
/// <returns>An array that contains the split sentence tokens.</returns>
public static string[] SplitSentence(string sentence, string[] alignments = null, bool isSourceSentence = true)
{
if (sentence == null)
@ -59,64 +58,80 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
throw new ArgumentNullException(nameof(sentence));
}
string[] alignSplitWrds = new string[0];
string[] words = sentence.Split(' ');
var alignSplitWrds = new string[0];
var words = sentence.Split(' ');
//if alignment information exists, use them otherwise return a simple splitting using white space delimiter
// if alignment information exists, use them otherwise return a simple splitting using white space delimiter
if (alignments != null && alignments.Length > 0)
{
List<string> outWords = new List<string>();
var outWords = new List<string>();
//index value to use in the splitted alignment pairs
//ex : var alignment = "0:2-0:1 4:6-3:6" , after splitting using white space it will be :
// index value to use in the splitted alignment pairs
// ex : var alignment = "0:2-0:1 4:6-3:6" , after splitting using white space it will be :
// = {"0:2-0:1", "4:6-3:6"}, after splitting each pair using "-" each pair will be like :
// = {"0:2", "0:1"} the first part of the splitted array represents the source message alignment information
//which will be referenced using wordIndexInAlignment = 0
//while the second part of this string array represents the translated alignment information which will be referenced using wordIndexInAlignment = 1
// = {"0:2", "0:1"} the first part of the splitted array represents the source message alignment information
// which will be referenced using wordIndexInAlignment = 0
// while the second part of this string array represents the translated alignment information which will be referenced using wordIndexInAlignment = 1
//TODO introducer a better data structure for alignment information for less confusing interpretation of string parsing
int wordIndexInAlignment = 0;
// TODO introducer a better data structure for alignment information for less confusing interpretation of string parsing
var wordIndexInAlignment = 0;
if (!isSourceSentence)
{
wordIndexInAlignment = 1;
// reorder alignments in case of target translated message to get ordered output words.
Array.Sort(alignments, (x, y) => Int32.Parse(x.Split('-')[wordIndexInAlignment].Split(':')[0]).CompareTo(Int32.Parse(y.Split('-')[wordIndexInAlignment].Split(':')[0])));
Array.Sort(alignments, (x, y) => int.Parse(x.Split('-')[wordIndexInAlignment].Split(':')[0]).CompareTo(int.Parse(y.Split('-')[wordIndexInAlignment].Split(':')[0])));
}
//remove all spaces and use alignment information to split the words instead
string withoutSpaceSentence = sentence.Replace(" ", "");
// remove all spaces and use alignment information to split the words instead
var withoutSpaceSentence = sentence.Replace(" ", string.Empty);
foreach (string alignData in alignments)
foreach (var alignData in alignments)
{
//initialize alignSplitWrds with e,pty list outWords
// initialize alignSplitWrds with e,pty list outWords
alignSplitWrds = outWords.ToArray();
//get the index range for the current word
//ex : before splittig alignment entry = "0:2-0:1"
//after splittig alignment entry = {"0:2", "0:1"} and whether wordIndexInAlignment = 0 or 1
//the value of wordIndexes according to the example will be either "0:2" or "0:1"
string wordIndexes = alignData.Split('-')[wordIndexInAlignment];
//get the start index of the current word
int startIndex = Int32.Parse(wordIndexes.Split(':')[0]);
//calculate the length of the word using start index and ending index
int length = Int32.Parse(wordIndexes.Split(':')[1]) - startIndex + 1;
//get the word that matches the calculated boundaries
string word = sentence.Substring(startIndex, length);
//the following block of code is customly written to hotfix a problem with the translator api output, that sometimes it duplicates alignment information,
//which leads to a dublicated token, so the whole trick is to make sure before adding the token to splitted output array
//that the this token exists in the correct sequence in the original string
string[] newWrds = new string[outWords.Count + 1];
if (newWrds.Length > 1)
alignSplitWrds.CopyTo(newWrds, 0);
// get the index range for the current word
// ex : before splittig alignment entry = "0:2-0:1"
// after splittig alignment entry = {"0:2", "0:1"} and whether wordIndexInAlignment = 0 or 1
// the value of wordIndexes according to the example will be either "0:2" or "0:1"
var wordIndexes = alignData.Split('-')[wordIndexInAlignment];
// get the start index of the current word
var startIndex = int.Parse(wordIndexes.Split(':')[0]);
// calculate the length of the word using start index and ending index
var length = int.Parse(wordIndexes.Split(':')[1]) - startIndex + 1;
// get the word that matches the calculated boundaries
var word = sentence.Substring(startIndex, length);
// the following block of code is customly written to hotfix a problem with the translator api output, that sometimes it duplicates alignment information,
// which leads to a dublicated token, so the whole trick is to make sure before adding the token to splitted output array
// that the this token exists in the correct sequence in the original string
var newWrds = new string[outWords.Count + 1];
if (newWrds.Length > 1)
{
alignSplitWrds.CopyTo(newWrds, 0);
}
newWrds[outWords.Count] = word;
string subSentence = Join("", newWrds.ToArray());
if (withoutSpaceSentence.Contains(subSentence))
outWords.Add(word);
var subSentence = Join(string.Empty, newWrds.ToArray());
if (withoutSpaceSentence.Contains(subSentence))
{
outWords.Add(word);
}
}
alignSplitWrds = outWords.ToArray();
}
char[] punctuationChars = new char[] { '.', ',', '?', '!' };
if (Join("", alignSplitWrds).TrimEnd(punctuationChars) == Join("", words).TrimEnd(punctuationChars))
return alignSplitWrds;
var punctuationChars = new char[] { '.', ',', '?', '!' };
if (Join(string.Empty, alignSplitWrds).TrimEnd(punctuationChars) == Join(string.Empty, words).TrimEnd(punctuationChars))
{
return alignSplitWrds;
}
return words;
}
@ -131,75 +146,53 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
/// <returns>A <see cref="Dictionary{Int32, UInt32}"/> represents the source to target tokens mapping.</returns>
public static Dictionary<int, int> WordAlignmentParse(string[] alignments, string[] sourceTokens, string[] translatedTokens)
{
//parameter validation
// parameter validation
ValidateParametersWordAlignmentParse(alignments, sourceTokens, translatedTokens);
Dictionary<int, int> alignMap = new Dictionary<int, int>();
string sourceMessage = Join(" ", sourceTokens);
string translatedMessage = Join(" ", translatedTokens);
var alignMap = new Dictionary<int, int>();
var sourceMessage = Join(" ", sourceTokens);
var translatedMessage = Join(" ", translatedTokens);
foreach (string alignData in alignments)
foreach (var alignData in alignments)
{
//split each alignment pair ie : source and target into separate string
//each alignment pair output from translator api follows the following pattern [source_start_character_index]:[source_ending_character_index]-[target_start_character_index]:[target_ending_character_index]
//ex : the following alignment pair "0:2-0:1" means characters from index 0 to index 2 in source message maps to characters from index 0 to index 1 in translated message ,
//that could map to something like trsnlating "mon" in French to "my" in English
string[] wordIndexes = alignData.Split('-');
//source token start index
int srcStartIndex = Int32.Parse(wordIndexes[0].Split(':')[0]);
//source token length
int srcLength = Int32.Parse(wordIndexes[0].Split(':')[1]) - srcStartIndex + 1;
//find the token/word that matches the calculated substring boundaries in source message
string srcWrd = sourceMessage.Substring(srcStartIndex, srcLength);
//get source word index
int sourceWordIndex = Array.FindIndex(sourceTokens, row => row == srcWrd);
// split each alignment pair ie : source and target into separate string
// each alignment pair output from translator api follows the following pattern [source_start_character_index]:[source_ending_character_index]-[target_start_character_index]:[target_ending_character_index]
// ex : the following alignment pair "0:2-0:1" means characters from index 0 to index 2 in source message maps to characters from index 0 to index 1 in translated message ,
// that could map to something like trsnlating "mon" in French to "my" in English
var wordIndexes = alignData.Split('-');
// source token start index
var srcStartIndex = int.Parse(wordIndexes[0].Split(':')[0]);
// source token length
var srcLength = int.Parse(wordIndexes[0].Split(':')[1]) - srcStartIndex + 1;
// find the token/word that matches the calculated substring boundaries in source message
var srcWrd = sourceMessage.Substring(srcStartIndex, srcLength);
// get source word index
var sourceWordIndex = Array.FindIndex(sourceTokens, row => row == srcWrd);
//target/translated token start index
int trgstartIndex = Int32.Parse(wordIndexes[1].Split(':')[0]);
//target/translated token length
int trgLength = Int32.Parse(wordIndexes[1].Split(':')[1]) - trgstartIndex + 1;
//find the token/word that matches the calculated substring boundaries in translated message
string trgWrd = translatedMessage.Substring(trgstartIndex, trgLength);
//get target/translated word index
int targetWordIndex = Array.FindIndex(translatedTokens, row => row == trgWrd);
// target/translated token start index
var trgstartIndex = int.Parse(wordIndexes[1].Split(':')[0]);
// target/translated token length
var trgLength = int.Parse(wordIndexes[1].Split(':')[1]) - trgstartIndex + 1;
// find the token/word that matches the calculated substring boundaries in translated message
var trgWrd = translatedMessage.Substring(trgstartIndex, trgLength);
// get target/translated word index
var targetWordIndex = Array.FindIndex(translatedTokens, row => row == trgWrd);
if (sourceWordIndex >= 0 && targetWordIndex >= 0)
{
alignMap[sourceWordIndex] = targetWordIndex;
}
}
return alignMap;
}
/// <summary>
/// Validate arguments of <see cref="WordAlignmentParse(string[], string[], string[])"/>.
/// </summary>
/// <param name="alignments">Alignments array.</param>
/// <param name="sourceTokens">Array containing source message tokens.</param>
/// <param name="translatedTokens">Array containing translated message tokens.</param>
private static void ValidateParametersWordAlignmentParse(string[] alignments, string[] sourceTokens, string[] translatedTokens)
{
//null check the alignments array
if (alignments == null)
{
throw new ArgumentNullException(nameof(alignments));
}
//validate alignment format
ValidateAlignmentsFormat(alignments);
//null check the source tokens array
if (sourceTokens == null)
{
throw new ArgumentNullException(nameof(sourceTokens));
}
//null check the translated tokens array
if (translatedTokens == null)
{
throw new ArgumentNullException(nameof(translatedTokens));
}
}
}
/// <summary>
/// Use alignment information source sentence and translated sentence
/// to keep a specific word from the source onto target translation.
@ -211,16 +204,47 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
/// <returns>An array that represents the translated tokens after processing.</returns>
public static string[] KeepSourceWordInTranslation(Dictionary<int, int> alignmentMap, string[] sourceTokens, string[] translatedTokens, int sourceTokenIndex)
{
//parameter validation
// parameter validation
ValidateParametersKeepSourceWordInTranslation(alignmentMap, sourceTokens, translatedTokens, sourceTokenIndex);
if (alignmentMap.ContainsKey(sourceTokenIndex))
{
translatedTokens[alignmentMap[sourceTokenIndex]] = sourceTokens[sourceTokenIndex];
}
return translatedTokens;
}
/// <summary>
/// Validate arguments of <see cref="WordAlignmentParse(string[], string[], string[])"/>.
/// </summary>
/// <param name="alignments">Alignments array.</param>
/// <param name="sourceTokens">Array containing source message tokens.</param>
/// <param name="translatedTokens">Array containing translated message tokens.</param>
private static void ValidateParametersWordAlignmentParse(string[] alignments, string[] sourceTokens, string[] translatedTokens)
{
// null check the alignments array
if (alignments == null)
{
throw new ArgumentNullException(nameof(alignments));
}
// validate alignment format
ValidateAlignmentsFormat(alignments);
// null check the source tokens array
if (sourceTokens == null)
{
throw new ArgumentNullException(nameof(sourceTokens));
}
// null check the translated tokens array
if (translatedTokens == null)
{
throw new ArgumentNullException(nameof(translatedTokens));
}
}
/// <summary>
/// Validate arguments of <see cref="KeepSourceWordInTranslation(Dictionary{int, int}, string[], string[], int)"/>.
/// </summary>
@ -235,10 +259,10 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
throw new ArgumentNullException(nameof(alignmentMap));
}
//validate that all values of alignment map are +ve values
// validate that all values of alignment map are +ve values
if (alignmentMap.Count > 0)
{
foreach (KeyValuePair<int, int> alignmentElement in alignmentMap)
foreach (var alignmentElement in alignmentMap)
{
if (alignmentElement.Value < 0)
{
@ -257,7 +281,7 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
throw new ArgumentNullException(nameof(translatedWords));
}
//validate that sourceWordIndex is +ve value
// validate that sourceWordIndex is +ve value
if (sourceWordIndex < 0)
{
throw new ArgumentException(MessagesProvider.NegativeValueSourceWordIndexErrorMessage);
@ -277,12 +301,12 @@ namespace Microsoft.Bot.Builder.Ai.Translation.PostProcessor
if (alignments.Length > 0)
{
foreach (string alignmentElement in alignments)
foreach (var alignmentElement in alignments)
{
//check if the alignment element matches the alignment pattern ,
//for example : 0:2-0:5 , the previous is a valid pattern,
//another example : 0:-23-0:13 this is an incorrect pattern,
//another example : test-test this is an incorrect pattern
// check if the alignment element matches the alignment pattern ,
// for example : 0:2-0:5 , the previous is a valid pattern,
// another example : 0:-23-0:13 this is an incorrect pattern,
// another example : test-test this is an incorrect pattern
if (!Regex.IsMatch(alignmentElement, "[0-9]*:[0-9]*-[0-9]*:[0-9]*"))
{
throw new FormatException(MessagesProvider.IncorrectAlignmentFormatErrorMessage);

Просмотреть файл

@ -3,44 +3,61 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder.Ai.Translation
{
/// <summary>
/// Translated document is the data object holding all information of the translator module output on an input string
/// Translated document is the data object holding all information of the translator module output on an input string.
/// </summary>
public class TranslatedDocument
{
/// <summary>
/// Construct Translated document object using only source message.
{
/// <summary>
/// Initializes a new instance of the <see cref="TranslatedDocument"/> class using only source message.
/// </summary>
/// <param name="sourceMessage">Source message</param>
/// <param name="sourceMessage">Source message.</param>
public TranslatedDocument(string sourceMessage)
{
if (string.IsNullOrWhiteSpace(sourceMessage)) throw new ArgumentNullException(nameof(sourceMessage));
if (string.IsNullOrWhiteSpace(sourceMessage))
{
throw new ArgumentNullException(nameof(sourceMessage));
}
this.SourceMessage = sourceMessage;
}
/// <summary>
/// Construct Translated document object using source message and target/translated message.
}
/// <summary>
/// Initializes a new instance of the <see cref="TranslatedDocument"/> class using source message and target/translated message.
/// </summary>
/// <param name="sourceMessage">Source message</param>
/// <param name="targetMessage">Target/translated message</param>
/// <param name="sourceMessage">Source message.</param>
/// <param name="targetMessage">Target/translated message.</param>
public TranslatedDocument(string sourceMessage, string targetMessage)
{
if (string.IsNullOrWhiteSpace(sourceMessage)) throw new ArgumentNullException(nameof(sourceMessage));
if (string.IsNullOrWhiteSpace(targetMessage)) throw new ArgumentNullException(nameof(targetMessage));
if (string.IsNullOrWhiteSpace(sourceMessage))
{
throw new ArgumentNullException(nameof(sourceMessage));
}
if (string.IsNullOrWhiteSpace(targetMessage))
{
throw new ArgumentNullException(nameof(targetMessage));
}
this.SourceMessage = sourceMessage;
this.TargetMessage = targetMessage;
}
public string SourceMessage { get; set; }
public string TargetMessage { get; set; }
public string RawAlignment { get; set; }
public Dictionary<int, int> IndexedAlignment { get; set; }
public string[] SourceTokens { get; set; }
public string[] TranslatedTokens { get; set; }
public HashSet<string> LiteranlNoTranslatePhrases { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
namespace Microsoft.Bot.Builder.Ai.Translation
{
/// <summary>
/// TextAndDateTime Class used to store text and date time object
/// from Microsoft Recognizer recognition result.
/// </summary>
internal class TextAndDateTime
{
public string Text { get; set; }
public DateTime DateTime { get; set; }
public string Type { get; set; }
public bool Range { get; set; }
public DateTime EndDateTime { get; set; }
}
}

Просмотреть файл

@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
@ -24,10 +24,10 @@ namespace Microsoft.Bot.Builder.Ai.Translation
private readonly Func<ITurnContext, string> _getUserLanguage;
private readonly Func<ITurnContext, Task<bool>> _isUserLanguageChanged;
private readonly bool _toUserLanguage;
private List<IPostProcessor> attachedPostProcessors;
/// <summary>
/// Creates a new <see cref="TranslationMiddleware"/> object.
private List<IPostProcessor> attachedPostProcessors;
/// <summary>
/// Initializes a new instance of the <see cref="TranslationMiddleware"/> class.
/// </summary>
/// <param name="nativeLanguages">The languages supported by your app.</param>
/// <param name="translatorKey">Your subscription key for the Microsoft Translator Text API.</param>
@ -36,77 +36,84 @@ namespace Microsoft.Bot.Builder.Ai.Translation
{
AssertValidNativeLanguages(nativeLanguages);
this._nativeLanguages = nativeLanguages;
if (string.IsNullOrEmpty(translatorKey))
throw new ArgumentNullException(nameof(translatorKey));
if (string.IsNullOrEmpty(translatorKey))
{
throw new ArgumentNullException(nameof(translatorKey));
}
this._translator = new Translator(translatorKey);
_patterns = new Dictionary<string, List<string>>();
_userCustomDictonaries = new CustomDictionary();
_toUserLanguage = toUserLanguage;
}
/// <summary>
/// Creates a new <see cref="TranslationMiddleware"/> object.
}
/// <summary>
/// Initializes a new instance of the <see cref="TranslationMiddleware"/> class.
/// </summary>
/// <param name="nativeLanguages">The languages supported by your app.</param>
/// <param name="translatorKey">Your subscription key for the Microsoft Translator Text API.</param>
/// <param name="patterns">List of regex patterns, indexed by language identifier,
/// <param name="patterns">List of regex patterns, indexed by language identifier,
/// that can be used to flag text that should not be translated.</param>
/// /// <param name="userCustomDictonaries">Custom languages dictionary object, used to store all the different languages dictionaries
/// configured by the user to overwrite the translator output to certain vocab by the custom dictionary translation.</param>
/// <param name="toUserLanguage">Indicates whether to translate messages sent from the bot into the user's language.</param>
/// <remarks>Each pattern the <paramref name="patterns"/> describes an entity that should not be translated.
/// For example, in French <c>je mappelle ([a-z]+)</c>, which will avoid translation of anything coming after je mappelle.</remarks>
public TranslationMiddleware(string[] nativeLanguages, string translatorKey, Dictionary<string, List<string>> patterns, CustomDictionary userCustomDictonaries, bool toUserLanguage = false) : this(nativeLanguages, translatorKey, toUserLanguage)
/// For example, in French <c>je mappelle ([a-z]+)</c>, which will avoid translation of anything coming after je mappelle.</remarks>
public TranslationMiddleware(string[] nativeLanguages, string translatorKey, Dictionary<string, List<string>> patterns, CustomDictionary userCustomDictonaries, bool toUserLanguage = false)
: this(nativeLanguages, translatorKey, toUserLanguage)
{
if (patterns != null)
this._patterns = patterns;
if (userCustomDictonaries != null)
this._userCustomDictonaries = userCustomDictonaries;
}
/// <summary>
/// Creates a new <see cref="TranslationMiddleware"/> object.
if (patterns != null)
{
this._patterns = patterns;
}
if (userCustomDictonaries != null)
{
this._userCustomDictonaries = userCustomDictonaries;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="TranslationMiddleware"/> class.
/// </summary>
/// <param name="nativeLanguages">List of languages supported by your app</param>
/// <param name="nativeLanguages">List of languages supported by your app.</param>
/// <param name="translatorKey">Your subscription key for the Microsoft Translator Text API.</param>
/// <param name="patterns">List of regex patterns, indexed by language identifier,
/// <param name="patterns">List of regex patterns, indexed by language identifier,
/// that can be used to flag text that should not be translated.</param>
/// <param name="userCustomDictonaries">Custom languages dictionary object, used to store all the different languages dictionaries
/// configured by the user to overwrite the translator output to certain vocab by the custom dictionary translation.</param>
/// <param name="getUserLanguage">A delegate for getting the user language,
/// <param name="getUserLanguage">A delegate for getting the user language,
/// to use in place of the Detect method of the Microsoft Translator Text API.</param>
/// <param name="isUserLanguageChanged">A delegate for checking whether the user requested to change their language.</param>
/// <param name="toUserLanguage">Indicates whether to translate messages sent from the bot into the user's language.</param>
/// <remarks>Each pattern the <paramref name="patterns"/> describes an entity that should not be translated.
/// For example, in French <c>je mappelle ([a-z]+)</c>, which will avoid translation of anything coming after je mappelle.</remarks>
public TranslationMiddleware(string[] nativeLanguages, string translatorKey, Dictionary<string, List<string>> patterns, CustomDictionary userCustomDictonaries, Func<ITurnContext, string> getUserLanguage, Func<ITurnContext, Task<bool>> isUserLanguageChanged, bool toUserLanguage = false) : this(nativeLanguages, translatorKey, patterns, userCustomDictonaries, toUserLanguage)
/// For example, in French <c>je mappelle ([a-z]+)</c>, which will avoid translation of anything coming after je mappelle.</remarks>
public TranslationMiddleware(string[] nativeLanguages, string translatorKey, Dictionary<string, List<string>> patterns, CustomDictionary userCustomDictonaries, Func<ITurnContext, string> getUserLanguage, Func<ITurnContext, Task<bool>> isUserLanguageChanged, bool toUserLanguage = false)
: this(nativeLanguages, translatorKey, patterns, userCustomDictonaries, toUserLanguage)
{
_getUserLanguage = getUserLanguage ?? throw new ArgumentNullException(nameof(getUserLanguage));
_isUserLanguageChanged = isUserLanguageChanged ?? throw new ArgumentNullException(nameof(isUserLanguageChanged));
}
private static void AssertValidNativeLanguages(string[] nativeLanguages)
{
if (nativeLanguages == null)
throw new ArgumentNullException(nameof(nativeLanguages));
}
/// <summary>
/// Processess an incoming activity.
/// </summary>
/// <param name="context">The context object for this turn.</param>
/// <param name="next">The delegate to call to continue the bot middleware pipeline.</param>
public virtual async Task OnTurn(ITurnContext context, NextDelegate next, CancellationToken cancellationToken)
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>This middleware converts the text of incoming message activities to the target language
/// on the leading edge of the middleware pipeline.</remarks>
public virtual async Task OnTurnAsync(ITurnContext context, NextDelegate next, CancellationToken cancellationToken)
{
if (context.Activity.Type == ActivityTypes.Message)
{
IMessageActivity message = context.Activity.AsMessageActivity();
var message = context.Activity.AsMessageActivity();
if (message != null)
{
if (!String.IsNullOrWhiteSpace(message.Text))
if (!string.IsNullOrWhiteSpace(message.Text))
{
var languageChanged = false;
if (_isUserLanguageChanged != null)
@ -117,37 +124,42 @@ namespace Microsoft.Bot.Builder.Ai.Translation
if (!languageChanged)
{
// determine the language we are using for this conversation
var sourceLanguage = "";
var targetLanguage = "";
if (_getUserLanguage == null)
sourceLanguage = await _translator.Detect(message.Text); //awaiting user language detection using Microsoft Translator API.
var sourceLanguage = string.Empty;
var targetLanguage = string.Empty;
if (_getUserLanguage == null)
{
sourceLanguage = await _translator.DetectAsync(message.Text); // awaiting user language detection using Microsoft Translator API.
}
else
{
sourceLanguage = _getUserLanguage(context);
}
targetLanguage = (_nativeLanguages.Contains(sourceLanguage)) ? sourceLanguage : _nativeLanguages.FirstOrDefault() ?? "en";
targetLanguage = _nativeLanguages.Contains(sourceLanguage) ? sourceLanguage : _nativeLanguages.FirstOrDefault() ?? "en";
await TranslateMessageAsync(context, message, sourceLanguage, targetLanguage, _nativeLanguages.Contains(sourceLanguage)).ConfigureAwait(false);
if (_toUserLanguage)
{
context.OnSendActivities(async (newContext, activities, nextSend) =>
{
//Translate messages sent to the user to user language
List<Task> tasks = new List<Task>();
foreach (Activity currentActivity in activities.Where(a => a.Type == ActivityTypes.Message))
// Translate messages sent to the user to user language
var tasks = new List<Task>();
foreach (var currentActivity in activities.Where(a => a.Type == ActivityTypes.Message))
{
tasks.Add(TranslateMessageAsync(newContext, currentActivity.AsMessageActivity(), targetLanguage, sourceLanguage, false));
}
if (tasks.Any())
await Task.WhenAll(tasks).ConfigureAwait(false);
if (tasks.Any())
{
await Task.WhenAll(tasks).ConfigureAwait(false);
}
return await nextSend();
});
context.OnUpdateActivity(async (newContext, activity, nextUpdate) =>
{
//Translate messages sent to the user to user language
// Translate messages sent to the user to user language
if (activity.Type == ActivityTypes.Message)
{
await TranslateMessageAsync(newContext, activity.AsMessageActivity(), targetLanguage, sourceLanguage, false).ConfigureAwait(false);
@ -161,12 +173,20 @@ namespace Microsoft.Bot.Builder.Ai.Translation
{
// skip routing in case of user changed the language
return;
}
}
}
}
await next(cancellationToken).ConfigureAwait(false);
}
private static void AssertValidNativeLanguages(string[] nativeLanguages)
{
if (nativeLanguages == null)
{
throw new ArgumentNullException(nameof(nativeLanguages));
}
}
/// <summary>
@ -179,6 +199,7 @@ namespace Microsoft.Bot.Builder.Ai.Translation
{
attachedPostProcessors.Add(new PatternsPostProcessor(_patterns));
}
if (_userCustomDictonaries != null && !_userCustomDictonaries.IsEmpty())
{
attachedPostProcessors.Add(new CustomDictionaryPostProcessor(_userCustomDictonaries));
@ -188,17 +209,18 @@ namespace Microsoft.Bot.Builder.Ai.Translation
/// <summary>
/// Applies all the attached post processors to the translated messages.
/// </summary>
/// <param name="translatedDocuments">List of <see cref="TranslatedDocument"/> represent the output of the translator module</param>
/// <param name="languageId">Current language id</param>
/// <param name="translatedDocuments">List of <see cref="TranslatedDocument"/> represent the output of the translator module.</param>
/// <param name="languageId">Current language id.</param>
private void PostProcesseDocuments(List<TranslatedDocument> translatedDocuments, string languageId)
{
if (attachedPostProcessors == null)
{
InitializePostProcessors();
}
foreach (TranslatedDocument translatedDocument in translatedDocuments)
foreach (var translatedDocument in translatedDocuments)
{
foreach (IPostProcessor postProcessor in attachedPostProcessors)
foreach (var postProcessor in attachedPostProcessors)
{
translatedDocument.TargetMessage = postProcessor.Process(translatedDocument, languageId).PostProcessedMessage;
}
@ -212,30 +234,34 @@ namespace Microsoft.Bot.Builder.Ai.Translation
/// <param name="message">The activity containing the text to translate.</param>
/// <param name="sourceLanguage">An identifier for the language to translate from.</param>
/// <param name="targetLanguage">An identifier for the language to translate to.</param>
/// <param name="inNativeLanguages">Indicates whether the input text does not need to be translated.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>When the task completes successfully, the <see cref="Activity.Text"/> property
/// of the message contains the translated text.</remarks>
private async Task TranslateMessageAsync(ITurnContext context, IMessageActivity message, string sourceLanguage, string targetLanguage, bool InNativeLanguages)
private async Task TranslateMessageAsync(ITurnContext context, IMessageActivity message, string sourceLanguage, string targetLanguage, bool inNativeLanguages)
{
if (!InNativeLanguages && sourceLanguage != targetLanguage)
if (!inNativeLanguages && sourceLanguage != targetLanguage)
{
// if we have text and a target language
if (!String.IsNullOrWhiteSpace(message.Text) && !String.IsNullOrEmpty(targetLanguage))
if (!string.IsNullOrWhiteSpace(message.Text) && !string.IsNullOrEmpty(targetLanguage))
{
if (targetLanguage == sourceLanguage)
return;
if (targetLanguage == sourceLanguage)
{
return;
}
var text = message.Text;
string[] lines = text.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None);
var translateResult = await this._translator.TranslateArray(lines, sourceLanguage, targetLanguage).ConfigureAwait(false);
var lines = text.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None);
var translateResult = await this._translator.TranslateArrayAsync(lines, sourceLanguage, targetLanguage).ConfigureAwait(false);
// post process all translated documents
PostProcesseDocuments(translateResult, sourceLanguage);
text = string.Empty;
foreach (TranslatedDocument translatedDocument in translateResult)
foreach (var translatedDocument in translateResult)
{
text += string.Join("\n", translatedDocument.TargetMessage);
}
message.Text = text;
}
}

Просмотреть файл

@ -1,19 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Microsoft.Bot.Builder.Ai.Translation.PostProcessor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.Bot.Builder.Ai.Translation.PostProcessor;
namespace Microsoft.Bot.Builder.Ai.Translation
{
@ -23,15 +21,15 @@ namespace Microsoft.Bot.Builder.Ai.Translation
/// </summary>
public class Translator
{
private readonly AzureAuthToken _authToken;
private static readonly HttpClient DefaultHttpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(20) };
private HttpClient _httpClient = null;
/// <summary>
/// Creates a new <see cref="Translator"/> object.
private readonly AzureAuthToken _authToken;
private HttpClient _httpClient = null;
/// <summary>
/// Initializes a new instance of the <see cref="Translator"/> class.
/// </summary>
/// <param name="apiKey">Your subscription key for the Microsoft Translator Text API.</param>
/// <param name="customHttpClient">alternate http client</param>
/// <param name="customHttpClient">An alternate HTTP client to use.</param>
public Translator(string apiKey, HttpClient customHttpClient = null)
{
_httpClient = customHttpClient ?? DefaultHttpClient;
@ -39,62 +37,20 @@ namespace Microsoft.Bot.Builder.Ai.Translation
{
throw new ArgumentNullException(nameof(apiKey));
}
_authToken = new AzureAuthToken(apiKey);
}
/// <summary>
/// Performs pre-processing to remove "literal" tags and flag sections of the text that will not be translated.
/// </summary>
/// <param name="textToTranslate">The text to translate</param>
/// <param name="processedTextToTranslate">The processed text after removing the literal tags and other unwanted characters</param>
/// <param name="noTranslatePhrases">The extracted no translate phrases</param>
private void PreprocessMessage(string textToTranslate, out string processedTextToTranslate, out HashSet<string> noTranslatePhrases)
{
textToTranslate = Regex.Replace(textToTranslate, @"\s+", " ");//used to remove multiple spaces in input user message
string literalPattern = "<literal>(.*)</literal>";
noTranslatePhrases = new HashSet<string>();
MatchCollection literalMatches = Regex.Matches(textToTranslate, literalPattern);
if (literalMatches.Count > 0)
{
foreach (Match literalMatch in literalMatches)
{
if (literalMatch.Groups.Count > 1)
{
noTranslatePhrases.Add("(" + literalMatch.Groups[1].Value + ")");
}
}
textToTranslate = Regex.Replace(textToTranslate, "</?literal>", " ");
}
textToTranslate = Regex.Replace(textToTranslate, @"\s+", " ");
processedTextToTranslate = textToTranslate;
}
/// <summary>
/// Performs pre-processing to remove "literal" tags .
/// </summary>
/// <param name="textToTranslate">The text to translate.</param>
private string PreprocessMessage(string textToTranslate)
{
textToTranslate = Regex.Replace(textToTranslate, @"\s+", " ");//used to remove multiple spaces in input user message
string literalPattern = "<literal>(.*)</literal>";
MatchCollection literalMatches = Regex.Matches(textToTranslate, literalPattern);
if (literalMatches.Count > 0)
{
textToTranslate = Regex.Replace(textToTranslate, "</?literal>", " ");
}
return Regex.Replace(textToTranslate, @"\s+", " ");
}
/// <summary>
/// Detects the language of the input text.
/// </summary>
/// <param name="textToDetect">The text to translate.</param>
/// <returns>The language identifier.</returns>
public async Task<string> Detect(string textToDetect)
public async Task<string> DetectAsync(string textToDetect)
{
textToDetect = PreprocessMessage(textToDetect);
string url = "http://api.microsofttranslator.com/v2/Http.svc/Detect";
string query = $"?text={System.Net.WebUtility.UrlEncode(textToDetect)}";
var url = "http://api.microsofttranslator.com/v2/Http.svc/Detect";
var query = $"?text={System.Net.WebUtility.UrlEncode(textToDetect)}";
using (var request = new HttpRequestMessage())
{
@ -105,9 +61,11 @@ namespace Microsoft.Bot.Builder.Ai.Translation
{
var result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
return "ERROR: " + result;
if (!response.IsSuccessStatusCode)
{
return "ERROR: " + result;
}
var detectedLang = XElement.Parse(result).Value;
return detectedLang;
}
@ -121,17 +79,15 @@ namespace Microsoft.Bot.Builder.Ai.Translation
/// <param name="from">The language code of the translation text. For example, "en" for English.</param>
/// <param name="to">The language code to translate the text into.</param>
/// <returns>The translated document.</returns>
public async Task<TranslatedDocument> Translate(string textToTranslate, string from, string to)
public async Task<TranslatedDocument> TranslateAsync(string textToTranslate, string from, string to)
{
TranslatedDocument currentTranslatedDocument = new TranslatedDocument(textToTranslate);
string processedText;
HashSet<string> literanlNoTranslateList;
PreprocessMessage(currentTranslatedDocument.SourceMessage, out processedText, out literanlNoTranslateList);
var currentTranslatedDocument = new TranslatedDocument(textToTranslate);
PreprocessMessage(currentTranslatedDocument.SourceMessage, out var processedText, out var literanlNoTranslateList);
currentTranslatedDocument.SourceMessage = processedText;
currentTranslatedDocument.LiteranlNoTranslatePhrases = literanlNoTranslateList;
string url = "http://api.microsofttranslator.com/v2/Http.svc/Translate";
string query = $"?text={System.Net.WebUtility.UrlEncode(textToTranslate)}" +
var url = "http://api.microsofttranslator.com/v2/Http.svc/Translate";
var query = $"?text={System.Net.WebUtility.UrlEncode(textToTranslate)}" +
$"&from={from}" +
$"&to={to}";
@ -144,9 +100,11 @@ namespace Microsoft.Bot.Builder.Ai.Translation
{
var result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new ArgumentException(result);
if (!response.IsSuccessStatusCode)
{
throw new ArgumentException(result);
}
var translatedText = XElement.Parse(result).Value.Trim();
currentTranslatedDocument.TargetMessage = translatedText;
@ -162,25 +120,22 @@ namespace Microsoft.Bot.Builder.Ai.Translation
/// <param name="from">The language code of the translation text. For example, "en" for English.</param>
/// <param name="to">The language code to translate the text into.</param>
/// <returns>An array of the translated documents.</returns>
public async Task<List<TranslatedDocument>> TranslateArray(string[] translateArraySourceTexts, string from, string to)
public async Task<List<TranslatedDocument>> TranslateArrayAsync(string[] translateArraySourceTexts, string from, string to)
{
List<TranslatedDocument> translatedDocuments = new List<TranslatedDocument>();
var translatedDocuments = new List<TranslatedDocument>();
var uri = "https://api.microsofttranslator.com/v2/Http.svc/TranslateArray2";
for (int srcTxtIndx = 0; srcTxtIndx < translateArraySourceTexts.Length; srcTxtIndx++)
for (var srcTxtIndx = 0; srcTxtIndx < translateArraySourceTexts.Length; srcTxtIndx++)
{
//Check for literal tag in input user message
TranslatedDocument currentTranslatedDocument = new TranslatedDocument(translateArraySourceTexts[srcTxtIndx]);
// Check for literal tag in input user message
var currentTranslatedDocument = new TranslatedDocument(translateArraySourceTexts[srcTxtIndx]);
translatedDocuments.Add(currentTranslatedDocument);
string processedText;
HashSet<string> literanlNoTranslateList;
PreprocessMessage(currentTranslatedDocument.SourceMessage, out processedText, out literanlNoTranslateList);
PreprocessMessage(currentTranslatedDocument.SourceMessage, out var processedText, out var literanlNoTranslateList);
currentTranslatedDocument.SourceMessage = processedText;
translateArraySourceTexts[srcTxtIndx] = processedText;
currentTranslatedDocument.LiteranlNoTranslatePhrases = literanlNoTranslateList;
}
//body of http request
// body of http request
var body = $"<TranslateArrayRequest>" +
"<AppId />" +
$"<From>{from}</From>" +
@ -193,7 +148,7 @@ namespace Microsoft.Bot.Builder.Ai.Translation
"<User xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2\" />" +
"</Options>" +
"<Texts>" +
String.Join("", translateArraySourceTexts.Select(s => $"<string xmlns=\"http://schemas.microsoft.com/2003/10/Serialization/Arrays\">{SecurityElement.Escape(s)}</string>\n"))
string.Join(string.Empty, translateArraySourceTexts.Select(s => $"<string xmlns=\"http://schemas.microsoft.com/2003/10/Serialization/Arrays\">{SecurityElement.Escape(s)}</string>\n"))
+ "</Texts>" +
$"<To>{to}</To>" +
"</TranslateArrayRequest>";
@ -216,110 +171,75 @@ namespace Microsoft.Bot.Builder.Ai.Translation
Console.WriteLine("Request status is OK. Result of translate array method is:");
var doc = XDocument.Parse(responseBody);
var ns = XNamespace.Get("http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2");
List<string> results = new List<string>();
int sentIndex = 0;
foreach (XElement xe in doc.Descendants(ns + "TranslateArray2Response"))
var results = new List<string>();
var sentIndex = 0;
foreach (var xe in doc.Descendants(ns + "TranslateArray2Response"))
{
TranslatedDocument currentTranslatedDocument = translatedDocuments[sentIndex];
currentTranslatedDocument.TargetMessage = xe.Element(ns + "TranslatedText").Value;
var currentTranslatedDocument = translatedDocuments[sentIndex];
currentTranslatedDocument.RawAlignment = xe.Element(ns + "Alignment").Value;
if (!string.IsNullOrEmpty(currentTranslatedDocument.RawAlignment))
{
string[] alignments = currentTranslatedDocument.RawAlignment.Trim().Split(' ');
var alignments = currentTranslatedDocument.RawAlignment.Trim().Split(' ');
currentTranslatedDocument.SourceTokens = PostProcessingUtilities.SplitSentence(currentTranslatedDocument.SourceMessage, alignments);
currentTranslatedDocument.TranslatedTokens = PostProcessingUtilities.SplitSentence(xe.Element(ns + "TranslatedText").Value, alignments, false);
currentTranslatedDocument.IndexedAlignment = PostProcessingUtilities.WordAlignmentParse(alignments, currentTranslatedDocument.SourceTokens, currentTranslatedDocument.TranslatedTokens);
}
sentIndex += 1;
}
return translatedDocuments;
default:
throw new Exception(response.ReasonPhrase);
}
}
}
}
}
internal class AzureAuthToken
{
private static HttpClient DefaultHttpClient = new HttpClient();
private HttpClient _httpClient = null;
/// URL of the token service
private static readonly Uri ServiceUrl = new Uri("https://api.cognitive.microsoft.com/sts/v1.0/issueToken");
/// Name of header used to pass the subscription key to the token service
private const string OcpApimSubscriptionKeyHeader = "Ocp-Apim-Subscription-Key";
/// After obtaining a valid token, this class will cache it for this duration.
/// Use a duration of 5 minutes, which is less than the actual token lifetime of 10 minutes.
private static readonly TimeSpan TokenCacheDuration = new TimeSpan(0, 5, 0);
/// Cache the value of the last valid token obtained from the token service.
private string _storedTokenValue = string.Empty;
/// When the last valid token was obtained.
private DateTime _storedTokenTime = DateTime.MinValue;
/// Gets the subscription key.
internal string SubscriptionKey { get; }
/// Gets the HTTP status code for the most recent request to the token service.
internal HttpStatusCode RequestStatusCode { get; private set; }
}
/// <summary>
/// Creates a client to obtain an access token.
/// Performs pre-processing to remove "literal" tags and flag sections of the text that will not be translated.
/// </summary>
/// <param name="key">Subscription key to use to get an authentication token.</param>
internal AzureAuthToken(string key, HttpClient client = null)
/// <param name="textToTranslate">The text to translate.</param>
/// <param name="processedTextToTranslate">The processed text after removing the literal tags and other unwanted characters.</param>
/// <param name="noTranslatePhrases">The extracted no translate phrases.</param>
private void PreprocessMessage(string textToTranslate, out string processedTextToTranslate, out HashSet<string> noTranslatePhrases)
{
_httpClient = client ?? DefaultHttpClient;
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key), "A subscription key is required");
this.SubscriptionKey = key;
this.RequestStatusCode = HttpStatusCode.InternalServerError;
}
/// <summary>
/// Gets a token for the specified subscription.
/// </summary>
/// <returns>The encoded JWT token prefixed with the string "Bearer ".</returns>
/// <remarks>
/// This method uses a cache to limit the number of request to the token service.
/// A fresh token can be re-used during its lifetime of 10 minutes. After a successful
/// request to the token service, this method caches the access token. Subsequent
/// invocations of the method return the cached token for the next 5 minutes. After
/// 5 minutes, a new token is fetched from the token service and the cache is updated.
/// </remarks>
internal async Task<string> GetAccessTokenAsync()
{
if (string.IsNullOrWhiteSpace(this.SubscriptionKey))
return string.Empty;
// Re-use the cached token if there is one.
if ((DateTime.Now - _storedTokenTime) < TokenCacheDuration)
return _storedTokenValue;
using (var request = new HttpRequestMessage())
textToTranslate = Regex.Replace(textToTranslate, @"\s+", " "); // used to remove multiple spaces in input user message
var literalPattern = "<literal>(.*)</literal>";
noTranslatePhrases = new HashSet<string>();
var literalMatches = Regex.Matches(textToTranslate, literalPattern);
if (literalMatches.Count > 0)
{
request.Method = HttpMethod.Post;
request.RequestUri = ServiceUrl;
request.Content = new StringContent(string.Empty);
request.Headers.TryAddWithoutValidation(OcpApimSubscriptionKeyHeader, this.SubscriptionKey);
using (var response = await _httpClient.SendAsync(request))
foreach (Match literalMatch in literalMatches)
{
this.RequestStatusCode = response.StatusCode;
response.EnsureSuccessStatusCode();
var token = await response.Content.ReadAsStringAsync();
_storedTokenTime = DateTime.Now;
_storedTokenValue = "Bearer " + token;
return _storedTokenValue;
if (literalMatch.Groups.Count > 1)
{
noTranslatePhrases.Add("(" + literalMatch.Groups[1].Value + ")");
}
}
textToTranslate = Regex.Replace(textToTranslate, "</?literal>", " ");
}
textToTranslate = Regex.Replace(textToTranslate, @"\s+", " ");
processedTextToTranslate = textToTranslate;
}
/// <summary>
/// Performs pre-processing to remove "literal" tags .
/// </summary>
/// <param name="textToTranslate">The text to translate.</param>
private string PreprocessMessage(string textToTranslate)
{
textToTranslate = Regex.Replace(textToTranslate, @"\s+", " "); // used to remove multiple spaces in input user message
var literalPattern = "<literal>(.*)</literal>";
var literalMatches = Regex.Matches(textToTranslate, literalPattern);
if (literalMatches.Count > 0)
{
textToTranslate = Regex.Replace(textToTranslate, "</?literal>", " ");
}
return Regex.Replace(textToTranslate, @"\s+", " ");
}
}
}

Просмотреть файл

@ -22,20 +22,17 @@ namespace Microsoft.Bot.Builder.Azure
public class CosmosDbStorage : IStorage
{
private static readonly char[] IllegalKeyCharacters = new char[] { '\\', '?', '/', '#', ' ' };
private static Lazy<Dictionary<char, string>> IllegalKeyCharacterReplacementMap = new Lazy<Dictionary<char, string>>(() => IllegalKeyCharacters.ToDictionary(c => c, c => '*' + ((int)c).ToString("x2")));
private static readonly Lazy<Dictionary<char, string>> IllegalKeyCharacterReplacementMap = new Lazy<Dictionary<char, string>>(() => IllegalKeyCharacters.ToDictionary(c => c, c => '*' + ((int)c).ToString("x2")));
private static JsonSerializer _jsonSerializer = JsonSerializer.Create(new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
private readonly string _databaseId;
private readonly string _collectionId;
private readonly DocumentClient _client;
private string _collectionLink = null;
private static JsonSerializer _jsonSerializer = JsonSerializer.Create(new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All
});
/// <summary>
/// Creates a new <see cref="CosmosDbStorage"/> object,
/// Initializes a new instance of the <see cref="CosmosDbStorage"/> class.
/// using the provided CosmosDB credentials, database ID, and collection ID.
/// </summary>
/// <param name="cosmosDbStorageOptions">Cosmos DB storage configuration options.</param>
@ -66,10 +63,7 @@ namespace Microsoft.Bot.Builder.Azure
// Inject BotBuilder version to CosmosDB Requests
var version = GetType().Assembly.GetName().Version;
var connectionPolicy = new ConnectionPolicy()
{
UserAgentSuffix = $"Microsoft-BotFramework {version}"
};
var connectionPolicy = new ConnectionPolicy { UserAgentSuffix = $"Microsoft-BotFramework {version}" };
// Invoke CollectionPolicy delegate to further customize settings
cosmosDbStorageOptions.ConnectionPolicyConfigurator?.Invoke(connectionPolicy);
@ -77,12 +71,67 @@ namespace Microsoft.Bot.Builder.Azure
}
/// <summary>
/// Removes store items from storage.
/// Converts the key into a DocumentID that can be used safely with CosmosDB.
/// The following characters are restricted and cannot be used in the Id property: '/', '\', '?', '#'
/// More information at https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.resource.id?view=azure-dotnet#remarks.
/// </summary>
/// <param name="keys">Array of item keys to remove from the store.</param>
public async Task Delete(string[] keys, CancellationToken cancellationToken)
/// <param name="key">The key to sanitize.</param>
/// <returns>A sanitized key that can be used safely with CosmosDB.</returns>
public static string SanitizeKey(string key)
{
if (keys == null || keys.Length == 0) return;
var firstIllegalCharIndex = key.IndexOfAny(IllegalKeyCharacters);
// If there are no illegal characters return immediately and avoid any further processing/allocations
if (firstIllegalCharIndex == -1)
{
return key;
}
// Allocate a builder that assumes that all remaining characters might be replaced to avoid any extra allocations
var sanitizedKeyBuilder = new StringBuilder(key.Length + ((key.Length - firstIllegalCharIndex + 1) * 3));
// Add all good characters up to the first bad character to the builder first
for (var index = 0; index < firstIllegalCharIndex; index++)
{
sanitizedKeyBuilder.Append(key[index]);
}
var illegalCharacterReplacementMap = IllegalKeyCharacterReplacementMap.Value;
// Now walk the remaining characters, starting at the first known bad character, replacing any bad ones with their designated replacement value from the map
for (var index = firstIllegalCharIndex; index < key.Length; index++)
{
var ch = key[index];
// Check if this next character is considered illegal and, if so, append its replacement; otherwise just append the good character as is
if (illegalCharacterReplacementMap.TryGetValue(ch, out var replacement))
{
sanitizedKeyBuilder.Append(replacement);
}
else
{
sanitizedKeyBuilder.Append(ch);
}
}
return sanitizedKeyBuilder.ToString();
}
/// <summary>
/// Deletes storage items from storage.
/// </summary>
/// <param name="keys">keys of the <see cref="IStoreItem"/> objects to remove from the store.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <seealso cref="ReadAsync(string[], CancellationToken)"/>
/// <seealso cref="WriteAsync(IDictionary{string, object}, CancellationToken)"/>
public async Task DeleteAsync(string[] keys, CancellationToken cancellationToken)
{
if (keys == null || keys.Length == 0)
{
return;
}
// Ensure collection exists
var collectionLink = await GetCollectionLink();
@ -96,10 +145,17 @@ namespace Microsoft.Bot.Builder.Azure
}
/// <summary>
/// Loads store items from storage.
/// Reads storage items from storage.
/// </summary>
/// <param name="keys">Array of item keys to read from the store.</param>
public async Task<IDictionary<string, object>> Read(string[] keys, CancellationToken cancellationToken)
/// <param name="keys">keys of the <see cref="IStoreItem"/> objects to read from the store.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activities are successfully sent, the task result contains
/// the items read, indexed by key.</remarks>
/// <seealso cref="DeleteAsync(string[], CancellationToken)"/>
/// <seealso cref="WriteAsync(IDictionary{string, object}, CancellationToken)"/>
public async Task<IDictionary<string, object>> ReadAsync(string[] keys, CancellationToken cancellationToken)
{
if (keys == null || keys.Length == 0)
{
@ -116,7 +172,7 @@ namespace Microsoft.Bot.Builder.Azure
var querySpec = new SqlQuerySpec
{
QueryText = $"SELECT c.id, c.realId, c.document, c._etag FROM c WHERE c.id in ({parameterSequence})",
Parameters = new SqlParameterCollection(parameterValues)
Parameters = new SqlParameterCollection(parameterValues),
};
var query = _client.CreateDocumentQuery<DocumentStoreItem>(collectionLink, querySpec).AsDocumentQuery();
@ -127,7 +183,7 @@ namespace Microsoft.Bot.Builder.Azure
var item = doc.Document.ToObject(typeof(object), _jsonSerializer);
if (item is IStoreItem storeItem)
{
storeItem.eTag = doc.ETag;
storeItem.ETag = doc.ETag;
}
// doc.Id cannot be used since it is escaped, read it from RealId property instead
@ -139,10 +195,15 @@ namespace Microsoft.Bot.Builder.Azure
}
/// <summary>
/// Saves store items to storage.
/// Writes storage items to storage.
/// </summary>
/// <param name="changes">Map of items to write to storage.</param>
public async Task Write(IDictionary<string, object> changes, CancellationToken cancellationToken)
/// <param name="changes">The items to write to storage, indexed by key.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <seealso cref="DeleteAsync(string[], CancellationToken)"/>
/// <seealso cref="ReadAsync(string[], CancellationToken)"/>
public async Task WriteAsync(IDictionary<string, object> changes, CancellationToken cancellationToken)
{
if (changes == null)
{
@ -162,20 +223,20 @@ namespace Microsoft.Bot.Builder.Azure
{
Id = SanitizeKey(change.Key),
ReadlId = change.Key,
Document = json
Document = json,
};
string eTag = (change.Value as IStoreItem)?.eTag;
if (eTag == null || eTag == "*")
var etag = (change.Value as IStoreItem)?.ETag;
if (etag == null || etag == "*")
{
// if new item or * then insert or replace unconditionaly
await _client.UpsertDocumentAsync(collectionLink, documentChange, disableAutomaticIdGeneration: true).ConfigureAwait(false);
}
else if (eTag.Length > 0)
else if (etag.Length > 0)
{
// if we have an etag, do opt. concurrency replace
var uri = UriFactory.CreateDocumentUri(_databaseId, _collectionId, documentChange.Id);
var ac = new AccessCondition { Condition = eTag, Type = AccessConditionType.IfMatch };
var ac = new AccessCondition { Condition = etag, Type = AccessConditionType.IfMatch };
await _client.ReplaceDocumentAsync(uri, documentChange, new RequestOptions { AccessCondition = ac }).ConfigureAwait(false);
}
else
@ -190,7 +251,7 @@ namespace Microsoft.Bot.Builder.Azure
/// </summary>
private async ValueTask<string> GetCollectionLink()
{
if(_collectionLink == null)
if (_collectionLink == null)
{
await _client.CreateDatabaseIfNotExistsAsync(new Database { Id = _databaseId }).ConfigureAwait(false);
@ -201,73 +262,31 @@ namespace Microsoft.Bot.Builder.Azure
return _collectionLink;
}
/// <summary>
/// Converts the key into a DocumentID that can be used safely with CosmosDB.
/// The following characters are restricted and cannot be used in the Id property: '/', '\', '?', '#'
/// More information at https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.resource.id?view=azure-dotnet#remarks
/// </summary>
public static string SanitizeKey(string key)
{
var firstIllegalCharIndex = key.IndexOfAny(IllegalKeyCharacters);
// If there are no illegal characters return immediately and avoid any further processing/allocations
if (firstIllegalCharIndex == -1) return key;
// Allocate a builder that assumes that all remaining characters might be replaced to avoid any extra allocations
var sanitizedKeyBuilder = new StringBuilder(key.Length + (key.Length - firstIllegalCharIndex + 1) * 3);
// Add all good characters up to the first bad character to the builder first
for (int index = 0; index < firstIllegalCharIndex; index++)
{
sanitizedKeyBuilder.Append(key[index]);
}
var illegalCharacterReplacementMap = IllegalKeyCharacterReplacementMap.Value;
// Now walk the remaining characters, starting at the first known bad character, replacing any bad ones with their designated replacement value from the map
for (int index = firstIllegalCharIndex; index < key.Length; index++)
{
var ch = key[index];
// Check if this next character is considered illegal and, if so, append its replacement; otherwise just append the good character as is
if (illegalCharacterReplacementMap.TryGetValue(ch, out var replacement))
{
sanitizedKeyBuilder.Append(replacement);
}
else
{
sanitizedKeyBuilder.Append(ch);
}
}
return sanitizedKeyBuilder.ToString();
}
/// <summary>
/// Internal data structure for storing items in a CosmosDB Collection.
/// </summary>
private class DocumentStoreItem
{
/// <summary>
/// Sanitized Id/Key an used as PrimaryKey.
/// Gets or sets the sanitized Id/Key used as PrimaryKey.
/// </summary>
[JsonProperty("id")]
public string Id { get; set; }
/// <summary>
/// Un-sanitized Id/Key.
/// Gets or sets the un-sanitized Id/Key.
/// </summary>
[JsonProperty("realId")]
public string ReadlId { get; internal set; }
/// <summary>
/// The persisted object.
/// Gets or sets the persisted object.
/// </summary>
[JsonProperty("document")]
public JObject Document { get; set; }
/// <summary>
/// ETag information for handling optimistic concurrency updates.
/// Gets or sets the ETag information for handling optimistic concurrency updates.
/// </summary>
[JsonProperty("_etag")]
public string ETag { get; set; }

Просмотреть файл

@ -14,31 +14,46 @@ namespace Microsoft.Bot.Builder.Azure
/// <summary>
/// Gets or sets the CosmosDB endpoint.
/// </summary>
/// <value>
/// The CosmosDB endpoint.
/// </value>
public Uri CosmosDBEndpoint { get; set; }
/// <summary>
/// Gets or sets the authentication key for Cosmos DB.
/// </summary>
/// <value>
/// The authentication key for Cosmos DB.
/// </value>
public string AuthKey { get; set; }
/// <summary>
/// Gets or sets the database identifier for Cosmos DB instance.
/// </summary>
/// <value>
/// The database identifier for Cosmos DB instance.
/// </value>
public string DatabaseId { get; set; }
/// <summary>
/// Gets or sets the collection identifier.
/// </summary>
/// <value>
/// The collection identifier.
/// </value>
public string CollectionId { get; set; }
/// <summary>
/// Gets or sets the connection policy configurator. This action allows you to customise the connection parameters.
/// </summary>
/// <remarks>You can use this delegate to
/// further customize the connection to CosmosDB,
/// <remarks>You can use this delegate to
/// further customize the connection to CosmosDB,
/// such as setting connection mode, retry options, timeouts, and so on.
/// See https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.client.connectionpolicy?view=azure-dotnet
/// for more information.</remarks>
/// <value>
/// The connection policy configurator.
/// </value>
public Action<ConnectionPolicy> ConnectionPolicyConfigurator { get; set; } = (options) => { };
}
}

Просмотреть файл

@ -44,10 +44,16 @@
<DocumentationFile>bin\$(Configuration)\netstandard2.0\Microsoft.Bot.Builder.Azure.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>Microsoft.Bot.Builder.Azure.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AsyncUsageAnalyzers" Version="1.0.0-alpha003" PrivateAssets="all" />
<PackageReference Include="Microsoft.Azure.DocumentDB.Core" Version="1.9.1" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="SourceLink.Create.CommandLine" Version="2.8.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta008" PrivateAssets="all" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.4.0" />
<PackageReference Include="Microsoft.Bot.Builder" Condition=" '$(PackageVersion)' == '' " Version="4.0.0-local" />
<PackageReference Include="Microsoft.Bot.Builder" Condition=" '$(PackageVersion)' != '' " Version="$(PackageVersion)" />

Просмотреть файл

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for StyleCop.Analyzers" Description="Code analysis rules for StyleCop.Analyzers.csproj." ToolsVersion="14.0">
<Rules AnalyzerId="AsyncUsageAnalyzers" RuleNamespace="AsyncUsageAnalyzers">
<Rule Id="UseConfigureAwait" Action="Warning" />
<Rule Id="AvoidAsyncVoid" Action="Warning" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Features" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Features">
<Rule Id="IDE0003" Action="None" />
</Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<Rule Id="SA1101" Action="None" />
<!-- local calls prefixed with this -->
<Rule Id="SA1200" Action="None" />
<Rule Id="SA1305" Action="Warning" />
<Rule Id="SA1309" Action="None" />
<Rule Id="SA1412" Action="Warning" />
<Rule Id="SA1600" Action="None" />
<Rule Id="SA1609" Action="Warning" />
<Rule Id="SA1633" Action="None" />
</Rules>
</RuleSet>

Просмотреть файл

@ -144,7 +144,7 @@ namespace Microsoft.Bot.Builder.Classic.Dialogs.Internals
Task IBotToUser.PostAsync(IMessageActivity message, CancellationToken cancellationToken)
{
// TODO, change this to context.SendActivity with M2 delta
return this.context.Adapter.SendActivities(this.context, new Activity[] { (Activity) message }, cancellationToken);
return this.context.Adapter.SendActivitiesAsync(this.context, new Activity[] { (Activity) message }, cancellationToken);
}
}

Просмотреть файл

@ -296,7 +296,7 @@ namespace Microsoft.Bot.Builder.Classic.FormFlow.Advanced
}
}
// Write out TEMPLATE;usage;field* -> pattern*
// WriteAsync out TEMPLATE;usage;field* -> pattern*
foreach (var entry in byPattern)
{
var elements = entry.Key.SplitList().ToArray();

Просмотреть файл

@ -82,7 +82,7 @@ namespace Microsoft.Bot.Builder.Dialogs
}
msg.InputHint = InputHints.ExpectingInput;
await context.SendActivity(msg);
await context.SendActivityAsync(msg);
}
public async Task Prompt(ITurnContext context, IMessageActivity prompt = null, string speak = null)
@ -92,7 +92,7 @@ namespace Microsoft.Bot.Builder.Dialogs
if (prompt != null)
{
prompt.Speak = speak ?? prompt.Speak;
await context.SendActivity(prompt);
await context.SendActivityAsync(prompt);
}
}

Просмотреть файл

@ -106,7 +106,7 @@ namespace Microsoft.Bot.Builder.Dialogs
}
msg.InputHint = InputHints.ExpectingInput;
await context.SendActivity(msg);
await context.SendActivityAsync(msg);
}
public async Task Prompt(ITurnContext context, IMessageActivity prompt = null, string speak = null)
@ -116,7 +116,7 @@ namespace Microsoft.Bot.Builder.Dialogs
if (prompt != null)
{
prompt.Speak = speak ?? prompt.Speak;
await context.SendActivity(prompt);
await context.SendActivityAsync(prompt);
}
}

Просмотреть файл

@ -49,7 +49,7 @@ namespace Microsoft.Bot.Builder.Dialogs
throw new InvalidOperationException("OAuthPrompt.Prompt(): at least one of the cards should be an oauth card");
var replyActivity = MessageFactory.Attachment(cards.First());//todo:send an oauth or signin card based on channel id
await context.SendActivity(replyActivity).ConfigureAwait(false);
await context.SendActivityAsync(replyActivity).ConfigureAwait(false);
}
/// <summary>
@ -69,7 +69,7 @@ namespace Microsoft.Bot.Builder.Dialogs
if (!ChannelSupportsOAuthCard(context.Activity.ChannelId))
{
var link = await adapter.GetOauthSignInLink(context, _settings.ConnectionName, default(CancellationToken)).ConfigureAwait(false);
var link = await adapter.GetOauthSignInLinkAsync(context, _settings.ConnectionName, default(CancellationToken)).ConfigureAwait(false);
cardAttachment = new Attachment
{
ContentType = SigninCard.ContentType,
@ -110,7 +110,7 @@ namespace Microsoft.Bot.Builder.Dialogs
};
}
var replyActivity = MessageFactory.Attachment(cardAttachment);
await context.SendActivity(replyActivity).ConfigureAwait(false);
await context.SendActivityAsync(replyActivity).ConfigureAwait(false);
}
/// <summary>
@ -139,7 +139,7 @@ namespace Microsoft.Bot.Builder.Dialogs
if (adapter == null)
throw new InvalidOperationException("OAuthPrompt.Recognize(): not supported by the current adapter");
var token = await adapter.GetUserToken(context, _settings.ConnectionName, matched.Value, default(CancellationToken)).ConfigureAwait(false);
var token = await adapter.GetUserTokenAsync(context, _settings.ConnectionName, matched.Value, default(CancellationToken)).ConfigureAwait(false);
var tokenResult = new TokenResult
{
Status = PromptStatus.Recognized,
@ -168,7 +168,7 @@ namespace Microsoft.Bot.Builder.Dialogs
if (adapter == null)
throw new InvalidOperationException("OAuthPrompt.GetUserToken(): not supported by the current adapter");
var token = await adapter.GetUserToken(context, _settings.ConnectionName, null, default(CancellationToken)).ConfigureAwait(false);
var token = await adapter.GetUserTokenAsync(context, _settings.ConnectionName, null, default(CancellationToken)).ConfigureAwait(false);
TokenResult tokenResult = null;
if (token == null)
{
@ -206,7 +206,7 @@ namespace Microsoft.Bot.Builder.Dialogs
throw new InvalidOperationException("OAuthPrompt.SignOutUser(): not supported by the current adapter");
// Sign out user
await adapter.SignOutUser(context, _settings.ConnectionName, default(CancellationToken)).ConfigureAwait(false);
await adapter.SignOutUserAsync(context, _settings.ConnectionName, default(CancellationToken)).ConfigureAwait(false);
}
private bool IsTokenResponseEvent(ITurnContext context)

Просмотреть файл

@ -22,7 +22,7 @@ namespace Microsoft.Bot.Builder.Dialogs
if (options == null)
throw new ArgumentNullException(nameof(options));
return dc.Context.SendActivity(PromptMessageFactory.CreateActivity(options, isRetry));
return dc.Context.SendActivityAsync(PromptMessageFactory.CreateActivity(options, isRetry));
}
protected override async Task<AttachmentResult> OnRecognize(DialogContext dc, PromptOptions options)

Просмотреть файл

@ -3,19 +3,58 @@
using System;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs.Choices;
using static Microsoft.Bot.Builder.Dialogs.PromptValidatorEx;
namespace Microsoft.Bot.Builder.Dialogs
{
/// <summary>
/// Prompts a user to confirm something with a yes/no response.
///
/// <remarks>By default the prompt will return to the calling dialog a `boolean` representing the users
/// selection.
/// When used with your bots 'DialogSet' you can simply add a new instance of the prompt as a named
/// dialog using <code>DialogSet.Add()</code>. You can then start the prompt from a waterfall step using either
/// <code>DialogContext.Begin()</code> or <code>DialogContext.Prompt()</code>. The user will be prompted to answer a
/// 'yes/no' or 'true/false' question and the users response will be passed as an argument to the
/// callers next waterfall step
/// </remarks>
/// </summary>
public class ConfirmPrompt : Prompt<ConfirmResult>
{
private ConfirmPromptInternal _prompt;
/// <summary>
/// Creates a new ConfirmPrompt instance
/// </summary>
/// <param name="culture">Culture to use if <code>DialogContext.Context.Activity.Locale</code> property not specified. Defaults to a value of <code>CultureInfo.CurrentCulture</code>.</param>
/// <param name="validator">Validator that will be called each time the user responds to the prompt. If the validator replies with a message no additional retry prompt will be sent.</param>
public ConfirmPrompt(string culture, PromptValidator<ConfirmResult> validator = null)
{
_prompt = new ConfirmPromptInternal(culture, validator);
}
/// <summary>
/// The style of the yes/no choices rendered to the user when prompting.
/// <seealso cref="Choices.ListStyle"/>
/// </summary>
public ListStyle Style
{
get { return _prompt.Style; }
set { _prompt.Style = value; }
}
/// <summary>
/// Additional options passed to the 'ChoiceFactory' and used to tweak the style of choices
/// rendered to the user.
/// <seealso cref="Choices.ChoiceFactoryOptions"/>
/// </summary>
public ChoiceFactoryOptions ChoiceOptions
{
get { return _prompt.ChoiceOptions; }
set { _prompt.ChoiceOptions = value; }
}
protected override async Task OnPrompt(DialogContext dc, PromptOptions options, bool isRetry)
{
if (dc == null)
@ -57,4 +96,4 @@ namespace Microsoft.Bot.Builder.Dialogs
return await _prompt.Recognize(dc.Context);
}
}
}
}

Просмотреть файл

@ -23,7 +23,7 @@ namespace Microsoft.Bot.Builder.Dialogs
if (options == null)
throw new ArgumentNullException(nameof(options));
return dc.Context.SendActivity(PromptMessageFactory.CreateActivity(options, isRetry));
return dc.Context.SendActivityAsync(PromptMessageFactory.CreateActivity(options, isRetry));
}
protected override async Task<DateTimeResult> OnRecognize(DialogContext dc, PromptOptions options)

Просмотреть файл

@ -22,7 +22,7 @@ namespace Microsoft.Bot.Builder.Dialogs
if (options == null)
throw new ArgumentNullException(nameof(options));
return dc.Context.SendActivity(PromptMessageFactory.CreateActivity(options, isRetry));
return dc.Context.SendActivityAsync(PromptMessageFactory.CreateActivity(options, isRetry));
}
protected override async Task<NumberResult<T>> OnRecognize(DialogContext dc, PromptOptions options)

Просмотреть файл

@ -111,7 +111,7 @@ namespace Microsoft.Bot.Builder.Dialogs
else if (!string.IsNullOrEmpty(promptOptions?.PromptString))
{
//send supplied prompt and then OAuthCard
await dc.Context.SendActivity(promptOptions.PromptString, promptOptions.Speak).ConfigureAwait(false);
await dc.Context.SendActivityAsync(promptOptions.PromptString, promptOptions.Speak).ConfigureAwait(false);
await _prompt.Prompt(dc.Context);
}
else
@ -152,7 +152,7 @@ namespace Microsoft.Bot.Builder.Dialogs
else if (isMessage && !string.IsNullOrEmpty(state.RetryPromptString))
{
// if this is a retry, then retry getting user credentials by resending the activity.
await dc.Context.SendActivity(state.RetryPromptString, state.RetrySpeak).ConfigureAwait(false);
await dc.Context.SendActivityAsync(state.RetryPromptString, state.RetrySpeak).ConfigureAwait(false);
}
}
}

Просмотреть файл

@ -23,7 +23,7 @@ namespace Microsoft.Bot.Builder.Dialogs
if (options == null)
throw new ArgumentNullException(nameof(options));
return dc.Context.SendActivity(PromptMessageFactory.CreateActivity(options, isRetry));
return dc.Context.SendActivityAsync(PromptMessageFactory.CreateActivity(options, isRetry));
}
protected override async Task<TextResult> OnRecognize(DialogContext dc, PromptOptions options)

Просмотреть файл

@ -67,7 +67,7 @@ namespace Microsoft.Bot.Builder.TemplateManager
Activity boundActivity = await this.RenderTemplate(context, context.Activity?.AsMessageActivity()?.Locale, templateId, data).ConfigureAwait(false);
if (boundActivity != null)
{
await context.SendActivity(boundActivity);
await context.SendActivityAsync(boundActivity);
return;
}
return;

Просмотреть файл

@ -16,13 +16,21 @@ namespace Microsoft.Bot.Builder.Adapters
/// <seealso cref="TestFlow"/>
public class TestAdapter : BotAdapter
{
private readonly bool _sendTraceActivity;
private object _conversationLock = new object();
private object _activeQueueLock = new object();
private int _nextId = 0;
public TestAdapter(ConversationReference conversation = null)
/// <summary>
/// Initializes a new instance of the <see cref="TestAdapter"/> class.
/// </summary>
/// <param name="conversation">A reference to the conversation to begin the adapter state with.</param>
/// <param name="sendTraceActivity">Indicates whether the adapter should add to its <see cref="ActiveQueue"/>
/// any trace activities generated by the bot.</param>
public TestAdapter(ConversationReference conversation = null, bool sendTraceActivity = false)
{
_sendTraceActivity = sendTraceActivity;
if (conversation != null)
{
Conversation = conversation;
@ -32,7 +40,7 @@ namespace Microsoft.Bot.Builder.Adapters
Conversation = new ConversationReference
{
ChannelId = "test",
ServiceUrl = "https://test.com"
ServiceUrl = "https://test.com",
};
Conversation.User = new ChannelAccount("user1", "User1");
@ -41,21 +49,50 @@ namespace Microsoft.Bot.Builder.Adapters
}
}
/// <summary>
/// Gets the queue of responses from the bot.
/// </summary>
/// <value>The queue of responses from the bot.</value>
public Queue<Activity> ActiveQueue { get; } = new Queue<Activity>();
/// <summary>
/// Gets or sets a reference to the current coversation.
/// </summary>
/// <value>A reference to the current conversation.</value>
public ConversationReference Conversation { get; set; }
/// <summary>
/// Adds middleware to the adapter's pipeline.
/// </summary>
/// <param name="middleware">The middleware to add.</param>
/// <returns>The updated adapter object.</returns>
/// <remarks>Middleware is added to the adapter at initialization time.
/// For each turn, the adapter calls middleware in the order in which you added it.
/// </remarks>
public new TestAdapter Use(IMiddleware middleware)
{
base.Use(middleware);
return this;
}
public async Task ProcessActivity(Activity activity, Func<ITurnContext, Task> callback, CancellationToken cancellationToken = default(CancellationToken))
/// <summary>
/// Receives an activity and runs it through the middleware pipeline.
/// </summary>
/// <param name="activity">The activity to process.</param>
/// <param name="callback">The bot logic to invoke.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public async Task ProcessActivityAsync(Activity activity, Func<ITurnContext, Task> callback, CancellationToken cancellationToken = default(CancellationToken))
{
lock (_conversationLock)
{
// ready for next reply
if (activity.Type == null)
{
activity.Type = ActivityTypes.Message;
}
activity.ChannelId = Conversation.ChannelId;
activity.From = Conversation.User;
activity.Recipient = Conversation.Bot;
@ -64,19 +101,31 @@ namespace Microsoft.Bot.Builder.Adapters
var id = activity.Id = (_nextId++).ToString();
}
if (activity.Timestamp == null || activity.Timestamp == default(DateTime))
if (activity.Timestamp == null || activity.Timestamp == default(DateTimeOffset))
{
activity.Timestamp = DateTime.UtcNow;
}
using (var context = new TurnContext(this, activity))
{
await RunPipeline(context, callback, cancellationToken);
await RunPipelineAsync(context, callback, cancellationToken).ConfigureAwait(false);
}
}
public ConversationReference Conversation { get; set; }
public async override Task<ResourceResponse[]> SendActivities(ITurnContext context, Activity[] activities, CancellationToken cancellationToken)
/// <summary>
/// Sends activities to the conversation.
/// </summary>
/// <param name="context">The context object for the turn.</param>
/// <param name="activities">The activities to send.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activities are successfully sent, the task result contains
/// an array of <see cref="ResourceResponse"/> objects containing the IDs that
/// the receiving channel assigned to the activities.</remarks>
/// <seealso cref="ITurnContext.OnSendActivities(SendActivitiesHandler)"/>
public async override Task<ResourceResponse[]> SendActivitiesAsync(ITurnContext context, Activity[] activities, CancellationToken cancellationToken)
{
if (context == null)
{
@ -102,7 +151,7 @@ namespace Microsoft.Bot.Builder.Adapters
{
var activity = activities[index];
if (String.IsNullOrEmpty(activity.Id))
if (string.IsNullOrEmpty(activity.Id))
{
activity.Id = Guid.NewGuid().ToString("n");
}
@ -112,16 +161,25 @@ namespace Microsoft.Bot.Builder.Adapters
activity.Timestamp = DateTime.UtcNow;
}
if (activity.Type == ActivityTypesEx.Delay)
{
// The BotFrameworkAdapter and Console adapter implement this
// hack directly in the POST method. Replicating that here
// to keep the behavior as close as possible to facillitate
// more realistic tests.
int delayMs = (int)activity.Value;
// more realistic tests.
var delayMs = (int)activity.Value;
await Task.Delay(delayMs);
await Task.Delay(delayMs).ConfigureAwait(false);
}
else if (activity.Type == ActivityTypes.Trace)
{
if (_sendTraceActivity)
{
lock (_activeQueueLock)
{
ActiveQueue.Enqueue(activity);
}
}
}
else
{
@ -137,7 +195,21 @@ namespace Microsoft.Bot.Builder.Adapters
return responses;
}
public override Task<ResourceResponse> UpdateActivity(ITurnContext context, Activity activity, CancellationToken cancellationToken)
/// <summary>
/// Replaces an existing activity in the <see cref="ActiveQueue"/>.
/// </summary>
/// <param name="context">The context object for the turn.</param>
/// <param name="activity">New replacement activity.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activity is successfully sent, the task result contains
/// a <see cref="ResourceResponse"/> object containing the ID that the receiving
/// channel assigned to the activity.
/// <para>Before calling this, set the ID of the replacement activity to the ID
/// of the activity to replace.</para></remarks>
/// <seealso cref="ITurnContext.OnUpdateActivity(UpdateActivityHandler)"/>
public override Task<ResourceResponse> UpdateActivityAsync(ITurnContext context, Activity activity, CancellationToken cancellationToken)
{
lock (_activeQueueLock)
{
@ -161,7 +233,18 @@ namespace Microsoft.Bot.Builder.Adapters
return Task.FromResult(new ResourceResponse());
}
public override Task DeleteActivity(ITurnContext context, ConversationReference reference, CancellationToken cancellationToken)
/// <summary>
/// Deletes an existing activity in the <see cref="ActiveQueue"/>.
/// </summary>
/// <param name="context">The context object for the turn.</param>
/// <param name="reference">Conversation reference for the activity to delete.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>The <see cref="ConversationReference.ActivityId"/> of the conversation
/// reference identifies the activity to delete.</remarks>
/// <seealso cref="ITurnContext.OnDeleteActivity(DeleteActivityHandler)"/>
public override Task DeleteActivityAsync(ITurnContext context, ConversationReference reference, CancellationToken cancellationToken)
{
lock (_activeQueueLock)
{
@ -181,17 +264,20 @@ namespace Microsoft.Bot.Builder.Adapters
}
}
}
return Task.CompletedTask;
}
/// <summary>
/// NOTE: this resets the queue, it doesn't actually maintain multiple converstion queues
/// Creates a new conversation on the specified channel.
/// </summary>
/// <param name="channelId"></param>
/// <param name="callback"></param>
/// <returns></returns>
//public override Task CreateConversation(string channelId, Func<ITurnContext, Task> callback)
public Task CreateConversation(string channelId, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
/// <param name="channelId">The ID of the channel.</param>
/// <param name="callback">The bot logic to call when the conversation is created.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>This resets the <see cref="ActiveQueue"/>, and does not maintain multiple converstion queues.</remarks>
public Task CreateConversationAsync(string channelId, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
{
ActiveQueue.Clear();
var update = Activity.CreateConversationUpdateActivity();
@ -201,9 +287,10 @@ namespace Microsoft.Bot.Builder.Adapters
}
/// <summary>
/// Called by TestFlow to check next reply
/// Dequeues and returns the next bot response from the <see cref="ActiveQueue"/>.
/// </summary>
/// <returns></returns>
/// <returns>The next activity in the queue; or null, if the queue is empty.</returns>
/// <remarks>A <see cref="TestFlow"/> object calls this to get the next response from the bot.</remarks>
public IActivity GetNextReply()
{
lock (_activeQueueLock)
@ -213,14 +300,17 @@ namespace Microsoft.Bot.Builder.Adapters
return ActiveQueue.Dequeue();
}
}
return null;
}
/// <summary>
/// Called by TestFlow to get appropriate activity for conversationReference of testbot
/// Creates a message activity from text and the current conversational context.
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
/// <param name="text">The message text.</param>
/// <returns>An appropriate message activity.</returns>
/// <remarks>A <see cref="TestFlow"/> object calls this to get a message activity
/// appropriate to the current conversation.</remarks>
public Activity MakeActivity(string text = null)
{
Activity activity = new Activity
@ -231,23 +321,23 @@ namespace Microsoft.Bot.Builder.Adapters
Conversation = Conversation.Conversation,
ServiceUrl = Conversation.ServiceUrl,
Id = (_nextId++).ToString(),
Text = text
Text = text,
};
return activity;
}
/// <summary>
/// Processes a message activity from a user.
/// </summary>
/// <param name="userSays">The text of the user's message.</param>
/// <param name="callback">The turn processing logic to use.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <seealso cref="TestFlow.Send(string)"/>
public Task SendTextToBot(string userSays, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
public Task SendTextToBotAsync(string userSays, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
{
return ProcessActivity(MakeActivity(userSays), callback, cancellationToken);
return ProcessActivityAsync(MakeActivity(userSays), callback, cancellationToken);
}
}
}

Просмотреть файл

@ -31,7 +31,7 @@ namespace Microsoft.Bot.Builder.Adapters
private Func<ITurnContext, Task> _callback;
/// <summary>
/// Creates a new test flow.
/// Initializes a new instance of the <see cref="TestFlow"/> class.
/// </summary>
/// <param name="adapter">The test adapter to use.</param>
/// <param name="callback">The bot turn processing logic to test.</param>
@ -42,6 +42,12 @@ namespace Microsoft.Bot.Builder.Adapters
_testTask = _testTask ?? Task.CompletedTask;
}
/// <summary>
/// Initializes a new instance of the <see cref="TestFlow"/> class from an existing flow.
/// </summary>
/// <param name="testTask">The exchange to add to the exchanges in the existing flow.</param>
/// <param name="flow">The flow to build up from. This provides the test adapter to use,
/// the bot turn processing locig to test, and a set of exchanges to model and test.</param>
public TestFlow(Task testTask, TestFlow flow)
{
_testTask = testTask ?? Task.CompletedTask;
@ -50,12 +56,14 @@ namespace Microsoft.Bot.Builder.Adapters
}
/// <summary>
/// Creates a new test flow.
/// Initializes a new instance of the <see cref="TestFlow"/> class.
/// </summary>
/// <param name="adapter">The test adapter to use.</param>
/// <param name="bot">The bot containing the turn processing logic to test.</param>
public TestFlow(TestAdapter adapter, IBot bot) : this(adapter, (ctx) => bot.OnTurn(ctx))
{ }
public TestFlow(TestAdapter adapter, IBot bot)
: this(adapter, (ctx) => bot.OnTurnAsync(ctx))
{
}
/// <summary>
/// Starts the execution of the test flow.
@ -64,7 +72,7 @@ namespace Microsoft.Bot.Builder.Adapters
/// <remarks>This methods sends the activities from the user to the bot and
/// checks the responses from the bot based on the activiies described in the
/// current test flow.</remarks>
public Task StartTest()
public Task StartTestAsync()
{
return _testTask;
}
@ -78,25 +86,29 @@ namespace Microsoft.Bot.Builder.Adapters
public TestFlow Send(string userSays)
{
if (userSays == null)
throw new ArgumentNullException("You have to pass a userSays parameter");
return new TestFlow(_testTask.ContinueWith((task) =>
{
// NOTE: we need to .Wait() on the original Task to properly observe any exceptions that might have occurred
// and to have them propagate correctly up through the chain to whomever is waiting on the parent task
// The following StackOverflow answer provides some more details on why you want to do this:
// https://stackoverflow.com/questions/11904821/proper-way-to-use-continuewith-for-tasks/11906865#11906865
//
// From the Docs:
// https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/exception-handling-task-parallel-library
// Exceptions are propagated when you use one of the static or instance Task.Wait or Wait
// methods, and you handle them by enclosing the call in a try/catch statement. If a task is the
// parent of attached child tasks, or if you are waiting on multiple tasks, multiple exceptions
// could be thrown.
task.Wait();
throw new ArgumentNullException("You have to pass a userSays parameter");
}
return _adapter.SendTextToBot(userSays, _callback, default(CancellationToken));
}).Unwrap(), this);
return new TestFlow(
_testTask.ContinueWith((task) =>
{
// NOTE: we need to .Wait() on the original Task to properly observe any exceptions that might have occurred
// and to have them propagate correctly up through the chain to whomever is waiting on the parent task
// The following StackOverflow answer provides some more details on why you want to do this:
// https://stackoverflow.com/questions/11904821/proper-way-to-use-continuewith-for-tasks/11906865#11906865
//
// From the Docs:
// https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/exception-handling-task-parallel-library
// Exceptions are propagated when you use one of the static or instance Task.Wait or Wait
// methods, and you handle them by enclosing the call in a try/catch statement. If a task is the
// parent of attached child tasks, or if you are waiting on multiple tasks, multiple exceptions
// could be thrown.
task.Wait();
return _adapter.SendTextToBotAsync(userSays, _callback, default(CancellationToken));
}).Unwrap(),
this);
}
/// <summary>
@ -108,15 +120,19 @@ namespace Microsoft.Bot.Builder.Adapters
public TestFlow Send(IActivity userActivity)
{
if (userActivity == null)
throw new ArgumentNullException("You have to pass an Activity");
return new TestFlow(_testTask.ContinueWith((task) =>
{
// NOTE: See details code in above method.
task.Wait();
throw new ArgumentNullException("You have to pass an Activity");
}
return _adapter.ProcessActivity((Activity)userActivity, _callback, default(CancellationToken));
}).Unwrap(), this);
return new TestFlow(
_testTask.ContinueWith((task) =>
{
// NOTE: See details code in above method.
task.Wait();
return _adapter.ProcessActivityAsync((Activity)userActivity, _callback, default(CancellationToken));
}).Unwrap(),
this);
}
/// <summary>
@ -125,15 +141,17 @@ namespace Microsoft.Bot.Builder.Adapters
/// <param name="ms">The delay length in milliseconds.</param>
/// <returns>A new <see cref="TestFlow"/> object that appends a delay to the modeled exchange.</returns>
/// <remarks>This method does not modify the original <see cref="TestFlow"/> object.</remarks>
public TestFlow Delay(UInt32 ms)
public TestFlow Delay(uint ms)
{
return new TestFlow(_testTask.ContinueWith((task) =>
{
// NOTE: See details code in above method.
task.Wait();
return new TestFlow(
_testTask.ContinueWith((task) =>
{
// NOTE: See details code in above method.
task.Wait();
return Task.Delay((int)ms);
}), this);
return Task.Delay((int)ms);
}),
this);
}
/// <summary>
@ -145,7 +163,7 @@ namespace Microsoft.Bot.Builder.Adapters
/// <returns>A new <see cref="TestFlow"/> object that appends this assertion to the modeled exchange.</returns>
/// <remarks>This method does not modify the original <see cref="TestFlow"/> object.</remarks>
/// <exception cref="Exception">The bot did not respond as expected.</exception>
public TestFlow AssertReply(string expected, string description = null, UInt32 timeout = 3000)
public TestFlow AssertReply(string expected, string description = null, uint timeout = 3000)
{
return AssertReply(_adapter.MakeActivity(expected), description, timeout);
}
@ -159,21 +177,30 @@ namespace Microsoft.Bot.Builder.Adapters
/// <returns>A new <see cref="TestFlow"/> object that appends this assertion to the modeled exchange.</returns>
/// <remarks>This method does not modify the original <see cref="TestFlow"/> object.</remarks>
/// <exception cref="Exception">The bot did not respond as expected.</exception>
public TestFlow AssertReply(IActivity expected, [CallerMemberName] string description = null, UInt32 timeout = 3000)
public TestFlow AssertReply(IActivity expected, [CallerMemberName] string description = null, uint timeout = 3000)
{
return AssertReply((reply) =>
{
if (expected.Type != reply.Type)
throw new Exception($"{description}: Type should match");
if (expected.AsMessageActivity().Text != reply.AsMessageActivity().Text)
return AssertReply(
(reply) =>
{
if (description == null)
throw new Exception($"Expected:{expected.AsMessageActivity().Text}\nReceived:{reply.AsMessageActivity().Text}");
else
throw new Exception($"{description}: Text should match");
}
// TODO, expand this to do all properties set on expected
}, description, timeout);
if (expected.Type != reply.Type)
{
throw new Exception($"{description}: Type should match");
}
if (expected.AsMessageActivity().Text != reply.AsMessageActivity().Text)
{
if (description == null)
{
throw new Exception($"Expected:{expected.AsMessageActivity().Text}\nReceived:{reply.AsMessageActivity().Text}");
}
else
{
throw new Exception($"{description}: Text should match");
}
}
},
description,
timeout);
}
/// <summary>
@ -185,35 +212,39 @@ namespace Microsoft.Bot.Builder.Adapters
/// <param name="timeout">The amount of time in milliseconds within which a response is expected.</param>
/// <returns>A new <see cref="TestFlow"/> object that appends this assertion to the modeled exchange.</returns>
/// <remarks>This method does not modify the original <see cref="TestFlow"/> object.</remarks>
public TestFlow AssertReply(Action<IActivity> validateActivity, [CallerMemberName] string description = null, UInt32 timeout = 3000)
public TestFlow AssertReply(Action<IActivity> validateActivity, [CallerMemberName] string description = null, uint timeout = 3000)
{
return new TestFlow(_testTask.ContinueWith((task) =>
{
// NOTE: See details code in above method.
task.Wait();
if (System.Diagnostics.Debugger.IsAttached)
timeout = UInt32.MaxValue;
var start = DateTime.UtcNow;
while (true)
return new TestFlow(
_testTask.ContinueWith((task) =>
{
var current = DateTime.UtcNow;
// NOTE: See details code in above method.
task.Wait();
if ((current - start).TotalMilliseconds > timeout)
if (System.Diagnostics.Debugger.IsAttached)
{
throw new TimeoutException($"{timeout}ms Timed out waiting for:'{description}'");
timeout = uint.MaxValue;
}
IActivity replyActivity = _adapter.GetNextReply();
if (replyActivity != null)
var start = DateTime.UtcNow;
while (true)
{
// if we have a reply
validateActivity(replyActivity);
return;
var current = DateTime.UtcNow;
if ((current - start).TotalMilliseconds > timeout)
{
throw new TimeoutException($"{timeout}ms Timed out waiting for:'{description}'");
}
IActivity replyActivity = _adapter.GetNextReply();
if (replyActivity != null)
{
// if we have a reply
validateActivity(replyActivity);
return;
}
}
}
}), this);
}),
this);
}
/// <summary>
@ -226,10 +257,12 @@ namespace Microsoft.Bot.Builder.Adapters
/// <returns>A new <see cref="TestFlow"/> object that appends this exchange to the modeled exchange.</returns>
/// <remarks>This method does not modify the original <see cref="TestFlow"/> object.</remarks>
/// <exception cref="Exception">The bot did not respond as expected.</exception>
public TestFlow Test(string userSays, string expected, string description = null, UInt32 timeout = 3000)
public TestFlow Test(string userSays, string expected, string description = null, uint timeout = 3000)
{
if (expected == null)
{
throw new ArgumentNullException(nameof(expected));
}
return Send(userSays)
.AssertReply(expected, description, timeout);
@ -245,10 +278,12 @@ namespace Microsoft.Bot.Builder.Adapters
/// <returns>A new <see cref="TestFlow"/> object that appends this exchange to the modeled exchange.</returns>
/// <remarks>This method does not modify the original <see cref="TestFlow"/> object.</remarks>
/// <exception cref="Exception">The bot did not respond as expected.</exception>
public TestFlow Test(string userSays, Activity expected, string description = null, UInt32 timeout = 3000)
public TestFlow Test(string userSays, Activity expected, string description = null, uint timeout = 3000)
{
if (expected == null)
{
throw new ArgumentNullException(nameof(expected));
}
return Send(userSays)
.AssertReply(expected, description, timeout);
@ -265,10 +300,12 @@ namespace Microsoft.Bot.Builder.Adapters
/// <returns>A new <see cref="TestFlow"/> object that appends this exchange to the modeled exchange.</returns>
/// <remarks>This method does not modify the original <see cref="TestFlow"/> object.</remarks>
/// <exception cref="Exception">The bot did not respond as expected.</exception>
public TestFlow Test(string userSays, Action<IActivity> validateActivity, string description = null, UInt32 timeout = 3000)
public TestFlow Test(string userSays, Action<IActivity> validateActivity, string description = null, uint timeout = 3000)
{
if (validateActivity == null)
{
throw new ArgumentNullException(nameof(validateActivity));
}
return Send(userSays)
.AssertReply(validateActivity, description, timeout);
@ -278,7 +315,7 @@ namespace Microsoft.Bot.Builder.Adapters
/// Shorcut for adding an arbitray exchange between the user and bot.
/// Each activity with a <see cref="IActivity.From"/>.<see cref="ChannelAccount.Role"/> equals to "bot"
/// will be processed with the <see cref="AssertReply(IActivity, string, uint)"/> method.
/// Every other activity will be processed as user's message via the <see cref="Send(IActivity)"/> method
/// Every other activity will be processed as user's message via the <see cref="Send(IActivity)"/> method.
/// </summary>
/// <param name="activities">The list of activities to test.</param>
/// <param name="description">A message to send if the actual response is not as expected.</param>
@ -286,10 +323,12 @@ namespace Microsoft.Bot.Builder.Adapters
/// <returns>A new <see cref="TestFlow"/> object that appends this exchange to the modeled exchange.</returns>
/// <remarks>This method does not modify the original <see cref="TestFlow"/> object.</remarks>
/// <exception cref="Exception">The bot did not respond as expected.</exception>
public TestFlow Test(IEnumerable<IActivity> activities, [CallerMemberName] string description = null, UInt32 timeout = 3000)
public TestFlow Test(IEnumerable<IActivity> activities, [CallerMemberName] string description = null, uint timeout = 3000)
{
if (activities == null)
{
throw new ArgumentNullException(nameof(activities));
}
// Chain all activities in a TestFlow, check if its a user message (send) or a bot reply (assert)
return activities.Aggregate(this, (flow, activity) =>
@ -304,7 +343,7 @@ namespace Microsoft.Bot.Builder.Adapters
/// Shorcut for adding an arbitray exchange between the user and bot.
/// Each activity with a <see cref="IActivity.From"/>.<see cref="ChannelAccount.Role"/> equals to "bot"
/// will be processed with the <see cref="AssertReply(IActivity, string, uint)"/> method.
/// Every other activity will be processed as user's message via the <see cref="Send(IActivity)"/> method
/// Every other activity will be processed as user's message via the <see cref="Send(IActivity)"/> method.
/// </summary>
/// <param name="activities">The list of activities to test.</param>
/// <param name="validateReply">The delegate to call to validate responses from the bot.</param>
@ -313,10 +352,12 @@ namespace Microsoft.Bot.Builder.Adapters
/// <returns>A new <see cref="TestFlow"/> object that appends this exchange to the modeled exchange.</returns>
/// <remarks>This method does not modify the original <see cref="TestFlow"/> object.</remarks>
/// <exception cref="Exception">The bot did not respond as expected.</exception>
public TestFlow Test(IEnumerable<IActivity> activities, ValidateReply validateReply, [CallerMemberName] string description = null, UInt32 timeout = 3000)
public TestFlow Test(IEnumerable<IActivity> activities, ValidateReply validateReply, [CallerMemberName] string description = null, uint timeout = 3000)
{
if (activities == null)
{
throw new ArgumentNullException(nameof(activities));
}
// Chain all activities in a TestFlow, check if its a user message (send) or a bot reply (assert)
return activities.Aggregate(this, (flow, activity) =>
@ -328,15 +369,10 @@ namespace Microsoft.Bot.Builder.Adapters
else
{
return flow.Send(activity);
};
}
});
}
private bool IsReply(IActivity activity)
{
return string.Equals("bot", activity.From?.Role, StringComparison.InvariantCultureIgnoreCase);
}
/// <summary>
/// Adds an assertion that the bot's response is contained within a set of acceptable responses.
/// </summary>
@ -346,20 +382,33 @@ namespace Microsoft.Bot.Builder.Adapters
/// <returns>A new <see cref="TestFlow"/> object that appends this assertion to the modeled exchange.</returns>
/// <remarks>This method does not modify the original <see cref="TestFlow"/> object.</remarks>
/// <exception cref="Exception">The bot did not respond as expected.</exception>
public TestFlow AssertReplyOneOf(string[] candidates, string description = null, UInt32 timeout = 3000)
public TestFlow AssertReplyOneOf(string[] candidates, string description = null, uint timeout = 3000)
{
if (candidates == null)
throw new ArgumentNullException(nameof(candidates));
return AssertReply((reply) =>
{
foreach (var candidate in candidates)
throw new ArgumentNullException(nameof(candidates));
}
return AssertReply(
(reply) =>
{
if (reply.AsMessageActivity().Text == candidate)
return;
}
throw new Exception(description ?? $"Not one of candidates: {String.Join("\n", candidates)}");
}, description, timeout);
foreach (var candidate in candidates)
{
if (reply.AsMessageActivity().Text == candidate)
{
return;
}
}
throw new Exception(description ?? $"Not one of candidates: {string.Join("\n", candidates)}");
},
description,
timeout);
}
private bool IsReply(IActivity activity)
{
return string.Equals("bot", activity.From?.Role, StringComparison.InvariantCultureIgnoreCase);
}
}
}
}

Просмотреть файл

@ -9,18 +9,18 @@ using Microsoft.Bot.Schema;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// Represents a bot adapter that can connect a bot to a service endpoint.
/// Represents a bot adapter that can connect a bot to a service endpoint.
/// This class is abstract.
/// </summary>
/// <remarks>The bot adapter encapsulates authentication processes and sends
/// activities to and receives activities from the Bot Connector Service. When your
/// bot receives an activity, the adapter creates a context object, passes it to your
/// <remarks>The bot adapter encapsulates authentication processes and sends
/// activities to and receives activities from the Bot Connector Service. When your
/// bot receives an activity, the adapter creates a context object, passes it to your
/// bot's application logic, and sends responses back to the user's channel.
/// <para>Use <see cref="Use(IMiddleware)"/> to add <see cref="IMiddleware"/> objects
/// to your adapters middleware collection. The adapter processes and directs
/// incoming activities in through the bot middleware pipeline to your bots logic
/// and then back out again. As each activity flows in and out of the bot, each piece
/// of middleware can inspect or act upon the activity, both before and after the bot
/// <para>Use <see cref="Use(IMiddleware)"/> to add <see cref="IMiddleware"/> objects
/// to your adapters middleware collection. The adapter processes and directs
/// incoming activities in through the bot middleware pipeline to your bots logic
/// and then back out again. As each activity flows in and out of the bot, each piece
/// of middleware can inspect or act upon the activity, both before and after the bot
/// logic runs.</para>
/// </remarks>
/// <seealso cref="ITurnContext"/>
@ -30,21 +30,24 @@ namespace Microsoft.Bot.Builder
public abstract class BotAdapter
{
/// <summary>
/// The collection of middleware in the adapter's pipeline.
/// Initializes a new instance of the <see cref="BotAdapter"/> class.
/// </summary>
protected readonly MiddlewareSet _middlewareSet = new MiddlewareSet();
/// <summary>
/// Creates a default adapter.
/// </summary>
public BotAdapter() : base()
public BotAdapter()
: base()
{
}
/// <summary>
/// Error handler that catches exceptions in the middleware or application
/// Gets or sets an error handler that can catche exceptions in the middleware or application.
/// </summary>
public Func<ITurnContext, Exception, Task> ErrorHandler { get; set; }
/// <value>An error handler that can catch exceptions in the middleware or application.</value>
public Func<ITurnContext, Exception, Task> OnTurnError { get; set; }
/// <summary>
/// Gets the collection of middleware in the adapter's pipeline.
/// </summary>
/// <value>The middleware collection for the pipeline.</value>
protected MiddlewareSet MiddlewareSet { get; } = new MiddlewareSet();
/// <summary>
/// Adds middleware to the adapter's pipeline.
@ -56,88 +59,118 @@ namespace Microsoft.Bot.Builder
/// </remarks>
public BotAdapter Use(IMiddleware middleware)
{
_middlewareSet.Use(middleware);
MiddlewareSet.Use(middleware);
return this;
}
/// <summary>
/// When overridden in a derived class, sends activities to the conversation.
/// </summary>
/// </summary>
/// <param name="context">The context object for the turn.</param>
/// <param name="activities">The activities to send.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activities are successfully sent, the task result contains
/// an array of <see cref="ResourceResponse"/> objects containing the IDs that
/// an array of <see cref="ResourceResponse"/> objects containing the IDs that
/// the receiving channel assigned to the activities.</remarks>
/// <seealso cref="ITurnContext.OnSendActivities(SendActivitiesHandler)"/>
public abstract Task<ResourceResponse[]> SendActivities(ITurnContext context, Activity[] activities, CancellationToken cancellationToken);
public abstract Task<ResourceResponse[]> SendActivitiesAsync(ITurnContext context, Activity[] activities, CancellationToken cancellationToken);
/// <summary>
/// When overridden in a derived class, replaces an existing activity in the
/// When overridden in a derived class, replaces an existing activity in the
/// conversation.
/// </summary>
/// <param name="context">The context object for the turn.</param>
/// <param name="activity">New replacement activity.</param>
/// <param name="activity">New replacement activity.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activity is successfully sent, the task result contains
/// a <see cref="ResourceResponse"/> object containing the ID that the receiving
/// a <see cref="ResourceResponse"/> object containing the ID that the receiving
/// channel assigned to the activity.
/// <para>Before calling this, set the ID of the replacement activity to the ID
/// of the activity to replace.</para></remarks>
/// <seealso cref="ITurnContext.OnUpdateActivity(UpdateActivityHandler)"/>
public abstract Task<ResourceResponse> UpdateActivity(ITurnContext context, Activity activity, CancellationToken cancellationToken);
public abstract Task<ResourceResponse> UpdateActivityAsync(ITurnContext context, Activity activity, CancellationToken cancellationToken);
/// <summary>
/// When overridden in a derived class, deletes an existing activity in the
/// When overridden in a derived class, deletes an existing activity in the
/// conversation.
/// </summary>
/// <param name="context">The context object for the turn.</param>
/// <param name="reference">Conversation reference for the activity to delete.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>The <see cref="ConversationReference.ActivityId"/> of the conversation
/// reference identifies the activity to delete.</remarks>
/// <seealso cref="ITurnContext.OnDeleteActivity(DeleteActivityHandler)"/>
public abstract Task DeleteActivity(ITurnContext context, ConversationReference reference, CancellationToken cancellationToken);
public abstract Task DeleteActivityAsync(ITurnContext context, ConversationReference reference, CancellationToken cancellationToken);
/// <summary>
/// Sends a proactive message to a conversation.
/// </summary>
/// <param name="botId">The application ID of the bot. This paramter is ignored in
/// single tenant the Adpters (Console, Test, etc) but is critical to the BotFrameworkAdapter
/// which is multi-tenant aware. </param>
/// <param name="reference">A reference to the conversation to continue.</param>
/// <param name="callback">The method to call for the resulting bot turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>Call this method to proactively send a message to a conversation.
/// Most _channels require a user to initiate a conversation with a bot
/// before the bot can send activities to the user.</remarks>
/// <seealso cref="RunPipelineAsync(ITurnContext, Func{ITurnContext, Task}, CancellationToken)"/>
public virtual Task ContinueConversationAsync(string botId, ConversationReference reference, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
{
using (var context = new TurnContext(this, reference.GetContinuationActivity()))
{
return RunPipelineAsync(context, callback, cancellationToken);
}
}
/// <summary>
/// Starts activity processing for the current bot turn.
/// </summary>
/// <param name="context">The turn's context object.</param>
/// <param name="callback">A callback method to run at the end of the pipeline.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="context"/> is null.</exception>
/// <remarks>The adapter calls middleware in the order in which you added it.
/// The adapter passes in the context object for the turn and a next delegate,
/// and the middleware calls the delegate to pass control to the next middleware
/// in the pipeline. Once control reaches the end of the pipeline, the adapter calls
/// the <paramref name="callback"/> method. If a middleware component doesnt call
/// the next delegate, the adapter does not call any of the subsequent middlewares
/// <see cref="IMiddleware.OnTurn(ITurnContext, MiddlewareSet.NextDelegate)"/>
/// <remarks>The adapter calls middleware in the order in which you added it.
/// The adapter passes in the context object for the turn and a next delegate,
/// and the middleware calls the delegate to pass control to the next middleware
/// in the pipeline. Once control reaches the end of the pipeline, the adapter calls
/// the <paramref name="callback"/> method. If a middleware component doesnt call
/// the next delegate, the adapter does not call any of the subsequent middlewares
/// <see cref="IMiddleware.OnTurnAsync(ITurnContext, NextDelegate, CancellationToken)"/>
/// methods or the callback method, and the pipeline short circuits.
/// <para>When the turn is initiated by a user activity (reactive messaging), the
/// callback method will be a reference to the bot's
/// <see cref="IBot.OnTurn(ITurnContext)"/> method. When the turn is
/// initiated by a call to <see cref="ContinueConversation(string, ConversationReference, Func{ITurnContext, Task})"/>
/// callback method will be a reference to the bot's
/// <see cref="IBot.OnTurnAsync(ITurnContext)"/> method. When the turn is
/// initiated by a call to <see cref="ContinueConversationAsync(string, ConversationReference, Func{ITurnContext, Task}, CancellationToken)"/>
/// (proactive messaging), the callback method is the callback method that was provided in the call.</para>
/// </remarks>
protected async Task RunPipeline(ITurnContext context, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
protected async Task RunPipelineAsync(ITurnContext context, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
{
BotAssert.ContextNotNull(context);
// Call any registered Middleware Components looking for ReceiveActivity()
// Call any registered Middleware Components looking for ReceiveActivityAsync()
if (context.Activity != null)
{
try
{
await _middlewareSet.ReceiveActivityWithStatus(context, callback, cancellationToken).ConfigureAwait(false);
await MiddlewareSet.ReceiveActivityWithStatusAsync(context, callback, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
if (ErrorHandler != null)
if (OnTurnError != null)
{
await ErrorHandler.Invoke(context, e).ConfigureAwait(false);
await OnTurnError.Invoke(context, e).ConfigureAwait(false);
}
else
{
@ -154,26 +187,5 @@ namespace Microsoft.Bot.Builder
}
}
}
/// <summary>
/// Sends a proactive message to a conversation.
/// </summary>
/// <param name="botId">The application ID of the bot. This paramter is ignored in
/// single tenant the Adpters (Console, Test, etc) but is critical to the BotFrameworkAdapter
/// which is multi-tenant aware. </param>
/// <param name="reference">A reference to the conversation to continue.</param>
/// <param name="callback">The method to call for the resulting bot turn.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>Call this method to proactively send a message to a conversation.
/// Most _channels require a user to initaiate a conversation with a bot
/// before the bot can send activities to the user.</remarks>
/// <seealso cref="RunPipeline(ITurnContext, Func{ITurnContext, Task})"/>
public virtual Task ContinueConversation(string botId, ConversationReference reference, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
{
using (var context = new TurnContext(this, reference.GetContinuationActivity()))
{
return RunPipeline(context, callback, cancellationToken);
}
}
}
}

Просмотреть файл

@ -21,7 +21,9 @@ namespace Microsoft.Bot.Builder
public static void ActivityNotNull(IActivity activity)
{
if (activity == null)
{
throw new ArgumentNullException(nameof(activity));
}
}
/// <summary>
@ -33,7 +35,9 @@ namespace Microsoft.Bot.Builder
public static void ContextNotNull(ITurnContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
{
throw new ArgumentNullException(nameof(context));
}
}
/// <summary>
@ -45,7 +49,9 @@ namespace Microsoft.Bot.Builder
public static void ConversationReferenceNotNull(ConversationReference reference)
{
if (reference == null)
{
throw new ArgumentNullException(nameof(reference));
}
}
/// <summary>
@ -57,7 +63,9 @@ namespace Microsoft.Bot.Builder
public static void ActivityListNotNull(IEnumerable<Activity> activities)
{
if (activities == null)
throw new ArgumentNullException(nameof(activities));
{
throw new ArgumentNullException(nameof(activities));
}
}
/// <summary>
@ -69,7 +77,9 @@ namespace Microsoft.Bot.Builder
public static void MiddlewareNotNull(IMiddleware middleware)
{
if (middleware == null)
{
throw new ArgumentNullException(nameof(middleware));
}
}
/// <summary>
@ -81,7 +91,9 @@ namespace Microsoft.Bot.Builder
public static void MiddlewareNotNull(IEnumerable<IMiddleware> middleware)
{
if (middleware == null)
{
throw new ArgumentNullException(nameof(middleware));
}
}
}
}

Просмотреть файл

@ -17,11 +17,11 @@ using Microsoft.Rest.TransientFaultHandling;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// A bot adapter that can connect a bot to a service endpoint.
/// A bot adapter that can connect a bot to a service endpoint.
/// </summary>
/// <remarks>The bot adapter encapsulates authentication processes and sends
/// activities to and receives activities from the Bot Connector Service. When your
/// bot receives an activity, the adapter creates a context object, passes it to your
/// <remarks>The bot adapter encapsulates authentication processes and sends
/// activities to and receives activities from the Bot Connector Service. When your
/// bot receives an activity, the adapter creates a context object, passes it to your
/// bot's application logic, and sends responses back to the user's channel.
/// <para>Use <see cref="Use(IMiddleware)"/> to add <see cref="IMiddleware"/> objects
/// to your adapters middleware collection. The adapter processes and directs
@ -36,14 +36,14 @@ namespace Microsoft.Bot.Builder
/// <seealso cref="IMiddleware"/>
public class BotFrameworkAdapter : BotAdapter
{
private readonly ICredentialProvider _credentialProvider;
private const string InvokeReponseKey = "BotFrameworkAdapter.InvokeResponse";
private const string BotIdentityKey = "BotIdentity";
private static readonly HttpClient DefaultHttpClient = new HttpClient();
private readonly ICredentialProvider _credentialProvider;
private readonly HttpClient _httpClient;
private readonly RetryPolicy _connectorClientRetryPolicy;
private Dictionary<string, MicrosoftAppCredentials> _appCredentialMap = new Dictionary<string, MicrosoftAppCredentials>();
private const string InvokeReponseKey = "BotFrameworkAdapter.InvokeResponse";
private const string BotIdentityKey = "BotIdentity";
private bool _isEmulatingOAuthCards = false;
/// <summary>
@ -57,7 +57,7 @@ namespace Microsoft.Bot.Builder
/// <exception cref="ArgumentNullException">
/// <paramref name="credentialProvider"/> is <c>null</c>.</exception>
/// <remarks>Use a <see cref="MiddlewareSet"/> object to add multiple middleware
/// components in the conustructor. Use the <see cref="Use(IMiddleware)"/> method to
/// components in the conustructor. Use the <see cref="Use(IMiddleware)"/> method to
/// add additional middleware to the adapter after construction.
/// </remarks>
public BotFrameworkAdapter(ICredentialProvider credentialProvider, RetryPolicy connectorClientRetryPolicy = null, HttpClient customHttpClient = null, IMiddleware middleware = null)
@ -92,23 +92,29 @@ namespace Microsoft.Bot.Builder
/// <item><see cref="IConnectorClient"/>, the channel connector client to use this turn.</item>
/// </list></para>
/// <para>
/// This overload differers from the Node implementation by requiring the BotId to be
/// This overload differers from the Node implementation by requiring the BotId to be
/// passed in. The .Net code allows multiple bots to be hosted in a single adapter which
/// isn't something supported by Node.
/// </para>
/// </remarks>
/// <seealso cref="ProcessActivity(string, Activity, Func{ITurnContext, Task})"/>
/// <seealso cref="BotAdapter.RunPipeline(ITurnContext, Func{ITurnContext, Task})"/>
public override async Task ContinueConversation(string botAppId, ConversationReference reference, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
/// <seealso cref="ProcessActivityAsync(string, Activity, Func{ITurnContext, Task}, CancellationToken)"/>
/// <seealso cref="BotAdapter.RunPipelineAsync(ITurnContext, Func{ITurnContext, Task}, CancellationToken)"/>
public override async Task ContinueConversationAsync(string botAppId, ConversationReference reference, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(botAppId))
{
throw new ArgumentNullException(nameof(botAppId));
}
if (reference == null)
{
throw new ArgumentNullException(nameof(reference));
}
if (callback == null)
{
throw new ArgumentNullException(nameof(callback));
}
using (var context = new TurnContext(this, reference.GetContinuationActivity()))
{
@ -117,13 +123,13 @@ namespace Microsoft.Bot.Builder
{
// Adding claims for both Emulator and Channel.
new Claim(AuthenticationConstants.AudienceClaim, botAppId),
new Claim(AuthenticationConstants.AppIdClaim, botAppId)
new Claim(AuthenticationConstants.AppIdClaim, botAppId),
});
context.Services.Add<IIdentity>(BotIdentityKey, claimsIdentity);
var connectorClient = await CreateConnectorClientAsync(reference.ServiceUrl, claimsIdentity, cancellationToken).ConfigureAwait(false);
context.Services.Add(connectorClient);
await RunPipeline(context, callback, cancellationToken);
await RunPipelineAsync(context, callback, cancellationToken).ConfigureAwait(false);
}
}
@ -137,7 +143,7 @@ namespace Microsoft.Bot.Builder
/// </remarks>
public new BotFrameworkAdapter Use(IMiddleware middleware)
{
_middlewareSet.Use(middleware);
MiddlewareSet.Use(middleware);
return this;
}
@ -146,33 +152,44 @@ namespace Microsoft.Bot.Builder
/// </summary>
/// <param name="authHeader">The HTTP authentication header of the request.</param>
/// <param name="activity">The incoming activity.</param>
/// <param name="callback">The code to run at the end of the adapter's middleware
/// pipeline.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="callback">The code to run at the end of the adapter's middleware pipeline.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute. If the activity type
/// was 'Invoke' and the corresponding key (channelId + activityId) was found
/// then an InvokeResponse is returned, otherwise null is returned.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="activity"/> is <c>null</c>.</exception>
/// <exception cref="UnauthorizedAccessException">
/// authentication failed.</exception>
/// <exception cref="ArgumentNullException"><paramref name="activity"/> is <c>null</c>.</exception>
/// <exception cref="UnauthorizedAccessException">authentication failed.</exception>
/// <remarks>Call this method to reactively send a message to a conversation.
/// If the task completes successfully, then if the activity's <see cref="Activity.Type"/>
/// is <see cref="ActivityTypes.Invoke"/> and the corresponding key
/// (<see cref="Activity.ChannelId"/> + <see cref="Activity.Id"/>) is found
/// then an <see cref="InvokeResponse"/> is returned, otherwise null is returned.
/// <para>This method registers the following services for the turn.<list type="bullet">
/// <item><see cref="IIdentity"/> (key = "BotIdentity"), a claims identity for the bot.</item>
/// <item><see cref="IConnectorClient"/>, the channel connector client to use this turn.</item>
/// </list></para>
/// </remarks>
/// <seealso cref="ContinueConversation(string, ConversationReference, Func{ITurnContext, Task})"/>
/// <seealso cref="BotAdapter.RunPipeline(ITurnContext, Func{ITurnContext, Task})"/>
public async Task<InvokeResponse> ProcessActivity(string authHeader, Activity activity, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
/// <seealso cref="ContinueConversationAsync(string, ConversationReference, Func{ITurnContext, Task}, CancellationToken)"/>
/// <seealso cref="BotAdapter.RunPipelineAsync(ITurnContext, Func{ITurnContext, Task}, CancellationToken)"/>
public async Task<InvokeResponse> ProcessActivityAsync(string authHeader, Activity activity, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
{
BotAssert.ActivityNotNull(activity);
var claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, _credentialProvider, _httpClient).ConfigureAwait(false);
return await ProcessActivity(claimsIdentity, activity, callback, cancellationToken).ConfigureAwait(false);
return await ProcessActivityAsync(claimsIdentity, activity, callback, cancellationToken).ConfigureAwait(false);
}
public async Task<InvokeResponse> ProcessActivity(ClaimsIdentity identity, Activity activity, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
/// <summary>
/// Creates a turn context and runs the middleware pipeline for an incoming activity.
/// </summary>
/// <param name="identity">A <see cref="ClaimsIdentity"/> for the request.</param>
/// <param name="activity">The incoming activity.</param>
/// <param name="callback">The code to run at the end of the adapter's middleware pipeline.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public async Task<InvokeResponse> ProcessActivityAsync(ClaimsIdentity identity, Activity activity, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
{
BotAssert.ActivityNotNull(activity);
@ -183,16 +200,16 @@ namespace Microsoft.Bot.Builder
var connectorClient = await CreateConnectorClientAsync(activity.ServiceUrl, identity, cancellationToken).ConfigureAwait(false);
context.Services.Add(connectorClient);
await RunPipeline(context, callback, cancellationToken).ConfigureAwait(false);
await RunPipelineAsync(context, callback, cancellationToken).ConfigureAwait(false);
// Handle Invoke scenarios, which deviate from the request/response model in that
// the Bot will return a specific body and return code.
// Handle Invoke scenarios, which deviate from the request/response model in that
// the Bot will return a specific body and return code.
if (activity.Type == ActivityTypes.Invoke)
{
Activity invokeResponse = context.Services.Get<Activity>(InvokeReponseKey);
if (invokeResponse == null)
{
// ToDo: Trace Here
// ToDo: Trace Here
throw new InvalidOperationException("Bot failed to return a valid 'invokeResponse' activity.");
}
else
@ -201,24 +218,24 @@ namespace Microsoft.Bot.Builder
}
}
// For all non-invoke scenarios, the HTTP layers above don't have to mess
// withthe Body and return codes.
// For all non-invoke scenarios, the HTTP layers above don't have to mess
// withthe Body and return codes.
return null;
}
}
/// <summary>
/// Sends activities to the conversation.
/// </summary>
/// </summary>
/// <param name="context">The context object for the turn.</param>
/// <param name="activities">The activities to send.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activities are successfully sent, the task result contains
/// an array of <see cref="ResourceResponse"/> objects containing the IDs that
/// an array of <see cref="ResourceResponse"/> objects containing the IDs that
/// the receiving channel assigned to the activities.</remarks>
/// <seealso cref="ITurnContext.OnSendActivities(SendActivitiesHandler)"/>
public override async Task<ResourceResponse[]> SendActivities(ITurnContext context, Activity[] activities, CancellationToken cancellationToken)
public override async Task<ResourceResponse[]> SendActivitiesAsync(ITurnContext context, Activity[] activities, CancellationToken cancellationToken)
{
if (context == null)
{
@ -237,7 +254,7 @@ namespace Microsoft.Bot.Builder
var responses = new ResourceResponse[activities.Length];
/*
/*
* NOTE: we're using for here (vs. foreach) because we want to simultaneously index into the
* activities array to get the activity to process as well as use that index to assign
* the response to the responses array and this is the most cost effective way to do that.
@ -253,11 +270,13 @@ namespace Microsoft.Bot.Builder
// here in the Bot. This matches the behavior in the Node connector.
int delayMs = (int)activity.Value;
await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false);
// No need to create a response. One will be created below.
}
else if (activity.Type == ActivityTypesEx.InvokeResponse) // Aligning name with Node
else if (activity.Type == ActivityTypesEx.InvokeResponse)
{
context.Services.Add(InvokeReponseKey, activity);
// No need to create a response. One will be created below.
}
else if (activity.Type == ActivityTypes.Trace && activity.ChannelId != "emulator")
@ -276,11 +295,11 @@ namespace Microsoft.Bot.Builder
}
// If No response is set, then defult to a "simple" response. This can't really be done
// above, as there are cases where the ReplyTo/SendTo methods will also return null
// (See below) so the check has to happen here.
// above, as there are cases where the ReplyTo/SendTo methods will also return null
// (See below) so the check has to happen here.
// Note: In addition to the Invoke / Delay / Activity cases, this code also applies
// with Skype and Teams with regards to typing events. When sending a typing event in
// with Skype and Teams with regards to typing events. When sending a typing event in
// these _channels they do not return a RequestResponse which causes the bot to blow up.
// https://github.com/Microsoft/botbuilder-dotnet/issues/460
// bug report : https://github.com/Microsoft/botbuilder-dotnet/issues/465
@ -299,16 +318,16 @@ namespace Microsoft.Bot.Builder
/// Replaces an existing activity in the conversation.
/// </summary>
/// <param name="context">The context object for the turn.</param>
/// <param name="activity">New replacement activity.</param>
/// <param name="activity">New replacement activity.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activity is successfully sent, the task result contains
/// a <see cref="ResourceResponse"/> object containing the ID that the receiving
/// a <see cref="ResourceResponse"/> object containing the ID that the receiving
/// channel assigned to the activity.
/// <para>Before calling this, set the ID of the replacement activity to the ID
/// of the activity to replace.</para></remarks>
/// <seealso cref="ITurnContext.OnUpdateActivity(UpdateActivityHandler)"/>
public override async Task<ResourceResponse> UpdateActivity(ITurnContext context, Activity activity, CancellationToken cancellationToken)
public override async Task<ResourceResponse> UpdateActivityAsync(ITurnContext context, Activity activity, CancellationToken cancellationToken)
{
var connectorClient = context.Services.Get<IConnectorClient>();
return await connectorClient.Conversations.UpdateActivityAsync(activity, cancellationToken).ConfigureAwait(false);
@ -324,26 +343,31 @@ namespace Microsoft.Bot.Builder
/// <remarks>The <see cref="ConversationReference.ActivityId"/> of the conversation
/// reference identifies the activity to delete.</remarks>
/// <seealso cref="ITurnContext.OnDeleteActivity(DeleteActivityHandler)"/>
public override async Task DeleteActivity(ITurnContext context, ConversationReference reference, CancellationToken cancellationToken)
public override async Task DeleteActivityAsync(ITurnContext context, ConversationReference reference, CancellationToken cancellationToken)
{
var connectorClient = context.Services.Get<IConnectorClient>();
await connectorClient.Conversations.DeleteActivityAsync(reference.Conversation.Id, reference.ActivityId, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Deletes a member from the current conversation
/// Removes a member from the current conversation.
/// </summary>
/// <param name="context">The context object for the turn.</param>
/// <param name="memberId">ID of the member to delete from the conversation</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns></returns>
public async Task DeleteConversationMember(ITurnContext context, string memberId, CancellationToken cancellationToken)
/// <param name="memberId">The ID of the member to remove from the conversation.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public async Task DeleteConversationMemberAsync(ITurnContext context, string memberId, CancellationToken cancellationToken)
{
if (context.Activity.Conversation == null)
{
throw new ArgumentNullException("BotFrameworkAdapter.deleteConversationMember(): missing conversation");
}
if (string.IsNullOrWhiteSpace(context.Activity.Conversation.Id))
{
throw new ArgumentNullException("BotFrameworkAdapter.deleteConversationMember(): missing conversation.id");
}
var connectorClient = context.Services.Get<IConnectorClient>();
@ -358,21 +382,27 @@ namespace Microsoft.Bot.Builder
/// <param name="context">The context object for the turn.</param>
/// <param name="activityId">(Optional) Activity ID to enumerate. If not specified the current activities ID will be used.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of Members of the activity</returns>
public async Task<IList<ChannelAccount>> GetActivityMembers(ITurnContext context, string activityId, CancellationToken cancellationToken)
/// <returns>List of Members of the activity.</returns>
public async Task<IList<ChannelAccount>> GetActivityMembersAsync(ITurnContext context, string activityId, CancellationToken cancellationToken)
{
// If no activity was passed in, use the current activity.
if (activityId == null)
{
activityId = context.Activity.Id;
}
if (context.Activity.Conversation == null)
{
throw new ArgumentNullException("BotFrameworkAdapter.GetActivityMembers(): missing conversation");
}
if (string.IsNullOrWhiteSpace(context.Activity.Conversation.Id))
{
throw new ArgumentNullException("BotFrameworkAdapter.GetActivityMembers(): missing conversation.id");
}
var connectorClient = context.Services.Get<IConnectorClient>();
string conversationId = context.Activity.Conversation.Id;
var conversationId = context.Activity.Conversation.Id;
IList<ChannelAccount> accounts = await connectorClient.Conversations.GetActivityMembersAsync(conversationId, activityId, cancellationToken).ConfigureAwait(false);
@ -382,71 +412,77 @@ namespace Microsoft.Bot.Builder
/// <summary>
/// Lists the members of the current conversation.
/// </summary>
/// <param name="context">The context object for the turn.</param>
/// <param name="context">The context object for the turn.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of Members of the current conversation</returns>
public async Task<IList<ChannelAccount>> GetConversationMembers(ITurnContext context, CancellationToken cancellationToken)
/// <returns>List of Members of the current conversation.</returns>
public async Task<IList<ChannelAccount>> GetConversationMembersAsync(ITurnContext context, CancellationToken cancellationToken)
{
if (context.Activity.Conversation == null)
{
throw new ArgumentNullException("BotFrameworkAdapter.GetActivityMembers(): missing conversation");
}
if (string.IsNullOrWhiteSpace(context.Activity.Conversation.Id))
{
throw new ArgumentNullException("BotFrameworkAdapter.GetActivityMembers(): missing conversation.id");
}
var connectorClient = context.Services.Get<IConnectorClient>();
string conversationId = context.Activity.Conversation.Id;
var conversationId = context.Activity.Conversation.Id;
IList<ChannelAccount> accounts = await connectorClient.Conversations.GetConversationMembersAsync(conversationId, cancellationToken).ConfigureAwait(false);
return accounts;
}
/// <summary>
/// Lists the Conversations in which this bot has participated for a given channel server. The
/// channel server returns results in pages and each page will include a `continuationToken`
/// Lists the Conversations in which this bot has participated for a given channel server. The
/// channel server returns results in pages and each page will include a `continuationToken`
/// that can be used to fetch the next page of results from the server.
/// </summary>
/// <param name="serviceUrl">The URL of the channel server to query. This can be retrieved
/// <param name="serviceUrl">The URL of the channel server to query. This can be retrieved
/// from `context.activity.serviceUrl`. </param>
/// <param name="credentials">The credentials needed for the Bot to connect to the services.</param>
/// <param name="continuationToken">(Optional) token used to fetch the next page of results
/// from the channel server. This should be left as `null` to retrieve the first page
/// of results.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of Members of the current conversation</returns>
/// <remarks>
/// This overload may be called from outside the context of a conversation, as only the
/// Bot's ServiceUrl and credentials are required.
/// <param name="continuationToken"></param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the task completes successfully, the result contains the members of the current conversation.
/// This overload may be called from outside the context of a conversation, as only the
/// bot's service URL and credentials are required.
/// </remarks>
public async Task<ConversationsResult> GetConversations(string serviceUrl, MicrosoftAppCredentials credentials, string continuationToken, CancellationToken cancellationToken)
public async Task<ConversationsResult> GetConversationsAsync(string serviceUrl, MicrosoftAppCredentials credentials, string continuationToken, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(serviceUrl))
{
throw new ArgumentNullException(nameof(serviceUrl));
}
if (credentials == null)
{
throw new ArgumentNullException(nameof(credentials));
}
var connectorClient = CreateConnectorClient(serviceUrl, credentials);
ConversationsResult results = await connectorClient.Conversations.GetConversationsAsync(continuationToken, cancellationToken).ConfigureAwait(false);
var results = await connectorClient.Conversations.GetConversationsAsync(continuationToken, cancellationToken).ConfigureAwait(false);
return results;
}
/// <summary>
/// Lists the Conversations in which this bot has participated for a given channel server. The
/// channel server returns results in pages and each page will include a `continuationToken`
/// Lists the Conversations in which this bot has participated for a given channel server. The
/// channel server returns results in pages and each page will include a `continuationToken`
/// that can be used to fetch the next page of results from the server.
/// </summary>
/// <param name="context">The context object for the turn.</param>
/// <param name="continuationToken">(Optional) token used to fetch the next page of results
/// from the channel server. This should be left as `null` to retrieve the first page
/// of results.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of Members of the current conversation</returns>
/// <remarks>
/// This overload may be called during standard Activity processing, at which point the Bot's
/// <param name="context">The context object for the turn.</param>
/// <param name="continuationToken"></param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the task completes successfully, the result contains the members of the current conversation.
/// This overload may be called during standard activity processing, at which point the Bot's
/// service URL and credentials that are part of the current activity processing pipeline
/// will be used.
/// will be used.
/// </remarks>
public async Task<ConversationsResult> GetConversations(ITurnContext context, string continuationToken, CancellationToken cancellationToken)
public async Task<ConversationsResult> GetConversationsAsync(ITurnContext context, string continuationToken, CancellationToken cancellationToken)
{
var connectorClient = context.Services.Get<IConnectorClient>();
var results = await connectorClient.Conversations.GetConversationsAsync(continuationToken, cancellationToken).ConfigureAwait(false);
@ -459,15 +495,19 @@ namespace Microsoft.Bot.Builder
/// <param name="connectionName">Name of the auth connection to use.</param>
/// <param name="magicCode">(Optional) Optional user entered code to validate.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Token Response</returns>
public async Task<TokenResponse> GetUserToken(ITurnContext context, string connectionName, string magicCode, CancellationToken cancellationToken)
/// <returns>Token Response.</returns>
public async Task<TokenResponse> GetUserTokenAsync(ITurnContext context, string connectionName, string magicCode, CancellationToken cancellationToken)
{
BotAssert.ContextNotNull(context);
if (context.Activity.From == null || string.IsNullOrWhiteSpace(context.Activity.From.Id))
{
throw new ArgumentNullException("BotFrameworkAdapter.GetuserToken(): missing from or from.id");
}
if (string.IsNullOrWhiteSpace(connectionName))
throw new ArgumentNullException(nameof(connectionName));
{
throw new ArgumentNullException(nameof(connectionName));
}
var client = CreateOAuthApiClient(context);
return await client.GetUserTokenAsync(context.Activity.From.Id, connectionName, magicCode, null, cancellationToken).ConfigureAwait(false);
@ -478,13 +518,17 @@ namespace Microsoft.Bot.Builder
/// </summary>
/// <param name="context">Context for the current turn of conversation with the user.</param>
/// <param name="connectionName">Name of the auth connection to use.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns></returns>
public async Task<string> GetOauthSignInLink(ITurnContext context, string connectionName, CancellationToken cancellationToken)
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the task completes successfully, the result contains the raw signin link.</remarks>
public async Task<string> GetOauthSignInLinkAsync(ITurnContext context, string connectionName, CancellationToken cancellationToken)
{
BotAssert.ContextNotNull(context);
if (string.IsNullOrWhiteSpace(connectionName))
{
throw new ArgumentNullException(nameof(connectionName));
}
var client = CreateOAuthApiClient(context);
return await client.GetSignInLinkAsync(context.Activity, connectionName, cancellationToken).ConfigureAwait(false);
@ -495,12 +539,16 @@ namespace Microsoft.Bot.Builder
/// </summary>
/// <param name="context">Context for the current turn of conversation with the user.</param>
/// <param name="connectionName">Name of the auth connection to use.</param>
/// <returns></returns>
public async Task SignOutUser(ITurnContext context, string connectionName, CancellationToken cancellationToken)
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public async Task SignOutUserAsync(ITurnContext context, string connectionName, CancellationToken cancellationToken)
{
BotAssert.ContextNotNull(context);
if (string.IsNullOrWhiteSpace(connectionName))
{
throw new ArgumentNullException(nameof(connectionName));
}
var client = CreateOAuthApiClient(context);
await client.SignOutUserAsync(context.Activity.From.Id, connectionName, cancellationToken).ConfigureAwait(false);
@ -512,27 +560,27 @@ namespace Microsoft.Bot.Builder
/// <param name="channelId">The ID for the channel.</param>
/// <param name="serviceUrl">The channel's service URL endpoint.</param>
/// <param name="credentials">The application credentials for the bot.</param>
/// <param name="conversationParameters">The conversation information to use to
/// <param name="conversationParameters">The conversation information to use to
/// create the conversation.</param>
/// <param name="callback">The method to call for the resulting bot turn.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>To start a conversation, your bot must know its account information
/// <remarks>To start a conversation, your bot must know its account information
/// and the user's account information on that channel.
/// Most _channels only support initiating a direct message (non-group) conversation.
/// <para>The adapter attempts to create a new conversation on the channel, and
/// then sends a <c>conversationUpdate</c> activity through its middleware pipeline
/// to the <paramref name="callback"/> method.</para>
/// <para>If the conversation is established with the
/// <para>If the conversation is established with the
/// specified users, the ID of the activity's <see cref="IActivity.Conversation"/>
/// will contain the ID of the new conversation.</para>
/// </remarks>
public virtual async Task CreateConversation(string channelId, string serviceUrl, MicrosoftAppCredentials credentials, ConversationParameters conversationParameters, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
public virtual async Task CreateConversationAsync(string channelId, string serviceUrl, MicrosoftAppCredentials credentials, ConversationParameters conversationParameters, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
{
var connectorClient = CreateConnectorClient(serviceUrl, credentials);
var result = await connectorClient.Conversations.CreateConversationAsync(conversationParameters, cancellationToken).ConfigureAwait(false);
// Create a conversation update activity to represent the result.
var eventActivity = Activity.CreateEventActivity();
@ -551,12 +599,21 @@ namespace Microsoft.Bot.Builder
claimsIdentity.AddClaim(new Claim(AuthenticationConstants.ServiceUrlClaim, serviceUrl));
context.Services.Add<IIdentity>(BotIdentityKey, claimsIdentity);
context.Services.Add<IConnectorClient>(connectorClient);
await RunPipeline(context, callback, cancellationToken).ConfigureAwait(false);
context.Services.Add(connectorClient);
await RunPipelineAsync(context, callback, cancellationToken).ConfigureAwait(false);
}
}
protected async Task<bool> TrySetEmulatingOAuthCards(ITurnContext turnContext, CancellationToken cancellationToken)
/// <summary>
/// Checks the current operating environment to determine whether to emulate the client for OAuth requests.
/// </summary>
/// <param name="turnContext">The context object for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the task completes successfully, the result indicates whether the adapter will
/// emaulate an OAuth client.</remarks>
protected async Task<bool> TrySetEmulatingOAuthCardsAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
if (!_isEmulatingOAuthCards &&
string.Equals(turnContext.Activity.ChannelId, "emulator", StringComparison.InvariantCultureIgnoreCase) &&
@ -568,6 +625,11 @@ namespace Microsoft.Bot.Builder
return _isEmulatingOAuthCards;
}
/// <summary>
/// Creates an OAuth client for the bot.
/// </summary>
/// <param name="context">The context object for the current turn.</param>
/// <returns>An OAuth client for the bot.</returns>
protected OAuthClient CreateOAuthApiClient(ITurnContext context)
{
var client = context.Services.Get<IConnectorClient>() as ConnectorClient;
@ -575,10 +637,12 @@ namespace Microsoft.Bot.Builder
{
throw new ArgumentNullException("CreateOAuthApiClient: OAuth requires a valid ConnectorClient instance");
}
if (_isEmulatingOAuthCards)
{
return new OAuthClient(client, context.Activity.ServiceUrl);
}
return new OAuthClient(client, AuthenticationConstants.OAuthUrl);
}
@ -597,12 +661,12 @@ namespace Microsoft.Bot.Builder
throw new NotSupportedException("ClaimsIdemtity cannot be null. Pass Anonymous ClaimsIdentity if authentication is turned off.");
}
// For requests from channel App Id is in Audience claim of JWT token. For emulator it is in AppId claim. For
// For requests from channel App Id is in Audience claim of JWT token. For emulator it is in AppId claim. For
// unauthenticated requests we have anonymouse identity provided auth is disabled.
var botAppIdClaim = (claimsIdentity.Claims?.SingleOrDefault(claim => claim.Type == AuthenticationConstants.AudienceClaim)
??
// For Activities coming from Emulator AppId claim contains the Bot's AAD AppId.
claimsIdentity.Claims?.SingleOrDefault(claim => claim.Type == AuthenticationConstants.AppIdClaim));
// For Activities coming from Emulator AppId claim contains the Bot's AAD AppId.
var botAppIdClaim = claimsIdentity.Claims?.SingleOrDefault(claim => claim.Type == AuthenticationConstants.AudienceClaim)
??
claimsIdentity.Claims?.SingleOrDefault(claim => claim.Type == AuthenticationConstants.AppIdClaim);
// For anonymous requests (requests with no header) appId is not set in claims.
if (botAppIdClaim != null)

Просмотреть файл

@ -14,7 +14,7 @@ namespace Microsoft.Bot.Builder
public class BotFrameworkHttpStatusCodeErrorDetectionStrategy : ITransientErrorDetectionStrategy
{
/// <summary>
/// Returns true if status code in HttpRequestExceptionWithStatus exception is RequestTimeout, TooManyRequests, NotFound or greater
/// Returns true if status code in HttpRequestExceptionWithStatus exception is RequestTimeout, TooManyRequests, NotFound or greater
/// than or equal to 500 and not NotImplemented (501) or HttpVersionNotSupported (505).
/// </summary>
/// <param name="ex">Exception to check against.</param>

Просмотреть файл

@ -9,11 +9,6 @@ using System.Threading.Tasks;
namespace Microsoft.Bot.Builder
{
public class StateSettings
{
public bool LastWriterWins { get; set; } = true;
}
/// <summary>
/// Base class which manages details of automatic loading and saving of bot state.
/// </summary>
@ -27,11 +22,11 @@ namespace Microsoft.Bot.Builder
private readonly string _propertyName;
/// <summary>
/// Creates a new <see cref="BotState{TState}"/> middleware object.
/// Initializes a new instance of the <see cref="BotState{TState}"/> class.
/// </summary>
/// <param name="storage">The storage provider to use.</param>
/// <param name="propertyName">The name to use to load or save the state object.</param>
/// <param name="keyDelegate"></param>
/// <param name="keyDelegate">A function that can provide the key.</param>
/// <param name="settings">The state persistance options to use.</param>
public BotState(IStorage storage, string propertyName, Func<ITurnContext, string> keyDelegate, StateSettings settings = null)
{
@ -46,31 +41,16 @@ namespace Microsoft.Bot.Builder
/// </summary>
/// <param name="context">The context object for this turn.</param>
/// <param name="next">The delegate to call to continue the bot middleware pipeline.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>This middleware loads the state object on the leading edge of the middleware pipeline
/// and persists the state object on the trailing edge.
/// </remarks>
public async Task OnTurn(ITurnContext context, NextDelegate next, CancellationToken cancellationToken)
public async Task OnTurnAsync(ITurnContext context, NextDelegate next, CancellationToken cancellationToken)
{
await ReadToContextService(context, cancellationToken).ConfigureAwait(false);
await ReadToContextServiceAsync(context, cancellationToken).ConfigureAwait(false);
await next(cancellationToken).ConfigureAwait(false);
await WriteFromContextService(context, cancellationToken).ConfigureAwait(false);
}
protected virtual async Task ReadToContextService(ITurnContext context, CancellationToken cancellationToken)
{
var key = _keyDelegate(context);
var items = await _storage.Read(new[] { key }, cancellationToken);
var state = items.Where(entry => entry.Key == key).Select(entry => entry.Value).OfType<TState>().FirstOrDefault();
if (state == null)
state = new TState();
context.Services.Add(_propertyName, state);
}
protected virtual async Task WriteFromContextService(ITurnContext context, CancellationToken cancellationToken)
{
var state = context.Services.Get<TState>(_propertyName);
await Write(context, state, cancellationToken);
await WriteFromContextServiceAsync(context, cancellationToken).ConfigureAwait(false);
}
/// <summary>
@ -80,10 +60,10 @@ namespace Microsoft.Bot.Builder
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If successful, the task result contains the state object, read from storage.</remarks>
public virtual async Task<TState> Read(ITurnContext context, CancellationToken cancellationToken = default(CancellationToken))
public virtual async Task<TState> ReadAsync(ITurnContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var key = _keyDelegate(context);
var items = await _storage.Read(new[] { key }, cancellationToken).ConfigureAwait(false);
var items = await _storage.ReadAsync(new[] { key }, cancellationToken).ConfigureAwait(false);
var state = items.Where(entry => entry.Key == key).Select(entry => entry.Value).OfType<TState>().FirstOrDefault();
if (state == null)
@ -99,12 +79,18 @@ namespace Microsoft.Bot.Builder
/// </summary>
/// <param name="context">The context object for this turn.</param>
/// <param name="state">The state object.</param>
public virtual async Task Write(ITurnContext context, TState state, CancellationToken cancellationToken = default(CancellationToken))
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public virtual async Task WriteAsync(ITurnContext context, TState state, CancellationToken cancellationToken = default(CancellationToken))
{
var changes = new Dictionary<string, object>();
if (state == null)
{
state = new TState();
}
var key = _keyDelegate(context);
changes.Add(key, state);
@ -115,12 +101,31 @@ namespace Microsoft.Bot.Builder
{
if (item.Value is IStoreItem valueStoreItem)
{
valueStoreItem.eTag = "*";
valueStoreItem.ETag = "*";
}
}
}
await _storage.Write(changes, cancellationToken).ConfigureAwait(false);
await _storage.WriteAsync(changes, cancellationToken).ConfigureAwait(false);
}
protected virtual async Task ReadToContextServiceAsync(ITurnContext context, CancellationToken cancellationToken)
{
var key = _keyDelegate(context);
var items = await _storage.ReadAsync(new[] { key }, cancellationToken).ConfigureAwait(false);
var state = items.Where(entry => entry.Key == key).Select(entry => entry.Value).OfType<TState>().FirstOrDefault();
if (state == null)
{
state = new TState();
}
context.Services.Add(_propertyName, state);
}
protected virtual async Task WriteFromContextServiceAsync(ITurnContext context, CancellationToken cancellationToken)
{
var state = context.Services.Get<TState>(_propertyName);
await WriteAsync(context, state, cancellationToken).ConfigureAwait(false);
}
}
}

Просмотреть файл

@ -13,15 +13,17 @@ namespace Microsoft.Bot.Builder
/// <summary>
/// The key to use to read and write this conversation state object to storage.
/// </summary>
public static string PropertyName = $"ConversationState:{typeof(ConversationState<TState>).Namespace}.{typeof(ConversationState<TState>).Name}";
private static string _propertyName = $"ConversationState:{typeof(ConversationState<TState>).Namespace}.{typeof(ConversationState<TState>).Name}";
/// <summary>
/// Creates a new <see cref="ConversationState{TState}"/> object.
/// Initializes a new instance of the <see cref="ConversationState{TState}"/> class.
/// </summary>
/// <param name="storage">The storage provider to use.</param>
/// <param name="settings">The state persistance options to use.</param>
public ConversationState(IStorage storage, StateSettings settings = null) :
base(storage, PropertyName,
public ConversationState(IStorage storage, StateSettings settings = null)
: base(
storage,
_propertyName,
(context) => $"conversation/{context.Activity.ChannelId}/{context.Activity.Conversation.Id}",
settings)
{
@ -34,7 +36,7 @@ namespace Microsoft.Bot.Builder
/// <returns>The coversation state object.</returns>
public static TState Get(ITurnContext context)
{
return context.Services.Get<TState>(PropertyName);
return context.Services.Get<TState>(_propertyName);
}
}
}

Просмотреть файл

@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.Bot.Builder
{
public class FrameRoot : IFrame
{
public FrameRoot(IStorage storage, IFrameDefinition definition)
{
if (storage == null)
{
throw new ArgumentNullException(nameof(storage));
}
if (definition == null)
{
throw new ArgumentNullException(nameof(definition));
}
if (string.IsNullOrWhiteSpace(definition.NameSpace))
{
throw new ArgumentException(nameof(definition.NameSpace));
}
if (string.IsNullOrWhiteSpace(definition.Scope))
{
throw new ArgumentException(nameof(definition.Scope));
}
this.Scope = definition.Scope;
this.Namespace = definition.NameSpace;
if (definition.SlotDefinitions != null)
{
foreach (var slotDefinition in definition.SlotDefinitions)
{
// Todo: This is strange code. Refactor.
new Slot(this, slotDefinition);
}
}
}
public IFrame Parent { get; set; } = null;
public string Scope { get; private set; }
public string Namespace { get; private set; }
public void AddSlot(IReadWriteSlot slot)
{
throw new NotImplementedException();
}
public Task LoadAsync(TurnContext context, bool accessed = false)
{
throw new NotImplementedException();
}
public Task SlotValueChangedAsync(TurnContext context, List<string> tags, object value)
{
throw new NotImplementedException();
}
}
}

Просмотреть файл

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Microsoft.Bot.Builder
{
public class FrameScope
{
public const string User = "User";
public const string Conversation = "conversation";
public const string ConversationMember = "conversationMember";
}
}

Просмотреть файл

@ -8,14 +8,14 @@ namespace Microsoft.Bot.Builder
/// <summary>
/// Represents a bot that can operate on incoming activities.
/// </summary>
/// <remarks>A <see cref="BotAdapter"/> passes incoming activities from the user's
/// channel to the bot's <see cref="OnTurn(ITurnContext)"/> method.</remarks>
/// <remarks>A <see cref="BotAdapter"/> passes incoming activities from the user's
/// channel to the bot's <see cref="OnTurnAsync(ITurnContext)"/> method.</remarks>
/// <example>
/// This defines a bot that responds with "Hello world!" to any incoming message.
/// <code>
/// public class EchoBot : IBot
/// {
/// public async Task OnTurn(ITurnContext context)
/// public async Task OnTurnAsync(ITurnContext context)
/// {
/// if (context.Activity.Type is ActivityTypes.Message)
/// {
@ -29,14 +29,14 @@ namespace Microsoft.Bot.Builder
public interface IBot
{
/// <summary>
/// Handles an incoming activity.
/// When implemented in a bot, handles an incoming activity.
/// </summary>
/// <param name="turnContext">The context object for this turn.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>The <paramref name="turnContext"/> provides information about the
/// <remarks>The <paramref name="turnContext"/> provides information about the
/// incoming activity, and other data needed to process the activity.</remarks>
/// <seealso cref="ITurnContext"/>
/// <seealso cref="Bot.Schema.IActivity"/>
Task OnTurn(ITurnContext turnContext);
Task OnTurnAsync(ITurnContext turnContext);
}
}
}

Просмотреть файл

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Microsoft.Bot.Builder
{
public interface IFrame
{
IFrame Parent { get; set; }
void AddSlot(IReadWriteSlot slot);
Task LoadAsync(TurnContext context, bool accessed = false);
Task SlotValueChangedAsync(TurnContext context, List<string> tags, object value);
}
}

Просмотреть файл

@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace Microsoft.Bot.Builder
{
public interface IFrameDefinition
{
string Scope { get; set; }
string NameSpace { get; set; }
List<ISlotDefinition> SlotDefinitions { get; set; }
}
}

Просмотреть файл

@ -11,52 +11,37 @@ namespace Microsoft.Bot.Builder
/// <summary>
/// Represents middleware that can operate on incoming activities.
/// </summary>
/// <remarks>A <see cref="BotAdapter"/> passes incoming activities from the user's
/// channel to the middleware's <see cref="OnTurn(ITurnContext, NextDelegate)"/>
/// <remarks>A <see cref="BotAdapter"/> passes incoming activities from the user's
/// channel to the middleware's <see cref="OnTurnAsync(ITurnContext, NextDelegate, CancellationToken)"/>
/// method.
/// <para>You can add middleware objects to your adapters middleware collection. The
/// adapter processes and directs incoming activities in through the bot middleware
/// pipeline to your bots logic and then back out again. As each activity flows in
/// and out of the bot, each piece of middleware can inspect or act upon the activity,
/// adapter processes and directs incoming activities in through the bot middleware
/// pipeline to your bots logic and then back out again. As each activity flows in
/// and out of the bot, each piece of middleware can inspect or act upon the activity,
/// both before and after the bot logic runs.</para>
/// <para>For each activity, the adapter calls middleware in the order in which you
/// <para>For each activity, the adapter calls middleware in the order in which you
/// added it.</para>
/// </remarks>
/// <example>
/// This defines middleware that sends "before" and "after" messages
/// before and after the adapter calls the bot's
/// <see cref="IBot.OnTurn(ITurnContext)"/> method.
/// <code>
/// public class SampleMiddleware : IMiddleware
/// {
/// public async Task OnTurn(ITurnContext context, MiddlewareSet.NextDelegate next)
/// {
/// context.SendActivity("before");
/// await next().ConfigureAwait(false);
/// context.SendActivity("after");
/// }
/// }
/// </code>
/// </example>
/// <seealso cref="IBot"/>
public interface IMiddleware
{
/// <summary>
/// Processess an incoming activity.
/// When implemented in middleware, processess an incoming activity.
/// </summary>
/// <param name="context">The context object for this turn.</param>
/// <param name="next">The delegate to call to continue the bot middleware pipeline.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>Middleware calls the <paramref name="next"/> delegate to pass control to
/// <remarks>Middleware calls the <paramref name="next"/> delegate to pass control to
/// the next middleware in the pipeline. If middleware doesnt call the next delegate,
/// the adapter does not call any of the subsequent middlewares request handlers or the
/// the adapter does not call any of the subsequent middlewares request handlers or the
/// bots receive handler, and the pipeline short circuits.
/// <para>The <paramref name="context"/> provides information about the
/// <para>The <paramref name="context"/> provides information about the
/// incoming activity, and other data needed to process the activity.</para>
/// </remarks>
/// <seealso cref="ITurnContext"/>
/// <seealso cref="Bot.Schema.IActivity"/>
Task OnTurn(ITurnContext context, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken));
Task OnTurnAsync(ITurnContext context, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken));
}
}

Просмотреть файл

@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Microsoft.Bot.Builder
{
public interface IReadOnlySlot
{
Task<object> GetAsync(TurnContext context);
Task<bool> HasAsync(TurnContext context);
Task<IEnumerable<SlotHistoryValue<object>>> HistoryAsync(TurnContext context);
}
}

Просмотреть файл

@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Threading.Tasks;
namespace Microsoft.Bot.Builder
{
public interface IReadWriteSlot : IReadOnlySlot
{
ISlotDefinition Definition { get; }
IFrame Frame { get; }
IReadOnlySlot AsReadOnly();
Task DeleteAsync(TurnContext context);
Task SetAsync(TurnContext context, object value);
}
}

Просмотреть файл

@ -6,18 +6,6 @@ using System.Threading.Tasks;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// Can convert from a generic recognizer result to a strongly typed one.
/// </summary>
public interface IRecognizerConvert
{
/// <summary>
/// Convert recognizer result.
/// </summary>
/// <param name="result">Result to convert.</param>
void Convert(dynamic result);
}
/// <summary>
/// Interface for Recognizers.
/// </summary>
@ -29,15 +17,16 @@ namespace Microsoft.Bot.Builder
/// <param name="utterance">Utterance to analyze.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Analysis of utterance.</returns>
Task<RecognizerResult> Recognize(string utterance, CancellationToken ct);
Task<RecognizerResult> RecognizeAsync(string utterance, CancellationToken ct);
/// <summary>
/// Runs an utterance through a recognizer and returns a strongly typed recognizer result.
/// Runs an utterance through a recognizer and returns a strongly-typed recognizer result.
/// </summary>
/// <typeparam name="T">The recognition result type.</typeparam>
/// <param name="utterance">Utterance to analyze.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Analysis of utterance.</returns>
Task<T> Recognize<T>(string utterance, CancellationToken ct)
Task<T> RecognizeAsync<T>(string utterance, CancellationToken ct)
where T : IRecognizerConvert, new();
}
}

Просмотреть файл

@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Microsoft.Bot.Builder
{
/// <summary>
/// Can convert from a generic recognizer result to a strongly typed one.
/// </summary>
public interface IRecognizerConvert
{
/// <summary>
/// Convert recognizer result.
/// </summary>
/// <param name="result">Result to convert.</param>
void Convert(dynamic result);
}
}

Просмотреть файл

@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Collections.ObjectModel;
namespace Microsoft.Bot.Builder
{
public interface ISlotDefinition
{
string Name { get; set; }
object DefaultValue { get; set; }
ReadOnlyCollection<string> ChangeTags { get; }
int ExpiresAfterSeconds { get; set; }
SlotHistoryPolicy History { get; set; }
}
}

Просмотреть файл

@ -7,56 +7,83 @@ using System.Threading.Tasks;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// Defines the interface for a storage layer.
/// </summary>
public interface IStorage
{
/// <summary>
/// Read StoreItems from storage.
/// Reads storage items from storage.
/// </summary>
/// <param name="keys">keys of the storeItems to read.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Dictionary of Key/Value pairs.</returns>
Task<IDictionary<string, object>> Read(string[] keys, CancellationToken cancellationToken = default(CancellationToken));
/// <param name="keys">keys of the <see cref="IStoreItem"/> objects to read.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activities are successfully sent, the task result contains
/// the items read, indexed by key.</remarks>
/// <seealso cref="DeleteAsync(string[], CancellationToken)"/>
/// <seealso cref="WriteAsync(IDictionary{string, object}, CancellationToken)"/>
Task<IDictionary<string, object>> ReadAsync(string[] keys, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Dictionary of Key/Value pairs to write.
/// Writes storage items to storage.
/// </summary>
/// <param name="changes">The changes to write.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task Write(IDictionary<string, object> changes, CancellationToken cancellationToken = default(CancellationToken));
/// <param name="changes">The items to write, indexed by key.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <seealso cref="DeleteAsync(string[], CancellationToken)"/>
/// <seealso cref="ReadAsync(string[], CancellationToken)"/>
Task WriteAsync(IDictionary<string, object> changes, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Delete StoreItems from storage.
/// Deletes storage items from storage.
/// </summary>
/// <param name="keys">keys of the storeItems to delete.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task Delete(string[] keys, CancellationToken cancellationToken = default(CancellationToken));
/// <param name="keys">keys of the <see cref="IStoreItem"/> objects to delete.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <seealso cref="ReadAsync(string[], CancellationToken)"/>
/// <seealso cref="WriteAsync(IDictionary{string, object}, CancellationToken)"/>
Task DeleteAsync(string[] keys, CancellationToken cancellationToken = default(CancellationToken));
}
/// <summary>
/// Exposes an ETag for concurrency control.
/// </summary>
public interface IStoreItem
{
/// <summary>
/// eTag for concurrency.
/// Gets or sets the ETag for concurrency control.
/// </summary>
string eTag { get; set; }
/// <value>The concurrency control ETag.</value>
string ETag { get; set; }
}
/// <summary>
/// Contains extension methods for <see cref="IStorage"/> objects.
/// </summary>
public static class StorageExtensions
{
/// <summary>
/// Storage extension to Read as strong typed StoreItem objects.
/// Gets and strongly types a collection of <see cref="IStoreItem"/> objects from state storage.
/// </summary>
/// <typeparam name="StoreItemT"></typeparam>
/// <param name="storage"></param>
/// <param name="keys"></param>
/// <returns></returns>
public static async Task<IDictionary<string, StoreItemT>> Read<StoreItemT>(this IStorage storage, string[] keys, CancellationToken cancellationToken = default(CancellationToken))
where StoreItemT : class
/// <typeparam name="TStoreItem">The type of item to get from storage.</typeparam>
/// <param name="storage">The state storage.</param>
/// <param name="keys">The collection of keys for the objects to get from storage.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the task completes successfully, the result contains a dictionary of the
/// strongly typed objects, indexed by the <paramref name="keys"/>.</remarks>
public static async Task<IDictionary<string, TStoreItem>> ReadAsync<TStoreItem>(this IStorage storage, string[] keys, CancellationToken cancellationToken = default(CancellationToken))
where TStoreItem : class
{
var storeItems = await storage.Read(keys, cancellationToken).ConfigureAwait(false);
var values = new Dictionary<string, StoreItemT>(keys.Length);
var storeItems = await storage.ReadAsync(keys, cancellationToken).ConfigureAwait(false);
var values = new Dictionary<string, TStoreItem>(keys.Length);
foreach (var entry in storeItems)
{
if (entry.Value is StoreItemT valueAsType)
if (entry.Value is TStoreItem valueAsType)
{
values.Add(entry.Key, valueAsType);
}

Просмотреть файл

@ -7,15 +7,15 @@ using Microsoft.Bot.Schema;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// Transcript logger stores activities for conversations for recall
/// Transcript logger stores activities for conversations for recall.
/// </summary>
public interface ITranscriptLogger
{
/// <summary>
/// Log an activity to the transcript
/// Log an activity to the transcript.
/// </summary>
/// <param name="activity"></param>
/// <returns></returns>
Task LogActivity(IActivity activity);
/// <param name="activity">The activity to transcribe.</param>
/// <returns>A task that represents the work queued to execute.</returns>
Task LogActivityAsync(IActivity activity);
}
}
}

Просмотреть файл

@ -8,34 +8,36 @@ using Microsoft.Bot.Schema;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// Transcript logger stores activities for conversations for recall.
/// Represents a store for recording conversations.
/// </summary>
public interface ITranscriptStore : ITranscriptLogger
{
/// <summary>
/// Get activities for a conversation (Aka the transcript).
/// Gets from the store activities that match a set of criteria.
/// </summary>
/// <param name="channelId">Channel id</param>
/// <param name="conversationId">Conversation id</param>
/// <param name="continuationToken">continuatuation token to page through results</param>
/// <param name="startDate">Earliest time to include.</param>
/// <returns>Enumeration over the recorded activities.</returns>
Task<PagedResult<IActivity>> GetTranscriptActivities(string channelId, string conversationId, string continuationToken=null, DateTime startDate = default(DateTime));
/// <param name="channelId">The ID of the channel the conversation is in.</param>
/// <param name="conversationId">The ID of the conversation.</param>
/// <param name="continuationToken"></param>
/// <param name="startDate">A cutoff date. Activities older than this date are not included.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the task completes successfully, the result contains the matching activities.</remarks>
Task<PagedResult<IActivity>> GetTranscriptActivitiesAsync(string channelId, string conversationId, string continuationToken = null, DateTime startDate = default(DateTime));
/// <summary>
/// List conversations in the channelId
/// Gets the conversations on a channel from the store.
/// </summary>
/// <param name="channelId"></param>
/// <param name="continuationToken">continuation token to get next page of results</param>
/// <returns></returns>
Task<PagedResult<Transcript>> ListTranscripts(string channelId, string continuationToken=null);
/// <param name="channelId">The ID of the channel.</param>
/// <param name="continuationToken"></param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks></remarks>
Task<PagedResult<Transcript>> ListTranscriptsAsync(string channelId, string continuationToken = null);
/// <summary>
/// Delete a specific conversation and all of it's activities
/// Deletes conversation data from the store.
/// </summary>
/// <param name="channelId">Channel where conversation took place.</param>
/// <param name="conversationId">Id of conversation to delete.</param>
/// <returns>Task.</returns>
Task DeleteTranscript(string channelId, string conversationId);
/// <param name="channelId">The ID of the channel the conversation is in.</param>
/// <param name="conversationId">The ID of the conversation to delete.</param>
/// <returns>A task that represents the work queued to execute.</returns>
Task DeleteTranscriptAsync(string channelId, string conversationId);
}
}

Просмотреть файл

@ -63,7 +63,7 @@ namespace Microsoft.Bot.Builder
/// <remarks>A handler calls the <paramref name="next"/> delegate to pass control to
/// the next registered handler. If a handler doesnt call the next delegate,
/// the adapter does not call any of the subsequent handlers and does not delete the
///activity.
/// activity.
/// <para>The conversation reference's <see cref="ConversationReference.ActivityId"/>
/// indicates the activity in the conversation to replace.</para>
/// </remarks>
@ -86,22 +86,25 @@ namespace Microsoft.Bot.Builder
/// <summary>
/// Gets the bot adapter that created this context object.
/// </summary>
/// <value>The bot adapter that created this context object.</value>
BotAdapter Adapter { get; }
/// <summary>
/// Gets the services registered on this context object.
/// </summary>
/// <value>The collection of services registered on this context object.</value>
TurnContextServiceCollection Services { get; }
/// <summary>
/// Incoming request
/// Gets the incoming request.
/// </summary>
/// <value>The incoming request.</value>
Activity Activity { get; }
/// <summary>
/// Indicates whether at least one response was sent for the current turn.
/// Gets a value indicating whether at least one response was sent for the current turn.
/// </summary>
/// <value><c>true</c> if at least one response was sent for the current turn.</value>
/// <value><c>true</c> if at least one response was sent for the current turn; otherwise, <c>false</c>.</value>
bool Responded { get; }
/// <summary>
@ -122,15 +125,15 @@ namespace Microsoft.Bot.Builder
/// <para>See the channel's documentation for limits imposed upon the contents of
/// <paramref name="textReplyToSend"/>.</para>
/// <para>To control various characteristics of your bot's speech such as voice,
/// rate, volume, pronunciation, and pitch, specify <paramref name="speak"/> in
/// rate, volume, pronunciation, and pitch, specify <paramref name="speak"/> in
/// Speech Synthesis Markup Language (SSML) format.</para>
/// </remarks>
/// <seealso cref="OnSendActivities(SendActivitiesHandler)"/>
/// <seealso cref="SendActivity(IActivity)"/>
/// <seealso cref="SendActivities(IActivity[])"/>
/// <seealso cref="UpdateActivity(IActivity)"/>
/// <seealso cref="DeleteActivity(ConversationReference)"/>
Task<ResourceResponse> SendActivity(string textReplyToSend, string speak = null, string inputHint = InputHints.AcceptingInput, CancellationToken cancellationToken = default(CancellationToken));
/// <seealso cref="SendActivityAsync(IActivity, CancellationToken)"/>
/// <seealso cref="SendActivitiesAsync(IActivity[], CancellationToken)"/>
/// <seealso cref="UpdateActivityAsync(IActivity, CancellationToken)"/>
/// <seealso cref="DeleteActivityAsync(ConversationReference, CancellationToken)"/>
Task<ResourceResponse> SendActivityAsync(string textReplyToSend, string speak = null, string inputHint = InputHints.AcceptingInput, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Sends an activity to the sender of the incoming activity.
@ -139,14 +142,14 @@ namespace Microsoft.Bot.Builder
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activity is successfully sent, the task result contains
/// a <see cref="ResourceResponse"/> object containing the ID that the receiving
/// a <see cref="ResourceResponse"/> object containing the ID that the receiving
/// channel assigned to the activity.</remarks>
/// <seealso cref="OnSendActivities(SendActivitiesHandler)"/>
/// <seealso cref="SendActivity(string, string, string)"/>
/// <seealso cref="SendActivities(IActivity[])"/>
/// <seealso cref="UpdateActivity(IActivity)"/>
/// <seealso cref="DeleteActivity(ConversationReference)"/>
Task<ResourceResponse> SendActivity(IActivity activity, CancellationToken cancellationToken = default(CancellationToken));
/// <seealso cref="SendActivityAsync(string, string, string, CancellationToken)"/>
/// <seealso cref="SendActivitiesAsync(IActivity[], CancellationToken)"/>
/// <seealso cref="UpdateActivityAsync(IActivity, CancellationToken)"/>
/// <seealso cref="DeleteActivityAsync(ConversationReference, CancellationToken)"/>
Task<ResourceResponse> SendActivityAsync(IActivity activity, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Sends a set of activities to the sender of the incoming activity.
@ -155,17 +158,17 @@ namespace Microsoft.Bot.Builder
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activities are successfully sent, the task result contains
/// an array of <see cref="ResourceResponse"/> objects containing the IDs that
/// an array of <see cref="ResourceResponse"/> objects containing the IDs that
/// the receiving channel assigned to the activities.</remarks>
/// <seealso cref="OnSendActivities(SendActivitiesHandler)"/>
/// <seealso cref="SendActivity(string, string, string)"/>
/// <seealso cref="SendActivity(IActivity)"/>
/// <seealso cref="UpdateActivity(IActivity)"/>
/// <seealso cref="DeleteActivity(ConversationReference)"/>
Task<ResourceResponse[]> SendActivities(IActivity[] activities, CancellationToken cancellationToken = default(CancellationToken));
/// <seealso cref="SendActivityAsync(string, string, string, CancellationToken)"/>
/// <seealso cref="SendActivityAsync(IActivity, CancellationToken)"/>
/// <seealso cref="UpdateActivityAsync(IActivity, CancellationToken)"/>
/// <seealso cref="DeleteActivityAsync(ConversationReference, CancellationToken)"/>
Task<ResourceResponse[]> SendActivitiesAsync(IActivity[] activities, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Replaces an existing activity.
/// Replaces an existing activity.
/// </summary>
/// <param name="activity">New replacement activity.</param>
/// <param name="cancellationToken">The cancellation token.</param>
@ -177,9 +180,9 @@ namespace Microsoft.Bot.Builder
/// of the activity to replace.</para>
/// <para>Not all channels support this operation. Channels that don't, may throw an exception.</para></remarks>
/// <seealso cref="OnUpdateActivity(UpdateActivityHandler)"/>
/// <seealso cref="SendActivities(IActivity[])"/>
/// <seealso cref="DeleteActivity(ConversationReference)"/>
Task<ResourceResponse> UpdateActivity(IActivity activity, CancellationToken cancellationToken = default(CancellationToken));
/// <seealso cref="SendActivitiesAsync(IActivity[], CancellationToken)"/>
/// <seealso cref="DeleteActivityAsync(ConversationReference, CancellationToken)"/>
Task<ResourceResponse> UpdateActivityAsync(IActivity activity, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Deletes an existing activity.
@ -189,10 +192,10 @@ namespace Microsoft.Bot.Builder
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>Not all channels support this operation. Channels that don't, may throw an exception.</remarks>
/// <seealso cref="OnDeleteActivity(DeleteActivityHandler)"/>
/// <seealso cref="DeleteActivity(ConversationReference)"/>
/// <seealso cref="SendActivities(IActivity[])"/>
/// <seealso cref="UpdateActivity(IActivity)"/>
Task DeleteActivity(string activityId, CancellationToken cancellationToken = default(CancellationToken));
/// <seealso cref="DeleteActivityAsync(ConversationReference, CancellationToken)"/>
/// <seealso cref="SendActivitiesAsync(IActivity[], CancellationToken)"/>
/// <seealso cref="UpdateActivityAsync(IActivity, CancellationToken)"/>
Task DeleteActivityAsync(string activityId, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Deletes an existing activity.
@ -204,25 +207,24 @@ namespace Microsoft.Bot.Builder
/// indicates the activity in the conversation to delete.
/// <para>Not all channels support this operation. Channels that don't, may throw an exception.</para></remarks>
/// <seealso cref="OnDeleteActivity(DeleteActivityHandler)"/>
/// <seealso cref="DeleteActivity(string)"/>
/// <seealso cref="SendActivities(IActivity[])"/>
/// <seealso cref="UpdateActivity(IActivity)"/>
Task DeleteActivity(ConversationReference conversationReference, CancellationToken cancellationToken = default(CancellationToken));
/// <seealso cref="DeleteActivityAsync(string, CancellationToken)"/>
/// <seealso cref="SendActivitiesAsync(IActivity[], CancellationToken)"/>
/// <seealso cref="UpdateActivityAsync(IActivity, CancellationToken)"/>
Task DeleteActivityAsync(ConversationReference conversationReference, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Adds a response handler for send activity operations.
/// </summary>
/// <param name="handler">The handler to add to the context object.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The updated context object.</returns>
/// <remarks>When the context's <see cref="SendActivity(IActivity)"/>
/// or <see cref="SendActivities(IActivity[])"/> methods are called,
/// <remarks>When the context's <see cref="SendActivityAsync(IActivity, CancellationToken)"/>
/// or <see cref="SendActivitiesAsync(IActivity[], CancellationToken)"/> methods are called,
/// the adapter calls the registered handlers in the order in which they were
/// added to the context object.
/// </remarks>
/// <seealso cref="SendActivity(string, string, string)"/>
/// <seealso cref="SendActivity(IActivity)"/>
/// <seealso cref="SendActivities(IActivity[])"/>
/// <seealso cref="SendActivityAsync(string, string, string, CancellationToken)"/>
/// <seealso cref="SendActivityAsync(IActivity, CancellationToken)"/>
/// <seealso cref="SendActivitiesAsync(IActivity[], CancellationToken)"/>
/// <seealso cref="SendActivitiesHandler"/>
/// <seealso cref="OnUpdateActivity(UpdateActivityHandler)"/>
/// <seealso cref="OnDeleteActivity(DeleteActivityHandler)"/>
@ -233,11 +235,11 @@ namespace Microsoft.Bot.Builder
/// </summary>
/// <param name="handler">The handler to add to the context object.</param>
/// <returns>The updated context object.</returns>
/// <remarks>When the context's <see cref="UpdateActivity(IActivity)"/> is called,
/// <remarks>When the context's <see cref="UpdateActivityAsync(IActivity, CancellationToken)"/> is called,
/// the adapter calls the registered handlers in the order in which they were
/// added to the context object.
/// </remarks>
/// <seealso cref="UpdateActivity(IActivity)"/>
/// <seealso cref="UpdateActivityAsync(IActivity, CancellationToken)"/>
/// <seealso cref="UpdateActivityHandler"/>
/// <seealso cref="OnSendActivities(SendActivitiesHandler)"/>
/// <seealso cref="OnDeleteActivity(DeleteActivityHandler)"/>
@ -249,12 +251,12 @@ namespace Microsoft.Bot.Builder
/// <param name="handler">The handler to add to the context object.</param>
/// <returns>The updated context object.</returns>
/// <exception cref="ArgumentNullException"><paramref name="handler"/> is <c>null</c>.</exception>
/// <remarks>When the context's <see cref="DeleteActivity(string)"/> is called,
/// <remarks>When the context's <see cref="DeleteActivityAsync(string, CancellationToken)"/> is called,
/// the adapter calls the registered handlers in the order in which they were
/// added to the context object.
/// </remarks>
/// <seealso cref="DeleteActivity(ConversationReference)"/>
/// <seealso cref="DeleteActivity(string)"/>
/// <seealso cref="DeleteActivityAsync(ConversationReference, CancellationToken)"/>
/// <seealso cref="DeleteActivityAsync(string, CancellationToken)"/>
/// <seealso cref="DeleteActivityHandler"/>
/// <seealso cref="OnSendActivities(SendActivitiesHandler)"/>
/// <seealso cref="OnUpdateActivity(UpdateActivityHandler)"/>

Просмотреть файл

@ -8,20 +8,29 @@ using Microsoft.Bot.Schema;
namespace Microsoft.Bot.Builder.TraceExtensions
{
/// <summary>
/// Contains methods for woring with <see cref="ITurnContext"/> objects.
/// </summary>
public static class ITurnContextExtensions
{
/// <summary>
/// Send a TraceActivity to transcript and/or emulator if attached
/// Sends a trace activity to the <see cref="BotAdapter"/> for logging purposes.
/// </summary>
/// <param name="turnContext"></param>
/// <param name="name">Name of the operation</param>
/// <param name="value">value of the operation</param>
/// <param name="valueType">valueType if helpful to identify the value schema (default is value.GetType().Name)</param>
/// <param name="label">descritive label of context. (Default is calling function name)</param>
/// <returns></returns>
public static Task<ResourceResponse> TraceActivity(this ITurnContext turnContext, string name, object value = null, string valueType = null, [CallerMemberName] string label = null, CancellationToken cancellationToken = default(CancellationToken))
/// <param name="turnContext">The context for the current turn.</param>
/// <param name="name">The value to assign to the activity's <see cref="Activity.Name"/> property.</param>
/// <param name="value">The value to assign to the activity's <see cref="Activity.Value"/> property.</param>
/// <param name="valueType">The value to assign to the activity's <see cref="Activity.ValueType"/> property.</param>
/// <param name="label">The value to assign to the activity's <see cref="Activity.Label"/> property.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the adapter is being hosted in the Emulator, the task result contains
/// a <see cref="ResourceResponse"/> object with the original trace activity's ID; otherwise,
/// it containsa <see cref="ResourceResponse"/> object containing the ID that the receiving
/// channel assigned to the activity.</remarks>
public static Task<ResourceResponse> TraceActivityAsync(this ITurnContext turnContext, string name, object value = null, string valueType = null, [CallerMemberName] string label = null, CancellationToken cancellationToken = default(CancellationToken))
{
return turnContext.SendActivity(turnContext.Activity.CreateTrace(name, value, valueType, label), cancellationToken);
return turnContext.SendActivityAsync(turnContext.Activity.CreateTrace(name, value, valueType, label), cancellationToken);
}
}
}

Просмотреть файл

@ -14,33 +14,37 @@ namespace Microsoft.Bot.Builder.Integration
/// Contains settings that your ASP.NET application uses to initialize the <see cref="BotAdapter"/>
/// that it adds to the HTTP request pipeline.
/// </summary>
/// <seealso cref="ApplicationBuilderExtensions"/>
public class BotFrameworkOptions
{
/// <summary>
/// Creates a <see cref="BotFrameworkOptions"/> object.
/// Initializes a new instance of the <see cref="BotFrameworkOptions"/> class.
/// </summary>
public BotFrameworkOptions()
{
}
/// <summary>
/// An <see cref="ICredentialProvider"/> that should be used to store and retrieve credentials used during authentication with the Bot Framework Service.
/// Gets or sets an <see cref="ICredentialProvider"/> that should be used to store and retrieve the
/// credentials used during authentication with the Bot Framework Service.
/// </summary>
/// <value>The credential provider.</value>
public ICredentialProvider CredentialProvider { get; set; }
/// <summary>
/// Error handler that catches exceptions in the middleware or application
/// Gets or sets an error handler to use to catche exceptions in the middleware or application.
/// </summary>
public Func<ITurnContext, Exception, Task> ErrorHandler { get; set; }
/// <value>The error handler.</value>
public Func<ITurnContext, Exception, Task> OnTurnError { get; set; }
/// <summary>
/// A list of <see cref="IMiddleware"/> that will be executed for each turn of the conversation.
/// Gets a list of the <see cref="IMiddleware"/> to use on each incoming activity.
/// </summary>
/// <value>The middleware list.</value>
/// <seealso cref="BotAdapter.Use(IMiddleware)"/>
public IList<IMiddleware> Middleware { get; } = new List<IMiddleware>();
/// <summary>
/// Gets or sets whether a proactive messaging endpoint should be exposed for the bot.
/// Gets or sets a value indicating whether gets or sets whether a proactive messaging endpoint should be exposed for the bot.
/// </summary>
/// <value>
/// True if the proactive messaging endpoint should be enabled, otherwise false.
@ -48,18 +52,21 @@ namespace Microsoft.Bot.Builder.Integration
public bool EnableProactiveMessages { get; set; }
/// <summary>
/// Gets or sets the retry policy to retry operations in case of errors from Bot Framework Service.
/// Gets or sets the retry policy to use in case of errors from Bot Framework Service.
/// </summary>
/// <value>The retry policy.</value>
public RetryPolicy ConnectorClientRetryPolicy { get; set; }
/// <summary>
/// Gets or sets the <see cref="HttpClient"/> instance that should be used to make requests to the Bot Framework Service.
/// </summary>
/// <value>The HTTP client.</value>
public HttpClient HttpClient { get; set; }
/// <summary>
/// Gets or sets what paths should be used when exposing the various bot endpoints.
/// </summary>
/// <value>The path strings.</value>
/// <seealso cref="BotFrameworkPaths"/>
public BotFrameworkPaths Paths { get; set; } = new BotFrameworkPaths();
}

Просмотреть файл

@ -6,21 +6,25 @@ namespace Microsoft.Bot.Builder
/// <summary>
/// Tuple class containing an HTTP Status Code and a JSON Serializable
/// object. The HTTP Status code is, in the invoke activity scenario, what will
/// be set in the resulting POST. The Body of the resulting POST will be
/// the JSON Serialized content from the Body property.
/// be set in the resulting POST. The Body of the resulting POST will be
/// the JSON Serialized content from the Body property.
/// </summary>
public class InvokeResponse
{
/// <summary>
/// <summary>Gets or sets the HTTP status code for the response.</summary>
/// <value>The HTTP status code.</value>
/// <remarks>
/// The POST that is generated in response to the incoming Invoke Activity
/// will have the HTTP Status code specificied by this field.
/// </summary>
/// </remarks>
public int Status { get; set; }
/// <summary>
/// <summary>Gets or sets the body content for the response.</summary>
/// <value>The body content.</value>
/// <remarks>
/// The POST that is generated in response to the incoming Invoke Activity
/// will have a body generated by JSON serializing the object in the Body field.
/// </summary>
/// will have a body generated by JSON serializing the object in this field.
/// </remarks>
public object Body { get; set; }
}
}

Просмотреть файл

@ -11,7 +11,7 @@ using Newtonsoft.Json.Linq;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// Models IStorage around a dictionary
/// A storage layer that uses an in-memory dictionary.
/// </summary>
public class MemoryStorage : IStorage
{
@ -21,12 +21,25 @@ namespace Microsoft.Bot.Builder
private readonly object _syncroot = new object();
private int _eTag = 0;
/// <summary>
/// Initializes a new instance of the <see cref="MemoryStorage"/> class.
/// </summary>
/// <param name="dictionary">A pre-existing dictionary to use; or null to use a new one.</param>
public MemoryStorage(Dictionary<string, JObject> dictionary = null)
{
_memory = dictionary ?? new Dictionary<string, JObject>();
}
public Task Delete(string[] keys, CancellationToken cancellationToken)
/// <summary>
/// Deletes storage items from storage.
/// </summary>
/// <param name="keys">keys of the <see cref="IStoreItem"/> objects to delete.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <seealso cref="ReadAsync(string[], CancellationToken)"/>
/// <seealso cref="WriteAsync(IDictionary{string, object}, CancellationToken)"/>
public Task DeleteAsync(string[] keys, CancellationToken cancellationToken)
{
lock (_syncroot)
{
@ -39,7 +52,18 @@ namespace Microsoft.Bot.Builder
return Task.CompletedTask;
}
public Task<IDictionary<string, object>> Read(string[] keys, CancellationToken cancellationToken)
/// <summary>
/// Reads storage items from storage.
/// </summary>
/// <param name="keys">keys of the <see cref="IStoreItem"/> objects to read.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activities are successfully sent, the task result contains
/// the items read, indexed by key.</remarks>
/// <seealso cref="DeleteAsync(string[], CancellationToken)"/>
/// <seealso cref="WriteAsync(IDictionary{string, object}, CancellationToken)"/>
public Task<IDictionary<string, object>> ReadAsync(string[] keys, CancellationToken cancellationToken)
{
var storeItems = new Dictionary<string, object>(keys.Length);
lock (_syncroot)
@ -58,8 +82,17 @@ namespace Microsoft.Bot.Builder
return Task.FromResult<IDictionary<string, object>>(storeItems);
}
public Task Write(IDictionary<string, object> changes, CancellationToken cancellationToken)
/// <summary>
/// Writes storage items to storage.
/// </summary>
/// <param name="changes">The items to write, indexed by key.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <seealso cref="DeleteAsync(string[], CancellationToken)"/>
/// <seealso cref="ReadAsync(string[], CancellationToken)"/>
public Task WriteAsync(IDictionary<string, object> changes, CancellationToken cancellationToken)
{
lock (_syncroot)
{
@ -71,12 +104,12 @@ namespace Microsoft.Bot.Builder
if (_memory.TryGetValue(change.Key, out var oldState))
{
if (oldState.TryGetValue("eTag", out var eTagToken))
if (oldState.TryGetValue("eTag", out var etag))
{
oldStateETag = eTagToken.Value<string>();
oldStateETag = etag.Value<string>();
}
}
var newState = JObject.FromObject(newValue, StateJsonSerializer);
// Set ETag if applicable
@ -84,11 +117,11 @@ namespace Microsoft.Bot.Builder
{
if (oldStateETag != null
&&
newStoreItem.eTag != "*"
newStoreItem.ETag != "*"
&&
newStoreItem.eTag != oldStateETag)
newStoreItem.ETag != oldStateETag)
{
throw new Exception($"Etag conflict.\r\n\r\nOriginal: {newStoreItem.eTag}\r\nCurrent: {oldStateETag}");
throw new Exception($"Etag conflict.\r\n\r\nOriginal: {newStoreItem.ETag}\r\nCurrent: {oldStateETag}");
}
newState["eTag"] = (_eTag++).ToString();

Просмотреть файл

@ -12,34 +12,34 @@ namespace Microsoft.Bot.Builder
/// <summary>
/// The memory transcript store stores transcripts in volatile memory in a Dictionary.
/// </summary>
/// <note>
/// Because this uses an unbounded volitile dictionary this should only be used for unit tests or non-production environments
/// </note>
/// <remarks>
/// Because this uses an unbounded volitile dictionary this should only be used for unit tests or non-production environments.
/// </remarks>
public class MemoryTranscriptStore : ITranscriptStore
{
private Dictionary<string, Dictionary<string, List<IActivity>>> _channels = new Dictionary<string, Dictionary<string, List<IActivity>>>();
/// <summary>
/// Log an activity to the transcript
/// Logs an activity to the transcript.
/// </summary>
/// <param name="activity">activity to log</param>
/// <returns></returns>
public Task LogActivity(IActivity activity)
/// <param name="activity">The activity to log.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public Task LogActivityAsync(IActivity activity)
{
if (activity == null)
{
throw new ArgumentNullException("activity cannot be null for LogActivity()");
}
lock (_channels)
{
Dictionary<string, List<IActivity>> channel;
if (!_channels.TryGetValue(activity.ChannelId, out channel))
if (!_channels.TryGetValue(activity.ChannelId, out var channel))
{
channel = new Dictionary<string, List<IActivity>>();
_channels[activity.ChannelId] = channel;
}
List<IActivity> transcript;
if (!channel.TryGetValue(activity.Conversation.Id, out transcript))
if (!channel.TryGetValue(activity.Conversation.Id, out var transcript))
{
transcript = new List<IActivity>();
channel[activity.Conversation.Id] = transcript;
@ -52,20 +52,25 @@ namespace Microsoft.Bot.Builder
}
/// <summary>
/// Get activities from the memory transcript store
/// Gets from the store activities that match a set of criteria.
/// </summary>
/// <param name="channelId">channelId</param>
/// <param name="conversationId">conversationId</param>
/// <param name="channelId">The ID of the channel the conversation is in.</param>
/// <param name="conversationId">The ID of the conversation.</param>
/// <param name="continuationToken"></param>
/// <param name="startDate"></param>
/// <returns></returns>
public Task<PagedResult<IActivity>> GetTranscriptActivities(string channelId, string conversationId, string continuationToken = null, DateTime startDate = default(DateTime))
/// <param name="startDate">A cutoff date. Activities older than this date are not included.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the task completes successfully, the result contains the matching activities.</remarks>
public Task<PagedResult<IActivity>> GetTranscriptActivitiesAsync(string channelId, string conversationId, string continuationToken = null, DateTime startDate = default(DateTime))
{
if (channelId == null)
{
throw new ArgumentNullException($"missing {nameof(channelId)}");
}
if (conversationId == null)
{
throw new ArgumentNullException($"missing {nameof(conversationId)}");
}
var pagedResult = new PagedResult<IActivity>();
lock (_channels)
@ -85,8 +90,11 @@ namespace Microsoft.Bot.Builder
.Skip(1)
.Take(20)
.ToArray();
if (pagedResult.Items.Count() == 20)
{
pagedResult.ContinuationToken = pagedResult.Items.Last().Id;
}
}
else
{
@ -95,8 +103,11 @@ namespace Microsoft.Bot.Builder
.Where(a => a.Timestamp >= startDate)
.Take(20)
.ToArray();
if (pagedResult.Items.Count() == 20)
{
pagedResult.ContinuationToken = pagedResult.Items.Last().Id;
}
}
}
}
@ -106,23 +117,26 @@ namespace Microsoft.Bot.Builder
}
/// <summary>
/// Delete a conversation
/// Deletes conversation data from the store.
/// </summary>
/// <param name="channelId">channelid for the conversation</param>
/// <param name="conversationId">conversation id</param>
/// <returns></returns>
public Task DeleteTranscript(string channelId, string conversationId)
/// <param name="channelId">The ID of the channel the conversation is in.</param>
/// <param name="conversationId">The ID of the conversation to delete.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public Task DeleteTranscriptAsync(string channelId, string conversationId)
{
if (channelId == null)
{
throw new ArgumentNullException($"{nameof(channelId)} should not be null");
}
if (conversationId == null)
{
throw new ArgumentNullException($"{nameof(conversationId)} should not be null");
}
lock (_channels)
{
Dictionary<string, List<IActivity>> channel;
if (_channels.TryGetValue(channelId, out channel))
if (_channels.TryGetValue(channelId, out var channel))
{
if (channel.ContainsKey(conversationId))
{
@ -135,21 +149,23 @@ namespace Microsoft.Bot.Builder
}
/// <summary>
/// List conversations in a channel.
/// Gets the conversations on a channel from the store.
/// </summary>
/// <param name="channelId"></param>
/// <param name="channelId">The ID of the channel.</param>
/// <param name="continuationToken"></param>
/// <returns></returns>
public Task<PagedResult<Transcript>> ListTranscripts(string channelId, string continuationToken = null)
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks></remarks>
public Task<PagedResult<Transcript>> ListTranscriptsAsync(string channelId, string continuationToken = null)
{
if (channelId == null)
{
throw new ArgumentNullException($"missing {nameof(channelId)}");
}
var pagedResult = new PagedResult<Transcript>();
lock (_channels)
{
Dictionary<string, List<IActivity>> channel;
if (_channels.TryGetValue(channelId, out channel))
if (_channels.TryGetValue(channelId, out var channel))
{
if (continuationToken != null)
{
@ -172,15 +188,17 @@ namespace Microsoft.Bot.Builder
}
else
{
pagedResult.Items = channel.Select(c => new Transcript
{
ChannelId = channelId,
Id = c.Key,
Created = c.Value.FirstOrDefault()?.Timestamp ?? default(DateTimeOffset),
})
.OrderBy(c => c.Created)
.Take(20)
.ToArray();
pagedResult.Items = channel.Select(
c => new Transcript
{
ChannelId = channelId,
Id = c.Key,
Created = c.Value.FirstOrDefault()?.Timestamp ?? default(DateTimeOffset),
})
.OrderBy(c => c.Created)
.Take(20)
.ToArray();
if (pagedResult.Items.Count() == 20)
{
pagedResult.ContinuationToken = pagedResult.Items.Last().Id;

Просмотреть файл

@ -14,8 +14,8 @@ namespace Microsoft.Bot.Builder
/// <example>
/// <code>
/// // Create and send a message.
/// var message = MessageFactory.Text("Hello World");
/// await context.SendActivity(message);
/// var message = MessageFactory.Text("Hello World");
/// await context.SendActivity(message);
/// </code>
/// </example>
/// <remarks>The following apply to message actions in general.
@ -25,12 +25,12 @@ namespace Microsoft.Bot.Builder
/// rate, volume, pronunciation, and pitch, specify test to speak in
/// Speech Synthesis Markup Language (SSML) format.</para>
/// <para>
/// Channels decide how each card action manifests in their user experience.
/// In most cases, the cards are clickable. In others, they may be selected by speech
/// input. In cases where the channel does not offer an interactive activation
/// experience (e.g., when interacting over SMS), the channel may not support
/// activation whatsoever. The decision about how to render actions is controlled by
/// normative requirements elsewhere in this document (e.g. within the card format,
/// Channels decide how each card action manifests in their user experience.
/// In most cases, the cards are clickable. In others, they may be selected by speech
/// input. In cases where the channel does not offer an interactive activation
/// experience (e.g., when interacting over SMS), the channel may not support
/// activation whatsoever. The decision about how to render actions is controlled by
/// normative requirements elsewhere in this document (e.g. within the card format,
/// or within the suggested actions definition).</para>
/// </remarks>
public static class MessageFactory
@ -39,10 +39,10 @@ namespace Microsoft.Bot.Builder
/// Returns a simple text message.
/// </summary>
/// <example>
/// <code>
/// <code>
/// // Create and send a message.
/// var message = MessageFactory.Text("Hello World");
/// await context.SendActivity(message);
/// var message = MessageFactory.Text("Hello World");
/// await context.SendActivity(message);
/// </code>
/// </example>
/// <param name="text">The text of the message to send.</param>
@ -69,14 +69,14 @@ namespace Microsoft.Bot.Builder
/// var activity = MessageFactory.SuggestedActions(
/// new string[] { "red", "green", "blue" },
/// text: "Choose a color");
///
///
/// // Send the activity as a reply to the user.
/// await context.SendActivity(activity);
/// </code>
/// </example>
/// <param name="actions">
/// The text of the actions to create.
/// </param>
/// </param>
/// <param name="text">The text of the message to send.</param>
/// <param name="ssml">Optional, text to be spoken by your bot on a speech-enabled
/// channel.</param>
@ -96,7 +96,9 @@ namespace Microsoft.Bot.Builder
public static IMessageActivity SuggestedActions(IEnumerable<string> actions, string text = null, string ssml = null, string inputHint = null)
{
if (actions == null)
{
throw new ArgumentNullException(nameof(actions));
}
var cardActions = new List<CardAction>();
foreach (string s in actions)
@ -105,7 +107,7 @@ namespace Microsoft.Bot.Builder
{
Type = ActionTypes.ImBack,
Value = s,
Title = s
Title = s,
};
cardActions.Add(ca);
@ -118,7 +120,7 @@ namespace Microsoft.Bot.Builder
/// Returns a message that includes a set of suggested actions and optional text.
/// </summary>
/// <example>
/// <code>
/// <code>
/// // Create the activity and add suggested actions.
/// var activity = MessageFactory.SuggestedActions(
/// new CardAction[]
@ -127,14 +129,14 @@ namespace Microsoft.Bot.Builder
/// new CardAction( title: "green", type: ActionTypes.ImBack, value: "green"),
/// new CardAction(title: "blue", type: ActionTypes.ImBack, value: "blue")
/// }, text: "Choose a color");
///
///
/// // Send the activity as a reply to the user.
/// await context.SendActivity(activity);
/// </code>
/// </example>
/// <param name="cardActions">
/// The card actions to include.
/// </param>
/// </param>
/// <param name="text">Optional, the text of the message to send.</param>
/// <param name="ssml">Optional, text to be spoken by your bot on a speech-enabled
/// channel.</param>
@ -149,7 +151,9 @@ namespace Microsoft.Bot.Builder
public static IMessageActivity SuggestedActions(IEnumerable<CardAction> cardActions, string text = null, string ssml = null, string inputHint = null)
{
if (cardActions == null)
{
throw new ArgumentNullException(nameof(cardActions));
}
var ma = Activity.CreateMessageActivity();
SetTextAndSpeak(ma, text, ssml, inputHint);
@ -178,7 +182,9 @@ namespace Microsoft.Bot.Builder
public static IMessageActivity Attachment(Attachment attachment, string text = null, string ssml = null, string inputHint = null)
{
if (attachment == null)
{
throw new ArgumentNullException(nameof(attachment));
}
return Attachment(new List<Attachment> { attachment }, text, ssml, inputHint);
}
@ -202,7 +208,9 @@ namespace Microsoft.Bot.Builder
public static IMessageActivity Attachment(IEnumerable<Attachment> attachments, string text = null, string ssml = null, string inputHint = null)
{
if (attachments == null)
{
throw new ArgumentNullException(nameof(attachments));
}
return AttachmentActivity(AttachmentLayoutTypes.List, attachments, text, ssml, inputHint);
}
@ -252,7 +260,7 @@ namespace Microsoft.Bot.Builder
/// })
/// .ToAttachment()
/// });
///
///
/// // Send the activity as a reply to the user.
/// await context.SendActivity(activity);
/// </code>
@ -261,7 +269,9 @@ namespace Microsoft.Bot.Builder
public static IMessageActivity Carousel(IEnumerable<Attachment> attachments, string text = null, string ssml = null, string inputHint = null)
{
if (attachments == null)
{
throw new ArgumentNullException(nameof(attachments));
}
return AttachmentActivity(AttachmentLayoutTypes.Carousel, attachments, text, ssml, inputHint);
}
@ -281,27 +291,31 @@ namespace Microsoft.Bot.Builder
/// Default is null.</param>
/// <returns>A message activity containing the attachment.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="url"/> or <paramref name="contentType"/> is <c>null</c>,
/// <paramref name="url"/> or <paramref name="contentType"/> is <c>null</c>,
/// empty, or white space.</exception>
/// <example>This code creates a message activity that contains an image.
/// <code>
/// IMessageActivity message =
/// IMessageActivity message =
/// MessageFactory.ContentUrl("https://{domainName}/cat.jpg", MediaTypeNames.Image.Jpeg, "Cat Picture");
/// </code>
/// </example>
public static IMessageActivity ContentUrl(string url, string contentType, string name = null, string text = null, string ssml = null, string inputHint = null)
{
if (string.IsNullOrWhiteSpace(url))
{
throw new ArgumentNullException(nameof(url));
}
if (string.IsNullOrWhiteSpace(contentType))
{
throw new ArgumentNullException(nameof(contentType));
}
var a = new Attachment
{
ContentType = contentType,
ContentUrl = url,
Name = !string.IsNullOrWhiteSpace(name) ? name : string.Empty
Name = !string.IsNullOrWhiteSpace(name) ? name : string.Empty,
};
return AttachmentActivity(AttachmentLayoutTypes.List, new List<Attachment> { a }, text, ssml, inputHint);

Просмотреть файл

@ -8,10 +8,19 @@ using System.Threading.Tasks;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// Contains an ordered set of <see cref="IMiddleware"/>.
/// </summary>
public class MiddlewareSet : IMiddleware
{
private readonly IList<IMiddleware> _middleware = new List<IMiddleware>();
/// <summary>
/// Adds a middleware object to the end of the set.
/// </summary>
/// <param name="middleware">The middleware to add.</param>
/// <returns>The updated middleware set.</returns>
/// <see cref="BotAdapter.Use(IMiddleware)"/>
public MiddlewareSet Use(IMiddleware middleware)
{
BotAssert.MiddlewareNotNull(middleware);
@ -20,33 +29,45 @@ namespace Microsoft.Bot.Builder
}
/// <summary>
/// Implementation of the IMiddleware interface
/// Processes an incoming activity.
/// </summary>
/// <param name="context">The turn context</param>
/// <param name="next">The next middleware component</param>
/// <param name="cancellationToken">cancellation token</param>
public async Task OnTurn(ITurnContext context, NextDelegate next, CancellationToken cancellationToken)
/// <param name="context">The context object for this turn.</param>
/// <param name="next">The delegate to call to continue the bot middleware pipeline.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public async Task OnTurnAsync(ITurnContext context, NextDelegate next, CancellationToken cancellationToken)
{
await ReceiveActivityInternal(context, null, 0, cancellationToken).ConfigureAwait(false);
await ReceiveActivityInternalAsync(context, null, 0, cancellationToken).ConfigureAwait(false);
await next(cancellationToken).ConfigureAwait(false);
}
public async Task ReceiveActivity(ITurnContext context, CancellationToken cancellationToken)
/// <summary>
/// Processes an activity.
/// </summary>
/// <param name="context">The context object for the turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public async Task ReceiveActivityAsync(ITurnContext context, CancellationToken cancellationToken)
{
await ReceiveActivityInternal(context, null, 0, cancellationToken).ConfigureAwait(false);
await ReceiveActivityInternalAsync(context, null, 0, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Intended to be called from Bot, this method performs exactly the same as the
/// standard ReceiveActivity, except that it runs a user-defined delegate returns
/// if all Middlware in the receive pipeline was run.
/// Processes an activity.
/// </summary>
public async Task ReceiveActivityWithStatus(ITurnContext context, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
/// <param name="context">The context object for the turn.</param>
/// <param name="callback">The delegate to call when the set finishes processing the activity.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public async Task ReceiveActivityWithStatusAsync(ITurnContext context, Func<ITurnContext, Task> callback, CancellationToken cancellationToken)
{
await ReceiveActivityInternal(context, callback, 0, cancellationToken).ConfigureAwait(false);
await ReceiveActivityInternalAsync(context, callback, 0, cancellationToken).ConfigureAwait(false);
}
private Task ReceiveActivityInternal(ITurnContext context, Func<ITurnContext, Task> callback, int nextMiddlewareIndex, CancellationToken cancellationToken)
private Task ReceiveActivityInternalAsync(ITurnContext context, Func<ITurnContext, Task> callback, int nextMiddlewareIndex, CancellationToken cancellationToken)
{
// Check if we're at the end of the middleware list yet
if (nextMiddlewareIndex == _middleware.Count)
@ -68,9 +89,9 @@ namespace Microsoft.Bot.Builder
var nextMiddleware = _middleware[nextMiddlewareIndex];
// Execute the next middleware passing a closure that will recurse back into this method at the next piece of middlware as the NextDelegate
return nextMiddleware.OnTurn(
return nextMiddleware.OnTurnAsync(
context,
(ct) => ReceiveActivityInternal(context, callback, nextMiddlewareIndex + 1, ct),
(ct) => ReceiveActivityInternalAsync(context, callback, nextMiddlewareIndex + 1, ct),
cancellationToken);
}
}

Просмотреть файл

@ -8,12 +8,13 @@ using Newtonsoft.Json.Linq;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// Recognizer return value.
/// Contains recognition results generated by an <see cref="IRecognizer"/>.
/// </summary>
/// <seealso cref="IRecognizer.RecognizeAsync(string, System.Threading.CancellationToken)"/>
public class RecognizerResult : IRecognizerConvert
{
/// <summary>
/// Gets or sets original text to recognizer.
/// Gets or sets the input text to recognize.
/// </summary>
/// <value>
/// Original text to recognizer.
@ -22,7 +23,7 @@ namespace Microsoft.Bot.Builder
public string Text { get; set; }
/// <summary>
/// Gets or sets text modified by recognizer, for example by spell correction.
/// Gets or sets the input text as modified by the recognizer, for example for spelling correction.
/// </summary>
/// <value>
/// Text modified by recognizer.
@ -31,7 +32,7 @@ namespace Microsoft.Bot.Builder
public string AlteredText { get; set; }
/// <summary>
/// Gets or sets object with the intent as key and the confidence as value.
/// Gets or sets the recognized intents, with the intent as key and the confidence as value.
/// </summary>
/// <value>
/// Mapping from intent to information about the intent.
@ -40,7 +41,7 @@ namespace Microsoft.Bot.Builder
public IDictionary<string, IntentScore> Intents { get; set; }
/// <summary>
/// Gets or sets object with each top-level recognized entity as a key.
/// Gets or sets the recognized top-level entities.
/// </summary>
/// <value>
/// Object with each top-level recognized entity as a key.
@ -49,11 +50,13 @@ namespace Microsoft.Bot.Builder
public JObject Entities { get; set; }
/// <summary>
/// Gets or sets any extra properties to include in the results.
/// Gets or sets properties that are not otherwise defined by the <see cref="RecognizerResult"/> type but that
/// might appear in the REST JSON object.
/// </summary>
/// <value>
/// Any extra properties to include in the results.
/// </value>
/// <value>The extended properties for the object.</value>
/// <remarks>With this, properties not represented in the defined type are not dropped when
/// the JSON object is deserialized, but are instead stored in this property. Such properties
/// will be written to a JSON object when the instance is serialized.</remarks>
[JsonExtensionData(ReadData = true, WriteData = true)]
public IDictionary<string, object> Properties { get; set; } = new Dictionary<string, object>();

Просмотреть файл

@ -5,6 +5,9 @@ using System;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// Contains helper methods for working with <see cref="RecognizerResult"/> objects.
/// </summary>
public static class RecognizerResultExtensions
{
/// <summary>

Просмотреть файл

@ -41,7 +41,19 @@ namespace Microsoft.Bot.Builder
_period = TimeSpan.FromMilliseconds(period);
}
public async Task OnTurn(ITurnContext context, NextDelegate next, CancellationToken cancellationToken)
/// <summary>
/// Processess an incoming activity.
/// </summary>
/// <param name="context">The context object for this turn.</param>
/// <param name="next">The delegate to call to continue the bot middleware pipeline.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>Spawns a thread that sends the periodic typing activities until the turn ends.
/// </remarks>
/// <seealso cref="ITurnContext"/>
/// <seealso cref="Bot.Schema.IActivity"/>
public async Task OnTurnAsync(ITurnContext context, NextDelegate next, CancellationToken cancellationToken)
{
CancellationTokenSource cts = null;
try
@ -98,7 +110,7 @@ namespace Microsoft.Bot.Builder
Type = ActivityTypes.Typing,
RelatesTo = context.Activity.RelatesTo,
};
await context.SendActivity(typingActivity, cancellationToken).ConfigureAwait(false);
await context.SendActivityAsync(typingActivity, cancellationToken).ConfigureAwait(false);
}
}
}

Просмотреть файл

@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.Bot.Builder
{
public class Slot : IReadWriteSlot
{
public Slot(IFrame frame, ISlotDefinition definition)
{
this.Frame = frame ?? throw new ArgumentNullException(nameof(frame));
this.Definition = definition ?? throw new ArgumentNullException(nameof(definition));
this.Frame.AddSlot(this);
}
public ISlotDefinition Definition { get; private set; }
public IFrame Frame { get; private set; }
public IReadOnlySlot AsReadOnly()
{
throw new NotImplementedException();
}
public Task DeleteAsync(TurnContext context)
{
throw new NotImplementedException();
}
public Task<object> GetAsync(TurnContext context)
{
throw new NotImplementedException();
}
public Task<bool> HasAsync(TurnContext context)
{
throw new NotImplementedException();
}
public Task<IEnumerable<SlotHistoryValue<object>>> HistoryAsync(TurnContext context)
{
throw new NotImplementedException();
}
public Task SetAsync(TurnContext context, object value)
{
throw new NotImplementedException();
}
}
}

Просмотреть файл

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Microsoft.Bot.Builder
{
public class SlotHistoryPolicy
{
public int MaxCount { get; set; }
public int ExpiresAfterSeconds { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
namespace Microsoft.Bot.Builder
{
public class SlotHistoryValue<T>
{
public T Value { get; set; }
public DateTime Timestamp { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Microsoft.Bot.Builder
{
public class StateSettings
{
public bool LastWriterWins { get; set; } = true;
}
}

Просмотреть файл

@ -9,22 +9,22 @@ using Newtonsoft.Json;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// TraceTranscriptLogger, writes activites to System.Diagnostics.Trace
/// Representas a transcript logger that writes activites to a <see cref="Trace"/> object.
/// </summary>
public class TraceTranscriptLogger : ITranscriptLogger
{
private static JsonSerializerSettings serializationSettings = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.Indented };
/// <summary>
/// Log an activity to the transcript
/// Log an activity to the transcript.
/// </summary>
/// <param name="activity">activity to log</param>
/// <returns></returns>
public Task LogActivity(IActivity activity)
/// <param name="activity">The activity to transcribe.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public Task LogActivityAsync(IActivity activity)
{
BotAssert.ActivityNotNull(activity);
Trace.TraceInformation(JsonConvert.SerializeObject(activity, serializationSettings));
return Task.CompletedTask;
}
}
}
}

Просмотреть файл

@ -6,23 +6,26 @@ using System;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// Transcript store item.
/// Represents a copy of a conversation.
/// </summary>
public class Transcript
{
/// <summary>
/// ChannelId that the transcript was taken from.
/// Gets or sets the ID of the channel in which the conversation occurred.
/// </summary>
/// <value>The ID of the channel in which the conversation occurred.</value>
public string ChannelId { get; set; }
/// <summary>
/// Conversation Id.
/// Gets or sets the ID of the conversation.
/// </summary>
/// <value>The ID of the conversation.</value>
public string Id { get; set; }
/// <summary>
/// Date conversation was started.
/// Gets or sets the date the conversation began.
/// </summary>
/// <value>The date then conversation began.</value>
public DateTimeOffset Created { get; set; }
}
}

Просмотреть файл

@ -11,32 +11,35 @@ using Newtonsoft.Json;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// When added, this middleware will log incoming and outgoing activitites to a ITranscriptStore
/// Middleware for logging incoming and outgoing activitites to an <see cref="ITranscriptStore"/>.
/// </summary>
public class TranscriptLoggerMiddleware : IMiddleware
{
private static JsonSerializerSettings JsonSettings = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore };
private static JsonSerializerSettings _jsonSettings = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore };
private ITranscriptLogger logger;
private Queue<IActivity> transcript = new Queue<IActivity>();
/// <summary>
/// Middleware for logging incoming and outgoing activities to a transcript store.
/// Initializes a new instance of the <see cref="TranscriptLoggerMiddleware"/> class.
/// </summary>
/// <param name="transcriptLogger">The transcript logger to use.</param>
/// <param name="transcriptLogger">The conversation store to use.</param>
public TranscriptLoggerMiddleware(ITranscriptLogger transcriptLogger)
{
logger = transcriptLogger ?? throw new ArgumentNullException("TranscriptLoggerMiddleware requires a ITranscriptLogger implementation. ");
}
/// <summary>
/// initialization for middleware turn
/// Records incoming and outgoing activities to the conversation store.
/// </summary>
/// <param name="context"></param>
/// <param name="nextTurn"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task OnTurn(ITurnContext context, NextDelegate nextTurn, CancellationToken cancellationToken)
/// <param name="context">The context object for this turn.</param>
/// <param name="nextTurn">The delegate to call to continue the bot middleware pipeline.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <seealso cref="ITurnContext"/>
/// <seealso cref="Bot.Schema.IActivity"/>
public async Task OnTurnAsync(ITurnContext context, NextDelegate nextTurn, CancellationToken cancellationToken)
{
// log incoming activity at beginning of turn
if (context.Activity != null)
@ -104,7 +107,7 @@ namespace Microsoft.Bot.Builder
try
{
var activity = transcript.Dequeue();
await logger.LogActivity(activity).ConfigureAwait(false);
await logger.LogActivityAsync(activity).ConfigureAwait(false);
}
catch (Exception err)
{
@ -113,21 +116,23 @@ namespace Microsoft.Bot.Builder
}
}
private static IActivity CloneActivity(IActivity activity)
{
activity = JsonConvert.DeserializeObject<Activity>(JsonConvert.SerializeObject(activity, _jsonSettings));
return activity;
}
private void LogActivity(IActivity activity)
{
lock (transcript)
{
if (activity.Timestamp == null)
{
activity.Timestamp = DateTime.UtcNow;
}
transcript.Enqueue(activity);
}
}
private static IActivity CloneActivity(IActivity activity)
{
activity = JsonConvert.DeserializeObject<Activity>(JsonConvert.SerializeObject(activity, JsonSettings));
return activity;
}
}
}

Просмотреть файл

@ -25,7 +25,7 @@ namespace Microsoft.Bot.Builder
private readonly IList<DeleteActivityHandler> _onDeleteActivity = new List<DeleteActivityHandler>();
/// <summary>
/// Creates a context object.
/// Initializes a new instance of the <see cref="TurnContext"/> class.
/// </summary>
/// <param name="adapter">The adapter creating the context.</param>
/// <param name="activity">The incoming activity for the turn;
@ -39,21 +39,53 @@ namespace Microsoft.Bot.Builder
Activity = activity ?? throw new ArgumentNullException(nameof(activity));
}
/// <summary>
/// Gets the bot adapter that created this context object.
/// </summary>
/// <value>The bot adapter that created this context object.</value>
public BotAdapter Adapter { get; }
/// <summary>
/// Gets the services registered on this context object.
/// </summary>
/// <value>The services registered on this context object.</value>
public TurnContextServiceCollection Services { get; } = new TurnContextServiceCollection();
/// <summary>
/// Gets the activity associated with this turn; or <c>null</c> when processing
/// a proactive message.
/// </summary>
/// <value>The activity associated with this turn.</value>
public Activity Activity { get; }
/// <summary>
/// Gets a value indicating whether at least one response was sent for the current turn.
/// </summary>
/// <value><c>true</c> if at least one response was sent for the current turn.</value>
/// <remarks><see cref="ITraceActivity"/> activities on their own do not set this flag.</remarks>
public bool Responded
{
get;
private set;
}
/// <summary>
/// Adds a response handler for send activity operations.
/// </summary>
/// <param name="handler">The handler to add to the context object.</param>
/// <returns>The updated context object.</returns>
/// <exception cref="ArgumentNullException"><paramref name="handler"/> is <c>null</c>.</exception>
/// <remarks>When the context's <see cref="SendActivity(IActivity)"/>
/// or <see cref="SendActivities(IActivity[])"/> methods are called,
/// <remarks>When the context's <see cref="SendActivityAsync(IActivity, CancellationToken)"/>
/// or <see cref="SendActivitiesAsync(IActivity[], CancellationToken)"/> methods are called,
/// the adapter calls the registered handlers in the order in which they were
/// added to the context object.
/// </remarks>
public ITurnContext OnSendActivities(SendActivitiesHandler handler)
{
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
_onSendActivities.Add(handler);
return this;
@ -65,14 +97,16 @@ namespace Microsoft.Bot.Builder
/// <param name="handler">The handler to add to the context object.</param>
/// <returns>The updated context object.</returns>
/// <exception cref="ArgumentNullException"><paramref name="handler"/> is <c>null</c>.</exception>
/// <remarks>When the context's <see cref="UpdateActivity(IActivity)"/> is called,
/// <remarks>When the context's <see cref="UpdateActivityAsync(IActivity, CancellationToken)"/> is called,
/// the adapter calls the registered handlers in the order in which they were
/// added to the context object.
/// </remarks>
public ITurnContext OnUpdateActivity(UpdateActivityHandler handler)
{
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
_onUpdateActivity.Add(handler);
return this;
@ -84,45 +118,22 @@ namespace Microsoft.Bot.Builder
/// <param name="handler">The handler to add to the context object.</param>
/// <returns>The updated context object.</returns>
/// <exception cref="ArgumentNullException"><paramref name="handler"/> is <c>null</c>.</exception>
/// <remarks>When the context's <see cref="DeleteActivity(string)"/> is called,
/// <remarks>When the context's <see cref="DeleteActivityAsync(ConversationReference, CancellationToken)"/>
/// or <see cref="DeleteActivityAsync(string, CancellationToken)"/> is called,
/// the adapter calls the registered handlers in the order in which they were
/// added to the context object.
/// </remarks>
public ITurnContext OnDeleteActivity(DeleteActivityHandler handler)
{
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
_onDeleteActivity.Add(handler);
return this;
}
/// <summary>
/// Gets the bot adapter that created this context object.
/// </summary>
public BotAdapter Adapter { get; }
/// <summary>
/// Gets the services registered on this context object.
/// </summary>
public TurnContextServiceCollection Services { get; } = new TurnContextServiceCollection();
/// <summary>
/// Gets the activity associated with this turn; or <c>null</c> when processing
/// a proactive message.
/// </summary>
public Activity Activity { get; }
/// <summary>
/// Indicates whether at least one response was sent for the current turn.
/// </summary>
/// <value><c>true</c> if at least one response was sent for the current turn.</value>
public bool Responded
{
get;
private set;
}
/// <summary>
/// Sends a message activity to the sender of the incoming activity.
/// </summary>
@ -146,36 +157,43 @@ namespace Microsoft.Bot.Builder
/// rate, volume, pronunciation, and pitch, specify <paramref name="speak"/> in
/// Speech Synthesis Markup Language (SSML) format.</para>
/// </remarks>
public async Task<ResourceResponse> SendActivity(string textReplyToSend, string speak = null, string inputHint = null, CancellationToken cancellationToken = default(CancellationToken))
public async Task<ResourceResponse> SendActivityAsync(string textReplyToSend, string speak = null, string inputHint = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (string.IsNullOrWhiteSpace(textReplyToSend))
{
throw new ArgumentNullException(nameof(textReplyToSend));
}
var activityToSend = new Activity(ActivityTypes.Message) { Text = textReplyToSend };
if (!string.IsNullOrEmpty(speak))
{
activityToSend.Speak = speak;
}
if (!string.IsNullOrEmpty(inputHint))
{
activityToSend.InputHint = inputHint;
}
return await SendActivity(activityToSend, cancellationToken).ConfigureAwait(false);
return await SendActivityAsync(activityToSend, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Sends an activity to the sender of the incoming activity.
/// </summary>
/// <param name="activity">The activity to send.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <exception cref="ArgumentNullException"><paramref name="activity"/> is <c>null</c>.</exception>
/// <remarks>If the activity is successfully sent, the task result contains
/// a <see cref="ResourceResponse"/> object containing the ID that the receiving
/// channel assigned to the activity.</remarks>
public async Task<ResourceResponse> SendActivity(IActivity activity, CancellationToken cancellationToken = default(CancellationToken))
public async Task<ResourceResponse> SendActivityAsync(IActivity activity, CancellationToken cancellationToken = default(CancellationToken))
{
BotAssert.ActivityNotNull(activity);
ResourceResponse[] responses = await SendActivities(new [] { activity }, cancellationToken).ConfigureAwait(false);
ResourceResponse[] responses = await SendActivitiesAsync(new[] { activity }, cancellationToken).ConfigureAwait(false);
if (responses == null || responses.Length == 0)
{
// It's possible an interceptor prevented the activity from having been sent.
@ -192,17 +210,22 @@ namespace Microsoft.Bot.Builder
/// Sends a set of activities to the sender of the incoming activity.
/// </summary>
/// <param name="activities">The activities to send.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activities are successfully sent, the task result contains
/// an array of <see cref="ResourceResponse"/> objects containing the IDs that
/// the receiving channel assigned to the activities.</remarks>
public Task<ResourceResponse[]> SendActivities(IActivity[] activities, CancellationToken cancellationToken = default(CancellationToken))
public Task<ResourceResponse[]> SendActivitiesAsync(IActivity[] activities, CancellationToken cancellationToken = default(CancellationToken))
{
if (activities == null)
{
throw new ArgumentNullException(nameof(activities));
}
if (activities.Length == 0)
{
throw new ArgumentException("Expecting one or more activities, but the array was empty.", nameof(activities));
}
var conversationReference = this.Activity.GetConversationReference();
@ -241,7 +264,7 @@ namespace Microsoft.Bot.Builder
// Send from the list which may have been manipulated via the event handlers.
// Note that 'responses' was captured from the root of the call, and will be
// returned to the original caller.
var responses = await Adapter.SendActivities(this, bufferedActivities.ToArray(), cancellationToken).ConfigureAwait(false);
var responses = await Adapter.SendActivitiesAsync(this, bufferedActivities.ToArray(), cancellationToken).ConfigureAwait(false);
var sentNonTraceActivity = false;
for (var index = 0; index < responses.Length; index++)
@ -266,6 +289,7 @@ namespace Microsoft.Bot.Builder
/// Replaces an existing activity.
/// </summary>
/// <param name="activity">New replacement activity.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <exception cref="Microsoft.Bot.Schema.ErrorResponseException">
/// The HTTP operation failed and the response contained additional information.</exception>
@ -276,69 +300,78 @@ namespace Microsoft.Bot.Builder
/// channel assigned to the activity.
/// <para>Before calling this, set the ID of the replacement activity to the ID
/// of the activity to replace.</para></remarks>
public async Task<ResourceResponse> UpdateActivity(IActivity activity, CancellationToken cancellationToken = default(CancellationToken))
public async Task<ResourceResponse> UpdateActivityAsync(IActivity activity, CancellationToken cancellationToken = default(CancellationToken))
{
Activity a = (Activity)activity;
async Task<ResourceResponse> ActuallyUpdateStuff()
{
return await Adapter.UpdateActivity(this, a, cancellationToken).ConfigureAwait(false);
return await Adapter.UpdateActivityAsync(this, a, cancellationToken).ConfigureAwait(false);
}
return await UpdateActivityInternal(a, _onUpdateActivity, ActuallyUpdateStuff, cancellationToken).ConfigureAwait(false);
return await UpdateActivityInternalAsync(a, _onUpdateActivity, ActuallyUpdateStuff, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Deletes an existing activity.
/// </summary>
/// <param name="activityId">The ID of the activity to delete.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <exception cref="Microsoft.Bot.Schema.ErrorResponseException">
/// The HTTP operation failed and the response contained additional information.</exception>
public async Task DeleteActivity(string activityId, CancellationToken cancellationToken = default(CancellationToken))
public async Task DeleteActivityAsync(string activityId, CancellationToken cancellationToken = default(CancellationToken))
{
if (string.IsNullOrWhiteSpace(activityId))
{
throw new ArgumentNullException(nameof(activityId));
}
ConversationReference cr = this.Activity.GetConversationReference();
var cr = Activity.GetConversationReference();
cr.ActivityId = activityId;
async Task ActuallyDeleteStuff()
{
await Adapter.DeleteActivity(this, cr, cancellationToken).ConfigureAwait(false);
await Adapter.DeleteActivityAsync(this, cr, cancellationToken).ConfigureAwait(false);
}
await DeleteActivityInternal(cr, _onDeleteActivity, ActuallyDeleteStuff, cancellationToken).ConfigureAwait(false);
await DeleteActivityInternalAsync(cr, _onDeleteActivity, ActuallyDeleteStuff, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Deletes an existing activity.
/// </summary>
/// <param name="conversationReference">The conversation containing the activity to delete.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <exception cref="Microsoft.Bot.Schema.ErrorResponseException">
/// The HTTP operation failed and the response contained additional information.</exception>
/// <remarks>The conversation reference's <see cref="ConversationReference.ActivityId"/>
/// indicates the activity in the conversation to delete.</remarks>
public async Task DeleteActivity(ConversationReference conversationReference, CancellationToken cancellationToken = default(CancellationToken))
public async Task DeleteActivityAsync(ConversationReference conversationReference, CancellationToken cancellationToken = default(CancellationToken))
{
if (conversationReference == null)
{
throw new ArgumentNullException(nameof(conversationReference));
}
async Task ActuallyDeleteStuff()
{
await Adapter.DeleteActivity(this, conversationReference, cancellationToken).ConfigureAwait(false);
await Adapter.DeleteActivityAsync(this, conversationReference, cancellationToken).ConfigureAwait(false);
}
await DeleteActivityInternal(conversationReference, _onDeleteActivity, ActuallyDeleteStuff, cancellationToken).ConfigureAwait(false);
await DeleteActivityInternalAsync(conversationReference, _onDeleteActivity, ActuallyDeleteStuff, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Frees resources.
/// </summary>
public void Dispose()
{
Services.Dispose();
}
private async Task<ResourceResponse> UpdateActivityInternal(
private async Task<ResourceResponse> UpdateActivityInternalAsync(
Activity activity,
IEnumerable<UpdateActivityHandler> updateHandlers,
Func<Task<ResourceResponse>> callAtBottom,
@ -346,9 +379,12 @@ namespace Microsoft.Bot.Builder
{
BotAssert.ActivityNotNull(activity);
if (updateHandlers == null)
{
throw new ArgumentException(nameof(updateHandlers));
}
if (updateHandlers.Count() == 0) // No middleware to run.
// No middleware to run.
if (updateHandlers.Count() == 0)
{
if (callAtBottom != null)
{
@ -359,22 +395,22 @@ namespace Microsoft.Bot.Builder
}
// Default to "No more Middleware after this".
async Task<ResourceResponse> next()
async Task<ResourceResponse> Next()
{
// Remove the first item from the list of middleware to call,
// so that the next call just has the remaining items to worry about.
IEnumerable<UpdateActivityHandler> remaining = updateHandlers.Skip(1);
var result = await UpdateActivityInternal(activity, remaining, callAtBottom, cancellationToken).ConfigureAwait(false);
var result = await UpdateActivityInternalAsync(activity, remaining, callAtBottom, cancellationToken).ConfigureAwait(false);
activity.Id = result.Id;
return result;
}
// Grab the current middleware, which is the 1st element in the array, and execute it
UpdateActivityHandler toCall = updateHandlers.First();
return await toCall(this, activity, next).ConfigureAwait(false);
return await toCall(this, activity, Next).ConfigureAwait(false);
}
private async Task DeleteActivityInternal(
private async Task DeleteActivityInternalAsync(
ConversationReference cr,
IEnumerable<DeleteActivityHandler> updateHandlers,
Func<Task> callAtBottom,
@ -383,9 +419,12 @@ namespace Microsoft.Bot.Builder
BotAssert.ConversationReferenceNotNull(cr);
if (updateHandlers == null)
{
throw new ArgumentException(nameof(updateHandlers));
}
if (updateHandlers.Count() == 0) // No middleware to run.
// No middleware to run.
if (updateHandlers.Count() == 0)
{
if (callAtBottom != null)
{
@ -396,17 +435,17 @@ namespace Microsoft.Bot.Builder
}
// Default to "No more Middleware after this".
async Task next()
async Task Next()
{
// Remove the first item from the list of middleware to call,
// so that the next call just has the remaining items to worry about.
IEnumerable<DeleteActivityHandler> remaining = updateHandlers.Skip(1);
await DeleteActivityInternal(cr, remaining, callAtBottom, cancellationToken).ConfigureAwait(false);
await DeleteActivityInternalAsync(cr, remaining, callAtBottom, cancellationToken).ConfigureAwait(false);
}
// Grab the current middleware, which is the 1st element in the array, and execute it.
DeleteActivityHandler toCall = updateHandlers.First();
await toCall(this, cr, next).ConfigureAwait(false);
await toCall(this, cr, Next).ConfigureAwait(false);
}
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше