Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingA
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage));
break;

case AssistantReasoningDeltaEvent reasoningDeltaEvent:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(reasoningDeltaEvent));
break;

case AssistantReasoningEvent reasoningEvent:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(reasoningEvent));
break;

case AssistantUsageEvent usageEvent:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(usageEvent));
break;
Expand Down Expand Up @@ -339,6 +347,34 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantUsageEvent usa
};
}

private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantReasoningDeltaEvent reasoningDeltaEvent)
{
TextReasoningContent reasoningContent = new(reasoningDeltaEvent.Data?.DeltaContent ?? string.Empty)
{
RawRepresentation = reasoningDeltaEvent
};

return new AgentResponseUpdate(ChatRole.Assistant, [reasoningContent])
{
AgentId = this.Id,
CreatedAt = reasoningDeltaEvent.Timestamp
};
}

private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantReasoningEvent reasoningEvent)
{
TextReasoningContent reasoningContent = new(reasoningEvent.Data?.Content ?? string.Empty)
{
RawRepresentation = reasoningEvent
};

return new AgentResponseUpdate(ChatRole.Assistant, [reasoningContent])
{
AgentId = this.Id,
CreatedAt = reasoningEvent.Timestamp
};
}

private static AdditionalPropertiesDictionary<long>? GetAdditionalCounts(AssistantUsageEvent usageEvent)
{
if (usageEvent.Data is null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Reflection;
using GitHub.Copilot.SDK;
using Microsoft.Extensions.AI;

namespace Microsoft.Agents.AI.GitHub.Copilot.UnitTests;

/// <summary>
/// Unit tests for the <see cref="GitHubCopilotAgent"/> reasoning event handling.
/// </summary>
public sealed class GitHubCopilotAgentReasoningTests
{
/// <summary>
/// Tests that ConvertToAgentResponseUpdate correctly handles AssistantReasoningDeltaEvent.
/// </summary>
[Fact]
public void ConvertToAgentResponseUpdate_WithReasoningDeltaEvent_CreatesTextReasoningContent()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
GitHubCopilotAgent agent = new(copilotClient, sessionConfig: null, ownsClient: false);

// Create an AssistantReasoningDeltaEvent using reflection
AssistantReasoningDeltaEvent reasoningDeltaEvent = new()
{
Data = new AssistantReasoningDeltaData
{
ReasoningId = "reasoning-123",
DeltaContent = "Thinking step "
},
Timestamp = DateTimeOffset.UtcNow
};

// Act - Use reflection to call the private method
MethodInfo? method = typeof(GitHubCopilotAgent).GetMethod(
"ConvertToAgentResponseUpdate",
BindingFlags.NonPublic | BindingFlags.Instance,
null,
[typeof(AssistantReasoningDeltaEvent)],
null);

Assert.NotNull(method);

AgentResponseUpdate? result = method.Invoke(agent, [reasoningDeltaEvent]) as AgentResponseUpdate;

// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.Assistant, result.Role);
Assert.NotEmpty(result.Contents);

AIContent content = result.Contents[0];
Assert.IsType<TextReasoningContent>(content);

TextReasoningContent reasoningContent = (TextReasoningContent)content;
Assert.Equal("Thinking step ", reasoningContent.Text);
Assert.Equal(reasoningDeltaEvent, reasoningContent.RawRepresentation);
Assert.Equal(agent.Id, result.AgentId);
Assert.Equal(reasoningDeltaEvent.Timestamp, result.CreatedAt);
}

/// <summary>
/// Tests that ConvertToAgentResponseUpdate correctly handles AssistantReasoningEvent.
/// </summary>
[Fact]
public void ConvertToAgentResponseUpdate_WithReasoningEvent_CreatesTextReasoningContent()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
GitHubCopilotAgent agent = new(copilotClient, sessionConfig: null, ownsClient: false);

// Create an AssistantReasoningEvent using reflection
AssistantReasoningEvent reasoningEvent = new()
{
Data = new AssistantReasoningData
{
ReasoningId = "reasoning-456",
Content = "Complete reasoning content"
},
Timestamp = DateTimeOffset.UtcNow
};

// Act - Use reflection to call the private method
MethodInfo? method = typeof(GitHubCopilotAgent).GetMethod(
"ConvertToAgentResponseUpdate",
BindingFlags.NonPublic | BindingFlags.Instance,
null,
[typeof(AssistantReasoningEvent)],
null);

Assert.NotNull(method);

AgentResponseUpdate? result = method.Invoke(agent, [reasoningEvent]) as AgentResponseUpdate;

// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.Assistant, result.Role);
Assert.NotEmpty(result.Contents);

AIContent content = result.Contents[0];
Assert.IsType<TextReasoningContent>(content);

TextReasoningContent reasoningContent = (TextReasoningContent)content;
Assert.Equal("Complete reasoning content", reasoningContent.Text);
Assert.Equal(reasoningEvent, reasoningContent.RawRepresentation);
Assert.Equal(agent.Id, result.AgentId);
Assert.Equal(reasoningEvent.Timestamp, result.CreatedAt);
}

/// <summary>
/// Tests that ConvertToAgentResponseUpdate handles null data in AssistantReasoningDeltaEvent.
/// </summary>
[Fact]
public void ConvertToAgentResponseUpdate_WithNullDataInReasoningDeltaEvent_CreatesEmptyTextReasoningContent()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
GitHubCopilotAgent agent = new(copilotClient, sessionConfig: null, ownsClient: false);

// Create an AssistantReasoningDeltaEvent with null data
AssistantReasoningDeltaEvent reasoningDeltaEvent = new()
{
Data = null!,
Timestamp = DateTimeOffset.UtcNow
};

// Act - Use reflection to call the private method
MethodInfo? method = typeof(GitHubCopilotAgent).GetMethod(
"ConvertToAgentResponseUpdate",
BindingFlags.NonPublic | BindingFlags.Instance,
null,
[typeof(AssistantReasoningDeltaEvent)],
null);

Assert.NotNull(method);

AgentResponseUpdate? result = method.Invoke(agent, [reasoningDeltaEvent]) as AgentResponseUpdate;

// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.Assistant, result.Role);
Assert.NotEmpty(result.Contents);

AIContent content = result.Contents[0];
Assert.IsType<TextReasoningContent>(content);

TextReasoningContent reasoningContent = (TextReasoningContent)content;
Assert.Equal(string.Empty, reasoningContent.Text);
}

/// <summary>
/// Tests that ConvertToAgentResponseUpdate handles null content in AssistantReasoningEvent.
/// </summary>
[Fact]
public void ConvertToAgentResponseUpdate_WithNullDataInReasoningEvent_CreatesEmptyTextReasoningContent()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
GitHubCopilotAgent agent = new(copilotClient, sessionConfig: null, ownsClient: false);

// Create an AssistantReasoningEvent with null data
AssistantReasoningEvent reasoningEvent = new()
{
Data = null!,
Timestamp = DateTimeOffset.UtcNow
};

// Act - Use reflection to call the private method
MethodInfo? method = typeof(GitHubCopilotAgent).GetMethod(
"ConvertToAgentResponseUpdate",
BindingFlags.NonPublic | BindingFlags.Instance,
null,
[typeof(AssistantReasoningEvent)],
null);

Assert.NotNull(method);

AgentResponseUpdate? result = method.Invoke(agent, [reasoningEvent]) as AgentResponseUpdate;

// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.Assistant, result.Role);
Assert.NotEmpty(result.Contents);

AIContent content = result.Contents[0];
Assert.IsType<TextReasoningContent>(content);

TextReasoningContent reasoningContent = (TextReasoningContent)content;
Assert.Equal(string.Empty, reasoningContent.Text);
}

/// <summary>
/// Tests that ConvertToAgentResponseUpdate handles null DeltaContent in AssistantReasoningDeltaEvent.
/// </summary>
[Fact]
public void ConvertToAgentResponseUpdate_WithNullDeltaContent_CreatesEmptyTextReasoningContent()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
GitHubCopilotAgent agent = new(copilotClient, sessionConfig: null, ownsClient: false);

// Create an AssistantReasoningDeltaEvent with null DeltaContent
AssistantReasoningDeltaEvent reasoningDeltaEvent = new()
{
Data = new AssistantReasoningDeltaData
{
ReasoningId = "reasoning-789",
DeltaContent = null!
},
Timestamp = DateTimeOffset.UtcNow
};

// Act - Use reflection to call the private method
MethodInfo? method = typeof(GitHubCopilotAgent).GetMethod(
"ConvertToAgentResponseUpdate",
BindingFlags.NonPublic | BindingFlags.Instance,
null,
[typeof(AssistantReasoningDeltaEvent)],
null);

Assert.NotNull(method);

AgentResponseUpdate? result = method.Invoke(agent, [reasoningDeltaEvent]) as AgentResponseUpdate;

// Assert
Assert.NotNull(result);
AIContent content = result.Contents[0];
TextReasoningContent reasoningContent = Assert.IsType<TextReasoningContent>(content);
Assert.Equal(string.Empty, reasoningContent.Text);
}

/// <summary>
/// Tests that ConvertToAgentResponseUpdate handles null Content in AssistantReasoningEvent.
/// </summary>
[Fact]
public void ConvertToAgentResponseUpdate_WithNullContent_CreatesEmptyTextReasoningContent()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
GitHubCopilotAgent agent = new(copilotClient, sessionConfig: null, ownsClient: false);

// Create an AssistantReasoningEvent with null Content
AssistantReasoningEvent reasoningEvent = new()
{
Data = new AssistantReasoningData
{
ReasoningId = "reasoning-999",
Content = null!
},
Timestamp = DateTimeOffset.UtcNow
};

// Act - Use reflection to call the private method
MethodInfo? method = typeof(GitHubCopilotAgent).GetMethod(
"ConvertToAgentResponseUpdate",
BindingFlags.NonPublic | BindingFlags.Instance,
null,
[typeof(AssistantReasoningEvent)],
null);

Assert.NotNull(method);

AgentResponseUpdate? result = method.Invoke(agent, [reasoningEvent]) as AgentResponseUpdate;

// Assert
Assert.NotNull(result);
AIContent content = result.Contents[0];
TextReasoningContent reasoningContent = Assert.IsType<TextReasoningContent>(content);
Assert.Equal(string.Empty, reasoningContent.Text);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,22 @@ def event_handler(event: SessionEvent) -> None:
raw_representation=event,
)
queue.put_nowait(update)
elif event.type == SessionEventType.ASSISTANT_REASONING_DELTA:
if event.data.delta_content:
update = AgentResponseUpdate(
role=Role.ASSISTANT,
contents=[Content.from_text_reasoning(event.data.delta_content)],
raw_representation=event,
)
queue.put_nowait(update)
elif event.type == SessionEventType.ASSISTANT_REASONING:
if event.data.content:
update = AgentResponseUpdate(
role=Role.ASSISTANT,
contents=[Content.from_text_reasoning(event.data.content)],
raw_representation=event,
)
queue.put_nowait(update)
elif event.type == SessionEventType.SESSION_IDLE:
queue.put_nowait(None)
elif event.type == SessionEventType.SESSION_ERROR:
Expand Down
Loading