mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET Workflows - Fix ability of ParseValue action to process list/table types. (#1577)
* Initial plan * Add test classes for extension methods Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Fix test issues and document bug in ExpandoObjectExtensions Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Address code review feedback - shorten Skip messages and add explanatory comments Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Replace Fields.ToDictionary with GetField calls and fix ExpandoObjectExtensions bug Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Update dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/DataValueExtensionsTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/DataValueExtensionsTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove unused using statement from DialogBaseExtensionsTests Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Add proper WrapWithBot tests using AdaptiveDialog and OnActivity Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Cleanup * Better * Better * One more test * Checkpoint * Checkpoint * Finally --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -94,6 +94,7 @@
|
||||
<File Path="../workflow-samples/HumanInLoop.yaml" />
|
||||
<File Path="../workflow-samples/Marketing.yaml" />
|
||||
<File Path="../workflow-samples/MathChat.yaml" />
|
||||
<File Path="../workflow-samples/ParseValue.yaml" />
|
||||
<File Path="../workflow-samples/README.md" />
|
||||
<File Path="../workflow-samples/wttr.json" />
|
||||
</Folder>
|
||||
|
||||
+16
@@ -219,6 +219,22 @@ internal static class FormulaValueExtensions
|
||||
elementType switch
|
||||
{
|
||||
null => FormulaValue.NewTable(RecordType.EmptySealed(), []),
|
||||
_ when elementType == typeof(string) =>
|
||||
FormulaValue.NewSingleColumnTable([.. value.OfType<string>().Select(element => FormulaValue.New(element))]),
|
||||
_ when elementType == typeof(bool) =>
|
||||
FormulaValue.NewSingleColumnTable([.. value.OfType<bool>().Select(element => FormulaValue.New(element))]),
|
||||
_ when elementType == typeof(int) =>
|
||||
FormulaValue.NewSingleColumnTable([.. value.OfType<int>().Select(element => FormulaValue.New(element))]),
|
||||
_ when elementType == typeof(long) =>
|
||||
FormulaValue.NewSingleColumnTable([.. value.OfType<long>().Select(element => FormulaValue.New(element))]),
|
||||
_ when elementType == typeof(decimal) =>
|
||||
FormulaValue.NewSingleColumnTable([.. value.OfType<decimal>().Select(element => FormulaValue.New(element))]),
|
||||
_ when elementType == typeof(float) =>
|
||||
FormulaValue.NewSingleColumnTable([.. value.OfType<float>().Select(element => FormulaValue.New(element))]),
|
||||
_ when elementType == typeof(DateTime) =>
|
||||
FormulaValue.NewSingleColumnTable([.. value.OfType<DateTime>().Select(element => FormulaValue.New(element))]),
|
||||
_ when elementType == typeof(TimeSpan) =>
|
||||
FormulaValue.NewSingleColumnTable([.. value.OfType<TimeSpan>().Select(element => FormulaValue.New(element))]),
|
||||
_ when elementType == typeof(ExpandoObject) =>
|
||||
FormulaValue.NewTable(
|
||||
value.ToTableType().ToRecord(),
|
||||
|
||||
+240
-62
@@ -1,99 +1,277 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class JsonDocumentExtensions
|
||||
{
|
||||
public static FrozenDictionary<string, object?> ParseRecord(this JsonDocument jsonDocument, VariableType recordType) => jsonDocument.RootElement.ParseRecord(recordType);
|
||||
|
||||
public static RecordValue ParseRecord(this JsonDocument jsonDocument, RecordDataType recordType) => jsonDocument.RootElement.ParseRecord(recordType);
|
||||
|
||||
private static FrozenDictionary<string, object?> ParseRecord(this JsonElement currentElement, VariableType recordType)
|
||||
public static List<object?> ParseList(this JsonDocument jsonDocument, VariableType targetType)
|
||||
{
|
||||
if (!recordType.IsRecord || recordType.Schema is null)
|
||||
return
|
||||
jsonDocument.RootElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Array => jsonDocument.RootElement.ParseTable(targetType),
|
||||
JsonValueKind.Object when targetType.HasSchema => [jsonDocument.RootElement.ParseRecord(targetType)],
|
||||
JsonValueKind.Null => [],
|
||||
_ => [jsonDocument.RootElement.ParseValue(targetType)],
|
||||
};
|
||||
}
|
||||
|
||||
public static Dictionary<string, object?> ParseRecord(this JsonDocument jsonDocument, VariableType targetType)
|
||||
{
|
||||
if (!targetType.IsRecord)
|
||||
{
|
||||
throw new DeclarativeActionException($"Unable to parse JSON element as {recordType.Type.Name}.");
|
||||
throw new DeclarativeActionException($"Unable to convert JSON to object with requested type {targetType.Type.Name}.");
|
||||
}
|
||||
|
||||
return ParseValues().ToFrozenDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
return
|
||||
jsonDocument.RootElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Array when targetType.HasSchema =>
|
||||
((Dictionary<string, object?>?)jsonDocument.RootElement.ParseTable(targetType).Single()) ?? [],
|
||||
JsonValueKind.Object => jsonDocument.RootElement.ParseRecord(targetType),
|
||||
JsonValueKind.Null => [],
|
||||
_ => throw new DeclarativeActionException($"Unable to convert JSON to object with requested type {targetType.Type.Name}."),
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> ParseRecord(this JsonElement currentElement, VariableType targetType)
|
||||
{
|
||||
if (targetType.Schema is null)
|
||||
{
|
||||
throw new DeclarativeActionException($"Object schema not defined for. {targetType.Type.Name}.");
|
||||
}
|
||||
|
||||
return ParseValues().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
IEnumerable<KeyValuePair<string, object?>> ParseValues()
|
||||
{
|
||||
foreach (KeyValuePair<string, VariableType?> property in recordType.Schema)
|
||||
foreach (KeyValuePair<string, VariableType> property in targetType.Schema)
|
||||
{
|
||||
JsonElement propertyElement = currentElement.GetProperty(property.Key);
|
||||
object? parsedValue =
|
||||
property.Value?.Type switch
|
||||
object? parsedValue = null;
|
||||
if (!currentElement.TryGetProperty(property.Key, out JsonElement propertyElement))
|
||||
{
|
||||
if (!property.Value.Type.IsNullable())
|
||||
{
|
||||
null => null,
|
||||
_ when property.Value.Type == typeof(string) => propertyElement.GetString(),
|
||||
_ when property.Value.Type == typeof(int) => propertyElement.GetInt32(),
|
||||
_ when property.Value.Type == typeof(long) => propertyElement.GetInt64(),
|
||||
_ when property.Value.Type == typeof(decimal) => propertyElement.GetDecimal(),
|
||||
_ when property.Value.Type == typeof(double) => propertyElement.GetDouble(),
|
||||
_ when property.Value.Type == typeof(bool) => propertyElement.GetBoolean(),
|
||||
_ when property.Value.Type == typeof(DateTime) => propertyElement.GetDateTime(),
|
||||
_ when property.Value.Type == typeof(TimeSpan) => propertyElement.GetDateTimeOffset().TimeOfDay,
|
||||
_ when property.Value.IsRecord => propertyElement.ParseRecord(property.Value),
|
||||
//TableDataType tableType => ParseTable(tableType, propertyElement),
|
||||
_ => throw new InvalidOperationException($"Unsupported data type '{property.Value.Type}' for property '{property.Key}'"),
|
||||
};
|
||||
throw new DeclarativeActionException($"Property '{property.Key}' undefined and not nullable.");
|
||||
}
|
||||
}
|
||||
else if (!propertyElement.TryParseValue(property.Value, out parsedValue))
|
||||
{
|
||||
throw new DeclarativeActionException($"Unsupported data type '{property.Value.Type}' for property '{property.Key}'");
|
||||
}
|
||||
|
||||
yield return new KeyValuePair<string, object?>(property.Key, parsedValue);
|
||||
}
|
||||
|
||||
//static TableValue ParseTable(TableDataType tableType, JsonElement propertyElement)
|
||||
//{
|
||||
// RecordDataType recordType = tableType.ToRecord();
|
||||
// return
|
||||
// FormulaValue.NewTable(
|
||||
// recordType.ToRecordType(),
|
||||
// propertyElement.EnumerateArray().Select(tableElement => tableElement.ParseRecord(recordType)));
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
private static RecordValue ParseRecord(this JsonElement currentElement, RecordDataType recordType)
|
||||
private static List<object?> ParseTable(this JsonElement currentElement, VariableType targetType)
|
||||
{
|
||||
return FormulaValue.NewRecordFromFields(ParseValues());
|
||||
|
||||
IEnumerable<NamedValue> ParseValues()
|
||||
if (!targetType.IsList)
|
||||
{
|
||||
foreach (KeyValuePair<string, PropertyInfo> property in recordType.Properties)
|
||||
throw new DeclarativeActionException($"Unable to convert JSON to list as requested type {targetType.Type.Name}.");
|
||||
}
|
||||
|
||||
VariableType listType = DetermineElementType();
|
||||
|
||||
return
|
||||
currentElement
|
||||
.EnumerateArray()
|
||||
.Select(element => element.ParseValue(listType))
|
||||
.ToList();
|
||||
|
||||
VariableType DetermineElementType()
|
||||
{
|
||||
Type? targetElementType = targetType.Type.GetElementType();
|
||||
VariableType? elementType = targetElementType is not null ? new(targetElementType) : null;
|
||||
if (elementType is null)
|
||||
{
|
||||
JsonElement propertyElement = currentElement.GetProperty(property.Key);
|
||||
FormulaValue? parsedValue =
|
||||
property.Value.Type switch
|
||||
foreach (JsonElement element in currentElement.EnumerateArray())
|
||||
{
|
||||
VariableType? currentType =
|
||||
element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => VariableType.Record(targetType.Schema?.Select(kvp => (kvp.Key, kvp.Value)) ?? []),
|
||||
JsonValueKind.String => typeof(string),
|
||||
JsonValueKind.True => typeof(bool),
|
||||
JsonValueKind.False => typeof(bool),
|
||||
JsonValueKind.Number => typeof(decimal),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (elementType is not null && currentType is not null && !elementType.Equals(currentType))
|
||||
{
|
||||
StringDataType => FormulaValue.New(propertyElement.GetString()),
|
||||
NumberDataType => FormulaValue.New(propertyElement.GetDecimal()),
|
||||
BooleanDataType => FormulaValue.New(propertyElement.GetBoolean()),
|
||||
DateTimeDataType => FormulaValue.New(propertyElement.GetDateTime()),
|
||||
DateDataType => FormulaValue.New(propertyElement.GetDateTime()),
|
||||
TimeDataType => FormulaValue.New(propertyElement.GetDateTimeOffset().TimeOfDay),
|
||||
RecordDataType recordType => propertyElement.ParseRecord(recordType),
|
||||
TableDataType tableType => ParseTable(tableType, propertyElement),
|
||||
_ => throw new InvalidOperationException($"Unsupported data type '{property.Value.Type}' for property '{property.Key}'"),
|
||||
};
|
||||
yield return new NamedValue(property.Key, parsedValue);
|
||||
throw new DeclarativeActionException("Inconsistent element types in list.");
|
||||
}
|
||||
|
||||
elementType ??= currentType;
|
||||
}
|
||||
}
|
||||
|
||||
static TableValue ParseTable(TableDataType tableType, JsonElement propertyElement)
|
||||
return
|
||||
elementType ??
|
||||
throw new DeclarativeActionException("Unable to determine element type for list.");
|
||||
}
|
||||
}
|
||||
|
||||
private static object? ParseValue(this JsonElement propertyElement, VariableType targetType)
|
||||
{
|
||||
if (!propertyElement.TryParseValue(targetType, out object? value))
|
||||
{
|
||||
throw new DeclarativeActionException($"Unable to parse {propertyElement.ValueKind} as '{targetType.Type.Name}'");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static bool TryParseValue(this JsonElement propertyElement, VariableType targetType, out object? value) =>
|
||||
propertyElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => TryParseString(propertyElement, targetType.Type, out value),
|
||||
JsonValueKind.Number => TryParseNumber(propertyElement, targetType.Type, out value),
|
||||
JsonValueKind.True or JsonValueKind.False => TryParseBoolean(propertyElement, out value),
|
||||
JsonValueKind.Object => TryParseObject(propertyElement, targetType, out value),
|
||||
JsonValueKind.Array => TryParseList(propertyElement, targetType, out value),
|
||||
JsonValueKind.Null => TryParseNull(targetType.Type, out value),
|
||||
_ => throw new DeclarativeActionException($"JSON element of type {propertyElement.ValueKind} is not supported."),
|
||||
};
|
||||
|
||||
private static bool TryParseNull(Type valueType, out object? value)
|
||||
{
|
||||
// If the target type is not nullable, we cannot assign null to it
|
||||
if (!valueType.IsNullable())
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseBoolean(JsonElement propertyElement, out object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
value = propertyElement.GetBoolean();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseString(JsonElement propertyElement, Type valueType, out object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? propertyValue = propertyElement.GetString();
|
||||
if (propertyValue is null)
|
||||
{
|
||||
RecordDataType recordType = tableType.ToRecord();
|
||||
return
|
||||
FormulaValue.NewTable(
|
||||
recordType.ToRecordType(),
|
||||
propertyElement.EnumerateArray().Select(tableElement => tableElement.ParseRecord(recordType)));
|
||||
value = null;
|
||||
return valueType.IsNullable(); // Parse fails if value is null and requested type is not.
|
||||
}
|
||||
|
||||
switch (valueType)
|
||||
{
|
||||
case Type targetType when targetType == typeof(string):
|
||||
value = propertyValue;
|
||||
break;
|
||||
case Type targetType when targetType == typeof(DateTime):
|
||||
value = DateTime.Parse(propertyValue, provider: null, styles: DateTimeStyles.RoundtripKind);
|
||||
break;
|
||||
case Type targetType when targetType == typeof(TimeSpan):
|
||||
value = TimeSpan.Parse(propertyValue);
|
||||
break;
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNumber(JsonElement element, Type valueType, out object? value)
|
||||
{
|
||||
// Try parsing as integer types first (most precise representation)
|
||||
if (element.TryGetInt32(out int intValue))
|
||||
{
|
||||
return ConvertToExpectedType(valueType, intValue, out value);
|
||||
}
|
||||
|
||||
if (element.TryGetInt64(out long longValue))
|
||||
{
|
||||
return ConvertToExpectedType(valueType, longValue, out value);
|
||||
}
|
||||
|
||||
// Try decimal for precise decimal values
|
||||
if (element.TryGetDecimal(out decimal decimalValue))
|
||||
{
|
||||
return ConvertToExpectedType(valueType, decimalValue, out value);
|
||||
}
|
||||
|
||||
// Fall back to double for other numeric values
|
||||
if (element.TryGetDouble(out double doubleValue))
|
||||
{
|
||||
return ConvertToExpectedType(valueType, doubleValue, out value);
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
|
||||
static bool ConvertToExpectedType(Type valueType, object sourceValue, out object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
value = Convert.ChangeType(sourceValue, valueType);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseObject(JsonElement propertyElement, VariableType targetType, out object? value)
|
||||
{
|
||||
if (!targetType.HasSchema)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = propertyElement.ParseRecord(targetType);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseList(JsonElement propertyElement, VariableType targetType, out object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
value = ParseTable(propertyElement, targetType);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-2
@@ -104,7 +104,17 @@ internal static class ObjectExtensions
|
||||
throw new DeclarativeActionException($"Unsupported type: '{targetType.Type.Name}'.");
|
||||
}
|
||||
|
||||
if (sourceValue != null && targetType.Type.IsAssignableFrom(sourceValue.GetType()))
|
||||
if (sourceValue is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Type sourceType = sourceValue.GetType();
|
||||
|
||||
// Converting string to list requires explicit conversion.
|
||||
// Avoid short-circuit based on string is IEnumerable<char>
|
||||
if ((sourceType != typeof(string) || !targetType.IsList) &&
|
||||
targetType.Type.IsAssignableFrom(sourceType))
|
||||
{
|
||||
return sourceValue;
|
||||
}
|
||||
@@ -226,7 +236,7 @@ internal static class ObjectExtensions
|
||||
sourceValue switch
|
||||
{
|
||||
null => null,
|
||||
//string jsonText => JsonDocument.Parse(jsonText.TrimJsonDelimiter()).ParseRecord(targetType),
|
||||
string jsonText => JsonDocument.Parse(jsonText.TrimJsonDelimiter()).ParseList(targetType),
|
||||
_ => throw new DeclarativeActionException($"Cannot convert '{sourceValue?.GetType().Name}' to 'Record' (expected JSON string)."),
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class TypeExtensions
|
||||
{
|
||||
public static bool IsNullable(this Type type)
|
||||
{
|
||||
if (!type.IsValueType)
|
||||
{
|
||||
return true; // Reference types are nullable
|
||||
}
|
||||
|
||||
return Nullable.GetUnderlyingType(type) != null;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
@@ -13,10 +14,11 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
/// Describes an allowed declarative variable/type used in workflow configuration (primitives, lists, or record-like objects).
|
||||
/// A record is modeled as IDictionary<string, VariableType?> along with an immutable schema for its fields.
|
||||
/// </summary>
|
||||
public sealed class VariableType
|
||||
public sealed class VariableType : IEquatable<VariableType>
|
||||
{
|
||||
// Canonical CLR type used to mark a "record" (object with named fields and per-field types).
|
||||
internal static readonly Type RecordType = typeof(IDictionary<string, object?>);
|
||||
|
||||
// Any list of primitive values or records.
|
||||
internal static readonly Type ListType = typeof(IEnumerable);
|
||||
|
||||
@@ -50,13 +52,26 @@ public sealed class VariableType
|
||||
/// <summary>
|
||||
/// Returns true if the provided CLR <paramref name="type"/> is one of the supported root types.
|
||||
/// </summary>
|
||||
public static bool IsValid(Type type) => s_supportedTypes.Contains(type);
|
||||
public static bool IsValid(Type type) =>
|
||||
s_supportedTypes.Contains(type) ||
|
||||
ListType.IsAssignableFrom(type) ||
|
||||
RecordType.IsAssignableFrom(type);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a list (object) variable type with the supplied <paramref name="fields"/> schema.
|
||||
/// Each tuple's Key is the field name; Type is the declared VariableType (nullable to allow "unknown"/late binding).
|
||||
/// </summary>
|
||||
public static VariableType List(params IEnumerable<(string Key, VariableType Type)> fields) =>
|
||||
new(typeof(IEnumerable))
|
||||
{
|
||||
Schema = fields.ToFrozenDictionary(kv => kv.Key, kv => kv.Type),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a record (object) variable type with the supplied <paramref name="fields"/> schema.
|
||||
/// Each tuple's Key is the field name; Type is the declared VariableType (nullable to allow "unknown"/late binding).
|
||||
/// </summary>
|
||||
public static VariableType Record(params IEnumerable<(string Key, VariableType? Type)> fields) =>
|
||||
public static VariableType Record(params IEnumerable<(string Key, VariableType Type)> fields) =>
|
||||
new(typeof(IDictionary<string, object?>))
|
||||
{
|
||||
Schema = fields.ToFrozenDictionary(kv => kv.Key, kv => kv.Type),
|
||||
@@ -78,9 +93,9 @@ public sealed class VariableType
|
||||
this.Schema = CreateSchema(tableDataType.Properties);
|
||||
}
|
||||
|
||||
static FrozenDictionary<string, VariableType?> CreateSchema(IEnumerable<KeyValuePair<string, PropertyInfo>> properties)
|
||||
static FrozenDictionary<string, VariableType> CreateSchema(IEnumerable<KeyValuePair<string, PropertyInfo>> properties)
|
||||
{
|
||||
Dictionary<string, VariableType?> schema = [];
|
||||
Dictionary<string, VariableType> schema = [];
|
||||
|
||||
foreach (KeyValuePair<string, PropertyInfo> field in properties)
|
||||
{
|
||||
@@ -104,7 +119,7 @@ public sealed class VariableType
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The underlying CLR type that categorizes this variable (primitive, list, or record sentinel type).
|
||||
/// The underlying CLR type that categorizes this variable (primitive, list, or record type).
|
||||
/// </summary>
|
||||
public Type Type { get; }
|
||||
|
||||
@@ -112,15 +127,20 @@ public sealed class VariableType
|
||||
/// Schema for record types: immutable mapping of field name to field VariableType (null means unspecified).
|
||||
/// Null for non-record VariableTypes.
|
||||
/// </summary>
|
||||
public FrozenDictionary<string, VariableType?>? Schema { get; init; }
|
||||
public FrozenDictionary<string, VariableType>? Schema { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// True if this instance represents a record/object with a field schema.
|
||||
/// </summary>
|
||||
public bool IsList => ListType.IsAssignableFrom(this.Type);
|
||||
public bool HasSchema => (this.Schema?.Count ?? 0) > 0;
|
||||
|
||||
/// <summary>
|
||||
/// True if this instance represents a record/object with a field schema.
|
||||
/// True if this instance represents a list
|
||||
/// </summary>
|
||||
public bool IsList => !this.IsRecord && ListType.IsAssignableFrom(this.Type);
|
||||
|
||||
/// <summary>
|
||||
/// True if this instance represents a record/object
|
||||
/// </summary>
|
||||
public bool IsRecord => RecordType.IsAssignableFrom(this.Type);
|
||||
|
||||
@@ -128,4 +148,28 @@ public sealed class VariableType
|
||||
/// Instance convenience wrapper for <see cref="IsValid(Type)"/> on this VariableType's underlying CLR type.
|
||||
/// </summary>
|
||||
public bool IsValid() => IsValid(this.Type);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
obj switch
|
||||
{
|
||||
null => false,
|
||||
Type type => this.Type == type,
|
||||
VariableType other => this.Equals(other),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => HashCode.Combine(this.Type.GetHashCode(), this.Schema?.GetHashCode() ?? 0);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(VariableType? other) =>
|
||||
other is not null &&
|
||||
this.Type == other.Type &&
|
||||
this.Schema switch
|
||||
{
|
||||
null => other.Schema is null,
|
||||
_ when other.Schema is null => false,
|
||||
_ => this.Schema.Count == other.Schema.Count && this.Schema.Union(other.Schema).Count() == this.Schema.Count,
|
||||
};
|
||||
}
|
||||
|
||||
+2
@@ -150,12 +150,14 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
[InlineData("EditTable.yaml", 2, "edit_var")]
|
||||
[InlineData("EditTableV2.yaml", 2, "edit_var")]
|
||||
[InlineData("ParseValue.yaml", 2, "parse_var")]
|
||||
[InlineData("ParseValueList.yaml", 2, "parse_var")]
|
||||
[InlineData("SendActivity.yaml", 2, "activity_input")]
|
||||
[InlineData("SetVariable.yaml", 1, "set_var")]
|
||||
[InlineData("SetTextVariable.yaml", 1, "set_text")]
|
||||
[InlineData("ClearAllVariables.yaml", 1, "clear_all")]
|
||||
[InlineData("ResetVariable.yaml", 2, "clear_var")]
|
||||
[InlineData("MixedScopes.yaml", 2, "activity_input")]
|
||||
[InlineData("CaseInsensitive.yaml", 6, "end_when_match")]
|
||||
public async Task ExecuteActionAsync(string workflowFile, int expectedCount, string expectedId)
|
||||
{
|
||||
await this.RunWorkflowAsync(workflowFile);
|
||||
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
|
||||
|
||||
public sealed class JsonDocumentExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParseRecord_Object_PrimitiveFields_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
VariableType recordType =
|
||||
VariableType.Record(
|
||||
[
|
||||
("text", typeof(string)),
|
||||
("numberInt", typeof(int)),
|
||||
("numberLong", typeof(long)),
|
||||
("numberDecimal", typeof(decimal)),
|
||||
("numberDouble", typeof(double)),
|
||||
("flag", typeof(bool)),
|
||||
("date", typeof(DateTime)),
|
||||
("time", typeof(TimeSpan))
|
||||
]);
|
||||
|
||||
DateTime expectedDateTime = new(2024, 10, 01, 12, 34, 56, DateTimeKind.Utc);
|
||||
TimeSpan expectedTimeSpan = new(12, 34, 56);
|
||||
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
{
|
||||
"text": "hello",
|
||||
"numberInt": 7,
|
||||
"numberLong": 9223372036854775807,
|
||||
"numberDecimal": 12.5,
|
||||
"numberDouble": 3.99E99,
|
||||
"flag": true,
|
||||
"date": "2024-10-01T12:34:56Z",
|
||||
"time": "12:34:56"
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
Dictionary<string, object?> result = document.ParseRecord(recordType);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("hello", result["text"]);
|
||||
Assert.Equal(7, result["numberInt"]);
|
||||
Assert.Equal(9223372036854775807L, result["numberLong"]);
|
||||
Assert.Equal(12.5m, result["numberDecimal"]);
|
||||
Assert.Equal(3.99E99, result["numberDouble"]);
|
||||
Assert.Equal(true, result["flag"]);
|
||||
Assert.Equal(expectedDateTime, result["date"]);
|
||||
Assert.Equal(expectedTimeSpan, result["time"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRecord_Object_NestedRecord_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
VariableType innerRecord =
|
||||
VariableType.Record(
|
||||
[
|
||||
("innerText", typeof(string)),
|
||||
("innerNumber", typeof(int))
|
||||
]);
|
||||
|
||||
VariableType outerRecord =
|
||||
VariableType.Record(
|
||||
[
|
||||
("outerText", typeof(string)),
|
||||
("nested", innerRecord)
|
||||
]);
|
||||
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
{
|
||||
"outerText": "outer",
|
||||
"nested": {
|
||||
"innerText": "inner",
|
||||
"innerNumber": 42
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
Dictionary<string, object?> result = document.ParseRecord(outerRecord);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("outer", result["outerText"]);
|
||||
Dictionary<string, object?> nested = (Dictionary<string, object?>)result["nested"]!;
|
||||
Assert.NotNull(nested);
|
||||
Assert.True(nested.ContainsKey("innerText"));
|
||||
Assert.Equal("inner", nested["innerText"]);
|
||||
Assert.Equal(42, nested["innerNumber"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRecord_NullRoot_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
VariableType recordType =
|
||||
VariableType.Record(
|
||||
[
|
||||
("text", typeof(string))
|
||||
]);
|
||||
|
||||
JsonDocument document = JsonDocument.Parse("null");
|
||||
|
||||
// Act
|
||||
Dictionary<string, object?> result = document.ParseRecord(recordType);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRecord_ArrayWithSingleRecord_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
VariableType listType =
|
||||
VariableType.List(
|
||||
[
|
||||
("name", typeof(string)),
|
||||
("value", typeof(int))
|
||||
]);
|
||||
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[
|
||||
{
|
||||
"name": "item",
|
||||
"value": 5
|
||||
}
|
||||
]
|
||||
""");
|
||||
|
||||
// Act
|
||||
List<object?> result = document.ParseList(listType);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Dictionary<string, object?> element = Assert.IsType<Dictionary<string, object?>>(result[0]);
|
||||
Assert.Equal("item", element["name"]);
|
||||
Assert.Equal(5, element["value"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRecord_ArrayWithMultipleRecords_Throws()
|
||||
{
|
||||
// Arrange
|
||||
VariableType recordType =
|
||||
VariableType.Record(
|
||||
[
|
||||
("id", typeof(int))
|
||||
]);
|
||||
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[
|
||||
{ "id": 1 },
|
||||
{ "id": 2 }
|
||||
]
|
||||
""");
|
||||
|
||||
// Act / Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(recordType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRecord_InvalidTargetType_Throws()
|
||||
{
|
||||
// Arrange
|
||||
VariableType notARecord = typeof(string);
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
{ "x": 1 }
|
||||
""");
|
||||
|
||||
// Act / Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(notARecord));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRecord_InvalidRootKind_Throws()
|
||||
{
|
||||
// Arrange
|
||||
VariableType recordType =
|
||||
VariableType.Record(
|
||||
[
|
||||
("text", typeof(string))
|
||||
]);
|
||||
|
||||
JsonDocument document = JsonDocument.Parse(@"""not-an-object""");
|
||||
|
||||
// Act / Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(recordType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRecord_UnsupportedPropertyType_Throws()
|
||||
{
|
||||
// Arrange
|
||||
VariableType recordType =
|
||||
VariableType.Record(
|
||||
[
|
||||
("unsupported", typeof(Guid))
|
||||
]);
|
||||
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
{ "unsupported": "C2556C11-210E-4BB6-BF18-4A8968CB45A8" }
|
||||
""");
|
||||
|
||||
// Act / Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(recordType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRecord_MissingRequiredProperty_Throws()
|
||||
{
|
||||
// Arrange
|
||||
VariableType recordType =
|
||||
VariableType.Record(
|
||||
[
|
||||
("required", typeof(bool))
|
||||
]);
|
||||
|
||||
JsonDocument document = JsonDocument.Parse("{}");
|
||||
|
||||
// Act / Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(recordType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRecord_MissingNullableProperty_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
VariableType recordType =
|
||||
VariableType.Record(
|
||||
[
|
||||
("required", typeof(string))
|
||||
]);
|
||||
|
||||
JsonDocument document = JsonDocument.Parse("{}");
|
||||
|
||||
// Act
|
||||
Dictionary<string, object?> result = document.ParseRecord(recordType);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Dictionary<string, object?> element = Assert.IsType<Dictionary<string, object?>>(result);
|
||||
Assert.Null(element["required"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_NullRoot_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse("null");
|
||||
|
||||
// Act
|
||||
List<object?> result = document.ParseList(typeof(int[]));
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_Array_Primitives_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse("[1,2,3]");
|
||||
|
||||
// Act
|
||||
List<object?> result = document.ParseList(typeof(int[]));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Equal(1, result[0]);
|
||||
Assert.Equal(2, result[1]);
|
||||
Assert.Equal(3, result[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_PrimitiveRoot_WrappedAsSingleElement_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse("7");
|
||||
|
||||
// Act
|
||||
List<object?> result = document.ParseList(typeof(int));
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(7, result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_Array_Records_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
VariableType listType =
|
||||
VariableType.List(
|
||||
[
|
||||
("id", typeof(int)),
|
||||
("name", typeof(string))
|
||||
]);
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[
|
||||
{ "id": 1, "name": "a" },
|
||||
{ "id": 2, "name": "b" }
|
||||
]
|
||||
""");
|
||||
|
||||
// Act
|
||||
List<object?> result = document.ParseList(listType);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Dictionary<string, object?> first = (Dictionary<string, object?>)result[0]!;
|
||||
Dictionary<string, object?> second = (Dictionary<string, object?>)result[1]!;
|
||||
Assert.NotNull(first);
|
||||
Assert.Equal(1, first["id"]);
|
||||
Assert.Equal("a", first["name"]);
|
||||
Assert.NotNull(second);
|
||||
Assert.Equal(2, second["id"]);
|
||||
Assert.Equal("b", second["name"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_InvalidTargetType_Throws()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse("[1,2]");
|
||||
|
||||
// Act / Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => document.ParseList(typeof(int)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_Array_MixedTypes_Throws()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse("[1,\"two\",3]");
|
||||
|
||||
// Act / Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => document.ParseList(typeof(int[])));
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
|
||||
|
||||
public sealed class TypeExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void ReferenceType() => VerifyIsNullable(typeof(string));
|
||||
|
||||
[Fact]
|
||||
public void ClassType() => VerifyIsNullable(typeof(object));
|
||||
|
||||
[Fact]
|
||||
public void InterfaceType() => VerifyIsNullable(typeof(IDisposable));
|
||||
|
||||
[Fact]
|
||||
public void ArrayType() => VerifyIsNullable(typeof(int[]));
|
||||
|
||||
[Fact]
|
||||
public void NonNullableValueType() => VerifyNotNullable(typeof(int));
|
||||
|
||||
[Fact]
|
||||
public void NonNullableStructType() => VerifyNotNullable(typeof(DateTime));
|
||||
|
||||
[Fact]
|
||||
public void NonNullableEnumType() => VerifyNotNullable(typeof(DayOfWeek));
|
||||
|
||||
[Fact]
|
||||
public void NullableInt() => VerifyIsNullable(typeof(int?));
|
||||
|
||||
[Fact]
|
||||
public void NullableDateTime() => VerifyIsNullable(typeof(DateTime?));
|
||||
|
||||
[Fact]
|
||||
public void NullableEnum() => VerifyIsNullable(typeof(DayOfWeek?));
|
||||
|
||||
[Fact]
|
||||
public void NullableCustomStruct() => VerifyIsNullable(typeof(TestStruct?));
|
||||
|
||||
private static void VerifyNotNullable(Type targetType)
|
||||
{
|
||||
// Act
|
||||
bool result = targetType.IsNullable();
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
private static void VerifyIsNullable(Type targetType)
|
||||
{
|
||||
// Act
|
||||
bool result = targetType.IsNullable();
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
private struct TestStruct
|
||||
{
|
||||
public int Value { get; set; }
|
||||
}
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Kit;
|
||||
|
||||
public sealed class VariableTypeTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsValidPrimitivesReturnTrue()
|
||||
{
|
||||
Assert.True(VariableType.IsValid<bool>());
|
||||
Assert.True(VariableType.IsValid<int>());
|
||||
Assert.True(VariableType.IsValid<long>());
|
||||
Assert.True(VariableType.IsValid<float>());
|
||||
Assert.True(VariableType.IsValid<decimal>());
|
||||
Assert.True(VariableType.IsValid<double>());
|
||||
Assert.True(VariableType.IsValid<string>());
|
||||
Assert.True(VariableType.IsValid<DateTime>());
|
||||
Assert.True(VariableType.IsValid<TimeSpan>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidUnsupportedTypeReturnFalse()
|
||||
{
|
||||
Assert.False(VariableType.IsValid<Guid>());
|
||||
Assert.False(VariableType.IsValid<Uri>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsListForListTypeReturnTrue()
|
||||
{
|
||||
VariableType listType = new(typeof(List<int>));
|
||||
Assert.True(listType.IsList);
|
||||
Assert.False(listType.IsRecord);
|
||||
Assert.True(listType.IsValid());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordForDictionaryInterfaceReturnTrue()
|
||||
{
|
||||
VariableType recordType = new(typeof(IDictionary<string, object?>));
|
||||
Assert.True(recordType.IsRecord);
|
||||
Assert.False(recordType.IsList);
|
||||
Assert.True(recordType.IsValid());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordFactoryCreatesSchema()
|
||||
{
|
||||
// Assuming the intended signature supports tuple params; adjust if needed.
|
||||
VariableType nameType = new(typeof(string));
|
||||
VariableType ageType = new(typeof(int));
|
||||
|
||||
// If the actual signature differs (params IEnumerable<...>), adapt test accordingly.
|
||||
VariableType recordType = VariableType.Record(
|
||||
[("name", nameType), ("age", ageType)]
|
||||
);
|
||||
|
||||
Assert.True(recordType.IsRecord);
|
||||
Assert.True(recordType.HasSchema);
|
||||
Assert.NotNull(recordType.Schema);
|
||||
Assert.Equal(2, recordType.Schema.Count);
|
||||
Assert.True(recordType.Schema.ContainsKey("name"));
|
||||
Assert.True(recordType.Schema.ContainsKey("age"));
|
||||
Assert.Equal(typeof(string), recordType.Schema["name"].Type);
|
||||
Assert.Equal(typeof(int), recordType.Schema["age"].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualsPrimitiveTypeEquality()
|
||||
{
|
||||
VariableType t1 = new(typeof(int));
|
||||
VariableType t2 = new(typeof(int));
|
||||
VariableType t3 = new(typeof(string));
|
||||
|
||||
Assert.True(t1.Equals(t2));
|
||||
Assert.True(t1.Equals(typeof(int)));
|
||||
Assert.False(t1.Equals(t3));
|
||||
Assert.False(t1.Equals(typeof(string)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualsRecordEqualityIgnoresOrder()
|
||||
{
|
||||
VariableType strType = new(typeof(string));
|
||||
VariableType intType = new(typeof(int));
|
||||
|
||||
VariableType recordA = VariableType.Record(
|
||||
[("first", strType), ("second", intType)]
|
||||
);
|
||||
VariableType recordB = VariableType.Record(
|
||||
[("second", intType), ("first", strType)]
|
||||
);
|
||||
|
||||
Assert.True(recordA.Equals(recordB));
|
||||
Assert.True(recordB.Equals(recordA));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualsRecordInequalityDifferentSchema()
|
||||
{
|
||||
VariableType strType = new(typeof(string));
|
||||
VariableType intType = new(typeof(int));
|
||||
|
||||
VariableType recordA = VariableType.Record(
|
||||
[("first", strType), ("second", intType)]
|
||||
);
|
||||
VariableType recordB = VariableType.Record(
|
||||
[("first", strType)]
|
||||
);
|
||||
|
||||
Assert.False(recordA.Equals(recordB));
|
||||
Assert.False(recordB.Equals(recordA));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCodePrimitiveConsistency()
|
||||
{
|
||||
VariableType a = new(typeof(double));
|
||||
VariableType b = new(typeof(double));
|
||||
Assert.Equal(a, b);
|
||||
Assert.Equal(a, typeof(double));
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCodeRecordConsistency()
|
||||
{
|
||||
VariableType a = VariableType.Record(("a", typeof(string)), ("b", typeof(int)));
|
||||
VariableType b = VariableType.Record(("a", typeof(string)), ("b", typeof(int)));
|
||||
Assert.Equal(a, b);
|
||||
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasSchemaFalseForNonRecord()
|
||||
{
|
||||
VariableType primitive = new(typeof(int));
|
||||
Assert.False(primitive.HasSchema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitOperatorFromTypeWrapsCorrectly()
|
||||
{
|
||||
VariableType vt = typeof(string);
|
||||
Assert.Equal(typeof(string), vt.Type);
|
||||
Assert.True(vt.IsValid());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualsNullAndDifferentTypes()
|
||||
{
|
||||
VariableType vt = new(typeof(int));
|
||||
VariableType? nullType = null;
|
||||
object? nullObj = null;
|
||||
object different = "test";
|
||||
|
||||
Assert.False(vt.Equals(nullObj));
|
||||
Assert.False(vt.Equals(nullType));
|
||||
Assert.False(vt.Equals(different));
|
||||
Assert.True(vt.Equals((object)typeof(int)));
|
||||
}
|
||||
}
|
||||
+30
-3
@@ -13,6 +13,33 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
/// </summary>
|
||||
public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task ParseRecordAsync()
|
||||
{
|
||||
// Arrange
|
||||
RecordDataType.Builder recordBuilder =
|
||||
new()
|
||||
{
|
||||
Properties =
|
||||
{
|
||||
{"key1", new PropertyInfo.Builder() { Type = DataType.String } },
|
||||
}
|
||||
};
|
||||
ParseValue model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(ParseRecordAsync)),
|
||||
recordBuilder,
|
||||
@"{ ""key1"": ""val1"" }");
|
||||
|
||||
// Act
|
||||
ParseValueExecutor action = new(model, this.State);
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
this.VerifyState("Target", FormulaValue.NewRecordFromFields(new NamedValue("key1", FormulaValue.New("val1"))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseTableAsync()
|
||||
{
|
||||
@@ -28,8 +55,8 @@ public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowA
|
||||
ParseValue model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(ParseTableAsync)),
|
||||
recordBuilder,
|
||||
@"{ ""key1"": ""val1"" }");
|
||||
DataType.EmptyTable,
|
||||
@"[""apple"",""banana"",""cat""]");
|
||||
|
||||
// Act
|
||||
ParseValueExecutor action = new(model, this.State);
|
||||
@@ -37,7 +64,7 @@ public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowA
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
this.VerifyState("Target", FormulaValue.NewRecordFromFields(new NamedValue("key1", FormulaValue.New("val1"))));
|
||||
this.VerifyState("Target", FormulaValue.NewSingleColumnTable(FormulaValue.New("apple"), FormulaValue.New("banana"), FormulaValue.New("cat")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+13
@@ -10,6 +10,7 @@ using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
@@ -33,6 +34,18 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor
|
||||
WorkflowEvent[] events = await run.WatchStreamAsync().ToArrayAsync();
|
||||
Assert.Contains(events, e => e is DeclarativeActionInvokedEvent);
|
||||
Assert.Contains(events, e => e is DeclarativeActionCompletedEvent);
|
||||
ExecutorFailedEvent[] failureEvents = events.OfType<ExecutorFailedEvent>().ToArray();
|
||||
switch (failureEvents.Length)
|
||||
{
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
throw failureEvents[0].Data ?? new XunitException("Executor failed without exception data.");
|
||||
default:
|
||||
AggregateException aggregateException = new("One or more executor failures occurred.", failureEvents.Select(e => e.Data).Where(e => e is not null).Cast<Exception>());
|
||||
throw aggregateException;
|
||||
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
kind: WORKFLOW
|
||||
trigger:
|
||||
|
||||
kind: onconversationstart
|
||||
id: my_workflow
|
||||
actions:
|
||||
|
||||
- kind: SETVARIABLE
|
||||
id: set_input1
|
||||
variable: Local.TestValue1
|
||||
value: =3
|
||||
|
||||
- kind: setvariable
|
||||
id: set_input2
|
||||
variable: Local.TestValue2
|
||||
value: =4
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: condition_test
|
||||
conditions:
|
||||
- id: condition_match
|
||||
condition: =Local.TestValue1 + Local.TestValue2 = 7
|
||||
actions:
|
||||
- kind: EndDialog
|
||||
id: end_when_match
|
||||
|
||||
- kind: SendActivity
|
||||
id: activity_error
|
||||
activity: Unexpected
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: my_workflow
|
||||
|
||||
actions:
|
||||
- kind: SetVariable
|
||||
id: set_var
|
||||
variable: Local.MySource
|
||||
value: '["apple","banana","cat"]'
|
||||
|
||||
- kind: ParseValue
|
||||
id: parse_var
|
||||
variable: Local.MyVar
|
||||
value: =Local.MySource
|
||||
valueType: Table
|
||||
@@ -0,0 +1,39 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
kind: OnConversationStart
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
- kind: SetVariable
|
||||
id: set-variable-text
|
||||
variable: Local.ItemsText
|
||||
value: '["apple","banana","cat"]'
|
||||
|
||||
|
||||
- kind: SendActivity
|
||||
id: display-text
|
||||
activity: |-
|
||||
(1)
|
||||
{Local.ItemsText}
|
||||
|
||||
- kind: ParseValue
|
||||
id: parse-list
|
||||
variable: Local.ItemsList
|
||||
value: =Local.ItemsText
|
||||
valueType: Table
|
||||
|
||||
- kind: SendActivity
|
||||
id: display-list
|
||||
activity: |-
|
||||
(2)
|
||||
{Local.ItemsList}
|
||||
|
||||
- kind: Foreach
|
||||
id: loop-list
|
||||
items: =Local.ItemsList
|
||||
value: Local.LoopValue
|
||||
index: Local.LoopIndex
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
id: display-list-item
|
||||
activity: "{Local.LoopValue} (x{Local.LoopIndex + 1})"
|
||||
Reference in New Issue
Block a user