Skip to content

Commit 74ff46f

Browse files
authored
.Net - Expose Agent Thread Messages (#5486)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> Retrieving a thread based on ID is supported, but there's not a good way to retrieve messages. ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> Don't want to store messages in thread since this could impact memory pressure but they _do_ need to be exposed when re-approaching an existing thread. Turns out there was some dead-code from the early POC to clean-up also. ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄
1 parent b794c4a commit 74ff46f

File tree

5 files changed

+54
-6
lines changed

5 files changed

+54
-6
lines changed

dotnet/src/Experimental/Agents.UnitTests/Integration/ThreadHarness.cs

+28
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,32 @@ public async Task VerifyThreadLifecycleAsync()
6868

6969
await Assert.ThrowsAsync<HttpOperationException>(() => context.GetThreadModelAsync(thread.Id)).ConfigureAwait(true);
7070
}
71+
72+
/// <summary>
73+
/// Verify retrieval of thread messages
74+
/// </summary>
75+
[Fact(Skip = SkipReason)]
76+
public async Task GetThreadAsync()
77+
{
78+
var threadId = "<your thread-id>";
79+
80+
var context = new OpenAIRestContext(AgentBuilder.OpenAIBaseUrl, TestConfig.OpenAIApiKey);
81+
var thread = await ChatThread.GetAsync(context, threadId);
82+
83+
int index = 0;
84+
string? messageId = null;
85+
while (messageId != null || index == 0)
86+
{
87+
var messages = await thread.GetMessagesAsync(count: 100, lastMessageId: messageId).ConfigureAwait(true);
88+
foreach (var message in messages)
89+
{
90+
++index;
91+
this._output.WriteLine($"#{index:000} [{message.Id}] {message.Role} [{message.AgentId ?? "n/a"}]");
92+
93+
this._output.WriteLine(message.Content);
94+
}
95+
96+
messageId = messages.Count > 0 ? messages[messages.Count - 1].Id : null;
97+
}
98+
}
7199
}

dotnet/src/Experimental/Agents.UnitTests/MockExtensions.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
using System;
43
using System.Net.Http;
54
using System.Threading;
65
using Moq;
@@ -15,7 +14,7 @@ public static void VerifyMock(this Mock<HttpMessageHandler> mockHandler, HttpMet
1514
mockHandler.Protected().Verify(
1615
"SendAsync",
1716
Times.Exactly(times),
18-
ItExpr.Is<HttpRequestMessage>(req => req.Method == method && (uri == null || req.RequestUri == new Uri(uri))),
17+
ItExpr.Is<HttpRequestMessage>(req => req.Method == method && (uri == null || req.RequestUri!.AbsoluteUri.StartsWith(uri))),
1918
ItExpr.IsAny<CancellationToken>());
2019
}
2120
}

dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.Messages.cs

+5
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,21 @@ public static Task<ThreadMessageModel> GetMessageAsync(
7272
/// </summary>
7373
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
7474
/// <param name="threadId">The thread identifier</param>
75+
/// <param name="lastId">The identifier of the last message retrieved</param>
76+
/// <param name="count">The maximum number of messages requested (up to 100 / default: 25)</param>
7577
/// <param name="cancellationToken">A cancellation token</param>
7678
/// <returns>A message list definition</returns>
7779
public static Task<ThreadMessageListModel> GetMessagesAsync(
7880
this OpenAIRestContext context,
7981
string threadId,
82+
string? lastId = null,
83+
int? count = null,
8084
CancellationToken cancellationToken = default)
8185
{
8286
return
8387
context.ExecuteGetAsync<ThreadMessageListModel>(
8488
context.GetMessagesUrl(threadId),
89+
$"limit={count ?? 25}&after={lastId ?? string.Empty}",
8590
cancellationToken);
8691
}
8792

dotnet/src/Experimental/Agents/IAgentThread.cs

+9
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ public interface IAgentThread
2525
/// <returns></returns>
2626
Task<IChatMessage> AddUserMessageAsync(string message, IEnumerable<string>? fileIds = null, CancellationToken cancellationToken = default);
2727

28+
/// <summary>
29+
/// Retrieve thread messages in descending order (most recent first).
30+
/// </summary>
31+
/// <param name="count">The maximum number of messages requested</param>
32+
/// <param name="lastMessageId">The identifier of the last message retrieved</param>
33+
/// <param name="cancellationToken">A cancellation token</param>
34+
/// <returns>An list of <see cref="IChatMessage"/>.</returns>
35+
Task<IReadOnlyList<IChatMessage>> GetMessagesAsync(int? count = null, string? lastMessageId = null, CancellationToken cancellationToken = default);
36+
2837
/// <summary>
2938
/// Advance the thread with the specified agent.
3039
/// </summary>

dotnet/src/Experimental/Agents/Internal/ChatThread.cs

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System.Collections.Generic;
4+
using System.Linq;
45
using System.Runtime.CompilerServices;
56
using System.Threading;
67
using System.Threading.Tasks;
@@ -31,7 +32,7 @@ public static async Task<IAgentThread> CreateAsync(OpenAIRestContext restContext
3132
// Common case is for failure exception to be raised by REST invocation. Null result is a logical possibility, but unlikely edge case.
3233
var threadModel = await restContext.CreateThreadModelAsync(cancellationToken).ConfigureAwait(false);
3334

34-
return new ChatThread(threadModel, messageListModel: null, restContext);
35+
return new ChatThread(threadModel, restContext);
3536
}
3637

3738
/// <summary>
@@ -44,9 +45,8 @@ public static async Task<IAgentThread> CreateAsync(OpenAIRestContext restContext
4445
public static async Task<IAgentThread> GetAsync(OpenAIRestContext restContext, string threadId, CancellationToken cancellationToken = default)
4546
{
4647
var threadModel = await restContext.GetThreadModelAsync(threadId, cancellationToken).ConfigureAwait(false);
47-
var messageListModel = await restContext.GetMessagesAsync(threadId, cancellationToken).ConfigureAwait(false);
4848

49-
return new ChatThread(threadModel, messageListModel, restContext);
49+
return new ChatThread(threadModel, restContext);
5050
}
5151

5252
/// <inheritdoc/>
@@ -59,6 +59,14 @@ public async Task<IChatMessage> AddUserMessageAsync(string message, IEnumerable<
5959
return new ChatMessage(messageModel);
6060
}
6161

62+
/// <inheritdoc/>
63+
public async Task<IReadOnlyList<IChatMessage>> GetMessagesAsync(int? count = null, string? lastMessageId = null, CancellationToken cancellationToken = default)
64+
{
65+
var messageModel = await this._restContext.GetMessagesAsync(this.Id, lastMessageId, count, cancellationToken).ConfigureAwait(false);
66+
67+
return messageModel.Data.Select(m => new ChatMessage(m)).ToArray();
68+
}
69+
6270
/// <inheritdoc/>
6371
public IAsyncEnumerable<IChatMessage> InvokeAsync(IAgent agent, KernelArguments? arguments = null, CancellationToken cancellationToken = default)
6472
{
@@ -109,7 +117,6 @@ public async Task DeleteAsync(CancellationToken cancellationToken)
109117
/// </summary>
110118
private ChatThread(
111119
ThreadModel threadModel,
112-
ThreadMessageListModel? messageListModel,
113120
OpenAIRestContext restContext)
114121
{
115122
this.Id = threadModel.Id;

0 commit comments

Comments
 (0)