|
1 | 1 | // Copyright (c) Microsoft. All rights reserved.
|
2 | 2 |
|
3 | 3 | using System.ClientModel;
|
| 4 | +using System.Text; |
| 5 | +using Azure.AI.Agents.Persistent; |
4 | 6 | using Azure.Identity;
|
5 | 7 | using Microsoft.SemanticKernel;
|
6 |
| -using Microsoft.SemanticKernel.Agents; |
7 | 8 | using Microsoft.SemanticKernel.Agents.AzureAI;
|
8 | 9 | using Microsoft.SemanticKernel.Agents.OpenAI;
|
9 | 10 | using OpenAI;
|
@@ -33,77 +34,137 @@ public Step06_FoundryAgentProcess(ITestOutputHelper output) : base(output, redir
|
33 | 34 | [Fact]
|
34 | 35 | public async Task ProcessWithTwoAgentMathChat()
|
35 | 36 | {
|
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 | + { |
82 | 94 | { "Question", "_variables_.TeacherMessages" },
|
83 | 95 | { "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 | + } |
92 | 125 |
|
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(); |
96 | 130 |
|
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); |
100 | 135 |
|
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 ?? []); |
104 | 138 |
|
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 | + } |
107 | 168 | }
|
108 | 169 |
|
109 | 170 | /// <summary>
|
|
0 commit comments