-
Notifications
You must be signed in to change notification settings - Fork 487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update IRecognizer and LUIS to support strongly typed results #477
Conversation
…xtensions. They are intended to support the new schema model for recognizers. Revamp the LUIS transform to the new schema to include all built-in types. Add Recognize and classes to support generation of strongly typed LUIS results. LuisResult is now added as an extended property on RecognizerResult. Tests of the corresponding functionality.
I love the removal of ILuisRecognizer, it was really bothering me! Thanks :-) |
/// <summary> | ||
/// Normalized recognized unit. | ||
/// </summary> | ||
[JsonProperty("unit")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be "units"? Or should the property be "Unit"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to be number and units.
/// </summary> | ||
/// <param name="luisModel">The LUIS model to use to recognize text.</param> | ||
/// <param name="luisRecognizerOptions">The LUIS recognizer options to use.</param> | ||
/// <param name="options">The LUIS request options to use.</param> | ||
public LuisRecognizer(ILuisModel luisModel, ILuisRecognizerOptions luisRecognizerOptions = null, ILuisOptions options = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any specific reason for removing the docs on the ctor()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added back and also did inheritdoc on the strongly typed recognize and made the CallAndRecognize private.
public async Task<RecognizerResult> Recognize(string utterance, CancellationToken ct) | ||
{ | ||
var result = await CallAndRecognize(utterance, ct).ConfigureAwait(false); | ||
return result.recognizerResult; | ||
return (await CallAndRecognize(utterance, ct).ConfigureAwait(false)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return CallAndRecognize(); without awaiting, also now that CallAndRecognize() returns one thing (awesome) I think RecognizeInternal is a better name (the old name).
public async Task<T> Recognize<T>(string utterance, CancellationToken ct) | ||
{ | ||
return (T) Activator.CreateInstance(typeof(T), (await CallAndRecognize(utterance, ct).ConfigureAwait(false))); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an interesting way to implement this, it means thought that any type T must have a constructor that accepts a RecognizereResults object, right? I don't think that's an ideal design, I don't see why developers should tie their schema to ours?
If you go with this implementation, it'd be good to make it self documenting using Types, we should have a constraint on T that restricts it to an abstract class that has a ctor(RecognizeResults). But I think we'd be putting too much work on people who are implementing these converters instead of creating them for them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was the least evil alternative Tom and I came up with. You either couple on serialization or a constructor on the generic version. The strongly typed Recognize makes it discoverable. The question is how to get that constructed. Did you have a different idea on how to do so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what we are really strongly typing is Intents and Entities properties (the JObjects).
The method could be written as such:
public interface ITypedRecognizerResult<TIntents, TEntiies>
{
string Text {set; get;}
TIntents Intents;
TEntiies Entities;
}
public Task<TResult> Recognize<TResult, TIntents,TEntities>(string utterance)
where TResult: ITypedRecognizerResult<TIntents, TEntities>, new()
{
var rr = await CallAndRecognize();
var typedRR = new TResult
{
Text = rr.Text,
Intents = rr.Intents as TIntents,
Entities = rr.Entities as TEntities
};
return Task.FromResult(typedRR);
}
The beauty of this implementation is that it's not LUIS specific and can be shared across all recognizers, that's why i think it should actually be part of RecognizerResults class and not every specific Recognizer:
public class RecognizerResults
{
public TResult As<TResult, TIntents, TEntities>()
where TResult: ITypedRecognizeResult<TIntents, TEntities>, new()
{
return new TResult
{
Text = this.Text,
Intents = this.Intents as TIntents,
Entities = this.Entities as TEntities
};
}
}
And it can then be used this way:
var rr = luisRecognize.Recognize();
var typedRr = rr.As<LUIS.Contoso_App>();
The only constraint that it adds on the schema writer is that their schema must extend ITypedRecognizerResult. If my Schema is:
class MyStrongType
{
MyIntents Intents;
MyEntities Entities;
string text;
}
All I have to do is:
- If I can modify my schema file:
class MyStrongType : ITypedRecognizedResult<MyIntents, MyEntities>{
...
}
- If I don't have access to to the type source code or it's already compiled:
class MyStrongTypeRecognizedResult: MyStrongType, ITTypedRecognizedResult<MyIntents, MyEntities> {
...
}
There's a lot of complexity here introduced because part of RecognizeResult is Typed (text, alteredText etc) and part isn't (intents, entities), my proposal will be MUCH simpler if RecognizerResult is just a JObject that happens to have a text/alteredText property, then we don't need any of these interfaces and the implementation of As() would be simply:
return rr as T;
My #1 preference would be the latter (i.e. just use JObject all the way for untyped, especially that we now have a way to Type it)
My #2 preference is the former.
And I'm ok with my current implementation too as long as we're aware of its limitation...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I changed this to an interface with type constraints.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Emad's solution here is very interesting and seems broadly applicable.
{ | ||
return null; | ||
} | ||
return long.TryParse((string)value, out var longVal) ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought you wanted to get rid of this and always return double? I'm ok with keeping it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I map to double in the strong conversion--fine to leave it either here in JSON.
/// <summary> | ||
/// Recognizer return value. | ||
/// </summary> | ||
public class RecognizerResult |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added JsonProperties to that class with camelCasing that seem to have been lost.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
1) Added IRecognizerConvert interface for strong types. 2) Renamed CallAndRecognize 3) Added some missing JSonProperty and xmldocs.
{183B3324-4CFF-477A-8EAE-73953D3E383D}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug - NuGet Packages|Any CPU | ||
{183B3324-4CFF-477A-8EAE-73953D3E383D}.Debug - NuGet Packages|Any CPU.Build.0 = Debug - NuGet Packages|Any CPU | ||
{183B3324-4CFF-477A-8EAE-73953D3E383D}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU | ||
{183B3324-4CFF-477A-8EAE-73953D3E383D}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to make sure the Release configurations are setup as well. Should be trivial...
|
||
public DateTimeSpec(string type, IEnumerable<string> expressions) | ||
{ | ||
Type = type; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Argument Null checking. (Public method of a public class)
if (string.isNullOrWhitespace(type)) throw new ArgumentNullException(nameof(type))
if (expressions == null) throw new ArgumentNullException(nameof(expressions))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
/// Type of expression. | ||
/// </summary> | ||
[JsonProperty("type")] | ||
public readonly string Type; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to avoid the word type. Using Language Keywords (and Type is C# has a zillion meanings) is often problematic in the long term. It's bad enough we do this on Activity.Type, I would prefer to avoid making the problem worse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a better word, so leaving for now.
public class DateTimeSpec | ||
{ | ||
/// <summary> | ||
/// Type of expression. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As code comments go, this one could use a bit of work. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added reference to GitHub and some comments on type.
/// Strongly typed information corresponding to LUIS $instance value. | ||
/// </summary> | ||
/// <remarks> | ||
/// This is a partial class in order to support adding custom meta-data. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
metadata is one word. I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
@@ -36,7 +38,7 @@ public class LuisRecognizerMiddleware : IMiddleware | |||
/// </summary> | |||
public const string Obfuscated = "****"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern of using this "Obfuscated" is really odd and just sets off all kind of weird red-flags.
We removed it from the QnA Maker components
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added bug for Emad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cleemullins it's just a string constant to indicate that the key property includes sensitive data, could be null or empty if you prefer, what's the "pattern" and red flags just so I would understand? We aren't really "obfuscating" anything or editing any existing data structures. This "****" string goes to a brand new data structure.
I could introduce a new data structure that's identical to the existing one and not include the key property, but I was trying to keep the diagnostic info mapped as much as possible to existing data structures to 1. enforce the schema and 2. avoid introducing a new data structure that the developer will have to map back to the existing schema. We need all the data in the existing data structure so there's no point in introducing a new one.
/// <summary> | ||
/// Interface for Recognizers. | ||
/// </summary> | ||
public interface IRecognizer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a nice basic definition for a recognizer.
@@ -58,6 +54,7 @@ | |||
<ItemGroup> | |||
<PackageReference Include="Microsoft.CSharp" Version="4.4.1" /> | |||
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> | |||
<PackageReference Include="System.Dynamic.Runtime" Version="4.3.0" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would STRONGLY like to avoid pulling in system.dynamic to the Extensions package.
How can we avoid doing this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm. I removed it and it seems to work fine--even though there are some dynamics in here.
/// <summary> | ||
/// Object with the intent as key and the confidence as value. | ||
/// </summary> | ||
public JObject Intents { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not nearly as comfortable with the Recognizer Result here. Returning JObjects from our core SDK seems like a big abstraction leak. Let's sync on this before we commit.
/// Shallow copy constructor. | ||
/// </summary> | ||
/// <param name="other">Result to copy results from.</param> | ||
public RecognizerResult(RecognizerResult other) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this useful? Seems it might introduce more bugs than it would prevent.
I would have a real copy constructor (that round-trips through the JSON to clone) or nothing. This approach seems like it's going to end badly.
Changed EndIndex to be +1 to better match language conventions. Beefed up DateTimeExpression robustness and docs. Moved AlteredText back to null. Minor renaming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Chris!
…crosoft#477) * [QnA Maker] IsTest and Ranker type support for QnAMaker.GetAnswer * Formatting fix * Formatting fix
…xtensions. They are intended to support the new schema model for recognizers.
Revamp the LUIS transform to the new schema to include all built-in types.
Add Recognize and classes to support generation of strongly typed LUIS results.
LuisResult is now added as an extended property on RecognizerResult.
Tests of the corresponding functionality.