Basic expression tree and added triggerTree.

This commit is contained in:
Chris McConnell 2019-03-20 13:49:49 -07:00
Родитель d55bc239e0
Коммит 11c6e70306
24 изменённых файлов: 2438 добавлений и 16 удалений

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

@ -76,8 +76,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialo
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Expressions", "libraries\Microsoft.CommonExpressions\Microsoft.Expressions.csproj", "{68899A42-6375-43BC-95BB-2E4798D2C0EB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Expressions.Tests", "tests\Microsoft.Expressions.Tests\Microsoft.Expressions.Tests.csproj", "{A7637631-40EF-4E8D-A90A-422A5FD593D1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialogs.Declarative", "libraries\Microsoft.Bot.Builder.Dialogs.Declarative\Microsoft.Bot.Builder.Dialogs.Declarative.csproj", "{1BC05915-044E-4776-8956-B44BBEFF2F84}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialogs.Declarative.Tests", "tests\Microsoft.Bot.Builder.Dialogs.Declarative.Tests\Microsoft.Bot.Builder.Dialogs.Declarative.Tests.csproj", "{CD473E17-DA3C-4E64-9AC6-31B4FB6D69E7}"
@ -98,6 +96,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.TestB
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Builder.TestBot.WebApi", "samples\Microsoft.Bot.Builder.TestBot.WebApi\Microsoft.Bot.Builder.TestBot.WebApi.csproj", "{A920C660-CAAC-421B-AE0C-B79BE31DA472}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.AI.TriggerTrees", "libraries\Microsoft.Bot.Builder.AI.TriggerTrees\Microsoft.Bot.Builder.AI.TriggerTrees.csproj", "{A8BC2784-763B-4256-89ED-24DE029F9B38}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Expressions.Tests", "tests\Microsoft.Expressions.Tests\Microsoft.Expressions.Tests.csproj", "{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug - NuGet Packages|Any CPU = Debug - NuGet Packages|Any CPU
@ -355,14 +357,6 @@ Global
{68899A42-6375-43BC-95BB-2E4798D2C0EB}.Documentation|Any CPU.Build.0 = Debug|Any CPU
{68899A42-6375-43BC-95BB-2E4798D2C0EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{68899A42-6375-43BC-95BB-2E4798D2C0EB}.Release|Any CPU.Build.0 = Release|Any CPU
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Documentation|Any CPU.Build.0 = Debug|Any CPU
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Release|Any CPU.Build.0 = Release|Any CPU
{1BC05915-044E-4776-8956-B44BBEFF2F84}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug - NuGet Packages|Any CPU
{1BC05915-044E-4776-8956-B44BBEFF2F84}.Debug - NuGet Packages|Any CPU.Build.0 = Debug - NuGet Packages|Any CPU
{1BC05915-044E-4776-8956-B44BBEFF2F84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@ -435,6 +429,20 @@ Global
{A920C660-CAAC-421B-AE0C-B79BE31DA472}.Documentation|Any CPU.Build.0 = Debug|Any CPU
{A920C660-CAAC-421B-AE0C-B79BE31DA472}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A920C660-CAAC-421B-AE0C-B79BE31DA472}.Release|Any CPU.Build.0 = Release|Any CPU
{A8BC2784-763B-4256-89ED-24DE029F9B38}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU
{A8BC2784-763B-4256-89ED-24DE029F9B38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8BC2784-763B-4256-89ED-24DE029F9B38}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU
{A8BC2784-763B-4256-89ED-24DE029F9B38}.Documentation|Any CPU.Build.0 = Debug|Any CPU
{A8BC2784-763B-4256-89ED-24DE029F9B38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8BC2784-763B-4256-89ED-24DE029F9B38}.Release|Any CPU.Build.0 = Release|Any CPU
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Documentation|Any CPU.Build.0 = Debug|Any CPU
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -475,7 +483,6 @@ Global
{2255FF5B-8010-4C8D-897E-BDDEBB629967} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
{78D16CCC-6184-4678-87D8-E8C4299317CE} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{68899A42-6375-43BC-95BB-2E4798D2C0EB} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
{A7637631-40EF-4E8D-A90A-422A5FD593D1} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{1BC05915-044E-4776-8956-B44BBEFF2F84} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
{CD473E17-DA3C-4E64-9AC6-31B4FB6D69E7} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{150BC344-4534-41E1-9700-9D231CF327D2} = {763168FA-A590-482C-84D8-2922F7ADB1A2}
@ -485,6 +492,8 @@ Global
{9940D79A-B7D7-42ED-A353-0A889B458842} = {1DEDDC46-F2F4-448F-A0B7-FD48682EEC80}
{758AA8A8-D476-473A-9761-84D2317D8268} = {1DEDDC46-F2F4-448F-A0B7-FD48682EEC80}
{A920C660-CAAC-421B-AE0C-B79BE31DA472} = {1DEDDC46-F2F4-448F-A0B7-FD48682EEC80}
{A8BC2784-763B-4256-89ED-24DE029F9B38} = {763168FA-A590-482C-84D8-2922F7ADB1A2}
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16}

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

@ -0,0 +1,305 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.Bot.Builder.AI.TriggerTrees
{
public class Clause
{
public List<Expression> Predicates = new List<Expression>();
public Dictionary<string, string> AnyBindings = new Dictionary<string, string>();
internal bool Subsumed = false;
internal Clause()
{ }
internal Clause(Clause fromClause)
{
foreach (var pair in fromClause.AnyBindings)
{
AnyBindings.Add(pair.Key, pair.Value);
}
}
internal Clause(Expression expression)
{
Predicates.Add(expression);
}
public override string ToString()
{
var builder = new StringBuilder();
ToString(builder);
return builder.ToString();
}
public void ToString(StringBuilder builder, int indent = 0)
{
builder.Append(' ', indent);
if (Subsumed)
{
builder.Append('*');
}
builder.Append('(');
var first = true;
foreach (var predicate in Predicates)
{
if (first)
{
first = false;
}
else
{
builder.Append(" && ");
}
builder.Append(predicate.ToString());
}
builder.Append(')');
foreach (var binding in AnyBindings)
{
builder.Append($" {binding.Key}->{binding.Value}");
}
}
public RelationshipType Relationship(Clause other, Dictionary<string, IPredicateComparer> comparers)
{
var soFar = RelationshipType.Incomparable;
var shorter = this;
var shorterCount = shorter.PredicateCount();
var longer = other;
var longerCount = longer.PredicateCount();
var swapped = false;
if (longerCount < shorterCount)
{
longer = this;
shorter = other;
var tmp = longerCount;
longerCount = shorterCount;
shorterCount = tmp;
swapped = true;
}
if (shorterCount == 0)
{
if (longerCount == 0)
{
soFar = RelationshipType.Equal;
}
else
{
soFar = RelationshipType.Generalizes;
}
}
else
{
// If every one of shorter predicates is equal or superset of one in longer, then shoter is a superset of longer
foreach (var shortPredicate in shorter.Predicates)
{
var shorterRel = RelationshipType.Incomparable;
if (!IsIgnore(shortPredicate))
{
foreach (var longPredicate in longer.Predicates)
{
shorterRel = Relationship(shortPredicate, longPredicate, comparers);
if (shorterRel != RelationshipType.Incomparable)
{
// Found related predicates
break;
}
}
if (shorterRel == RelationshipType.Incomparable)
{
// Predicate in shorter is incomparable so done
soFar = RelationshipType.Incomparable;
break;
}
else
{
if (soFar == RelationshipType.Incomparable)
{
soFar = shorterRel;
}
if (soFar == RelationshipType.Equal)
{
if (shorterRel == RelationshipType.Generalizes
|| (shorterRel == RelationshipType.Specializes && shorterCount == longerCount)
|| shorterRel == RelationshipType.Equal)
{
soFar = shorterRel;
}
else
{
break;
}
}
else if (soFar != shorterRel)
{
// Not continued with sub/super so incomparable
break;
}
}
}
}
if (shorterCount != longerCount)
{
switch (soFar)
{
case RelationshipType.Equal:
case RelationshipType.Generalizes: soFar = RelationshipType.Generalizes; break;
default: soFar = RelationshipType.Incomparable; break;
}
}
soFar = BindingsRelationship(soFar, shorter, longer);
}
soFar = IgnoreRelationship(soFar, shorter, longer);
return swapped ? soFar.Swap() : soFar;
}
private RelationshipType BindingsRelationship(RelationshipType soFar, Clause shorterClause, Clause longerClause)
{
if (soFar == RelationshipType.Equal)
{
var swapped = false;
var shorter = shorterClause.AnyBindings;
var longer = longerClause.AnyBindings;
if (shorterClause.AnyBindings.Count > longerClause.AnyBindings.Count)
{
shorter = longerClause.AnyBindings;
longer = shorterClause.AnyBindings;
swapped = true;
}
foreach (var shortBinding in shorter)
{
var found = false;
foreach (var longBinding in longer)
{
if (shortBinding.Key == longBinding.Key && shortBinding.Value == longBinding.Value)
{
found = true;
break;
}
}
if (!found)
{
soFar = RelationshipType.Incomparable;
break;
}
}
if (soFar == RelationshipType.Equal && shorter.Count < longer.Count)
{
soFar = RelationshipType.Specializes;
}
soFar = Swap(soFar, swapped);
}
return soFar;
}
private RelationshipType Swap(RelationshipType soFar, bool swapped)
{
if (swapped)
{
switch (soFar)
{
case RelationshipType.Specializes: soFar = RelationshipType.Generalizes; break;
case RelationshipType.Generalizes: soFar = RelationshipType.Specializes; break;
}
}
return soFar;
}
private RelationshipType IgnoreRelationship(RelationshipType soFar, Clause shorterClause, Clause longerClause)
{
if (soFar == RelationshipType.Equal)
{
var shortIgnores = shorterClause.Predicates.Where(p => IsIgnore(p));
var longIgnores = longerClause.Predicates.Where(p => IsIgnore(p));
var swapped = false;
if (longIgnores.Count() < shortIgnores.Count())
{
var old = longIgnores;
longIgnores = shortIgnores;
shortIgnores = old;
swapped = true;
}
foreach (var shortPredicate in shortIgnores)
{
bool found = false;
foreach (var longPredicate in longIgnores)
{
if (shortPredicate.DeepEquals(longPredicate))
{
found = true;
break;
}
}
if (!found)
{
soFar = RelationshipType.Incomparable;
break;
}
}
if (soFar == RelationshipType.Equal)
{
if (shorterClause.Predicates.Count() == 0 && longerClause.Predicates.Count() > 0)
{
soFar = RelationshipType.Generalizes;
}
else if (shortIgnores.Count() < longIgnores.Count())
{
soFar = RelationshipType.Incomparable;
}
}
soFar = Swap(soFar, swapped);
}
return soFar;
}
private RelationshipType Relationship(Expression expr, Expression other, Dictionary<string, IPredicateComparer> comparers)
{
RelationshipType relationship = RelationshipType.Incomparable;
var root = expr;
var rootOther = other;
if (expr.NodeType == ExpressionType.Not && other.NodeType == ExpressionType.Not)
{
root = ((UnaryExpression)expr).Operand;
rootOther = ((UnaryExpression)other).Operand;
}
IPredicateComparer comparer = null;
if (root.NodeType == ExpressionType.Call && rootOther.NodeType == ExpressionType.Call)
{
var name = ((MethodCallExpression)root).Method.Name;
var nameOther = ((MethodCallExpression)rootOther).Method.Name;
if (name == nameOther)
{
comparers.TryGetValue(name, out comparer);
}
}
if (comparer != null)
{
relationship = comparer.Relationship(root, rootOther);
}
else
{
relationship = expr.DeepEquals(other) ? RelationshipType.Equal : RelationshipType.Incomparable;
}
return relationship;
}
private static readonly MethodInfo IGNORE = typeof(TriggerTree).GetMethod("Ignore");
private int PredicateCount()
{
return Predicates.Count(e => !IsIgnore(e));
}
private bool IsIgnore(Expression expression)
{
return expression is MethodCallExpression call && call.Method == IGNORE;
}
}
}

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

@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.Bot.Builder.AI.TriggerTrees
{
public static partial class Extensions
{
public static bool DeepEquals(this Expression expr, Expression other)
{
bool eq = true;
if (expr != null && other != null)
{
eq = expr.NodeType == other.NodeType && expr.Type == other.Type;
if (eq)
{
switch (expr.NodeType)
{
case ExpressionType.Call:
{
var call = (MethodCallExpression)expr;
var callOther = (MethodCallExpression)other;
eq = call.Object.DeepEquals(callOther.Object)
&& call.Method == callOther.Method
&& call.Arguments.Count == callOther.Arguments.Count;
for (var i = 0; eq && i < call.Arguments.Count; ++i)
{
eq = call.Arguments[i].DeepEquals(callOther.Arguments[i]);
}
}
break;
case ExpressionType.Conditional:
{
var cond = (ConditionalExpression)expr;
var condOther = (ConditionalExpression)other;
eq = cond.Test.DeepEquals(condOther.Test) &&
cond.IfFalse.DeepEquals(condOther.IfFalse) &&
cond.IfTrue.DeepEquals(condOther.IfTrue);
}
break;
case ExpressionType.Constant:
{
var constant = (ConstantExpression)expr;
var constantOther = (ConstantExpression)other;
if (constant.Value == null)
{
eq = constantOther.Value == null;
}
else
{
eq = constant.Value.Equals(constantOther.Value);
}
}
break;
case ExpressionType.MemberAccess:
{
var member = (MemberExpression)expr;
var memberOther = (MemberExpression)other;
eq = member.Member.Equals(memberOther.Member)
&& member.Expression.DeepEquals(memberOther.Expression);
}
break;
case ExpressionType.MemberInit:
{
var member = (MemberInitExpression)expr;
var memberOther = (MemberInitExpression)other;
eq = member.DeepEquals(memberOther);
}
break;
case ExpressionType.New:
{
var newExpr = (NewExpression)expr;
var newOther = (NewExpression)other;
eq = newExpr.Constructor.Equals(newOther.Constructor)
&& newExpr.Arguments.Count == newOther.Arguments.Count;
for (var i = 0; eq && i < newExpr.Arguments.Count; ++i)
{
eq = newExpr.Arguments[i].DeepEquals(newOther.Arguments[i]);
}
eq = eq && newExpr.Members?.Count == newOther.Members?.Count;
if (newExpr.Members != null)
{
for (var i = 0; eq && i < newExpr.Members.Count; ++i)
{
eq = newExpr.Members[i].Equals(newOther.Members[i]);
}
}
}
break;
case ExpressionType.NewArrayInit:
{
var init = (NewArrayExpression)expr;
var initOther = (NewArrayExpression)other;
eq = init.Expressions.Count == initOther.Expressions.Count;
for (var i = 0; eq && i < init.Expressions.Count; ++i)
{
eq = init.Expressions[i].DeepEquals(initOther.Expressions[i]);
}
}
break;
case ExpressionType.Parameter:
{
var parameter = (ParameterExpression)expr;
var parameterOther = (ParameterExpression)other;
// NOTE: This assumes that all parameters of the same type are the same.
// This is so you can use different names for the parameter across expressions.
eq = parameter.Type == parameterOther.Type;
}
break;
default:
{
if (expr is UnaryExpression unary && other is UnaryExpression unaryOther)
{
eq = unary.Operand.DeepEquals(unaryOther.Operand);
}
else
{
if (expr is BinaryExpression binary && other is BinaryExpression binaryOther)
{
eq = binary.Left.DeepEquals(binaryOther.Left)
&& binary.Right.DeepEquals(binaryOther.Right);
}
else
{
throw new ArgumentException($"{expr.NodeType} is not handled");
}
}
}
break;
}
}
}
return eq;
}
public static bool DeepEquals(this MemberInitExpression init, MemberInitExpression other)
{
var eq =
init.Bindings.Count == other.Bindings.Count
&& init.NewExpression.DeepEquals(other.NewExpression);
for (var i = 0; eq && i < init.Bindings.Count; ++i)
{
eq = init.Bindings[i].DeepEquals(other.Bindings[i]);
}
return eq;
}
public static bool DeepEquals(this MemberBinding binding, MemberBinding bindingOther)
{
var eq = binding.Member.Equals(bindingOther.Member)
&& binding.BindingType.Equals(bindingOther.BindingType);
if (eq)
{
switch (binding.BindingType)
{
case MemberBindingType.Assignment:
{
var assignment = (MemberAssignment)binding;
var assignmentOther = (MemberAssignment)bindingOther;
eq = assignment.Expression.DeepEquals(assignmentOther.Expression);
}
break;
case MemberBindingType.ListBinding:
{
var list = (MemberListBinding)binding;
var listOther = (MemberListBinding)bindingOther;
eq = list.Initializers.Count == listOther.Initializers.Count;
for (var j = 0; eq && j < list.Initializers.Count; ++j)
{
var initializer = list.Initializers[j];
var initializerOther = listOther.Initializers[j];
eq = initializer.Arguments.Count == initializerOther.Arguments.Count;
for (var k = 0; eq && k < initializer.Arguments.Count; ++k)
{
eq = initializer.Arguments[k].DeepEquals(initializerOther.Arguments[k]);
}
}
}
break;
case MemberBindingType.MemberBinding:
{
eq = binding.DeepEquals(bindingOther);
}
break;
}
}
return eq;
}
}
}

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

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.Bot.Builder.AI.TriggerTrees
{
public static partial class Extensions
{
public static RelationshipType Swap(this RelationshipType original)
{
var relationship = original;
switch (original)
{
case RelationshipType.Specializes:
relationship = RelationshipType.Generalizes;
break;
case RelationshipType.Generalizes:
relationship = RelationshipType.Specializes;
break;
}
return relationship;
}
}
}

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

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.Bot.Builder.AI.TriggerTrees
{
/// <summary>
/// Delegate to test memory to see if a trigger expression is true.
/// </summary>
/// <param name="memory"></param>
/// <returns></returns>
public delegate bool Evaluator(IDictionary<string, object> memory);
/// <summary>
/// Optimize a clause by rewriting it.
/// </summary>
/// <remarks>
/// If returned clause is null, then the expression will always be false.
/// This is to allow things like combining simple comparisons into a range predicate.
/// </remarks>
public interface IOptimizer
{
/// <summary>
/// Optionally rewrite a clause.
/// </summary>
/// <param name="clause">Original clause.</param>
/// <returns></returns>
Clause Optimize(Clause clause);
}
/// <summary>
/// Relationship between trigger expressions.
/// </summary>
public enum RelationshipType {
/// <summary>
/// First argument specializes the second, i.e. applies to a subset of the states the second argument covers.
/// </summary>
Specializes,
/// <summary>
/// First and second argument are the same expression.
/// </summary>
Equal,
/// <summary>
/// First argument generalizes the second, i.e. applies to a superset of the states the second argument covers.
/// </summary>
Generalizes,
/// <summary>
/// Cannot tell how the first and second arguments relate.
/// </summary>
Incomparable };
/// <summary>
/// Compare two predicates to identify the relationship between them.
/// </summary>
public interface IPredicateComparer
{
/// <summary>
/// Name of predicate.
/// </summary>
string Predicate { get; }
/// <summary>
/// Identify the relationship between two predicates.
/// </summary>
/// <param name="predicate">First predicate.</param>
/// <param name="other">Second predicate.</param>
/// <returns>Relationship beteween predicates.</returns>
/// <remarks>
/// This is useful for doing things like identifying that Range("size", 1, 5) is more specialized than Range("size", 1, 10).
/// </remarks>
RelationshipType Relationship(Expression predicate, Expression other);
}
}

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

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RootNamespace>Microsoft.Bot.Builder.AI.TriggerTrees</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.CommonExpressions\Microsoft.Expressions.csproj" />
</ItemGroup>
</Project>

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

@ -0,0 +1,525 @@
// This will trace the whole process, but will generate a lot of output
// #define TraceTree
// This adds a counter to each comparison when building the tree so that you can find it in the trace.
// There is a node static count and boolean ShowTrace that can be turned on/off if needed.
// #define Count
// This will verify the tree as it is built by checking invariants
// #define VerifyTree
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
namespace Microsoft.Bot.Builder.AI.TriggerTrees
{
/// <summary>
/// Node in a trigger tree.
/// </summary>
public class Node
{
/// <summary>
/// All of the most specific triggers that contain the <see cref="Clause"/> in this node.
/// </summary>
public IReadOnlyList<Trigger> Triggers { get { return _triggers; } }
/// <summary>
/// All triggers that contain the <see cref="Clause"/> in this node.
/// </summary>
/// <remarks>
/// Triggers only contain the most specific trigger, so if this node
/// as Pred(A) and there was a rule R1: Pred(A) -> A1 and R2: Pred(A) v Pred(B) -> A2
/// then the second trigger would be in AllTriggers, but not Triggers because it
/// is more general.
/// </remarks>
public IReadOnlyList<Trigger> AllTriggers { get { return _allTriggers; } }
/// <summary>
/// Specialized children of this node.
/// </summary>
public IReadOnlyList<Node> Specializations { get { return _specializations; } }
/// <summary>
/// The logical conjunction this node represents.
/// </summary>
public Clause Clause { get; }
/// <summary>
/// The tree this node is found in.
/// </summary>
public TriggerTree Tree { get; }
private List<Trigger> _allTriggers = new List<Trigger>();
private List<Trigger> _triggers = new List<Trigger>();
private List<Node> _specializations = new List<Node>();
private readonly Evaluator _evaluator;
#if Count
private static int _count = 0;
#endif
#if TraceTree
public static bool ShowTrace = true;
#endif
internal Node(Clause clause, TriggerTree tree, Trigger trigger = null)
{
Clause = clause;
Tree = tree;
if (trigger != null)
{
_allTriggers.Add(trigger);
_triggers.Add(trigger);
if (Clause != null)
{
var firstPredicate = true;
Expression body = null;
foreach (var clausePredicate in Clause.Predicates)
{
var predicate = clausePredicate;
var ignore = TriggerTree.GetIgnore(predicate);
if (ignore != null)
{
predicate = ignore.Arguments[0];
}
if (firstPredicate)
{
firstPredicate = false;
body = predicate;
}
else
{
body = Expression.AndAlso(body, predicate);
}
}
if (body != null)
{
_evaluator = Expression.Lambda<Evaluator>(body, trigger.OriginalExpression.Parameters).Compile();
}
}
}
}
public override string ToString()
{
var builder = new StringBuilder();
ToString(builder);
return builder.ToString();
}
public void ToString(StringBuilder builder, int indent = 0)
{
Clause.ToString(builder, indent);
}
/// <summary>
/// Return the most specific matches below this node.
/// </summary>
/// <param name="frame">Frame to evaluate against.</param>
/// <returns>List of the most specific matches found.</returns>
internal IReadOnlyList<Node> Matches(IDictionary<string, object> frame)
{
var matches = new List<Node>();
Matches(frame, matches, new Dictionary<Node, bool>());
return matches;
}
/// <summary>
/// Identify the relationship between two nodes.
/// </summary>
/// <param name="other"></param>
/// <returns>Relationship between this node and the other.</returns>
public RelationshipType Relationship(Node other)
{
return Clause.Relationship(other.Clause, Tree.Comparers);
}
private enum Operation { None, Found, Added, Removed, Inserted };
internal bool AddNode(Node triggerNode)
{
#if TraceTree
if (Node.ShowTrace)
{
Debug.WriteLine("");
Debug.WriteLine($"***** Add Trigger {triggerNode.Triggers.First().OriginalExpression} *****");
Debug.IndentSize = 2;
}
#endif
return AddNode(triggerNode, new Dictionary<Node, Operation>()) == Operation.Added;
}
internal bool RemoveTrigger(Trigger trigger)
{
var removed = new Dictionary<Node, Operation>();
return RemoveTrigger(trigger, null, removed) == Operation.Removed;
}
// In order to add a trigger we have to walk over the whole tree
// If I am adding B and encounter A, A could have a specialization of A & B without B being present.
private Operation AddNode(Node triggerNode, Dictionary<Node, Operation> ops)
{
var op = Operation.None;
if (!ops.TryGetValue(this, out op))
{
var trigger = triggerNode.Triggers.First();
var relationship = Relationship(triggerNode);
#if TraceTree
if (Node.ShowTrace)
{
Debug.WriteLine("");
#if Count
Debug.Write($"{_count}:");
#endif
Debug.WriteLine(this);
Debug.WriteLine($"{relationship}");
Debug.WriteLine(triggerNode);
}
#endif
#if Count
++_count;
#endif
switch (relationship)
{
case RelationshipType.Equal:
{
// Ensure action is not already there
bool found = false;
foreach (var existing in _allTriggers)
{
if (trigger.Action != null && trigger.Action.Equals(existing.Action))
{
found = true;
break;
}
}
op = Operation.Found;
if (!found)
{
_allTriggers.Add(trigger);
var add = true;
for (var i = 0; i < _triggers.Count(); )
{
var existing = _triggers[i];
var reln = trigger.Relationship(existing, Tree.Comparers);
if (reln == RelationshipType.Generalizes)
{
#if TraceTree
if (Node.ShowTrace) Debug.WriteLine($"Trigger specialized by {existing}");
#endif
add = false;
break;
}
else if (reln == RelationshipType.Specializes)
{
#if TraceTree
if (Node.ShowTrace) Debug.WriteLine($"Trigger replaces {existing}");
#endif
_triggers.RemoveAt(i);
}
else
{
++i;
}
}
if (add)
{
#if TraceTree
if (Node.ShowTrace) Debug.WriteLine("Add trigger");
#endif
_triggers.Add(trigger);
}
#if DEBUG
Debug.Assert(CheckInvariants());
#endif
op = Operation.Added;
}
}
break;
case RelationshipType.Incomparable:
{
foreach (var child in _specializations)
{
child.AddNode(triggerNode, ops);
}
}
break;
case RelationshipType.Specializes:
{
triggerNode.AddSpecialization(this);
#if DEBUG
Debug.Assert(triggerNode.CheckInvariants());
#endif
op = Operation.Inserted;
}
break;
case RelationshipType.Generalizes:
{
bool foundOne = false;
List<Node> removals = null;
#if TraceTree
if (Node.ShowTrace) ++Debug.IndentLevel;
#endif
foreach (var child in _specializations)
{
var childOp = child.AddNode(triggerNode, ops);
if (childOp != Operation.None)
{
foundOne = true;
if (childOp == Operation.Inserted)
{
if (removals == null)
{
removals = new List<Node>();
}
removals.Add(child);
op = Operation.Added;
}
else
{
op = childOp;
}
}
}
#if TraceTree
if (Node.ShowTrace) --Debug.IndentLevel;
#endif
if (removals != null)
{
foreach (var child in removals)
{
_specializations.Remove(child);
}
#if TraceTree
if (Node.ShowTrace)
{
Debug.WriteLine("Generalized");
foreach (var removal in removals)
{
Debug.WriteLine(removal);
}
Debug.WriteLine($"in {this}");
}
#endif
_specializations.Add(triggerNode);
#if DEBUG
Debug.Assert(CheckInvariants());
#endif
}
if (!foundOne)
{
_specializations.Add(triggerNode);
#if DEBUG
Debug.Assert(triggerNode.CheckInvariants());
#endif
op = Operation.Added;
}
}
break;
}
// Prevent visiting this node again
ops[this] = op;
}
return op;
}
#if DEBUG
private bool CheckInvariants()
{
#if VerifyTree
foreach (var child in _specializations)
{
var reln = Relationship(child);
Debug.Assert(reln == RelationshipType.Generalizes);
}
for (var i = 0; i < _specializations.Count; ++i)
{
var first = _specializations[i];
for (var j = i + 1; j < _specializations.Count; ++j)
{
var second = _specializations[j];
var reln = first.Relationship(second);
Debug.Assert(reln == RelationshipType.Incomparable);
}
}
// Triggers should be incomparable
for (var i = 0; i < _triggers.Count(); ++i)
{
for (var j = i + 1; j < _triggers.Count(); ++j)
{
var reln = _triggers[i].Relationship(_triggers[j], Tree.Comparers);
if (reln == RelationshipType.Specializes || reln == RelationshipType.Generalizes)
{
Debug.Assert(false, $"{this} triggers overlap");
break;
}
}
}
// All triggers should all be found in triggers
for (var i = 0; i < _allTriggers.Count(); ++i)
{
var allTrigger = _allTriggers[i];
var found = false;
for (var j = 0; j < _triggers.Count(); ++j)
{
var trigger = _triggers[j];
var reln = allTrigger.Relationship(trigger, Tree.Comparers);
if (allTrigger == trigger || reln == RelationshipType.Generalizes)
{
found = true;
break;
}
}
if (!found)
{
Debug.Assert(false, $"{this} missing all trigger {allTrigger}");
}
}
#endif
return true;
}
#endif
private bool AddSpecialization(Node specialization)
{
var added = false;
List<Node> removals = null;
bool skip = false;
foreach (var child in _specializations)
{
var reln = specialization.Relationship(child);
if (reln == RelationshipType.Equal)
{
skip = true;
#if TraceTree
if (Node.ShowTrace) Debug.WriteLine($"Already has child");
#endif
break;
}
if (reln == RelationshipType.Generalizes)
{
if (removals == null) removals = new List<Node>();
removals.Add(child);
}
else if (reln == RelationshipType.Specializes)
{
#if TraceTree
if (Node.ShowTrace) Debug.WriteLine($"Specialized by {child}");
#endif
skip = true;
break;
}
}
if (!skip)
{
if (removals != null)
{
foreach (var removal in removals)
{
// Don't need to add back because specialization already has them
_specializations.Remove(removal);
#if TraceTree
if (Node.ShowTrace)
{
Debug.WriteLine($"Replaced {removal}");
++Debug.IndentLevel;
}
#endif
specialization.AddSpecialization(removal);
#if TraceTree
if (Node.ShowTrace) --Debug.IndentLevel;
#endif
}
}
_specializations.Add(specialization);
added = true;
#if TraceTree
if (Node.ShowTrace) Debug.WriteLine("Added as specialization");
#endif
}
return added;
}
// TODO: Implement remove and drop parent
// Need to make sure we update triggers from allTriggers as well.
private Operation RemoveTrigger(Trigger trigger, Node parent, Dictionary<Node, Operation> removed)
{
var op = Operation.None;
/*
if (!removed.TryGetValue(this, out op))
{
var relationship = trigger.Relationship(_triggers.First());
#if TraceTree
Debug.WriteLine(trigger.ToString());
Debug.WriteLine($"Remove {relationship}");
Debug.WriteLine(ToString());
Debug.WriteLine("");
#endif
switch (relationship)
{
case RelationshipType.Equal:
{
if (_triggers.Remove(trigger))
{
op = Operation.Removed;
}
else
{
op = Operation.Found;
}
if (parent != null && _triggers.Count == 0)
{
parent._specializations.AddRange(_specializations);
}
}
break;
case RelationshipType.Specializes:
{
foreach (var child in new List<Node>(_specializations))
{
op = child.RemoveTrigger(trigger, this, removed);
if (op != Operation.None)
{
break;
}
}
}
break;
}
removed[this] = op;
}
*/
return op;
}
private bool Matches(IDictionary<string, object> memory, List<Node> matches, Dictionary<Node, bool> matched)
{
if (!matched.TryGetValue(this, out bool found))
{
found = false;
foreach (var child in _specializations)
{
if (child.Matches(memory, matches, matched))
{
found = true;
}
}
// No child matched so we might
if (!found)
{
if (_evaluator == null ? Triggers.Any() : _evaluator(memory))
{
matches.Add(this);
found = true;
}
}
matched.Add(this, found);
}
return found;
}
}
}

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

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.Bot.Builder.AI.TriggerTrees
{
/// <summary>
/// Type of quantifier for expanding trigger expressions.
/// </summary>
public enum QuantifierType {
/// <summary>
/// Within a clause, duplicate any predicate with variable for each possible binding.
/// </summary>
All,
/// <summary>
/// Create a new clause for each possible binding of variable.
/// </summary>
Any };
/// <summary>
/// Quantifier for allowing runtime expansion of expressions.
/// </summary>
public class Quantifier
{
/// <summary>
/// Name of variable that will be replaced.
/// </summary>
public string Variable { get; }
/// <summary>
/// Type of quantifier.
/// </summary>
public QuantifierType Type { get; }
/// <summary>
/// Possible bindings for quantifier.
/// </summary>
public IEnumerable<string> Bindings { get; }
/// <summary>
/// Create a quantifier.
/// </summary>
/// <param name="variable">Name of variable to replace.</param>
/// <param name="type">Type of quantifier.</param>
/// <param name="bindings">Possible bindings for variable.</param>
public Quantifier(string variable, QuantifierType type, IEnumerable<string> bindings)
{
Variable = variable;
Type = type;
Bindings = bindings;
}
public override string ToString()
{
return $"{Type} {Variable} {Bindings.Count()}";
}
}
}

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

@ -0,0 +1,626 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Expressions;
using Antlr4.Runtime.Tree;
namespace Microsoft.Bot.Builder.AI.TriggerTrees
{
/// <summary>
/// A trigger is a combination of a trigger expression and the corresponding action.
/// </summary>
public class Trigger
{
/// <summary>
/// Original trigger expression.
/// </summary>
public string OriginalExpression;
private IParseTree _parse;
private TriggerTree _tree;
private IEnumerable<Quantifier> _quantifiers;
private List<Clause> _clauses;
/// <summary>
/// Action to take when trigger is true.
/// </summary>
public object Action { get; }
/// <summary>
/// Expressions are converted into Disjunctive Normal Form where ! is pushed to the leaves and there is an implicit || between clauses and && within a clause.
/// </summary>
public IReadOnlyList<Clause> Clauses { get { return _clauses; } }
/// <summary>
/// Construct a trigger expression.
/// </summary>
/// <param name="tree">Trigger tree that contains this trigger.</param>
/// <param name="expression">Expression for when the trigger action is possible.</param>
/// <param name="action">Action to take when a trigger matches.</param>
/// <param name="quantifiers">Quantifiers to dynamically expand the expression.</param>
internal Trigger(TriggerTree tree, string expression, object action, params Quantifier[] quantifiers)
{
_parse = ExpressionEngine.Parse(expression);
_tree = tree;
Action = action;
OriginalExpression = expression;
_quantifiers = quantifiers;
if (expression != null)
{
var notNormalized = PushDownNot(_parse, false);
_clauses = GenerateClauses(notNormalized).ToList();
RemoveDuplicatedPredicates();
OptimizeClauses();
ExpandQuantifiers();
RemoveDuplicates();
MarkSubsumedClauses();
}
else
{
_clauses = new List<Clause>();
}
}
public override string ToString()
{
var builder = new StringBuilder();
ToString(builder);
return builder.ToString();
}
public RelationshipType Relationship(Trigger other, Dictionary<string, IPredicateComparer> comparers)
{
RelationshipType result;
var first = Relationship(this, other, comparers);
var second = Relationship(other, this, comparers);
if (first == RelationshipType.Equal)
{
if (second == RelationshipType.Equal)
{
// All first clauses == second clauses
result = RelationshipType.Equal;
}
else
{
// All first clauses found in second
result = RelationshipType.Specializes;
}
}
else if (first == RelationshipType.Specializes)
{
// All first clauses specialize or equal a second clause
result = RelationshipType.Specializes;
}
else if (second == RelationshipType.Equal || second == RelationshipType.Specializes)
{
// All second clauses are equal or specialize a first clause
result = RelationshipType.Generalizes;
}
else
{
// All other cases are incomparable
result = RelationshipType.Incomparable;
}
return result;
}
private RelationshipType Relationship(Trigger trigger, Trigger other, Dictionary<string, IPredicateComparer> comparers)
{
var soFar = RelationshipType.Incomparable;
foreach (var clause in trigger.Clauses)
{
if (!clause.Subsumed)
{
// Check other for = or clause that is specialized
var clauseSoFar = RelationshipType.Incomparable;
foreach (var second in other.Clauses)
{
if (!second.Subsumed)
{
var reln = clause.Relationship(second, comparers);
if (reln == RelationshipType.Equal || reln == RelationshipType.Specializes)
{
clauseSoFar = reln;
break;
}
}
}
if (clauseSoFar == RelationshipType.Incomparable || clauseSoFar == RelationshipType.Generalizes)
{
// Some clause is not comparable
soFar = RelationshipType.Incomparable;
break;
}
else if (clauseSoFar == RelationshipType.Equal)
{
if (soFar == RelationshipType.Incomparable)
{
// Start on equal clause
soFar = clauseSoFar;
}
}
else if (clauseSoFar == RelationshipType.Specializes)
{
// Either going from incomparable or equal to specializes
soFar = clauseSoFar;
}
}
}
// Either incomparable, equal or specializes
return soFar;
}
protected void ToString(StringBuilder builder, int indent = 0)
{
builder.Append(' ', indent);
if (_clauses.Any())
{
bool first = true;
foreach (var clause in _clauses)
{
if (first)
{
first = false;
}
else
{
builder.AppendLine();
builder.Append(' ', indent);
builder.Append("|| ");
}
builder.Append(clause.ToString());
}
}
else
{
builder.Append("<Empty>");
}
}
// Push not down to leaves using De Morgan's rule
private Expression PushDownNot(IParseTree expression, bool inNot)
{
var e = new Parse
Expression newExpr = expression;
var unary = expression as UnaryExpression;
var binary = expression as BinaryExpression;
switch (expression.NodeType)
{
case ExpressionType.AndAlso:
{
if (inNot)
{
newExpr = System.Linq.Expressions.Expression.OrElse(PushDownNot(binary.Left, true), PushDownNot(binary.Right, true));
}
else
{
newExpr = System.Linq.Expressions.Expression.AndAlso(PushDownNot(binary.Left, false), PushDownNot(binary.Right, false));
}
}
break;
case ExpressionType.OrElse:
{
if (inNot)
{
newExpr = System.Linq.Expressions.Expression.AndAlso(PushDownNot(binary.Left, true), PushDownNot(binary.Right, true));
}
else
{
newExpr = System.Linq.Expressions.Expression.OrElse(PushDownNot(binary.Left, false), PushDownNot(binary.Right, false));
}
}
break;
case ExpressionType.Not:
newExpr = PushDownNot(((UnaryExpression)expression).Operand, !inNot);
break;
// Rewrite comparison operators
case ExpressionType.LessThan:
if (inNot)
{
newExpr = Expression.GreaterThanOrEqual(binary.Left, binary.Right);
}
break;
case ExpressionType.LessThanOrEqual:
if (inNot)
{
newExpr = Expression.GreaterThan(binary.Left, binary.Right);
}
break;
case ExpressionType.Equal:
if (inNot)
{
newExpr = Expression.NotEqual(binary.Left, binary.Right);
}
break;
case ExpressionType.GreaterThanOrEqual:
if (inNot)
{
newExpr = Expression.LessThan(binary.Left, binary.Right);
}
break;
case ExpressionType.GreaterThan:
if (inNot)
{
newExpr = Expression.LessThanOrEqual(binary.Left, binary.Right);
}
break;
case ExpressionType.Call:
{
var special = TriggerTree.GetOptional(expression) ?? TriggerTree.GetIgnore(expression);
if (special != null)
{
// Pass not through optional/ignore
newExpr = Expression.Call(special.Method, PushDownNot(special.Arguments[0], inNot));
}
else
{
if (inNot)
{
newExpr = Expression.Not(expression);
}
}
}
break;
default:
if (inNot)
{
newExpr = Expression.Not(expression);
}
break;
}
return newExpr;
}
private IEnumerable<Expression> OrLeaves(Expression expression)
{
if (expression.NodeType == ExpressionType.OrElse)
{
var or = (BinaryExpression)expression;
foreach (var leaf in OrLeaves(or.Left))
{
yield return leaf;
}
foreach (var leaf in OrLeaves(or.Right))
{
yield return leaf;
}
}
else
{
yield return expression;
}
}
private IEnumerable<Clause> GenerateClauses(Expression expression)
{
var binary = expression as BinaryExpression;
switch (expression.NodeType)
{
case ExpressionType.AndAlso:
{
var rightClauses = GenerateClauses(binary.Right);
foreach (var left in GenerateClauses(binary.Left))
{
foreach (var right in rightClauses)
{
var clause = new Clause();
clause.Predicates.AddRange(left.Predicates);
clause.Predicates.AddRange(right.Predicates);
yield return clause;
}
}
}
break;
case ExpressionType.OrElse:
{
foreach (var left in GenerateClauses(binary.Left))
{
yield return left;
}
foreach (var right in GenerateClauses(binary.Right))
{
yield return right;
}
}
break;
case ExpressionType.Call:
{
var optional = TriggerTree.GetOptional(expression);
if (optional != null)
{
yield return new Clause();
foreach (var clause in GenerateClauses(optional.Arguments[0]))
{
yield return clause;
}
}
else
{
yield return new Clause(expression);
}
}
break;
default:
yield return new Clause(expression);
break;
}
}
// Remove any duplicate predicates within a clause
// NOTE: This is annoying but expression hash codes of DeepEquals expressions are different
private void RemoveDuplicatedPredicates()
{
// Rewrite clauses to remove duplicated tests
for (var i = 0; i < _clauses.Count(); ++i)
{
var clause = _clauses[i];
var newClause = new Clause();
for (var p = 0; p < clause.Predicates.Count(); ++p)
{
var pred = clause.Predicates[p];
var found = false;
for (var q = p + 1; q < clause.Predicates.Count(); ++q)
{
if (pred.DeepEquals(clause.Predicates[q]))
{
found = true;
break;
}
}
if (!found)
{
newClause.Predicates.Add(pred);
}
}
_clauses[i] = newClause;
}
}
// Mark clauses that are more specific than another clause as subsumed and also remove any = clauses.
private void MarkSubsumedClauses()
{
for (var i = 0; i < _clauses.Count(); ++i)
{
var clause = _clauses[i];
if (!clause.Subsumed)
{
for (var j = i + 1; j < _clauses.Count(); ++j)
{
var other = _clauses[j];
if (!other.Subsumed)
{
var reln = clause.Relationship(other, _tree.Comparers);
if (reln == RelationshipType.Equal)
{
_clauses.RemoveAt(j);
}
else
{
if (reln == RelationshipType.Specializes)
{
clause.Subsumed = true;
break;
}
else if (reln == RelationshipType.Generalizes)
{
other.Subsumed = true;
}
}
}
}
}
}
}
private void OptimizeClauses()
{
foreach (var clause in _clauses)
{
foreach (var optimizer in _tree.Optimizers)
{
optimizer.Optimize(clause);
}
}
}
private void ExpandQuantifiers()
{
if (_quantifiers != null && _quantifiers.Any())
{
foreach (var quantifier in _quantifiers)
{
var newClauses = new List<Clause>();
foreach (var clause in _clauses)
{
newClauses.AddRange(ExpandQuantifier(quantifier, clause));
}
_clauses = newClauses;
}
}
}
private Expression SubstituteVariable(string variable, string binding, Expression expression, ref bool changed)
{
var newExpr = expression;
var unary = expression as UnaryExpression;
var binary = expression as BinaryExpression;
switch (expression.NodeType)
{
case ExpressionType.Call:
{
var call = (MethodCallExpression)expression;
var args = new List<Expression>();
foreach (var arg in call.Arguments)
{
args.Add(SubstituteVariable(variable, binding, arg, ref changed));
}
newExpr = System.Linq.Expressions.Expression.Call(call.Object, call.Method, args);
}
break;
case ExpressionType.Not:
newExpr = System.Linq.Expressions.Expression.Not(SubstituteVariable(variable, binding, unary.Operand, ref changed));
break;
case ExpressionType.LessThan:
newExpr = System.Linq.Expressions.Expression.LessThan(
SubstituteVariable(variable, binding, binary.Left, ref changed),
SubstituteVariable(variable, binding, binary.Right, ref changed));
break;
case ExpressionType.LessThanOrEqual:
newExpr = System.Linq.Expressions.Expression.LessThanOrEqual(
SubstituteVariable(variable, binding, binary.Left, ref changed),
SubstituteVariable(variable, binding, binary.Right, ref changed));
break;
case ExpressionType.Equal:
newExpr = System.Linq.Expressions.Expression.Equal(
SubstituteVariable(variable, binding, binary.Left, ref changed),
SubstituteVariable(variable, binding, binary.Right, ref changed));
break;
case ExpressionType.NotEqual:
newExpr = System.Linq.Expressions.Expression.NotEqual(
SubstituteVariable(variable, binding, binary.Left, ref changed),
SubstituteVariable(variable, binding, binary.Right, ref changed));
break;
case ExpressionType.GreaterThan:
newExpr = System.Linq.Expressions.Expression.GreaterThan(
SubstituteVariable(variable, binding, binary.Left, ref changed),
SubstituteVariable(variable, binding, binary.Right, ref changed));
break;
case ExpressionType.GreaterThanOrEqual:
newExpr = System.Linq.Expressions.Expression.GreaterThanOrEqual(
SubstituteVariable(variable, binding, binary.Left, ref changed),
SubstituteVariable(variable, binding, binary.Right, ref changed));
break;
case ExpressionType.Constant:
{
var constant = (ConstantExpression)expression;
if (constant.Type == typeof(string) && (string)constant.Value == variable)
{
newExpr = System.Linq.Expressions.Expression.Constant(binding);
changed = true;
}
}
break;
case ExpressionType.Convert:
newExpr = System.Linq.Expressions.Expression.Convert(SubstituteVariable(variable, binding, unary.Operand, ref changed), unary.Type);
break;
case ExpressionType.ConvertChecked:
newExpr = System.Linq.Expressions.Expression.ConvertChecked(SubstituteVariable(variable, binding, unary.Operand, ref changed), unary.Type);
break;
default:
break;
}
return newExpr;
}
private IEnumerable<Clause> ExpandQuantifier(Quantifier quantifier, Clause clause)
{
if (quantifier.Type == QuantifierType.All)
{
var newClause = new Clause(clause);
if (quantifier.Bindings.Any())
{
foreach (var predicate in clause.Predicates)
{
foreach (var binding in quantifier.Bindings)
{
var changed = false;
var newPredicate = SubstituteVariable(quantifier.Variable, binding, predicate, ref changed);
newClause.Predicates.Add(newPredicate);
if (!changed)
{
// No change to first predicate, so can stop
break;
}
}
}
}
else
{
// Empty quantifier is trivially true so remove any predicate that refers to quantifier
foreach (var predicate in clause.Predicates)
{
var changed = false;
SubstituteVariable(quantifier.Variable, string.Empty, predicate, ref changed);
if (!changed)
{
newClause.Predicates.Add(predicate);
}
}
}
yield return newClause;
}
else
{
if (quantifier.Bindings.Any())
{
var changed = false;
foreach (var binding in quantifier.Bindings)
{
var newClause = new Clause(clause);
foreach (var predicate in clause.Predicates)
{
var newPredicate = SubstituteVariable(quantifier.Variable, binding, predicate, ref changed);
newClause.Predicates.Add(newPredicate);
}
if (changed)
{
newClause.AnyBindings.Add(quantifier.Variable, binding);
}
yield return newClause;
if (!changed)
{
break;
}
}
}
else
{
// Keep clause if does not contain any binding
var changed = false;
foreach (var predicate in clause.Predicates)
{
SubstituteVariable(quantifier.Variable, string.Empty, predicate, ref changed);
if (changed)
{
break;
}
}
if (!changed)
{
yield return clause;
}
}
}
}
private void RemoveDuplicates()
{
foreach (var clause in _clauses)
{
// NOTE: This is quadratic in clause length but GetHashCode is not equal for expressions and we expect the number of clauses to be small.
var predicates = clause.Predicates;
for (var i = 0; i < predicates.Count; ++i)
{
var first = predicates[i];
for (var j = i + 1; j < predicates.Count;)
{
var second = predicates[j];
if (first.DeepEquals(second))
{
predicates.RemoveAt(j);
}
else
{
++j;
}
}
}
}
}
}
}

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

@ -0,0 +1,359 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
namespace Microsoft.Bot.Builder.AI.TriggerTrees
{
// Each trigger is normalized to disjunctive normal form and then expanded with quantifiers.
// Each of those clauses is then put into a DAG where the most restrictive clauses are at the bottom.
// When matching the most specific clauses block out any more general clauses.
//
// Disjunctions and quantification do not change the tree construction, but they are used in determing
// what triggers are returned. For example, from a strictly logical sense A&B v C&D is more general then A&B or C. If we had these rules:
// R1(A)
// R2(A&B)
// R3(A&BvC&D)
// R4(C)
// Then from a strictly logic viewpoint the tree should be:
// Root
// | | |
// R3(A&B v C&D) R1(A) R4(C)
// | /
// R2(A&B)
// The problem is that from the developer standpoint R3 is more of a shortcut for two rules, i.e.A&B and another rule for C&D.
// In the tree above if you had C&D you would get both R3 and R4—which does not seem like what you really want.
// Even though R3 is a disjunction, C&D is more specific than just C.
// The fix is build the tree just based on the conjunctions and then filter triggers on a specific clause so that more specific triggers remove more general ones, i.e. disjunctions.
// This is what the correspoinding tree looks like:
// Root
// | |
// A: R1(A) C: R4(C)
// | |
// A&B: R2(A&B), R3(A&BvC&D) C&D: R3(A&BvC&D)
// If you had A&B you can look at the triggers and return R2 instead of R3—that seems appropriate.
// But, if you also had C&D at the same time you would still get R3 triggering because of C&D, I think this is the right thing.
// Even though R3 was filtered out of the A&B branch, it is still the most specific answer in the C&D branch.
// If we remove R3 all together then we would end up returning R4 instead which doesnt seem correct from the standpoint of disjunctions being a shortcut for multiple rules.
/// <summary>
/// A trigger tree organizes evaluators according to generalization/specialization in order to make it easier to use rules.
/// </summary>
/// <remarks>
/// A trigger expression generates true if the expression evaluated on a frame is true.
/// The expression itself consists of arbitrary boolean functions ("predicates") combined with && || !.
/// Most predicates are expressed over the frame passed in, but they can be anything--there are even ways of optimizing or comparing them.
/// By organizing evaluators into a tree (techinically a DAG) it becomes easier to use rules by reducing the coupling between rules.
/// For example if a rule applies if some predicate A is true, then another rule that applies if A && B are true is
/// more specialized. If the second expression is true, then because we know of the relationship we can ignore the first
/// rule--even though its expression is true. Without this kind of capability in order to add the second rule, you would
/// have to change the first to become A && !B.
/// </remarks>
[DebuggerDisplay("{ToString()}"), DebuggerTypeProxy(typeof(Debugger))]
public class TriggerTree
{
public List<IOptimizer> Optimizers = new List<IOptimizer>();
public Dictionary<string, IPredicateComparer> Comparers = new Dictionary<string, IPredicateComparer>();
public Node Root;
public int TotalTriggers = 0;
private class Debugger
{
public string TreeString;
public List<IOptimizer> _optimizers;
public Dictionary<string, IPredicateComparer> _comparers;
public Debugger(TriggerTree triggers)
{
TreeString = triggers.TreeToString();
_optimizers = triggers.Optimizers;
_comparers = triggers.Comparers;
}
}
/// <summary>
/// Method to mark a trigger expression as optional.
/// </summary>
/// <remarks>
/// When an expression is being processed, optional creates a disjunction where the expression is both included and not
/// included with the rest of the expression. This is a simple way to express this common relationship. By generating
/// both clauses then matching the expression can be more specific when the optional expression is true.
/// </remarks>
/// <param name="flag">Optional expression.</param>
/// <returns>True</returns>
public static bool Optional(bool flag)
{
return true;
}
private static readonly MethodInfo OPTIONAL = typeof(TriggerTree).GetMethod("Optional");
internal static bool IsOptional(Expression expression) => expression is MethodCallExpression call && call.Method == OPTIONAL;
internal static MethodCallExpression GetOptional(Expression expression)
{
return expression is MethodCallExpression call && call.Method == OPTIONAL ? call : null;
}
/// <summary>
/// Any predicate expression wrapped in this will be ignored for specialization.
/// </summary>
/// <param name="flag">Boolean expression.</param>
/// <returns>Flag value.</returns>
/// <remarks>
/// This is useful for when you need to add expression to the trigger that are part of rule mechanics rather than of intent.
/// For example, if you have a counter for how often a particular message is displayed, then that is part of the triggering condition,
/// but all such rules would be incomparable because they counter is per-rule.
/// </remarks>
public static bool Ignore(bool flag)
{
return flag;
}
private static readonly MethodInfo IGNORE = typeof(TriggerTree).GetMethod("Ignore");
internal static bool IsIgnore(Expression expression) => expression is MethodCallExpression call && call.Method == IGNORE;
internal static MethodCallExpression GetIgnore(Expression expression)
{
return expression is MethodCallExpression call && call.Method == IGNORE ? call : null;
}
/// <summary>
/// Construct an empty trigger tree.
/// </summary>
public TriggerTree()
{
Root = new Node(new Clause(), this);
}
public override string ToString()
{
return $"TriggerTree with {TotalTriggers} triggers";
}
/// <summary>
/// Add a trigger expression to the tree.
/// </summary>
/// <param name="expression">Trigger to add</param>
/// <param name="action">Action when triggered.</param>
/// <param name="quantifiers">Quantifiers to use when expanding expressions.</param>
/// <returns>New trigger.</returns>
public Trigger AddTrigger(string expression, object action, params Quantifier[] quantifiers)
{
var trigger = new Trigger(this, expression, action, quantifiers);
var added = false;
if (trigger.Clauses.Any())
{
foreach (var clause in trigger.Clauses)
{
var newNode = new Node(clause, this, trigger);
if (Root.AddNode(newNode))
{
added = true;
}
}
}
else
{
added = Root.AddNode(new Node(new Clause(), this, trigger));
}
if (added)
{
++TotalTriggers;
}
return trigger;
}
/// <summary>
/// Remove trigger from tree.
/// </summary>
/// <param name="trigger">Trigger to remove.</param>
/// <returns>True if removed trigger.</returns>
public bool RemoveTrigger(Trigger trigger)
{
var result = Root.RemoveTrigger(trigger);
if (result)
{
--TotalTriggers;
}
return result;
}
/// <summary>
/// Generate a string describing the tree.
/// </summary>
/// <param name="indent">Current indent level.</param>
/// <returns>string describing the tree.</returns>
public string TreeToString(int indent = 0)
{
var builder = new StringBuilder();
TreeToString(builder, Root, indent);
return builder.ToString();
}
private void TreeToString(StringBuilder builder, Node node, int indent)
{
node.ToString(builder, indent);
builder.Append($" [{node.Triggers.Count}]");
builder.AppendLine();
foreach (var child in node.Specializations)
{
TreeToString(builder, child, indent + 2);
}
}
public void GenerateGraph(string outPath)
{
using (var output = new StreamWriter(outPath))
{
output.WriteLine("digraph TriggerTree {");
GenerateGraph(output, Root, 0);
output.WriteLine("}");
}
}
private string NameNode(Node node)
{
return '"' + node.ToString().Replace("\"", "\\\"") + '"';
}
private void GenerateGraph(StreamWriter output, Node node, int indent)
{
output.Write($"{"".PadLeft(indent)}{NameNode(node)}");
var first = true;
foreach (var child in node.Specializations)
{
if (first)
{
output.Write(" -> {");
first = false;
}
output.Write($" {NameNode(child)}");
}
if (!first) output.WriteLine($"{"".PadLeft(indent)}}}");
else output.WriteLine();
foreach (var child in node.Specializations)
{
GenerateGraph(output, child, indent + 2);
}
}
private static void IWrite(StreamWriter writer, params string[] strings)
{
writer.Write(" ");
foreach (var str in strings)
{
writer.Write(str);
}
writer.WriteLine();
}
/// <summary>
/// Return the possible matches given the current frame.
/// </summary>
/// <param name="frame">Frame to evaluate against.</param>
/// <returns>Enumeration of possible matches.</returns>
/// <remarks>
/// Most predicates work on the current frame, but predicates are free to access other frames or use other sources of knowledge.
/// </remarks>
public IEnumerable<Node> Matches(IDictionary<string, object> frame)
{
return Root.Matches(frame);
}
/// <summary>
/// Verify the tree meets speicalization/generalization invariants.
/// </summary>
/// <returns></returns>
public Node VerifyTree()
{
return VerifyTree(Root, new HashSet<Node>());
}
private Node VerifyTree(Node node, HashSet<Node> visited)
{
Node badNode = null;
if (!visited.Contains(node))
{
visited.Add(node);
for (var i = 0; badNode == null && i < node.Specializations.Count; ++i)
{
var first = node.Specializations[i];
if (node.Relationship(first) != RelationshipType.Generalizes)
{
badNode = node;
}
else
{
VerifyTree(node.Specializations[i], visited);
for (var j = i + 1; j < node.Specializations.Count; ++j)
{
var second = node.Specializations[j];
if (first.Relationship(second) != RelationshipType.Incomparable)
{
badNode = node;
break;
}
}
}
}
}
return badNode;
}
/// <summary>
/// Merge together evaluator expressions.
/// </summary>
/// <param name="combination">How to combine expressions.</param>
/// <param name="allExpressions">Lambda expressions to combine.</param>
/// <returns>New evaluator lambda expression.</returns>
static public Expression<Evaluator> MergeEvaluators(ExpressionType combination, params Expression<Evaluator>[] allExpressions)
{
Expression<Evaluator> result = null;
var expressions = allExpressions.ToList();
expressions.RemoveAll(e => e == null);
if (expressions.Any())
{
if (expressions.Count == 1)
{
result = expressions.First();
}
else
{
var parameter = expressions.First().Parameters.First();
var replacer = new ReplaceParameter();
Expression body = null;
foreach (var expr in expressions)
{
var newBody = replacer.Modify(expr.Body, parameter);
if (body == null)
{
body = newBody;
}
else
{
body = Expression.MakeBinary(combination, body, newBody);
}
}
result = Expression.Lambda<Evaluator>(body, parameter);
}
}
return result;
}
private class ReplaceParameter : ExpressionVisitor
{
private ParameterExpression _parameter;
public Expression Modify(Expression expression, ParameterExpression parameter)
{
_parameter = parameter;
return Visit(expression);
}
protected override Expression VisitParameter(ParameterExpression node)
{
return _parameter;
}
}
}
}

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

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder.Dialogs.Expressions
{
public class Binary : Expression
{
public Binary(string type, Expression left, Expression right)
: base(type)
{
Left = left;
Right = right;
}
public Expression Left { get; }
public Expression Right { get; }
public override string ToString()
{
return $"{Type}({Left}, {Right})";
}
}
}

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

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder.Dialogs.Expressions
{
public class Call : NAry
{
public Call(string function, IEnumerable<Expression> args)
: base(ExpressionType.Call, args)
{
Function = function;
}
public string Function { get; }
public override string ToString()
{
return base.ToString(Function);
}
}
}

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

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder.Dialogs.Expressions
{
public class Constant: Expression
{
public Constant(object value)
: base(ExpressionType.Constant)
{
Value = value;
}
public object Value { get; }
}
}

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

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder.Dialogs.Expressions
{
public abstract class Expression
{
public Expression(string type)
{
Type = type;
}
public string Type { get; }
// TODO: Do I need these?
public static Binary Add(Expression left, Expression right) => new Binary(ExpressionType.Add, left, right);
}
}

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

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder.Dialogs.Expressions
{
public class ExpressionType
{
// Math
public const string Add = "+";
public const string Subtract = "-";
public const string Multiply = "*";
// Comparisons
public const string LessThan = "<";
public const string LessThanOrEqual = "<=";
public new const string Equals = "==";
public const string NotEquals = "!=";
public const string GreaterThan = ">";
public const string GreaterThanOrEqual = ">=";
// Logic
public const string And = "&&";
public const string Or = "||";
public const string Not = "!";
// Misc
public const string Call = "Call";
public const string Constant = "Constant";
public const string Variable = "Variable";
}
}

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

@ -15,7 +15,7 @@ namespace Microsoft.Bot.Builder.Dialogs.Expressions
Task<object> Evaluate(string expression, IDictionary<string, object> vars);
IParseTree Parse();
Expression Parse();
}
}

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

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.Bot.Builder.Dialogs.Expressions
{
public class NAry : Expression
{
public NAry(string type, IEnumerable<Expression> children)
: base(type)
{
Children = children.ToArray();
}
public Expression[] Children { get; }
public override string ToString()
{
return ToString(Type);
}
protected string ToString(string name)
{
var builder = new StringBuilder();
builder.Append(Type);
builder.Append('(');
var first = true;
foreach (var child in Children)
{
if (first)
{
first = false;
}
else
{
builder.Append(", ");
}
builder.Append(child.ToString());
}
return builder.ToString();
}
}
}

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

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder.Dialogs.Expressions
{
public class Unary : Expression
{
public Unary(string type, Expression child)
: base(type)
{
Child = child;
}
public Expression Child { get; }
public override string ToString()
{
return $"{Type}({Child})";
}
}
}

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

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder.Dialogs.Expressions
{
public class Variable : Expression
{
public Variable(string path, string name = null)
: base(ExpressionType.Variable)
{
Path = path;
Name = name;
}
public string Path { get; }
public string Name { get; }
public override string ToString()
{
return Name == null ? $"{{{Path}}}" : $"{Name}={{{Path}}}";
}
}
}

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

@ -17,7 +17,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
</ItemGroup>

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

@ -0,0 +1,16 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.Expressions.Tests
{
[TestClass]
public class ExpressionTree
{
[TestMethod]
public void ExprTree()
{
var t1 = ExpressionEngine.Parse("{a} > 3");
Console.WriteLine(t1);
}
}
}

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

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

@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="2.1.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="Moq" Version="4.10.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />

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

@ -82,7 +82,7 @@
<Version>15.9.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk">
<Version>15.9.0</Version>
<Version>15.8.0</Version>
</PackageReference>
<PackageReference Include="Moq">
<Version>4.10.0</Version>