Public Access
Update meeting summary agent UI
This commit is contained in:
@@ -42,6 +42,99 @@ public sealed class LiteLlmResponsesChatClientTests
|
||||
Assert.Equal("gpt-5.5-2026-04-23", response.ModelId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClientReportsVisibleReasoningSummariesSeparatelyFromResponseText()
|
||||
{
|
||||
var handler = new SequencedHttpMessageHandler(
|
||||
new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("""
|
||||
{
|
||||
"output": [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"summary": [
|
||||
{
|
||||
"type": "summary_text",
|
||||
"text": "Checked the configured rules file."
|
||||
},
|
||||
{
|
||||
"type": "summary_text",
|
||||
"text": "Prepared a targeted update."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "Done."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""")
|
||||
});
|
||||
var reasoningSummaries = new List<string>();
|
||||
using var client = CreateClient(
|
||||
handler,
|
||||
reconnectionAttempts: 0,
|
||||
reasoningSummaryChanged: reasoningSummaries.Add);
|
||||
|
||||
var response = await client.GetResponseAsync([new ChatMessage(ChatRole.User, "update rules")]);
|
||||
|
||||
Assert.Equal("Done.", response.Text);
|
||||
Assert.Equal(
|
||||
[
|
||||
"Checked the configured rules file.",
|
||||
"Prepared a targeted update."
|
||||
],
|
||||
reasoningSummaries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClientDoesNotFailResponseWhenReasoningSummaryCallbackFails()
|
||||
{
|
||||
var handler = new SequencedHttpMessageHandler(
|
||||
new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("""
|
||||
{
|
||||
"output": [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"summary": [
|
||||
{
|
||||
"type": "summary_text",
|
||||
"text": "Checked the configured rules file."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "Done."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""")
|
||||
});
|
||||
using var client = CreateClient(
|
||||
handler,
|
||||
reconnectionAttempts: 0,
|
||||
reasoningSummaryChanged: _ => throw new InvalidOperationException("UI failed"));
|
||||
|
||||
var response = await client.GetResponseAsync([new ChatMessage(ChatRole.User, "update rules")]);
|
||||
|
||||
Assert.Equal("Done.", response.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParserReadsFunctionCalls()
|
||||
{
|
||||
@@ -306,7 +399,8 @@ public sealed class LiteLlmResponsesChatClientTests
|
||||
HttpMessageHandler handler,
|
||||
int reconnectionAttempts,
|
||||
LiteLlmResponsesCompactionOptions? compactionOptions = null,
|
||||
Action? retrying = null)
|
||||
Action? retrying = null,
|
||||
Action<string>? reasoningSummaryChanged = null)
|
||||
{
|
||||
return new LiteLlmResponsesChatClient(
|
||||
new HttpClient(handler)
|
||||
@@ -320,7 +414,8 @@ public sealed class LiteLlmResponsesChatClientTests
|
||||
reconnectionAttempts,
|
||||
TimeSpan.Zero,
|
||||
compactionOptions,
|
||||
retrying: retrying);
|
||||
retrying: retrying,
|
||||
reasoningSummaryChanged: reasoningSummaryChanged);
|
||||
}
|
||||
|
||||
private sealed class SequencedHttpMessageHandler : HttpMessageHandler
|
||||
|
||||
@@ -117,7 +117,7 @@ public sealed class RecordingCoordinatorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TranscriptLineWorkflowRuleTransformsLiveTranscriptBeforePersistingMarkdown()
|
||||
public async Task TranscriptLineWorkflowRuleTransformsLiveTranscriptAfterDurableAppend()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
|
||||
var rulesPath = Path.Combine(root, "rules.yaml");
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class TaskbarIconTests
|
||||
Assert.Equal(RecordingProcessState.Idle, menu.State);
|
||||
Assert.Contains(menu.Items, item =>
|
||||
item.Action == MeetingTaskbarAction.EditRules &&
|
||||
item.Text == "Settings and logs");
|
||||
item.Text == "Open agent");
|
||||
Assert.Contains(menu.Items, item =>
|
||||
item.Action == MeetingTaskbarAction.StartRecording &&
|
||||
item.ProfileName == "default" &&
|
||||
|
||||
@@ -857,15 +857,13 @@ public sealed class WorkflowRulesEditorTests
|
||||
Assert.Equal("", viewModel.Draft);
|
||||
Assert.Equal(
|
||||
[new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, "Rename ABS Daily")],
|
||||
viewModel.Messages.ToArray());
|
||||
viewModel.Messages.Select(item => item.Message).OfType<WorkflowRulesEditorChatMessage>().ToArray());
|
||||
|
||||
pipeline.Release.SetResult();
|
||||
await sendTask.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
|
||||
Assert.False(viewModel.IsThinking);
|
||||
Assert.Equal(2, viewModel.Messages.Count);
|
||||
Assert.Equal(WorkflowRulesEditorChatRole.Agent, viewModel.Messages[1].Role);
|
||||
Assert.Equal("Updated rules.", viewModel.Messages[1].Content);
|
||||
AssertCompletedActivity(viewModel, ["Thinking..."], "Updated rules.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -881,20 +879,19 @@ public sealed class WorkflowRulesEditorTests
|
||||
|
||||
Assert.False(viewModel.IsThinking);
|
||||
Assert.Equal("", viewModel.Draft);
|
||||
Assert.Equal(
|
||||
[
|
||||
new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, "check logs"),
|
||||
new WorkflowRulesEditorChatMessage(
|
||||
WorkflowRulesEditorChatRole.Agent,
|
||||
"Settings and logs failed: The request timed out.")
|
||||
],
|
||||
viewModel.Messages.ToArray());
|
||||
Assert.Equal(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, "check logs"), viewModel.Messages[0].Message);
|
||||
AssertCompletedActivity(
|
||||
viewModel,
|
||||
["Thinking..."],
|
||||
"Meeting Summary Agent failed: The request timed out.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ViewModelShowsReconnectStatusReportedByPipeline()
|
||||
{
|
||||
var pipeline = new StatusReportingRulesEditorPipeline("Updated rules.");
|
||||
var pipeline = new ReportingRulesEditorPipeline(
|
||||
"Updated rules.",
|
||||
WorkflowRulesEditorActivityUpdate.Status("Reconnecting..."));
|
||||
var viewModel = new WorkflowRulesEditorChatViewModel(pipeline)
|
||||
{
|
||||
Draft = "check logs"
|
||||
@@ -911,7 +908,146 @@ public sealed class WorkflowRulesEditorTests
|
||||
|
||||
Assert.False(viewModel.IsThinking);
|
||||
Assert.Equal("Thinking...", viewModel.ActivityMessage);
|
||||
Assert.Equal("Updated rules.", viewModel.Messages[1].Content);
|
||||
AssertCompletedActivity(viewModel, ["Thinking...", "Reconnecting..."], "Updated rules.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ViewModelShowsToolCallsAbovePersistentThinkingLine()
|
||||
{
|
||||
var pipeline = new ReportingRulesEditorPipeline(
|
||||
"Updated rules.",
|
||||
WorkflowRulesEditorActivityUpdate.ToolCall("read_logs"));
|
||||
var viewModel = new WorkflowRulesEditorChatViewModel(pipeline)
|
||||
{
|
||||
Draft = "check logs"
|
||||
};
|
||||
|
||||
var sendTask = viewModel.SendAsync();
|
||||
await pipeline.Reported.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
|
||||
Assert.True(viewModel.IsThinking);
|
||||
Assert.Equal("Thinking...", viewModel.ActivityMessage);
|
||||
Assert.Equal(["Called tool: read_logs"], viewModel.ActivityMessages.ToArray());
|
||||
|
||||
pipeline.Release.SetResult();
|
||||
await sendTask.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
|
||||
Assert.False(viewModel.IsThinking);
|
||||
Assert.Empty(viewModel.ActivityMessages);
|
||||
AssertCompletedActivity(viewModel, ["Thinking...", "Called tool: read_logs"], "Updated rules.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ViewModelShowsVisibleThinkingOutputInActivityExpander()
|
||||
{
|
||||
var pipeline = new ReportingRulesEditorPipeline(
|
||||
"Updated rules.",
|
||||
WorkflowRulesEditorActivityUpdate.Thinking("Checked the recent logs."),
|
||||
WorkflowRulesEditorActivityUpdate.ToolCall("read_logs"),
|
||||
WorkflowRulesEditorActivityUpdate.Thinking("Matched the rule to the user request."));
|
||||
var viewModel = new WorkflowRulesEditorChatViewModel(pipeline)
|
||||
{
|
||||
Draft = "check logs"
|
||||
};
|
||||
|
||||
var sendTask = viewModel.SendAsync();
|
||||
await pipeline.Reported.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
|
||||
Assert.True(viewModel.IsThinking);
|
||||
Assert.Equal("Thinking...", viewModel.ActivityMessage);
|
||||
Assert.Equal(
|
||||
[
|
||||
"Checked the recent logs.",
|
||||
"Called tool: read_logs",
|
||||
"Matched the rule to the user request."
|
||||
],
|
||||
viewModel.ActivityMessages.ToArray());
|
||||
|
||||
pipeline.Release.SetResult();
|
||||
await sendTask.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
|
||||
Assert.False(viewModel.IsThinking);
|
||||
Assert.Empty(viewModel.ActivityMessages);
|
||||
AssertCompletedActivity(
|
||||
viewModel,
|
||||
[
|
||||
"Checked the recent logs.",
|
||||
"Called tool: read_logs",
|
||||
"Matched the rule to the user request."
|
||||
],
|
||||
"Updated rules.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ViewModelMarshalsBackgroundActivityUpdatesToCapturedContext()
|
||||
{
|
||||
var previousContext = SynchronizationContext.Current;
|
||||
var context = new RecordingSynchronizationContext();
|
||||
var pipeline = new BackgroundReportingRulesEditorPipeline(
|
||||
"Updated rules.",
|
||||
WorkflowRulesEditorActivityUpdate.Thinking("Checked the recent logs."));
|
||||
var viewModel = new WorkflowRulesEditorChatViewModel(pipeline)
|
||||
{
|
||||
Draft = "check logs"
|
||||
};
|
||||
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
var sendTask = viewModel.SendAsync();
|
||||
SynchronizationContext.SetSynchronizationContext(previousContext);
|
||||
await pipeline.Reported.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
|
||||
Assert.True(viewModel.IsThinking);
|
||||
Assert.Equal(["Checked the recent logs."], viewModel.ActivityMessages.ToArray());
|
||||
Assert.Equal(1, context.SendCount);
|
||||
|
||||
pipeline.Release.SetResult();
|
||||
await sendTask.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
|
||||
AssertCompletedActivity(viewModel, ["Checked the recent logs."], "Updated rules.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ViewModelKeepsActivityItemsOutOfModelConversation()
|
||||
{
|
||||
var pipeline = new BlockingRulesEditorPipeline("Updated rules.");
|
||||
var viewModel = new WorkflowRulesEditorChatViewModel(pipeline)
|
||||
{
|
||||
Draft = "first"
|
||||
};
|
||||
|
||||
var firstSend = viewModel.SendAsync();
|
||||
await pipeline.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
pipeline.Release.SetResult();
|
||||
await firstSend.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
|
||||
pipeline.Reset("Second response.");
|
||||
viewModel.Draft = "second";
|
||||
var secondSend = viewModel.SendAsync();
|
||||
await pipeline.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, "first"),
|
||||
new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.Agent, "Updated rules.")
|
||||
],
|
||||
pipeline.LastConversation);
|
||||
|
||||
pipeline.Release.SetResult();
|
||||
await secondSend.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
private static void AssertCompletedActivity(
|
||||
WorkflowRulesEditorChatViewModel viewModel,
|
||||
IReadOnlyList<string> expectedActivityLines,
|
||||
string expectedAgentMessage)
|
||||
{
|
||||
Assert.Equal(3, viewModel.Messages.Count);
|
||||
Assert.Equal(WorkflowRulesEditorConversationItemKind.Activity, viewModel.Messages[1].Kind);
|
||||
Assert.StartsWith("Worked for ", viewModel.Messages[1].Content);
|
||||
Assert.Equal(expectedActivityLines, viewModel.Messages[1].ActivityLines);
|
||||
Assert.Equal(WorkflowRulesEditorConversationItemKind.Message, viewModel.Messages[2].Kind);
|
||||
Assert.Equal(WorkflowRulesEditorChatRole.Agent, viewModel.Messages[2].Message?.Role);
|
||||
Assert.Equal(expectedAgentMessage, viewModel.Messages[2].Message?.Content);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -1196,31 +1332,36 @@ public sealed class WorkflowRulesEditorTests
|
||||
|
||||
private sealed class BlockingRulesEditorPipeline : IWorkflowRulesEditorChatPipeline
|
||||
{
|
||||
private readonly string response;
|
||||
private string response;
|
||||
|
||||
public BlockingRulesEditorPipeline(string response)
|
||||
{
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public TaskCompletionSource Started { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public TaskCompletionSource Started { get; private set; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public TaskCompletionSource Release { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public TaskCompletionSource Release { get; private set; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public IReadOnlyList<WorkflowRulesEditorChatMessage> LastConversation { get; private set; } = [];
|
||||
|
||||
public void Reset(string nextResponse)
|
||||
{
|
||||
response = nextResponse;
|
||||
Started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
Release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
|
||||
public async Task<WorkflowRulesEditorChatResult> SendAsync(
|
||||
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
|
||||
string userMessage,
|
||||
CancellationToken cancellationToken,
|
||||
Action<string>? statusChanged = null)
|
||||
Action<WorkflowRulesEditorActivityUpdate>? activityChanged = null)
|
||||
{
|
||||
LastConversation = conversation;
|
||||
Started.SetResult();
|
||||
await Release.Task.WaitAsync(cancellationToken);
|
||||
return new WorkflowRulesEditorChatResult(
|
||||
response,
|
||||
conversation
|
||||
.Append(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, userMessage))
|
||||
.Append(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.Agent, response))
|
||||
.ToList());
|
||||
return new WorkflowRulesEditorChatResult(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1237,19 +1378,23 @@ public sealed class WorkflowRulesEditorTests
|
||||
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
|
||||
string userMessage,
|
||||
CancellationToken cancellationToken,
|
||||
Action<string>? statusChanged = null)
|
||||
Action<WorkflowRulesEditorActivityUpdate>? activityChanged = null)
|
||||
{
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StatusReportingRulesEditorPipeline : IWorkflowRulesEditorChatPipeline
|
||||
private sealed class ReportingRulesEditorPipeline : IWorkflowRulesEditorChatPipeline
|
||||
{
|
||||
private readonly string response;
|
||||
private readonly IReadOnlyList<WorkflowRulesEditorActivityUpdate> updates;
|
||||
|
||||
public StatusReportingRulesEditorPipeline(string response)
|
||||
public ReportingRulesEditorPipeline(
|
||||
string response,
|
||||
params WorkflowRulesEditorActivityUpdate[] updates)
|
||||
{
|
||||
this.response = response;
|
||||
this.updates = updates;
|
||||
}
|
||||
|
||||
public TaskCompletionSource Reported { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
@@ -1260,17 +1405,57 @@ public sealed class WorkflowRulesEditorTests
|
||||
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
|
||||
string userMessage,
|
||||
CancellationToken cancellationToken,
|
||||
Action<string>? statusChanged = null)
|
||||
Action<WorkflowRulesEditorActivityUpdate>? activityChanged = null)
|
||||
{
|
||||
statusChanged?.Invoke("Reconnecting...");
|
||||
foreach (var update in updates)
|
||||
{
|
||||
activityChanged?.Invoke(update);
|
||||
}
|
||||
|
||||
Reported.SetResult();
|
||||
await Release.Task.WaitAsync(cancellationToken);
|
||||
return new WorkflowRulesEditorChatResult(
|
||||
response,
|
||||
conversation
|
||||
.Append(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, userMessage))
|
||||
.Append(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.Agent, response))
|
||||
.ToList());
|
||||
return new WorkflowRulesEditorChatResult(response);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BackgroundReportingRulesEditorPipeline : IWorkflowRulesEditorChatPipeline
|
||||
{
|
||||
private readonly string response;
|
||||
private readonly WorkflowRulesEditorActivityUpdate update;
|
||||
|
||||
public BackgroundReportingRulesEditorPipeline(
|
||||
string response,
|
||||
WorkflowRulesEditorActivityUpdate update)
|
||||
{
|
||||
this.response = response;
|
||||
this.update = update;
|
||||
}
|
||||
|
||||
public TaskCompletionSource Reported { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public TaskCompletionSource Release { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public async Task<WorkflowRulesEditorChatResult> SendAsync(
|
||||
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
|
||||
string userMessage,
|
||||
CancellationToken cancellationToken,
|
||||
Action<WorkflowRulesEditorActivityUpdate>? activityChanged = null)
|
||||
{
|
||||
await Task.Run(() => activityChanged?.Invoke(update), cancellationToken);
|
||||
Reported.SetResult();
|
||||
await Release.Task.WaitAsync(cancellationToken);
|
||||
return new WorkflowRulesEditorChatResult(response);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingSynchronizationContext : SynchronizationContext
|
||||
{
|
||||
public int SendCount { get; private set; }
|
||||
|
||||
public override void Send(SendOrPostCallback callback, object? state)
|
||||
{
|
||||
SendCount++;
|
||||
callback(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user