Skip to content

Commit c00e729

Browse files
authored
.Net: Adding Foundry workflow management client. (#12326)
### Description - Adding Foundry workflow management client to the project. - Updated Foundry process sample with deployment, execution, and cleanup. ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [ ] The code builds clean without any errors or warnings - [ ] 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 - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone 😄
1 parent f9b4a15 commit c00e729

File tree

8 files changed

+625
-70
lines changed

8 files changed

+625
-70
lines changed

dotnet/samples/GettingStartedWithProcesses/Step06/Step06_FoundryAgentProcess.cs

Lines changed: 127 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System.ClientModel;
4+
using System.Text;
5+
using Azure.AI.Agents.Persistent;
46
using Azure.Identity;
57
using Microsoft.SemanticKernel;
6-
using Microsoft.SemanticKernel.Agents;
78
using Microsoft.SemanticKernel.Agents.AzureAI;
89
using Microsoft.SemanticKernel.Agents.OpenAI;
910
using OpenAI;
@@ -33,77 +34,137 @@ public Step06_FoundryAgentProcess(ITestOutputHelper output) : base(output, redir
3334
[Fact]
3435
public async Task ProcessWithTwoAgentMathChat()
3536
{
36-
// Define the agents. IMPORTANT: replace with your own agent IDs
37-
var studentDefinition = new AgentDefinition { Id = "{YOUR_STUDENT_AGENT_ID}", Name = "Student", Type = AzureAIAgentFactory.AzureAIAgentType };
38-
var teacherDefinition = new AgentDefinition { Id = "{YOUR_TEACHER_AGENT_ID}", Name = "Teacher", Type = AzureAIAgentFactory.AzureAIAgentType };
39-
40-
// Define the process with a state type
41-
var processBuilder = new FoundryProcessBuilder<TwoAgentMathState>("two_agent_math_chat");
42-
43-
// Create a thread for the student
44-
processBuilder.AddThread("Student", KernelProcessThreadLifetime.Scoped);
45-
processBuilder.AddThread("Teacher", KernelProcessThreadLifetime.Scoped);
46-
47-
// Add the student
48-
var student = processBuilder.AddStepFromAgent(studentDefinition);
49-
50-
// Add the teacher
51-
var teacher = processBuilder.AddStepFromAgent(teacherDefinition);
52-
53-
/**************************** Orchestrate ***************************/
54-
55-
// When the process starts, activate the student agent
56-
processBuilder.OnProcessEnter().SendEventTo(
57-
student,
58-
thread: "_variables_.Student",
59-
messagesIn: ["_variables_.TeacherMessages"],
60-
inputs: new Dictionary<string, string>
61-
{
62-
{ "InteractionCount", "_variables_.StudentState.InteractionCount" }
63-
});
64-
65-
// When the student agent exits, update the process state to save the student's messages and update interaction counts
66-
processBuilder.OnStepExit(student)
67-
.UpdateProcessState(path: "StudentMessages", operation: StateUpdateOperations.Set, value: "_agent_.messages_out")
68-
.UpdateProcessState(path: "InteractionCount", operation: StateUpdateOperations.Increment, value: 1)
69-
.UpdateProcessState(path: "StudentState.InteractionCount", operation: StateUpdateOperations.Increment, value: 1)
70-
.UpdateProcessState(path: "StudentState.Name", operation: StateUpdateOperations.Set, value: "Runhan");
71-
72-
// When the student agent is finished, send the messages to the teacher agent
73-
processBuilder.OnEvent(student, "_default_")
74-
.SendEventTo(teacher, messagesIn: ["_variables_.StudentMessages"], thread: "Teacher");
75-
76-
// When the teacher agent exits with a message containing '[COMPLETE]', update the process state to save the teacher's messages and update interaction counts and emit the `correct_answer` event
77-
processBuilder.OnStepExit(teacher, condition: "contains(to_string(_agent_.messages_out), '[COMPLETE]')")
78-
.EmitEvent(
79-
eventName: "correct_answer",
80-
payload: new Dictionary<string, string>
81-
{
37+
var endpoint = TestConfiguration.AzureAI.Endpoint;
38+
PersistentAgentsClient client = new(endpoint.TrimEnd('/'), new DefaultAzureCredential(), new PersistentAgentsAdministrationClientOptions().WithPolicy(endpoint, "2025-05-15-preview"));
39+
40+
Azure.Response<PersistentAgent>? studentAgent = null;
41+
Azure.Response<PersistentAgent>? teacherAgent = null;
42+
43+
try
44+
{
45+
// Create the single agents
46+
studentAgent = await client.Administration.CreateAgentAsync(
47+
model: "gpt-4o",
48+
name: "Student",
49+
instructions: "You are a student that answer question from teacher, when teacher gives you question you answer them."
50+
);
51+
52+
teacherAgent = await client.Administration.CreateAgentAsync(
53+
model: "gpt-4o",
54+
name: "Teacher",
55+
instructions: "You are a teacher that create pre-school math question for student and check answer.\nIf the answer is correct, you stop the conversation by saying [COMPLETE].\nIf the answer is wrong, you ask student to fix it."
56+
);
57+
58+
// Define the process with a state type
59+
var processBuilder = new FoundryProcessBuilder<TwoAgentMathState>("two_agent_math_chat");
60+
61+
// Create a thread for the student
62+
processBuilder.AddThread("Student", KernelProcessThreadLifetime.Scoped);
63+
processBuilder.AddThread("Teacher", KernelProcessThreadLifetime.Scoped);
64+
65+
// Add the student
66+
var student = processBuilder.AddStepFromAgent(studentAgent);
67+
68+
// Add the teacher
69+
var teacher = processBuilder.AddStepFromAgent(teacherAgent);
70+
71+
/**************************** Orchestrate ***************************/
72+
73+
// When the process starts, activate the student agent
74+
processBuilder.OnProcessEnter().SendEventTo(
75+
student,
76+
thread: "_variables_.Student",
77+
messagesIn: ["_variables_.TeacherMessages"],
78+
inputs: new Dictionary<string, string> { });
79+
80+
// When the student agent exits, update the process state to save the student's messages and update interaction counts
81+
processBuilder.OnStepExit(student)
82+
.UpdateProcessState(path: "StudentMessages", operation: StateUpdateOperations.Set, value: "_agent_.messages_out");
83+
84+
// When the student agent is finished, send the messages to the teacher agent
85+
processBuilder.OnEvent(student, "_default_")
86+
.SendEventTo(teacher, messagesIn: ["_variables_.StudentMessages"], thread: "Teacher");
87+
88+
// When the teacher agent exits with a message containing '[COMPLETE]', update the process state to save the teacher's messages and update interaction counts and emit the `correct_answer` event
89+
processBuilder.OnStepExit(teacher, condition: "jmespath(contains(to_string(_agent_.messages_out), '[COMPLETE]'))")
90+
.EmitEvent(
91+
eventName: "correct_answer",
92+
payload: new Dictionary<string, string>
93+
{
8294
{ "Question", "_variables_.TeacherMessages" },
8395
{ "Answer", "_variables_.StudentMessages" }
84-
})
85-
.UpdateProcessState(path: "_variables_.TeacherMessages", operation: StateUpdateOperations.Set, value: "_agent_.messages_out")
86-
.UpdateProcessState(path: "_variables_.InteractionCount", operation: StateUpdateOperations.Increment, value: 1);
87-
88-
// When the teacher agent exits with a message not containing '[COMPLETE]', update the process state to save the teacher's messages and update interaction counts
89-
processBuilder.OnStepExit(teacher, condition: "_default_")
90-
.UpdateProcessState(path: "_variables_.TeacherMessages", operation: StateUpdateOperations.Set, value: "_agent_.messages_out")
91-
.UpdateProcessState(path: "_variables_.InteractionCount", operation: StateUpdateOperations.Increment, value: 1);
96+
})
97+
.UpdateProcessState(path: "_variables_.TeacherMessages", operation: StateUpdateOperations.Set, value: "_agent_.messages_out");
98+
99+
// When the teacher agent exits with a message not containing '[COMPLETE]', update the process state to save the teacher's messages and update interaction counts
100+
processBuilder.OnStepExit(teacher, condition: "_default_")
101+
.UpdateProcessState(path: "_variables_.TeacherMessages", operation: StateUpdateOperations.Set, value: "_agent_.messages_out");
102+
103+
// When the teacher agent is finished, send the messages to the student agent
104+
processBuilder.OnEvent(teacher, "_default_", condition: "_default_")
105+
.SendEventTo(student, messagesIn: ["_variables_.TeacherMessages"], thread: "Student");
106+
107+
// When the teacher agent emits the `correct_answer` event, stop the process
108+
processBuilder.OnEvent(teacher, "correct_answer")
109+
.StopProcess();
110+
111+
// Verify that the process can be built and serialized to json
112+
var processJson = await processBuilder.ToJsonAsync();
113+
Assert.NotEmpty(processJson);
114+
115+
var content = await RunWorkflowAsync(client, processBuilder, [new(MessageRole.User, "Go")]);
116+
Assert.NotEmpty(content);
117+
}
118+
finally
119+
{
120+
// Clean up the agents
121+
await client.Administration.DeleteAgentAsync(studentAgent?.Value.Id);
122+
await client.Administration.DeleteAgentAsync(teacherAgent?.Value.Id);
123+
}
124+
}
92125

93-
// When the teacher agent is finished, send the messages to the student agent
94-
processBuilder.OnEvent(teacher, "_default_", condition: "_default_")
95-
.SendEventTo(student, messagesIn: ["_variables_.TeacherMessages"], thread: "Student");
126+
private async Task<string> RunWorkflowAsync<T>(PersistentAgentsClient client, FoundryProcessBuilder<T> processBuilder, List<ThreadMessageOptions>? initialMessages = null) where T : class, new()
127+
{
128+
Workflow? workflow = null;
129+
StringBuilder output = new();
96130

97-
// When the teacher agent emits the `correct_answer` event, stop the process
98-
processBuilder.OnEvent(teacher, "correct_answer")
99-
.StopProcess();
131+
try
132+
{
133+
// publish the workflow
134+
workflow = await client.Administration.Pipeline.PublishWorkflowAsync(processBuilder);
100135

101-
// Verify that the process can be built and serialized to json
102-
var processJson = await processBuilder.ToJsonAsync();
103-
Assert.NotEmpty(processJson);
136+
// threadId is used to store the thread ID
137+
PersistentAgentThread thread = await client.Threads.CreateThreadAsync(messages: initialMessages ?? []);
104138

105-
var foundryWorkflowId = await processBuilder.DeployToFoundryAsync(TestConfiguration.AzureAI.WorkflowEndpoint);
106-
Assert.NotEmpty(foundryWorkflowId);
139+
// create run
140+
await foreach (var run in client.Runs.CreateRunStreamingAsync(thread.Id, workflow.Id))
141+
{
142+
if (run is Azure.AI.Agents.Persistent.MessageContentUpdate contentUpdate)
143+
{
144+
output.Append(contentUpdate.Text);
145+
Console.Write(contentUpdate.Text);
146+
}
147+
else if (run is Azure.AI.Agents.Persistent.RunUpdate runUpdate)
148+
{
149+
if (runUpdate.UpdateKind == Azure.AI.Agents.Persistent.StreamingUpdateReason.RunInProgress && !runUpdate.Value.Id.StartsWith("wf_run", StringComparison.OrdinalIgnoreCase))
150+
{
151+
Console.WriteLine();
152+
Console.Write($"{runUpdate.Value.Metadata["x-agent-name"]}> ");
153+
}
154+
}
155+
}
156+
157+
// delete thread, so we can start over
158+
Console.WriteLine($"\nDeleting thread {thread?.Id}...");
159+
await client.Threads.DeleteThreadAsync(thread?.Id);
160+
return output.ToString();
161+
}
162+
finally
163+
{
164+
// // delete workflow
165+
Console.WriteLine($"Deleting workflow {workflow?.Id}...");
166+
await client.Administration.Pipeline.DeleteWorkflowAsync(workflow!);
167+
}
107168
}
108169

109170
/// <summary>

dotnet/src/Agents/AzureAI/Agents.AzureAI.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
</PropertyGroup>
2020

2121
<ItemGroup>
22-
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/azure/Policies/GeneratedActionPipelinePolicy.cs" Link="%(RecursiveDir)Azure/%(Filename)%(Extension)" />
2322
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/planning/Schema/JsonSchemaFunctionParameters.cs" Link="%(RecursiveDir)Schema/%(Filename)%(Extension)" />
2423
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/src/Diagnostics/*" Link="%(RecursiveDir)Utilities/%(Filename)%(Extension)" />
2524
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/src/Http/*" Link="%(RecursiveDir)Http/%(Filename)%(Extension)" />
@@ -31,6 +30,7 @@
3130
</ItemGroup>
3231

3332
<Import Project="$(RepoRoot)/dotnet/src/InternalUtilities/agents/AgentUtilities.props" />
33+
<Import Project="$(RepoRoot)/dotnet/src/InternalUtilities/azure/AzureAIUtilities.props" />
3434

3535
<ItemGroup>
3636
<ProjectReference Include="..\Abstractions\Agents.Abstractions.csproj" />
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using Azure.AI.Agents.Persistent;
5+
using Azure.Core;
6+
7+
namespace Microsoft.SemanticKernel.Agents.AzureAI;
8+
9+
/// <summary>
10+
/// Extensions for configuring the PersistentAgentsAdministrationClientOptions with a routing policy for Foundry Workflows.
11+
/// </summary>
12+
public static class FoundryWorkflowExtensions
13+
{
14+
/// <summary>
15+
/// Adds a routing policy to the PersistentAgentsAdministrationClientOptions for Foundry Workflows.
16+
/// </summary>
17+
/// <param name="options"></param>
18+
/// <param name="endpoint"></param>
19+
/// <param name="apiVersion"></param>
20+
/// <returns></returns>
21+
/// <exception cref="ArgumentException"></exception>
22+
public static PersistentAgentsAdministrationClientOptions WithPolicy(this PersistentAgentsAdministrationClientOptions options, string endpoint, string apiVersion)
23+
{
24+
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var _endpoint))
25+
{
26+
throw new ArgumentException("The endpoint must be an absolute URI.", nameof(endpoint));
27+
}
28+
29+
options.AddPolicy(new HttpPipelineRoutingPolicy(_endpoint, apiVersion), HttpPipelinePosition.PerCall);
30+
31+
return options;
32+
}
33+
}

dotnet/src/Experimental/Process.Core/FoundryProcessBuilder.cs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Text.Json.Serialization;
99
using System.Threading;
1010
using System.Threading.Tasks;
11+
using Azure.AI.Agents.Persistent;
1112
using Azure.Core;
1213
using Azure.Identity;
1314
using Microsoft.SemanticKernel.Agents;
@@ -50,19 +51,42 @@ public ProcessBuilder AddThread(string threadName, KernelProcessThreadLifetime t
5051
/// Adds a step to the process from a declarative agent.
5152
/// </summary>
5253
/// <param name="agentDefinition">The <see cref="AgentDefinition"/></param>
53-
/// <param name="id">The unique Id of the step. If not provided, the name of the step Type will be used.</param>
54+
/// <param name="stepId">The unique Id of the step. If not provided, the name of the step Type will be used.</param>
5455
/// <param name="aliases">Aliases that have been used by previous versions of the step, used for supporting backward compatibility when reading old version Process States</param>
5556
/// <param name="defaultThread">Specifies the thread reference to be used by the agent. If not provided, the agent will create a new thread for each invocation.</param>
5657
/// <param name="humanInLoopMode">Specifies the human-in-the-loop mode for the agent. If not provided, the default is <see cref="HITLMode.Never"/>.</param>
57-
public ProcessAgentBuilder<TProcessState> AddStepFromAgent(AgentDefinition agentDefinition, string? id = null, IReadOnlyList<string>? aliases = null, string? defaultThread = null, HITLMode humanInLoopMode = HITLMode.Never)
58+
public ProcessAgentBuilder<TProcessState> AddStepFromAgent(AgentDefinition agentDefinition, string? stepId = null, IReadOnlyList<string>? aliases = null, string? defaultThread = null, HITLMode humanInLoopMode = HITLMode.Never)
5859
{
5960
Verify.NotNull(agentDefinition);
6061
if (agentDefinition.Type != AzureAIAgentFactory.AzureAIAgentType)
6162
{
6263
throw new ArgumentException($"The agent type '{agentDefinition.Type}' is not supported. Only '{AzureAIAgentFactory.AzureAIAgentType}' is supported.");
6364
}
6465

65-
return this._processBuilder.AddStepFromAgent<TProcessState>(agentDefinition, id, aliases, defaultThread, humanInLoopMode);
66+
return this._processBuilder.AddStepFromAgent<TProcessState>(agentDefinition, stepId, aliases, defaultThread, humanInLoopMode);
67+
}
68+
69+
/// <summary>
70+
/// Adds a step to the process from a <see cref="PersistentAgent"/>.
71+
/// </summary>
72+
/// <param name="persistentAgent">The <see cref="AgentDefinition"/></param>
73+
/// <param name="stepId">The unique Id of the step. If not provided, the name of the step Type will be used.</param>
74+
/// <param name="aliases">Aliases that have been used by previous versions of the step, used for supporting backward compatibility when reading old version Process States</param>
75+
/// <param name="defaultThread">Specifies the thread reference to be used by the agent. If not provided, the agent will create a new thread for each invocation.</param>
76+
/// <param name="humanInLoopMode">Specifies the human-in-the-loop mode for the agent. If not provided, the default is <see cref="HITLMode.Never"/>.</param>
77+
public ProcessAgentBuilder<TProcessState> AddStepFromAgent(PersistentAgent persistentAgent, string? stepId = null, IReadOnlyList<string>? aliases = null, string? defaultThread = null, HITLMode humanInLoopMode = HITLMode.Never)
78+
{
79+
Verify.NotNull(persistentAgent);
80+
81+
var agentDefinition = new AgentDefinition
82+
{
83+
Id = persistentAgent.Id,
84+
Type = AzureAIAgentFactory.AzureAIAgentType,
85+
Name = persistentAgent.Name,
86+
Description = persistentAgent.Description
87+
};
88+
89+
return this._processBuilder.AddStepFromAgent<TProcessState>(agentDefinition, stepId, aliases, defaultThread, humanInLoopMode);
6690
}
6791

6892
/// <summary>
@@ -205,6 +229,16 @@ public async Task<string> ToJsonAsync()
205229
return WorkflowSerializer.SerializeToJson(workflow);
206230
}
207231

232+
/// <summary>
233+
/// Serializes the process to YAML.
234+
/// </summary>
235+
public async Task<string> ToYamlAsync()
236+
{
237+
var process = this.Build();
238+
var workflow = await WorkflowBuilder.BuildWorkflow(process).ConfigureAwait(false);
239+
return WorkflowSerializer.SerializeToYaml(workflow);
240+
}
241+
208242
private class FoundryWorkflow
209243
{
210244
[JsonPropertyName("id")]

0 commit comments

Comments
 (0)