In the previous blog posts I ported a Python application to C#. However, I did not take advantage of the Microsoft Agent Framework (MAF). In this admittedly long post, I’ll migrate that code to MAF.
The Microsoft Agent Framework (MAF) is an open-source software development kit (SDK) designed to facilitate the creation of agentic AI solutions and multi-agent workflows, primarily utilizing Python and .NET.
In the previous blog posts we ported a Python implementation of an agentic application to C# and Microsoft Agent Framework. We used interfaces, but we did not use Dependency Injection (DI). It is pretty easy to add.
Agents, tools, executors and workflows all depend on interfaces, and DI depends on registering the relationship between these interfaces and the concrete class that implements them.
For example, here is how you create a workflow
services.AddSingleton<Workflow>(sp =>
{
var blogger = sp.GetRequiredService<BloggerExecutor>();
var researcher = sp.GetRequiredService<ResearcherExecutor>();
var author = sp.GetRequiredService<AuthorExecutor>();
var reviewer = sp.GetRequiredService<ReviewerExecutor>();
return new WorkflowBuilder(blogger)
.AddEdge(blogger, researcher)
.AddEdge(researcher, author)
.AddEdge(author, reviewer)
.AddEdge<ResearchState>(reviewer, author, s => s?.NeedsRevision == true)
.WithOutputFrom(reviewer)
.Build();
});
In the previous post we finished up creating our agents. You’ll remember that each of the agents declared nodes. We’re finally going to put them to use in a class BlogWorkflow. However, up to now we’ve not fully taken advantage of the Microsoft Agent Framework (MAF). Let’s fix that up first. To do so we’ll add a class BlogExecutors. We’ll create MAF workflow executors that will wrap existing “node” chains so that the business logic is reused unchanged.
Note: In this post we’re only going to move towards Microsoft Agent Framework from where things currently stand. To see this fully migrated to an application that truly utilizes MAF, navigate to here
Let’s start with the BloggerExecutor which will allow the blogger to plan the task and seed the sub-task:
using Microsoft.Agents.AI.Workflows;
namespace BlogMigration;
/// <summary>Entry executor: lets the blogger plan the task and seed the sub-task.</summary>
internal sealed partial class BloggerExecutor(IBloggerChain blogger) : Executor("Blogger")
{
[MessageHandler]
private async ValueTask<ResearchState> HandleAsync(ResearchState state, IWorkflowContext context)
=> await blogger.BloggerNodeAsync(state);
}
An executor is a node in Microsoft Agent Framework. Each executor can receive messages, process them and emit new messages. In this case, whenever the workflow engine routes a ResearchState message to the executor, this method is invoked. The method takes the current workflow state and the workflow context and returns the updated ResearchState.
The executor is just a thin wrapper around the Blogger agent. The handler calls the Blogger node and the node updates the state.
In the previous post we looked at implementing the Researcher in C#. In this, as promised, we’ll look at the Author and the Reviewer.
The Author is handed two objects when instantiated: the llm (an IChatClient object) and the chatOptions. Its primary method is InvokeAsync, which is passed the current ResearchState.
public class AuthorChain(IChatClient llm, ChatOptions chatOptions) : IAuthorChain
{
public async Task<string> InvokeAsync(ResearchState state)
{
List<string> research = state.ResearchFindings;
string researchText = research.Count > 0 ? string.Join("\n\n", research) : "No research available.";
Its expectation is that the state object will have research information from the Researcher. It creates its prompt based on the state and then sends that prompt, along with its options, to the llm. What it gets back is its first draft which it will pass to the Reviewer
In the previous blog post (Part 2) we began the migration by setting up the configuration. In this post, we’ll tackle the Blogger, which acts as an orchestrator for the agents.
In the python version of our program the blogger_prompt_template is in its own cell and fairly short. In the C# version we create a file Prompts.cs. The class is static and has a const string for each prompt. Let’s start with the Blogger prompt:
namespace BlogMigration;
public static class Prompts
{
public const string BloggerPromptTemplate = """
You are a blogger managing a blog post creation workflow.
Current Task: {main_task}
Current State:
- Research Findings: {research_findings}
- Blog Draft: {draft}
- Reviewer Feedback: {review_notes}
- Revision Number: {revision_number}
Your goal is to ensure a clear, engaging, and valuable blog post targeted at software developers.
Decide the next step and respond only with a JSON object (no extra text):
{
"next_step": "researcher" or "author" or "END",
"task_description": "Brief description of what needs to be done next"
}
Decision Rules:
- If no research exists, choose "researcher"
- If research exists but no draft, choose "author"
- If draft exists and reviewer says "APPROVED", choose "END"
- If draft needs revision, choose "author"
- If revision_number >= 4, choose "END"
""";
The triple quotes in a C# string create a raw string literal, introduced in C# 11. With this no escaping is needed and the string can be multi-line. There’s more to it, and I’ll refer you to the C# documentation.
In Part 1 of this multi-part series, I laid out my goal to migrate the Python agentics program from the previous series to C#. To do this migration I’m going to work my way down through my Python script and refactor it breaking out classes and refactoring to use Microsoft Agent Framework.
Note: to make sense of this code, you’ll want to start with the Python example. The code for that begins here.
We begin with bringing in the config.json file. We’ll use the identical file, and bring it into Program.cs
const string fileName = "config.json";
using var stream = File.OpenRead(fileName);
using var document = JsonDocument.Parse(stream);
JsonElement config = document.RootElement;
string? GetValue(string key) =>
config.TryGetProperty(key, out JsonElement value) ? value.GetString() : null;
Environment.SetEnvironmentVariable("OPENAI_API_KEY", GetValue("API_KEY"));
Environment.SetEnvironmentVariable("OPENAI_BASE_URL", GetValue("OPENAI_API_BASE"));
Environment.SetEnvironmentVariable("TAVILY_API_KEY", GetValue("TAVILY_API_KEY"));
string modelName = "gpt-4o-mini";
var openAIClient = new OpenAIClient(
new ApiKeyCredential(Environment.GetEnvironmentVariable("OPENAI_API_KEY")!),
new OpenAIClientOptions
{
Endpoint = new Uri(Environment.GetEnvironmentVariable("OPENAI_BASE_URL")!)
});
In part 4 of this series we created our final two agents. In this final part of the series we’ll review the workflow that we create with the StateGraph class of LangGraph.
In part 3 we looked at creating the researcher. As promised, today we’ll look at the author.
You’ll notice in the following code a great deal of similarity to what we’ve seen before. The goal is to create a code “template” that we can follow as we create any agent; departing only for the agent’s special requirements and abilities.
As usual, we start with the factory method:
def create_author_chain():
"""Creates the author chain."""
def author_invoke(state):
research = state.get("research_findings", [])
research_text = "\n\n".join(research) if research else "No research available."
prompt = author_prompt_template.format(
main_task=state.get("main_task", ""),
research_findings=research_text,
draft=state.get("draft", ""),
review_notes=state.get("review_notes", "")
)
try:
response = llm.invoke(prompt)
content = response.content if hasattr(response, 'content') else str(response)
return content if content else "Draft in progress..."
except Exception as e:
print(f"Author error: {e}")
return "Error generating draft. Please try again."
return author_invoke
# Creating a callable object
author_chain = create_author_chain()
In the previous post, we examined how to load the libraries we need and how to create the Blogger agent. In this post, we’ll examine the Research agent. You’ll no doubt notice the pattern of defining the template, the agent and the node. This will carry through for all the agents we’ll create.
researcher_prompt_template = """You are a researcher for a technical blog
focused on .NET and AI with examples in C# and Python
Research Topic: {task}
Your goal is to find relevant, up-to-date insights for developers. Focus on:
- Key trends, challenges, or innovations
- Real-world use cases
- Supporting data or quotes from credible sources
- Simple explanations
- Short code examples in C# or Python
Summarize your findings concisely.
"""
In this template we start by telling the researcher what role it will play. We then provide a goal and narrow that goal to a series of topics to focus on and how to present that data.