.NET: NET Workflows - Skip conversation initialization when identifier is provided (#1087)

* Fix

* Code-gen case

* Tests

* Autosend logic

* Build fix

* Namespace

* Validation enhancement
This commit is contained in:
Chris
2025-10-02 14:27:46 +00:00
committed by GitHub
parent cb62407fb8
commit 08ea625de0
23 changed files with 102 additions and 31 deletions
@@ -18,17 +18,20 @@ public sealed class DeclarativeCodeGenTest(ITestOutputHelper output) : WorkflowT
[Theory]
[InlineData("SendActivity.yaml", "SendActivity.json")]
[InlineData("InvokeAgent.yaml", "InvokeAgent.json")]
[InlineData("InvokeAgent.yaml", "InvokeAgent.json", true)]
[InlineData("ConversationMessages.yaml", "ConversationMessages.json")]
public Task ValidateCaseAsync(string workflowFileName, string testcaseFileName) =>
this.RunWorkflowAsync(Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName), testcaseFileName);
[InlineData("ConversationMessages.yaml", "ConversationMessages.json", true)]
public Task ValidateCaseAsync(string workflowFileName, string testcaseFileName, bool externalConveration = false) =>
this.RunWorkflowAsync(Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName), testcaseFileName, externalConveration);
[Theory]
[InlineData("Marketing.yaml", "Marketing.json")]
[InlineData("MathChat.yaml", "MathChat.json")]
[InlineData("Marketing.yaml", "Marketing.json", true)]
[InlineData("MathChat.yaml", "MathChat.json", true)]
[InlineData("DeepResearch.yaml", "DeepResearch.json", Skip = "Long running")]
[InlineData("HumanInLoop.yaml", "HumanInLoop.json", Skip = "Needs test support")]
public Task ValidateScenarioAsync(string workflowFileName, string testcaseFileName) =>
this.RunWorkflowAsync(Path.Combine(GetRepoFolder(), "workflow-samples", workflowFileName), testcaseFileName);
public Task ValidateScenarioAsync(string workflowFileName, string testcaseFileName, bool externalConveration = false) =>
this.RunWorkflowAsync(Path.Combine(GetRepoFolder(), "workflow-samples", workflowFileName), testcaseFileName, externalConveration);
protected override async Task RunAndVerifyAsync<TInput>(Testcase testcase, string workflowPath, DeclarativeWorkflowOptions workflowOptions)
{
@@ -46,6 +49,7 @@ public sealed class DeclarativeCodeGenTest(ITestOutputHelper output) : WorkflowT
Assert.Empty(workflowEvents.ActionInvokeEvents);
Assert.Empty(workflowEvents.ActionCompleteEvents);
AssertWorkflow.Conversation(workflowOptions.ConversationId, testcase.Validation.ConversationCount, workflowEvents.ConversationEvents);
AssertWorkflow.EventCounts(workflowEvents.ExecutorInvokeEvents.Count - 2, testcase);
AssertWorkflow.EventCounts(workflowEvents.ExecutorCompleteEvents.Count - 2, testcase);
AssertWorkflow.EventSequence(workflowEvents.ExecutorInvokeEvents.Select(e => e.ExecutorId), testcase);
@@ -18,17 +18,20 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
[Theory]
[InlineData("SendActivity.yaml", "SendActivity.json")]
[InlineData("InvokeAgent.yaml", "InvokeAgent.json")]
[InlineData("InvokeAgent.yaml", "InvokeAgent.json", true)]
[InlineData("ConversationMessages.yaml", "ConversationMessages.json")]
public Task ValidateCaseAsync(string workflowFileName, string testcaseFileName) =>
this.RunWorkflowAsync(Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName), testcaseFileName);
[InlineData("ConversationMessages.yaml", "ConversationMessages.json", true)]
public Task ValidateCaseAsync(string workflowFileName, string testcaseFileName, bool externalConveration = false) =>
this.RunWorkflowAsync(Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName), testcaseFileName, externalConveration);
[Theory]
[InlineData("Marketing.yaml", "Marketing.json")]
[InlineData("MathChat.yaml", "MathChat.json")]
[InlineData("Marketing.yaml", "Marketing.json", true)]
[InlineData("MathChat.yaml", "MathChat.json", true)]
[InlineData("DeepResearch.yaml", "DeepResearch.json", Skip = "Long running")]
[InlineData("HumanInLoop.yaml", "HumanInLoop.json", Skip = "Needs test support")]
public Task ValidateScenarioAsync(string workflowFileName, string testcaseFileName) =>
this.RunWorkflowAsync(Path.Combine(GetRepoFolder(), "workflow-samples", workflowFileName), testcaseFileName);
public Task ValidateScenarioAsync(string workflowFileName, string testcaseFileName, bool externalConveration = false) =>
this.RunWorkflowAsync(Path.Combine(GetRepoFolder(), "workflow-samples", workflowFileName), testcaseFileName, externalConveration);
protected override async Task RunAndVerifyAsync<TInput>(Testcase testcase, string workflowPath, DeclarativeWorkflowOptions workflowOptions)
{
@@ -42,6 +45,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
Assert.NotEmpty(workflowEvents.ExecutorInvokeEvents);
Assert.NotEmpty(workflowEvents.ExecutorCompleteEvents);
AssertWorkflow.Conversation(workflowOptions.ConversationId, testcase.Validation.ConversationCount, workflowEvents.ConversationEvents);
AssertWorkflow.EventCounts(workflowEvents.ActionInvokeEvents.Count, testcase);
AssertWorkflow.EventCounts(workflowEvents.ActionCompleteEvents.Count, testcase);
AssertWorkflow.EventSequence(workflowEvents.ActionInvokeEvents.Select(e => e.ActionId), testcase);
@@ -51,14 +51,16 @@ public sealed class TestcaseInput
public sealed class TestcaseValidation
{
[JsonConstructor]
public TestcaseValidation(int minActionCount, int? maxActionCount = null, TestcaseValidationActions? actions = null)
public TestcaseValidation(int conversationCount, int minActionCount, int? maxActionCount = null, TestcaseValidationActions? actions = null)
{
this.ConversationCount = conversationCount;
this.MinActionCount = minActionCount;
this.MaxActionCount = maxActionCount;
this.Actions = actions ?? new TestcaseValidationActions([]);
}
public TestcaseValidationActions Actions { get; }
public int ConversationCount { get; }
public int MinActionCount { get; }
public int? MaxActionCount { get; }
}
@@ -14,12 +14,14 @@ internal sealed class WorkflowEvents
this.EventCounts = workflowEvents.GroupBy(e => e.GetType()).ToDictionary(e => e.Key, e => e.Count());
this.ActionInvokeEvents = workflowEvents.OfType<DeclarativeActionInvokedEvent>().ToList();
this.ActionCompleteEvents = workflowEvents.OfType<DeclarativeActionCompletedEvent>().ToList();
this.ConversationEvents = workflowEvents.OfType<ConversationUpdateEvent>().ToList();
this.ExecutorInvokeEvents = workflowEvents.OfType<ExecutorInvokedEvent>().ToList();
this.ExecutorCompleteEvents = workflowEvents.OfType<ExecutorCompletedEvent>().ToList();
}
public IReadOnlyList<WorkflowEvent> Events { get; }
public IReadOnlyDictionary<Type, int> EventCounts { get; }
public IReadOnlyList<ConversationUpdateEvent> ConversationEvents { get; }
public IReadOnlyList<DeclarativeActionInvokedEvent> ActionInvokeEvents { get; }
public IReadOnlyList<DeclarativeActionCompletedEvent> ActionCompleteEvents { get; }
public IReadOnlyList<ExecutorInvokedEvent> ExecutorInvokeEvents { get; }
@@ -21,9 +21,15 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
/// </summary>
public abstract class WorkflowTest(ITestOutputHelper output) : IntegrationTest(output)
{
protected abstract Task RunAndVerifyAsync<TInput>(Testcase testcase, string workflowPath, DeclarativeWorkflowOptions workflowOptions) where TInput : notnull;
protected abstract Task RunAndVerifyAsync<TInput>(
Testcase testcase,
string workflowPath,
DeclarativeWorkflowOptions workflowOptions) where TInput : notnull;
protected Task RunWorkflowAsync(string workflowPath, string testcaseFileName)
protected Task RunWorkflowAsync(
string workflowPath,
string testcaseFileName,
bool externalConversation = false)
{
this.Output.WriteLine($"WORKFLOW: {workflowPath}");
this.Output.WriteLine($"TESTCASE: {testcaseFileName}");
@@ -45,7 +51,8 @@ public abstract class WorkflowTest(ITestOutputHelper output) : IntegrationTest(o
protected async Task TestWorkflowAsync<TInput>(
Testcase testcase,
string workflowPath,
IConfiguration configuration) where TInput : notnull
IConfiguration configuration,
bool externalConversation = false) where TInput : notnull
{
this.Output.WriteLine($"INPUT: {testcase.Setup.Input.Value}");
@@ -59,12 +66,22 @@ public abstract class WorkflowTest(ITestOutputHelper output) : IntegrationTest(o
.AddInMemoryCollection(agentMap)
.Build();
AzureAgentProvider agentProvider = new(foundryConfig.Endpoint, new AzureCliCredential());
string? conversationId = null;
if (externalConversation)
{
conversationId = await agentProvider.CreateConversationAsync().ConfigureAwait(false);
}
DeclarativeWorkflowOptions workflowOptions =
new(new AzureAgentProvider(foundryConfig.Endpoint, new AzureCliCredential()))
new(agentProvider)
{
Configuration = workflowConfig,
ConversationId = conversationId,
LoggerFactory = this.Output
};
await this.RunAndVerifyAsync<TInput>(testcase, workflowPath, workflowOptions);
}
@@ -103,6 +120,18 @@ public abstract class WorkflowTest(ITestOutputHelper output) : IntegrationTest(o
protected static class AssertWorkflow
{
public static void Conversation(string? conversationId, int expectedCount, IReadOnlyList<ConversationUpdateEvent> conversationEvents)
{
if (string.IsNullOrEmpty(conversationId))
{
Assert.Equal(expectedCount, conversationEvents.Count);
}
else
{
Assert.Equal(expectedCount - 1, conversationEvents.Count);
}
}
public static void EventCounts(int actualCount, Testcase testcase)
{
Assert.True(actualCount >= testcase.Validation.MinActionCount, $"Event count less than expected: {testcase.Validation.MinActionCount} ({actualCount}).");
@@ -7,6 +7,7 @@
}
},
"validation": {
"conversation_count": 3,
"min_action_count": 7,
"actions": {
"start": [
@@ -7,6 +7,7 @@
}
},
"validation": {
"conversation_count": 2,
"min_action_count": 25,
"max_action_count": 56,
"actions": {
@@ -7,6 +7,7 @@
}
},
"validation": {
"conversation_count": 1,
"min_action_count": 1,
"actions": {
"start": [
@@ -7,6 +7,7 @@
}
},
"validation": {
"conversation_count": 2,
"min_action_count": 1,
"actions": {
"start": [
@@ -7,6 +7,7 @@
}
},
"validation": {
"conversation_count": 1,
"min_action_count": 4,
"actions": {
"start": [
@@ -7,6 +7,7 @@
}
},
"validation": {
"conversation_count": 1,
"min_action_count": 6,
"max_action_count": 56,
"actions": {
@@ -7,6 +7,7 @@
}
},
"validation": {
"conversation_count": 1,
"min_action_count": 3,
"actions": {
"start": [
@@ -12,4 +12,5 @@ trigger:
input:
messages: =[UserMessage(System.LastMessageText)]
output:
autoSend: false
messages: Local.Answer
@@ -216,7 +216,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
WorkflowFormulaState state = new(RecalcEngineFactory.Create());
Mock<WorkflowAgentProvider> mockAgentProvider = CreateMockProvider();
DeclarativeWorkflowOptions options = new(mockAgentProvider.Object);
WorkflowActionVisitor visitor = new(new DeclarativeWorkflowExecutor<string>(WorkflowActionVisitor.Steps.Root("anything"), mockAgentProvider.Object, state, (message) => DeclarativeWorkflowBuilder.DefaultTransform(message)), state, options);
WorkflowActionVisitor visitor = new(new DeclarativeWorkflowExecutor<string>(WorkflowActionVisitor.Steps.Root("anything"), options, state, (message) => DeclarativeWorkflowBuilder.DefaultTransform(message)), state, options);
WorkflowElementWalker walker = new(visitor);
walker.Visit(dialog);
Assert.True(visitor.HasUnsupportedActions);