Files
agent-framework/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/JsonDocumentExtensions.cs
T
Chris 2b869c2396 .NET Workflows - WIP Declarative action update (#1761)
* WIP

* Fixed build errors (#1638)

Comment and nullable type alignment

* Sync to SDK update

* Checkpoint

* Checkpoint: Tests passing

* Checkpoint: EndWorkflow

* Add trace

* .NET: Azure.AI.Agents Package Split + Initial Extensions (#1657)

* Move packages

* Update nuget.config

* Address Xmldoc

* Remove format from branches checks

* Address Xmldocs

* Add more details to the implementation

* Moving Agent logic to ChatClient

* Adding Name and Id overrides to AzureAIAgent

* Updating extensions

* Add GetAiAgent extensions

* Adding support for version as name can conflict 409 using the Agents API with same name

* Addressing more updates to the extensions

* More improvements

* Remove debugging code from sample

* Address copilot feedback

* Apply suggestions from co-pilot code review

* Checkpoint

* Update Directory.Packages.props

Fix package version rollback:

Azure.AI.Agents.Persistent (beta-6 => beta-7)

* Add project reference

* .NET: Add comprehensive unit tests for Microsoft.Agents.AI.AzureAIAgents extension methods (#1786)

* Initial plan

* Add comprehensive unit test project for Microsoft.Agents.AI.AzureAIAgents

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Add README documenting test project and package dependency requirements

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Fix documentation URL to use learn.microsoft.com

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Bump back AAAP 1.2.0-beta.7

* Address AI generated UT's

* Remove UT Readme

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* .NET: Change model to be required just for prompt agent definition specific extensions (#1812)

* Remove unneeded model from extensions

* Add noop justification

* Update Package Nameing: V1 -> AzureAI.Persistent / V2 -> AzureAI (#1829)

* Checkpoint for merge

* No build errors

* .NET: Update Extensions for Strict Agent Definitions + Improvements (#1892)

* Update Package Nameing: V1 -> AzureAI.Persistent / V2 -> AzureAI

* Update agents and extensions to comply with strict agent definitions

* More static updates

* Address UT, and ResponseTool support

* Improving reusability extensions

* Addressing ResponseTools Unit Tests and extension setup

* Adapted workaround on breaking AAA with OpenAI 2.6.0

* Small updates

* Remove strictness when retrieving agents, improved XmlDocs

* Improve sample comments

* Update dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AgentsClientExtensionsTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Address PR comments

* Address UT failing

* Address Copilot feedback

* Address Copilot feedback

* Address comment typo

* Address PR feedback

* Address typo

* Add missing Extensions with ChatClientAgentOptions

* Address comments

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Updated package version (#1897)

* Version update (#1901)

* Checkpoint

* Updated package version (#1906)

* Checkpoint

* Checkpoint

* Checkpoint

* Align with azure ai agent

* Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/GettingStarted/Workflows/Declarative/MCPToolApproval/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactored external input

* Update dotnet/samples/GettingStarted/Workflows/Declarative/MCPToolApproval/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Agent tools patch

* Demos validated

* Checkpoint

* Hygiene

* Checkpoint - Samples

* Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Checkpoint

* Checkpoint - Deep Research

* Update baseline

* Update

* Typo

* Checkpoint

* Typos

* Sample cleanup

* Update dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/AzureAgentProvider.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI.AzureAI/AgentsClientExtensions.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Typo

* Typo

* Fix input loop

* Sample - Function Calling / External Input

* Typo

* Finessed

* Checkpoint

* Fix feed

* Checkpoint - so close

* Ding dong!

* "there" ***

* Fixup comments

* Fix sample

* Code analysis

* Header

* Typo (variableName)

* Remove dead code

* Skip test (agent api ratchet)

* Comment

* Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Typo

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
2025-11-07 11:37:21 -08:00

298 lines
11 KiB
C#

// 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;
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
internal static class JsonDocumentExtensions
{
public static List<object?> ParseList(this JsonDocument jsonDocument, VariableType targetType)
{
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 convert JSON to object with requested type {targetType.Type.Name}.");
}
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)
{
IEnumerable<KeyValuePair<string, object?>> keyValuePairs =
targetType.Schema is null ?
ParseValues() :
ParseSchema(targetType.Schema);
return keyValuePairs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
IEnumerable<KeyValuePair<string, object?>> ParseValues()
{
foreach (JsonProperty objectProperty in currentElement.EnumerateObject())
{
if (!objectProperty.Value.TryParseValue(targetType: null, out object? parsedValue))
{
throw new DeclarativeActionException($"Unsupported data type '{objectProperty.Value.ValueKind}' for property '{objectProperty.Name}'");
}
yield return new KeyValuePair<string, object?>(objectProperty.Name, parsedValue);
}
}
IEnumerable<KeyValuePair<string, object?>> ParseSchema(FrozenDictionary<string, VariableType> schema)
{
foreach (KeyValuePair<string, VariableType> property in schema)
{
object? parsedValue = null;
if (!currentElement.TryGetProperty(property.Key, out JsonElement propertyElement))
{
if (!property.Value.Type.IsNullable())
{
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);
}
}
}
private static List<object?> ParseTable(this JsonElement currentElement, VariableType targetType)
{
if (!targetType.IsList)
{
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)
{
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))
{
throw new DeclarativeActionException("Inconsistent element types in list.");
}
elementType ??= currentType;
}
}
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() == false)
{
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)
{
value = null;
return valueType?.IsNullable() ?? false; // Parse fails if value is null and requested type is not.
}
if (valueType is null)
{
value = propertyValue;
}
else
{
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)
{
if (valueType is null)
{
value = sourceValue;
return true;
}
try
{
value = Convert.ChangeType(sourceValue, valueType);
return true;
}
catch
{
value = null;
return false;
}
}
}
private static bool TryParseObject(JsonElement propertyElement, VariableType? targetType, out object? value)
{
value = propertyElement.ParseRecord(targetType ?? VariableType.RecordType);
return true;
}
private static bool TryParseList(JsonElement propertyElement, VariableType? targetType, out object? value)
{
try
{
value = ParseTable(propertyElement, targetType ?? VariableType.ListType);
return true;
}
catch
{
value = null;
return false;
}
}
}