mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
27324a8013
* fix: Mark Magentic Orchestration Experimental * Apply [Experimental] to all public Magentic types and suppress MAAIW001 in project Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/957a07c1-a805-40eb-989d-bd3425d4c0af Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>
271 lines
10 KiB
C#
271 lines
10 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using System.Text.Json.Serialization.Metadata;
|
|
|
|
namespace Microsoft.Agents.AI.Workflows;
|
|
|
|
/// <summary>
|
|
/// Maintains a ledger of progress made by the Magentic workflow.
|
|
/// </summary>
|
|
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
|
|
public class MagenticProgressLedger
|
|
{
|
|
internal static readonly BooleanProgressLedgerSlot IsRequestSatisfiedSlot = new("is_request_satisfied",
|
|
"Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed)");
|
|
|
|
internal static readonly BooleanProgressLedgerSlot IsInLoopSlot = new("is_in_loop",
|
|
"Are we in a loop where we are repeating the same requests and or getting the same responses as before? " +
|
|
"Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times.");
|
|
|
|
internal static readonly BooleanProgressLedgerSlot IsProgressBeingMadeSlot = new("is_progress_being_made",
|
|
"Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent " +
|
|
"messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success " +
|
|
"such as the inability to read from a required file)");
|
|
|
|
internal readonly StringProgressLedgerSlot NextSpeakerSlot;
|
|
|
|
internal static readonly StringProgressLedgerSlot InstructionOrQuestionSlot = new("instruction_or_question",
|
|
"What instruction or question would you give this team member? (Phrase as if speaking directly to them, and " +
|
|
"include any specific information they may need)");
|
|
|
|
internal MagenticProgressLedger(string teamNames, IEnumerable<ProgressLedgerSlot> additionalQuestions, JsonElement? state = null)
|
|
{
|
|
this.NextSpeakerSlot = new("next_speaker", $"Who should speak next? (select from: {teamNames})");
|
|
this.AdditionalQuestions = additionalQuestions as ProgressLedgerSlot[] ?? additionalQuestions.ToArray();
|
|
|
|
if (state != null)
|
|
{
|
|
this.TryUpdateState(state.Value);
|
|
}
|
|
}
|
|
|
|
internal ProgressLedgerSlot[] AdditionalQuestions { get; }
|
|
|
|
internal bool TryUpdateState(JsonElement element)
|
|
{
|
|
// In principle all of these should be inlineable, but the CodeAnalysis fails to properly chain through the and-chain to realize that
|
|
// all must be true for `requiredQuestionsAnswered` to be true, meaning all of the out parameters would be initialized properly.
|
|
bool isInLoop = false;
|
|
bool isProgressBeingMade = false;
|
|
string? nextSpeaker = string.Empty;
|
|
string? instructionOrQuestion = string.Empty;
|
|
|
|
bool requiredQuestionsAnswered =
|
|
IsRequestSatisfiedSlot.TryGetValueFrom(element, out bool isRequestSatisfied) &&
|
|
IsInLoopSlot.TryGetValueFrom(element, out isInLoop) &&
|
|
IsProgressBeingMadeSlot.TryGetValueFrom(element, out isProgressBeingMade) &&
|
|
this.NextSpeakerSlot.TryGetValueFrom(element, out nextSpeaker) &&
|
|
InstructionOrQuestionSlot.TryGetValueFrom(element, out instructionOrQuestion);
|
|
|
|
if (requiredQuestionsAnswered)
|
|
{
|
|
this.State = element;
|
|
|
|
this.IsRequestSatisfied = isRequestSatisfied;
|
|
this.IsInLoop = isInLoop;
|
|
this.IsProgressBeingMade = isProgressBeingMade;
|
|
|
|
this.NextSpeaker = nextSpeaker!;
|
|
this.InstructionOrQuestion = instructionOrQuestion!;
|
|
}
|
|
|
|
// TODO: To what extent do we want to enforce that the additional questions are also answered?
|
|
|
|
return requiredQuestionsAnswered;
|
|
}
|
|
|
|
[JsonInclude]
|
|
internal JsonElement? State;
|
|
|
|
/// <summary>
|
|
/// Specifies whether plan execution has started.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public bool IsStarted => this.State != null;
|
|
|
|
/// <summary>
|
|
/// Specifies whether the task has been fully satisfied.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public bool IsRequestSatisfied { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Specifies whether the team is in a loop.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public bool IsInLoop { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Specifies whether the team is making progress on the task.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public bool IsProgressBeingMade { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the next team member to take a turn.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string NextSpeaker { get; private set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets the instruction or question to send to the next team member.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string InstructionOrQuestion { get; private set; } = string.Empty;
|
|
|
|
[JsonIgnore]
|
|
internal IEnumerable<ProgressLedgerSlot> Slots =>
|
|
[
|
|
IsRequestSatisfiedSlot,
|
|
IsInLoopSlot,
|
|
IsProgressBeingMadeSlot,
|
|
this.NextSpeakerSlot,
|
|
InstructionOrQuestionSlot,
|
|
.. this.AdditionalQuestions
|
|
];
|
|
|
|
internal bool TryGetCurrentSlotValue<T>(ProgressLedgerSlot<T> slot, [NotNullWhen(true)] out T? value)
|
|
{
|
|
if (!this.State.HasValue)
|
|
{
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
return slot.TryGetValueFrom(this.State.Value, out value);
|
|
}
|
|
|
|
private (string QuestionBlock, string AnswerSchema)? _questionFormatCache;
|
|
internal (string QuestionBlock, string AnswerSchema) FormatQuestions()
|
|
{
|
|
if (!this._questionFormatCache.HasValue)
|
|
{
|
|
StringBuilder questionBuilder = new(), schemaBuilder = new();
|
|
|
|
schemaBuilder.AppendLine("{");
|
|
foreach (ProgressLedgerSlot slot in this.Slots)
|
|
{
|
|
questionBuilder.AppendLine(slot.FormattedQuestion);
|
|
|
|
schemaBuilder.AppendLine($"\"{slot.Key}\": {{")
|
|
.AppendLine($" \"{ProgressLedgerSlot.ValueKey}\": {slot.SchemaType}{slot.SuffixString},")
|
|
.AppendLine($" \"{ProgressLedgerSlot.ReasonKey}\": string")
|
|
.AppendLine("}");
|
|
}
|
|
schemaBuilder.AppendLine("}");
|
|
|
|
this._questionFormatCache = (questionBuilder.ToString(), schemaBuilder.ToString());
|
|
}
|
|
|
|
return this._questionFormatCache.Value;
|
|
}
|
|
}
|
|
|
|
internal abstract record ProgressLedgerSlot(string Key, string Question, string? SchemaTypeSuffix = null)
|
|
{
|
|
public const string ValueKey = "answer";
|
|
public const string ReasonKey = "reason";
|
|
|
|
internal string SuffixString => this.SchemaTypeSuffix == null ? string.Empty : $"({this.SchemaTypeSuffix})";
|
|
|
|
protected internal abstract string SchemaType { get; }
|
|
|
|
public string FormattedQuestion
|
|
{
|
|
get
|
|
{
|
|
if (field == null)
|
|
{
|
|
IEnumerable<string> questionLines = this.Question.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(line => line.TrimEnd());
|
|
|
|
field = $" - {string.Join("\n ", questionLines)}";
|
|
}
|
|
|
|
return field;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal abstract record ProgressLedgerSlot<T>(string Key, string Question, string? SchemaTypeSuffix = null, JsonSerializerOptions? SerializerOptions = null)
|
|
: ProgressLedgerSlot(Key, Question, SchemaTypeSuffix)
|
|
{
|
|
protected internal virtual JsonTypeInfo<T> GetJsonTypeInfo() =>
|
|
((this.SerializerOptions ?? WorkflowsJsonUtilities.DefaultOptions).TryGetTypeInfo(typeof(T), out JsonTypeInfo? typeInfo)
|
|
? typeInfo as JsonTypeInfo<T> : null)
|
|
?? throw new InvalidOperationException($"Cannot get TypeInfo for {typeof(T)} from {(this.SerializerOptions == null ? "provided" : "default")} SerializationOptions.");
|
|
|
|
public bool TryGetValueFrom(JsonElement answers, [NotNullWhen(true)] out T? value)
|
|
{
|
|
if (answers.TryGetProperty(this.Key, out JsonElement slotElement) &&
|
|
slotElement.ValueKind != JsonValueKind.Null &&
|
|
slotElement.TryGetProperty(ValueKey, out JsonElement answerValue))
|
|
{
|
|
try
|
|
{
|
|
T? result = answerValue.Deserialize(this.GetJsonTypeInfo());
|
|
if (result != null)
|
|
{
|
|
value = result;
|
|
return true;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
public bool TryGetReasonFrom(JsonElement answers, [NotNullWhen(true)] out string? value)
|
|
{
|
|
if (answers.TryGetProperty(this.Key, out JsonElement slotElement) &&
|
|
slotElement.ValueKind != JsonValueKind.Null &&
|
|
slotElement.TryGetProperty(ReasonKey, out JsonElement reasonValue))
|
|
{
|
|
try
|
|
{
|
|
string? result = reasonValue.Deserialize(WorkflowsJsonUtilities.JsonContext.Default.String);
|
|
if (result != null)
|
|
{
|
|
value = result;
|
|
return true;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
value = default;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
internal sealed record BooleanProgressLedgerSlot(string Key, string Question, string? SchemaTypeSuffix = null) : ProgressLedgerSlot<bool>(Key, Question, SchemaTypeSuffix)
|
|
{
|
|
// Since we know the type statically, we can directly return the JsonTypeInfo for string from our JsonContext,
|
|
// which is more efficient than looking it up via the options.
|
|
protected internal override JsonTypeInfo<bool> GetJsonTypeInfo() => WorkflowsJsonUtilities.JsonContext.Default.Boolean;
|
|
|
|
protected internal override string SchemaType => "boolean";
|
|
}
|
|
|
|
internal sealed record StringProgressLedgerSlot(string Key, string Question, string? SchemaTypeSuffix = null) : ProgressLedgerSlot<string>(Key, Question, SchemaTypeSuffix)
|
|
{
|
|
// Since we know the type statically, we can directly return the JsonTypeInfo for string from our JsonContext,
|
|
// which is more efficient than looking it up via the options.
|
|
protected internal override JsonTypeInfo<string> GetJsonTypeInfo() => WorkflowsJsonUtilities.JsonContext.Default.String;
|
|
|
|
protected internal override string SchemaType => "string";
|
|
}
|