// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Dynamic; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; using Microsoft.PowerFx.Types; using BlankType = Microsoft.PowerFx.Types.BlankType; namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions; internal static class FormulaValueExtensions { private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true }; public static FormulaValue NewBlank(this FormulaType? type) => FormulaValue.NewBlank(type ?? FormulaType.Blank); public static FormulaValue ToFormula(this object? value) => value switch { null => FormulaValue.NewBlank(), UnassignedValue => FormulaValue.NewBlank(), FormulaValue formulaValue => formulaValue, bool booleanValue => FormulaValue.New(booleanValue), int decimalValue => FormulaValue.New(decimalValue), long decimalValue => FormulaValue.New(decimalValue), float decimalValue => FormulaValue.New(decimalValue), decimal decimalValue => FormulaValue.New(decimalValue), double numberValue => FormulaValue.New(numberValue), string stringValue => FormulaValue.New(stringValue), DateTime dateonlyValue when dateonlyValue.TimeOfDay == TimeSpan.Zero => FormulaValue.NewDateOnly(dateonlyValue), DateTime datetimeValue => FormulaValue.New(datetimeValue), TimeSpan timeValue => FormulaValue.New(timeValue), ChatMessage chatMessage => chatMessage.ToRecord(), ExpandoObject expandoValue => expandoValue.ToRecord(), object when value is IDictionary dictionaryValue => dictionaryValue.ToRecord(), object when value is IEnumerable tableValue => tableValue.ToTable(), _ => throw new DeclarativeModelException($"Unsupported variable type: {value.GetType().Name}"), }; public static FormulaType GetFormulaType(this object? value) => value switch { null => FormulaType.Blank, bool => FormulaType.Boolean, int => FormulaType.Decimal, long => FormulaType.Decimal, float => FormulaType.Decimal, decimal => FormulaType.Decimal, double => FormulaType.Number, string => FormulaType.String, DateTime => FormulaType.DateTime, TimeSpan => FormulaType.Time, object when value is IEnumerable tableValue => tableValue.ToTableType(), ExpandoObject expandoValue => expandoValue.ToRecordType(), _ => FormulaType.Unknown, }; public static DataValue ToDataValue(this FormulaValue value) => value switch { BooleanValue booleanValue => BooleanDataValue.Create(booleanValue.Value), DecimalValue decimalValue => NumberDataValue.Create(decimalValue.Value), NumberValue numberValue => FloatDataValue.Create(numberValue.Value), DateValue dateValue => DateDataValue.Create(dateValue.GetConvertedValue(TimeZoneInfo.Utc)), DateTimeValue datetimeValue => DateTimeDataValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)), TimeValue timeValue => TimeDataValue.Create(timeValue.Value), StringValue stringValue => StringDataValue.Create(stringValue.Value), BlankValue => DataValue.Blank(), VoidValue => DataValue.Blank(), RecordValue recordValue => recordValue.ToRecord(), TableValue tableValue => tableValue.ToTable(), _ => throw new DeclarativeModelException($"Unsupported variable type: {value.GetType().Name}"), }; public static DataType GetDataType(this FormulaValue value) => value switch { null => DataType.Blank, BooleanValue => DataType.Boolean, DecimalValue => DataType.Number, NumberValue => DataType.Float, DateValue => DataType.Date, DateTimeValue => DataType.DateTime, TimeValue => DataType.Time, StringValue => DataType.String, BlankValue => DataType.Blank, ColorValue => DataType.Color, GuidValue => DataType.Guid, BlobValue => DataType.File, RecordValue recordValue => recordValue.Type.ToDataType(), TableValue tableValue => tableValue.Type.ToDataType(), UntypedObjectValue => DataType.Any, _ => DataType.Unspecified, }; public static DataType ToDataType(this FormulaType type) => type switch { null => DataType.Blank, BooleanType => DataType.Boolean, DecimalType => DataType.Number, NumberType => DataType.Float, DateType => DataType.Date, DateTimeType => DataType.DateTime, TimeType => DataType.Time, StringType => DataType.String, BlankType => DataType.Blank, ColorType => DataType.Color, GuidType => DataType.Guid, BlobType => DataType.File, RecordType recordType => recordType.ToDataType(), TableType tableType => tableType.ToDataType(), UntypedObjectType => DataType.Any, _ => DataType.Unspecified, }; public static object AsPortable(this FormulaValue? value) => (value?.ToObject()).AsPortable(); public static string Format(this FormulaValue value) => value switch { BooleanValue booleanValue => $"{booleanValue.Value}", DecimalValue decimalValue => $"{decimalValue.Value}", NumberValue numberValue => $"{numberValue.Value}", DateValue dateValue => $"{dateValue.GetConvertedValue(TimeZoneInfo.Utc)}", DateTimeValue datetimeValue => $"{datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)}", TimeValue timeValue => $"{timeValue.Value}", StringValue stringValue => stringValue.Value, BlankValue blankValue => string.Empty, VoidValue voidValue => string.Empty, ColorValue colorValue => colorValue.Value.ToString(), GuidValue guidValue => guidValue.Value.ToString("N"), TableValue tableValue => tableValue.ToJson().ToJsonString(s_options), RecordValue recordValue => recordValue.ToJson().ToJsonString(s_options), ErrorValue errorValue => $"Error:{Environment.NewLine}{string.Join(Environment.NewLine, errorValue.Errors.Select(error => $"{error.MessageKey}: {error.Message}"))}", _ => $"[{value.GetType().Name}]", }; public static TableDataValue ToTable(this TableValue value) => DataValue.TableFromRecords(value.Rows.Select(row => row.Value.ToRecord()).ToImmutableArray()); public static RecordDataValue ToRecord(this RecordValue value) => DataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair())); public static RecordValue ToRecord(this IDictionary value) { return FormulaValue.NewRecordFromFields(GetFields()); IEnumerable GetFields() { foreach (DictionaryEntry entry in value) { yield return new NamedValue((string)entry.Key, entry.Value.ToFormula()); } } } public static JsonNode ToJson(this FormulaValue value) => value switch { BooleanValue booleanValue => JsonValue.Create(booleanValue.Value), DecimalValue decimalValue => JsonValue.Create(decimalValue.Value), NumberValue numberValue => JsonValue.Create(numberValue.Value), DateValue dateValue => JsonValue.Create(dateValue.GetConvertedValue(TimeZoneInfo.Utc)), DateTimeValue datetimeValue => JsonValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)), TimeValue timeValue => JsonValue.Create($"{timeValue.Value}"), StringValue stringValue => JsonValue.Create(stringValue.Value), GuidValue guidValue => JsonValue.Create(guidValue.Value), RecordValue recordValue => recordValue.ToJson(), TableValue tableValue => tableValue.ToJson(), BlankValue => JsonValue.Create(string.Empty), _ => $"[{value.GetType().Name}]", }; public static RecordValue ToRecord(this Dictionary value) => FormulaValue.NewRecordFromFields( value.Select( property => new NamedValue(property.Key, property.Value.ToFormula()))); private static RecordDataType ToDataType(this RecordType record) { RecordDataType recordType = new(); foreach (string fieldName in record.FieldNames) { recordType.Properties.Add(fieldName, PropertyInfo.Create(record.GetFieldType(fieldName).ToDataType())); } return recordType; } private static TableDataType ToDataType(this TableType table) { TableDataType tableType = new(); foreach (string fieldName in table.FieldNames) { tableType.Properties.Add(fieldName, PropertyInfo.Create(table.GetFieldType(fieldName).ToDataType())); } return tableType; } private static TableType ToTableType(this IEnumerable value) { foreach (object? element in value) { if (element is not ExpandoObject expandoElement) { throw new DeclarativeModelException($"Invalid table element: {element.GetType().Name}"); } return expandoElement.ToRecordType().ToTable(); // Return first element } return TableType.Empty(); } private static TableValue ToTable(this IEnumerable value) { Type? elementType = value.GetType().GetElementType(); if (elementType is null || elementType == typeof(object)) { IEnumerator enumerator = value.GetEnumerator(); if (enumerator.MoveNext()) { elementType = enumerator.Current?.GetType(); } } return elementType switch { null => FormulaValue.NewTable(RecordType.EmptySealed(), []), _ when elementType == typeof(string) => FormulaValue.NewSingleColumnTable([.. value.OfType().Select(element => FormulaValue.New(element))]), _ when elementType == typeof(bool) => FormulaValue.NewSingleColumnTable([.. value.OfType().Select(element => FormulaValue.New(element))]), _ when elementType == typeof(int) => FormulaValue.NewSingleColumnTable([.. value.OfType().Select(element => FormulaValue.New(element))]), _ when elementType == typeof(long) => FormulaValue.NewSingleColumnTable([.. value.OfType().Select(element => FormulaValue.New(element))]), _ when elementType == typeof(decimal) => FormulaValue.NewSingleColumnTable([.. value.OfType().Select(element => FormulaValue.New(element))]), _ when elementType == typeof(float) => FormulaValue.NewSingleColumnTable([.. value.OfType().Select(element => FormulaValue.New(element))]), _ when elementType == typeof(DateTime) => FormulaValue.NewSingleColumnTable([.. value.OfType().Select(element => FormulaValue.New(element))]), _ when elementType == typeof(TimeSpan) => FormulaValue.NewSingleColumnTable([.. value.OfType().Select(element => FormulaValue.New(element))]), _ when elementType == typeof(ExpandoObject) => FormulaValue.NewTable( value.ToTableType().ToRecord(), [.. value.OfType().Select(element => element.ToRecord())]), _ when typeof(ChatMessage).IsAssignableFrom(elementType) => FormulaValue.NewTable( TypeSchema.Message.RecordType, [.. value.OfType().Select(message => message.ToRecord())]), _ when typeof(IDictionary).IsAssignableFrom(elementType) => value.ToTableOfRecords(), _ => throw new DeclarativeModelException($"Unsupported element type: {elementType.Name}"), }; } private static TableValue ToTableOfRecords(this IEnumerable list) { RecordValue[] elements = [.. list.OfType().Select(table => table.ToRecord())]; return FormulaValue.NewTable(elements.First().Type, elements); } private static KeyValuePair GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.ToDataValue()); private static JsonArray ToJson(this TableValue value) { return new([.. GetJsonElements()]); IEnumerable GetJsonElements() { foreach (DValue row in value.Rows) { RecordValue recordValue = row.Value; yield return recordValue.ToJson(); } } } private static JsonObject ToJson(this RecordValue value) { JsonObject jsonObject = []; foreach (NamedValue field in value.OriginalFields) { jsonObject.Add(field.Name, field.Value.ToJson()); } return jsonObject; } }