Skip to content
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

Merged
merged 4 commits into from
Apr 21, 2018

Conversation

chrimc62
Copy link
Contributor

…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.

…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.
@chrimc62 chrimc62 requested a review from emadelwany April 20, 2018 00:48
@emadelwany
Copy link
Contributor

I love the removal of ILuisRecognizer, it was really bothering me! Thanks :-)

/// <summary>
/// Normalized recognized unit.
/// </summary>
[JsonProperty("unit")]
Copy link
Contributor

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"?

Copy link
Contributor Author

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)
Copy link
Contributor

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()?

Copy link
Contributor Author

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));
}
Copy link
Contributor

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)));
}
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

@emadelwany emadelwany Apr 20, 2018

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:

  1. If I can modify my schema file:
class MyStrongType : ITypedRecognizedResult<MyIntents, MyEntities>{
...
}
  1. 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...

Copy link
Contributor Author

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.

Copy link
Contributor

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) ?
Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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.
@chrimc62 chrimc62 requested a review from cleemullins April 20, 2018 18:22
{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
Copy link
Contributor

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;
Copy link
Contributor

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))

Copy link
Contributor Author

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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.
Copy link
Contributor

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. :)

Copy link
Contributor Author

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.
Copy link
Contributor

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.

Copy link
Contributor Author

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 = "****";
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added bug for Emad.

Copy link
Contributor

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
Copy link
Contributor

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" />
Copy link
Contributor

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?

Copy link
Contributor Author

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; }
Copy link
Contributor

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)
Copy link
Contributor

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.
Copy link
Contributor

@emadelwany emadelwany left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Chris!

@chrimc62 chrimc62 merged commit 7d89262 into master Apr 21, 2018
ShYuPe pushed a commit to ShYuPe/botbuilder-dotnet that referenced this pull request Aug 25, 2020
…crosoft#477)

* [QnA Maker] IsTest and Ranker type support for QnAMaker.GetAnswer

* Formatting fix

* Formatting fix
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants