Files
agent-framework/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/TextReasoningContentEventGenerator.cs
T
Reuben BondandGitHub 33f84f9ed2 .NET: Improve fidelity of OpenAI Responses server and add Conversations (#1907)
* Improve fidelity of OpenAI Responses server and add Conversations

* Merge

* nit

* Undo prior change

* Undo prior change

* Review feedback

* Review feedback

* Fix test

* Use simpler JsonDocument approach for polymorphic deserialization

* More review feedback

* dotnet format
2025-11-05 18:46:19 +00:00

121 lines
3.6 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
/// <summary>
/// A state machine for generating streaming events from reasoning text content.
/// Processes TextReasoningContent instances one at a time and emits appropriate streaming events based on internal state.
/// </summary>
internal sealed class TextReasoningContentEventGenerator(
IdGenerator idGenerator,
SequenceNumber seq,
int outputIndex) : StreamingEventGenerator
{
private State _currentState = State.Initial;
private readonly string _itemId = idGenerator.GenerateReasoningId();
private readonly StringBuilder _text = new();
private const int SummaryIndex = 0; // Summary index for reasoning summary text
/// <summary>
/// Represents the state of the event generator.
/// </summary>
private enum State
{
Initial,
AccumulatingText,
Completed
}
public override bool IsSupported(AIContent content) => content is TextReasoningContent;
public override IEnumerable<StreamingResponseEvent> ProcessContent(AIContent content)
{
if (this._currentState == State.Completed)
{
throw new InvalidOperationException("Cannot process content after the generator has been completed.");
}
// Only process TextReasoningContent
if (content is not TextReasoningContent reasoningContent)
{
yield break;
}
// If is the first content, emit initial events
if (this._currentState == State.Initial)
{
var incompleteItem = new ReasoningItemResource
{
Id = this._itemId,
Status = "in_progress"
};
yield return new StreamingOutputItemAdded
{
SequenceNumber = seq.Increment(),
OutputIndex = outputIndex,
Item = incompleteItem
};
this._currentState = State.AccumulatingText;
}
// Accumulate text and emit delta event
this._text.Append(reasoningContent.Text);
yield return new StreamingReasoningSummaryTextDelta
{
SequenceNumber = seq.Increment(),
ItemId = this._itemId,
OutputIndex = outputIndex,
SummaryIndex = SummaryIndex,
Delta = reasoningContent.Text
};
}
public override IEnumerable<StreamingResponseEvent> Complete()
{
if (this._currentState == State.Completed)
{
throw new InvalidOperationException("Complete has already been called.");
}
// If no content was processed, emit initial events first
if (this._currentState == State.Initial)
{
yield break;
}
// Emit final events
var finalText = this._text.ToString();
yield return new StreamingReasoningSummaryTextDone
{
SequenceNumber = seq.Increment(),
ItemId = this._itemId,
OutputIndex = outputIndex,
SummaryIndex = SummaryIndex,
Text = finalText
};
yield return new StreamingOutputItemDone
{
SequenceNumber = seq.Increment(),
OutputIndex = outputIndex,
Item = new ReasoningItemResource
{
Id = this._itemId,
Status = "completed"
}
};
this._currentState = State.Completed;
}
}