mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
0086d38f58
* refactor: Normalize Run/RunStreaming with AIAgent * refactor: Clarify Session vs. Run -level concepts * Rename RunId to SessionId to better match Run/Session terminology in AIAgent * [BREAKING]: Will break existing checkpointed sessions in CosmosDb due to field rename * refactor: Rename and simplify interface around getting typed data out of ExternalRequest/Response * Also adds hints around using value types in PortableValue * refactor: Rename AddFanInEdge to AddFanInBarrierEdge This will prevent a breaking change later when we introduce a programmable FanIn edge, analogous to the FanOut edge's EdgeSelector. The goal, in the long run is to support a number of different FanIn scenarios, with naive FanIn (no barrier) by default, similar to FanOut. * refactor: AsAgent(this Workflow, ...) => AsAIAgent(...) * misc - part1: SwitchBuilder internal --------- Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
208 lines
7.8 KiB
C#
208 lines
7.8 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Text.Json.Serialization;
|
|
|
|
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
|
using Microsoft.Shared.Diagnostics;
|
|
|
|
namespace Microsoft.Agents.AI.Workflows;
|
|
|
|
/// <summary>
|
|
/// Represents a value that can be exported / imported to a workflow, e.g. through an external request/response, or
|
|
/// through checkpointing. Abstracts away delayed deserialization and type conversion where appropriate.
|
|
/// </summary>
|
|
public sealed class PortableValue
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance <see cref="PortableValue"/>.
|
|
/// </summary>
|
|
/// <param name="value">The represented value.</param>
|
|
public PortableValue(object value)
|
|
{
|
|
this._value = value;
|
|
this.TypeId = new(value.GetType());
|
|
}
|
|
|
|
[JsonConstructor]
|
|
internal PortableValue(TypeId typeId, object value)
|
|
{
|
|
this.TypeId = Throw.IfNull(typeId);
|
|
this._value = value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool Equals(object? obj)
|
|
{
|
|
if (obj is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (obj is not PortableValue other)
|
|
{
|
|
Type targetType = obj.GetType();
|
|
return this.AsType(targetType)?.Equals(obj) is true;
|
|
}
|
|
|
|
return this.TypeId == other.TypeId
|
|
&& ((this.Value is null && other.Value is null)
|
|
|| this.Value?.Equals(other.Value) is true);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override int GetHashCode()
|
|
{
|
|
return HashCode.Combine(this.TypeId, this.Value);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public static bool operator ==(PortableValue? left, PortableValue? right)
|
|
{
|
|
if (left is null)
|
|
{
|
|
return right is null;
|
|
}
|
|
|
|
return left.Equals(right);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public static bool operator !=(PortableValue? left, PortableValue? right) => !(left == right);
|
|
|
|
/// <summary>
|
|
/// The identifier of the type of the instance in <see cref="Value"/>.
|
|
/// </summary>
|
|
public TypeId TypeId { get; }
|
|
|
|
[JsonIgnore]
|
|
internal bool IsDelayedDeserialization => this.Value is IDelayedDeserialization;
|
|
|
|
[JsonIgnore]
|
|
internal bool IsDeserialized => this._deserializedValueCache is not null;
|
|
|
|
private readonly object _value;
|
|
private object? _deserializedValueCache;
|
|
|
|
/// <summary>
|
|
/// Gets the raw underlying value represented by this instance.
|
|
/// </summary>
|
|
[JsonInclude]
|
|
internal object Value => this._deserializedValueCache ?? Throw.IfNull(this._value);
|
|
|
|
/// <summary>
|
|
/// Attempts to retrieve the underlying value as the specified type, deserializing if necessary.
|
|
/// </summary>
|
|
/// <remarks>If the underlying value implements delayed deserialization, this method will attempt to
|
|
/// deserialize it to the specified type. If the value is already of the requested type, it is returned directly.
|
|
/// Otherwise, the default value for TValue is returned. For value types, the default is not <see langword="null"/>,
|
|
/// UNLESS <typeparamref name="TValue"/> is nullable, e.g. <c>int?</c>.
|
|
/// </remarks>
|
|
/// <typeparam name="TValue">The type to which the value should be cast or deserialized.</typeparam>
|
|
/// <returns>The value cast or deserialized to type TValue if possible; otherwise, the default value for type TValue.</returns>
|
|
public TValue? As<TValue>() => this.Is(out TValue? value) ? value : default;
|
|
|
|
/// <summary>
|
|
/// Determines whether the current value can be represented as the specified type.
|
|
/// </summary>
|
|
/// <typeparam name="TValue">The type to test for compatibility with the current value.</typeparam>
|
|
/// <returns>true if the current value can be represented as type TValue; otherwise, false.</returns>
|
|
public bool Is<TValue>() => this.Is<TValue>(out _);
|
|
|
|
/// <summary>
|
|
/// Determines whether the current value can be represented as the specified type.
|
|
/// </summary>
|
|
/// <typeparam name="TValue">The type to test for compatibility with the current value.</typeparam>
|
|
/// <param name="value">When this method returns, contains the value cast or deserialized to type TValue
|
|
/// if the conversion succeeded, or null if the conversion failed.</param>
|
|
/// <returns>true if the current value can be represented as type TValue; otherwise, false.</returns>
|
|
public bool Is<TValue>([NotNullWhen(true)] out TValue? value)
|
|
{
|
|
this.TryDeserializeAndUpdateCache(typeof(TValue), out _);
|
|
|
|
if (this.Value is TValue typedValue)
|
|
{
|
|
value = typedValue;
|
|
return true;
|
|
}
|
|
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to retrieve the underlying value as the specified type, deserializing if necessary.
|
|
/// </summary>
|
|
/// <param name="targetType">The type to which the value should be cast or deserialized.</param>
|
|
/// <returns>The value cast or deserialized to type targetType if possible; otherwise, null.</returns>
|
|
public object? AsType(Type targetType) => this.IsType(targetType, out object? value) ? value : null;
|
|
|
|
/// <summary>
|
|
/// Determines whether the current instance can be assigned to the specified target type.
|
|
/// </summary>
|
|
/// <param name="targetType">The type to compare with the current instance. Cannot be null.</param>
|
|
/// <returns>true if the current instance can be assigned to targetType; otherwise, false.</returns>
|
|
public bool IsType(Type targetType) => this.IsType(targetType, out _);
|
|
|
|
/// <summary>
|
|
/// Determines whether the current instance can be assigned to the specified target type.
|
|
/// </summary>
|
|
/// <param name="targetType">The type to compare with the current instance. Cannot be null.</param>
|
|
/// <param name="value">When this method returns, contains the value cast or deserialized to type TValue
|
|
/// if the conversion succeeded, or null if the conversion failed.</param>
|
|
/// <returns>true if the current instance can be assigned to targetType; otherwise, false.</returns>
|
|
public bool IsType(Type targetType, [NotNullWhen(true)] out object? value)
|
|
{
|
|
// Unfortunately, there is no way to check that the TypeId specified is assignable to the provided type
|
|
Throw.IfNull(targetType);
|
|
this.TryDeserializeAndUpdateCache(targetType, out _);
|
|
|
|
if (this.Value is not null && targetType.IsInstanceOfType(this.Value))
|
|
{
|
|
value = this.Value;
|
|
return true;
|
|
}
|
|
|
|
value = null;
|
|
return false;
|
|
}
|
|
|
|
private bool TryDeserializeAndUpdateCache(Type targetType, out object? replacedCacheValueOrNull)
|
|
{
|
|
replacedCacheValueOrNull = null;
|
|
|
|
// Explicitly use _value here since we do not want to be overridden by the cache, if any
|
|
if (this._value is not IDelayedDeserialization delayedDeserialization)
|
|
{
|
|
// Not a delayed deserialization; nothing to do
|
|
return false;
|
|
}
|
|
|
|
bool isCompatibleType = false;
|
|
if (this._deserializedValueCache == null || !(isCompatibleType = targetType.IsAssignableFrom(this._deserializedValueCache.GetType())))
|
|
{
|
|
// Either we have no cache, or the types are incompatible; see if we can deserialize
|
|
try
|
|
{
|
|
object? deserialized = delayedDeserialization.Deserialize(targetType);
|
|
|
|
if (deserialized != null && targetType.IsInstanceOfType(deserialized))
|
|
{
|
|
replacedCacheValueOrNull = this._deserializedValueCache;
|
|
this._deserializedValueCache = deserialized;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
isCompatibleType = false;
|
|
}
|
|
}
|
|
|
|
// The last possibility is that we already deserialized successfully
|
|
return isCompatibleType;
|
|
}
|
|
}
|