<?xml version="1.0" encoding="UTF-8" standalone="no"?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" version="2.0">

<channel>
	<title>Jesse Liberty - Silverlight Geek</title>
	<atom:link href="https://jesseliberty.com/feed/" rel="self" type="application/rss+xml"/>
	<link>https://jesseliberty.com</link>
	<description>More Signal - Less Noise</description>
	<lastBuildDate>Tue, 30 Jun 2026 10:52:16 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>
	<item>
		<title>Migrating C# -&gt; Microsoft Agent Framework</title>
		<link>https://jesseliberty.com/2026/06/28/migrating-c-microsoft-agent-framework/</link>
					<comments>https://jesseliberty.com/2026/06/28/migrating-c-microsoft-agent-framework/#respond</comments>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Sun, 28 Jun 2026 12:33:05 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Microsoft Agent Framework]]></category>
		<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13378</guid>

					<description><![CDATA[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&#8217;ll migrate that code to MAF. Note, the updated source code &#8230; <a href="https://jesseliberty.com/2026/06/28/migrating-c-microsoft-agent-framework/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>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&#8217;ll migrate that code to MAF.</p>



<p><em>Note, the updated source code for the .NET version of this demo application is available at&nbsp;</em><a href="https://github.com/JesseLiberty/blogMigration---public">https://github.com/JesseLiberty/blogMigration—public</a></p>



<figure class="wp-block-image size-large is-resized"><img fetchpriority="high" decoding="async" width="800" height="740" src="https://jesseliberty.com/wp-content/uploads/2026/06/wrench-800x740.jpg" alt="" class="wp-image-13379" style="aspect-ratio:1.081108337745506;width:318px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/06/wrench-800x740.jpg 800w, https://jesseliberty.com/wp-content/uploads/2026/06/wrench-300x278.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/06/wrench-150x139.jpg 150w, https://jesseliberty.com/wp-content/uploads/2026/06/wrench-768x711.jpg 768w, https://jesseliberty.com/wp-content/uploads/2026/06/wrench.jpg 922w" sizes="(max-width: 800px) 100vw, 800px" /></figure>



<p>The <strong>Microsoft Agent Framework (MAF)</strong> 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. </p>



<span id="more-13378"></span>



<p>MAF consolidates various tools and frameworks, including AutoGen and Semantic Kernel, into a single, cohesive development environment. </p>



<h3 class="wp-block-heading">Enterprise Focus</h3>



<p>The framework is tailored to meet the complexities of deploying agentic AI in enterprise settings. It incorporates observability, security, and compliance.</p>



<h3 class="wp-block-heading">Multi-Agent Workflows</h3>



<p>One of the standout features of MAF is its support for advanced orchestration of multiple agents. This capability enables different AI agents to collaborate effectively, which is crucial for applications that demand coordinated efforts across various tasks and functions. This is perfect for the application we&#8217;ve been working on.</p>



<h3 class="wp-block-heading">Integration with Azure</h3>



<p>MAF is deeply integrated with Azure services, allowing developers to harness cloud capabilities for enhanced scalability and performance. This integration ensures that applications built with MAF can efficiently handle varying workloads and user demands.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>Agentic AI</strong> refers to AI systems capable of acting autonomously and making decisions based on their environment. The Microsoft Agent Framework simplifies the development and management of these systems by providing a comprehensive set of tools and libraries</p>
</blockquote>



<p>With all that in mind, let&#8217;s look at modifying the raw C# to leverage the MAF&#8230;</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Note: as part of this port to Microsoft Agent Framework I made good use of the extraordinary utility <a href="https://github.com/joslat/maf-doctor/">MAF-Doctor</a>, which you can learn more about in my video interview with its author, <a href="https://www.youtube.com/jesseliberty">here</a>.</p>
</blockquote>



<p><span style="text-decoration: underline;"><strong>File Changes:</strong></span><br /><br /><strong>Prompts.cs </strong>Split each node&#8217;s prompt into a static instructions constant (the agent&#8217;s system prompt) — removed the {token} placeholder templating.<br /><br /><strong>ResearcherAgent.cs</strong> Now a ChatClientAgent with Tavily attached as a tool — the model searches and summarizes in one run. Deleted ~40 lines of manual tool invocation + JsonDocument parsing + the second summarize call.<br /><br /><strong>BloggerChain.cs</strong> LLM fallback uses structured output (RunAsync) — removed the &#8220;`-fence stripping and JsonSerializer.Deserialize. Deterministic routing preserved; comments clarify the workflow edges are the real router.<br /><br /><strong>AuthorChain.cs</strong> Converted to a ChatClientAgent; role in Instructions, state as the per-turn message.<br /><br /><strong>ReviewerChain.cs </strong>Converted to a ChatClientAgent + correctness fix: a failed review no longer auto-APPROVEDs — it requests revision instead (still bounded by MaxRevisions).<br /><br /><strong>Program.cs</strong> Added .UseFunctionInvocation() to the IChatClient pipeline — required for the researcher&#8217;s tool calls to actually execute.<br /><br /><strong>BlogWorkflow.cs </strong>Switched to RunStreamingAsync + WatchStreamAsync() to stream executor lifecycle events live; identical topology.</p>



<p>Let&#8217;s start with Prompt.cs. The comments show all the changes and the justification:</p>



<pre class="wp-block-code"><code>namespace BlogMigration;

/// &lt;summary&gt;
/// Prompt library for the blog-creation agents.
///
/// MAF idiom change (was: one big template per node mixing role + data):
/// each agent now has a *static* INSTRUCTIONS string (its system prompt / role)
/// that is set once on the &lt;c&gt;ChatClientAgent&lt;/c&gt;, while the *dynamic* state
/// (task, findings, draft, review notes) is passed per-turn as the user message.
/// Separating the durable role from the volatile input is the recommended
/// Microsoft Agent Framework pattern: it keeps the system prompt cacheable,
/// lets the model treat instructions with higher priority than user input,
/// and removes the brittle &lt;c&gt;string.Replace("{token}", ...)&lt;/c&gt; templating.
/// &lt;/summary&gt;
public static class Prompts
{
    /// &lt;summary&gt;
    /// Blogger system prompt. The concrete state is supplied as the user message;
    /// the decision is returned via MAF structured output (typed
    /// &lt;see cref="BloggerDecision"/&gt;), so this prompt no longer needs to describe
    /// the exact JSON shape or beg the model for "no extra text" — the schema is
    /// enforced by the framework.
    /// &lt;/summary&gt;
    public const string BloggerInstructions = """
You are a blogger managing a blog post creation workflow.

Your goal is to ensure a clear, engaging, and valuable blog post targeted at
software developers. Based on the current workflow state provided in the user
message, decide the next step.

Decision Rules:
- If no research exists, choose "researcher"
- If research exists but no draft, choose "author"
- If a draft exists and the reviewer said "APPROVED", choose "END"
- If the draft needs revision, choose "author"
- If revision_number &gt;= 4, choose "END"

Return the next step and a brief task description.
""";

    /// &lt;summary&gt;
    /// Researcher system prompt. The topic to research is supplied as the user
    /// message. The Tavily web-search tool is attached to the agent, so the model
    /// itself decides when to call it and then summarises the results — there is
    /// no longer any hand-written search-call + JSON-parsing orchestration.
    /// &lt;/summary&gt;
    public const string ResearcherInstructions = """
You are a researcher for a technical blog
focused on .NET and AI with examples in C# and Python.

You have access to a web-search tool. Use it to find relevant, up-to-date
insights for the topic given in the user message. 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

Call the search tool as needed, then summarize your findings concisely.
""";

    /// &lt;summary&gt;
    /// Author system prompt. The task, research findings, current draft and review
    /// notes are supplied as the user message each turn.
    /// &lt;/summary&gt;
    public const string AuthorInstructions = """
You are a professional blogger.

The user message contains the main task, the research findings, the current
draft (if any) and any reviewer notes.

Instructions:
- If this is the first draft (no current draft), create a comprehensive post based on the findings
- If there is a current draft and review notes, revise the draft to address all feedback
- Use a professional tone
- Make the post concise (aim for 250-500 words)

Write the complete post.
""";

    /// &lt;summary&gt;
    /// Reviewer system prompt. The task and the draft to review are supplied as the
    /// user message.
    /// &lt;/summary&gt;
    public const string ReviewerInstructions = """
You are a reviewer evaluating content for a blog post.

The user message contains the main task and the draft to review.

Evaluate the draft based on:
1. Hook Strength – Does the opening grab attention?
2. Clarity – Is the message easy to understand?
3. Value – Does the post offer real insights or lessons?
4. Structure – Are paragraphs short?
5. Tone – Is it authentic and professional?

Respond with one of:
- If the draft is satisfactory (minor issues are okay): "APPROVED - &#91;brief positive comment]"
- If the draft needs improvement: provide specific, actionable feedback for revision
""";
}
</code></pre>



<p>With that in place, we can turn to Researcher. The most important change here is the conversion to a <strong>ChatClientAgent </strong>&#8211; the heart of MAF. Tavily is now attached as a tool. This allowed me to remove about 40 lines of tool invocation and JSON parsing. It also eliminated a second call as shown in the comments:</p>



<pre class="wp-block-code"><code>using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

namespace BlogMigration;

/// &lt;summary&gt;
/// Researcher backed by a Microsoft Agent Framework &lt;see cref="ChatClientAgent"/&gt;.
///
/// MAF idiom change: previously this class manually invoked the Tavily tool,
/// hand-parsed the JSON response, then made a SECOND LLM call to summarise it.
/// Now the Tavily function is registered as a *tool on the agent*, so the model
/// itself decides when to search and produces the summary in a single agent run.
/// This requires the underlying &lt;see cref="IChatClient"/&gt; to have
/// function-invocation middleware enabled (wired in &lt;c&gt;Program.cs&lt;/c&gt; via
/// &lt;c&gt;UseFunctionInvocation()&lt;/c&gt;), which actually executes the tool calls the
/// model requests.
/// &lt;/summary&gt;
public class ResearcherAgent : IResearcherAgent
{
    // The agent is built once and reused for every research turn. It is stateless
    // across turns (no AgentSession is retained), which matches the original
    // per-call behaviour while gaining tool-calling for free.
    private readonly ChatClientAgent _agent;

    public ResearcherAgent(IChatClient llm, ChatOptions chatOptions, AIFunction tavilyTool)
    {
        _agent = new ChatClientAgent(llm, new ChatClientAgentOptions
        {
            // Name surfaces in OpenTelemetry traces and agent logs.
            Name = "Researcher",
            ChatOptions = new ChatOptions
            {
                // Static role/system prompt lives here instead of being concatenated
                // into every request body.
                Instructions = Prompts.ResearcherInstructions,
                // Preserve the original sampling/cost settings.
                Temperature = chatOptions.Temperature,
                MaxOutputTokens = chatOptions.MaxOutputTokens,
                // Attaching the tool lets the model call it autonomously.
                Tools = &#91;tavilyTool],
            },
        });
    }

    /// &lt;summary&gt;Execute research by letting the agent search and summarise.&lt;/summary&gt;
    public async Task&lt;string&gt; InvokeAsync(string query)
    {
        try
        {
            // A single agent run: the model may call tavily_search one or more
            // times, read the results, and return a concise summary as its text.
            AgentResponse response = await _agent.RunAsync(query);
            string summary = response.Text;

            return !string.IsNullOrEmpty(summary)
                ? summary
                : $"Research completed on: {query}. Key information has been gathered from web sources.";
        }
        catch (Exception e)
        {
            Console.WriteLine($"Research error: {e.Message}");
            return $"Research completed on: {query}. Key information has been gathered from web sources.";
        }
    }

    /// &lt;summary&gt;Research node that gathers information.&lt;/summary&gt;
    public async Task&lt;ResearchState&gt; ResearchNodeAsync(ResearchState state)
    {
        Console.WriteLine("\n&gt;&gt;&gt;RESEARCHER");

        string subTask = !string.IsNullOrEmpty(state.CurrentSubTask) ? state.CurrentSubTask : state.MainTask;
        Console.WriteLine($"Researching: {subTask}");

        string findings;
        try
        {
            findings = await InvokeAsync(subTask);
            string preview = findings.Length &gt; 100 ? findings&#91;..100] : findings;
            Console.WriteLine($"Found: {preview}...");
        }
        catch (Exception e)
        {
            Console.WriteLine($"Research error: {e.Message}");
            findings = $"Research on {subTask} - information gathered";
        }

        state.ResearchFindings.Add(findings);
        return state;
    }
}
</code></pre>



<p>Let&#8217;s turn to the Author. Again, we convert to ChatClientAgent,</p>



<pre class="wp-block-code"><code>using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

namespace BlogMigration;

/// &lt;summary&gt;
/// Author chain backed by a Microsoft Agent Framework &lt;see cref="ChatClientAgent"/&gt;.
///
/// MAF idiom change: the writing role now lives in the agent's Instructions
/// (set once), and only the volatile state (task, findings, draft, review notes)
/// is sent as the per-turn user message — replacing the previous
/// &lt;c&gt;string.Replace("{token}", ...)&lt;/c&gt; templating against the raw IChatClient.
/// &lt;/summary&gt;
public class AuthorChain : IAuthorChain
{
    private readonly ChatClientAgent _agent;

    public AuthorChain(IChatClient llm, ChatOptions chatOptions)
    {
        _agent = new ChatClientAgent(llm, new ChatClientAgentOptions
        {
            Name = "Author",
            ChatOptions = new ChatOptions
            {
                Instructions = Prompts.AuthorInstructions,
                Temperature = chatOptions.Temperature,
                MaxOutputTokens = chatOptions.MaxOutputTokens,
            },
        });
    }

    public async Task&lt;string&gt; InvokeAsync(ResearchState state)
    {
        List&lt;string&gt; research = state.ResearchFindings;
        string researchText = research.Count &gt; 0 ? string.Join("\n\n", research) : "No research available.";

        // Per-turn input only — the role/instructions are already on the agent.
        string message = $"""
            Main Task: {state.MainTask}

            Research Findings:
            {researchText}

            Current Draft: {(string.IsNullOrEmpty(state.Draft) ? "(none — write the first draft)" : state.Draft)}

            Review Notes: {(string.IsNullOrEmpty(state.ReviewNotes) ? "(none)" : state.ReviewNotes)}
            """;

        try
        {
            AgentResponse response = await _agent.RunAsync(message);
            string content = response.Text;
            return !string.IsNullOrEmpty(content) ? content : "Draft in progress...";
        }
        catch (Exception e)
        {
            Console.WriteLine($"Author error: {e.Message}");
            return "Error generating draft. Please try again.";
        }
    }

    /// &lt;summary&gt;Author node that creates or revises draft.&lt;/summary&gt;
    public async Task&lt;ResearchState&gt; AuthorNodeAsync(ResearchState state)
    {
        Console.WriteLine("\n&gt;&gt;&gt;Author");

        string draft = await InvokeAsync(state);
        Console.WriteLine($"Draft created: {draft.Length} characters");

        state.Draft = draft;
        state.RevisionNumber += 1;
        return state;
    }
}
</code></pre>



<p>To close the circle, let&#8217;s look at the changes in Reviewer. In addition to changing to ChatClientAgent, we fix the code so that if a review fails it no longer approves, instead it requests revision (which was supposed to happen in the first place):</p>



<pre class="wp-block-code"><code>using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

namespace BlogMigration;

/// &lt;summary&gt;
/// Reviewer chain backed by a Microsoft Agent Framework &lt;see cref="ChatClientAgent"/&gt;.
///
/// MAF idiom change: the evaluation role lives in the agent's Instructions; only
/// the task + draft are sent as the per-turn user message.
///
/// Correctness fix: the previous version's &lt;c&gt;catch&lt;/c&gt; returned
/// "APPROVED - Error in review..." — meaning any transient LLM/transport failure
/// would silently approve an unreviewed draft. It now returns a revision request
/// instead, so a failed review re-loops to the author (bounded by
/// &lt;see cref="ResearchState.MaxRevisions"/&gt;) rather than shipping unchecked content.
/// &lt;/summary&gt;
public class ReviewerChain : IReviewerChain
{
    private readonly ChatClientAgent _agent;

    public ReviewerChain(IChatClient llm, ChatOptions chatOptions)
    {
        _agent = new ChatClientAgent(llm, new ChatClientAgentOptions
        {
            Name = "Reviewer",
            ChatOptions = new ChatOptions
            {
                Instructions = Prompts.ReviewerInstructions,
                Temperature = chatOptions.Temperature,
                MaxOutputTokens = chatOptions.MaxOutputTokens,
            },
        });
    }

    public async Task&lt;string&gt; InvokeAsync(ResearchState state)
    {
        string draft = state.Draft;
        int revisionNum = state.RevisionNumber;

        if (draft.Trim().Length &lt; 100)
        {
            return "APPROVED - Draft is minimal but acceptable.";
        }

        if (revisionNum &gt;= ResearchState.MaxRevisions)
        {
            return "APPROVED - Maximum revisions reached. The report is satisfactory.";
        }

        // Per-turn input only — the evaluation criteria are on the agent.
        string message = $"""
            Main Task: {state.MainTask}

            Draft to Review:
            {draft}
            """;

        try
        {
            AgentResponse response = await _agent.RunAsync(message);
            string content = response.Text;
            return !string.IsNullOrEmpty(content) ? content : "APPROVED";
        }
        catch (Exception e)
        {
            // Do NOT approve on failure — that would ship an unreviewed draft.
            // Returning feedback (not "APPROVED") routes back to the author for
            // another attempt; the revision cap still guarantees termination.
            Console.WriteLine($"Review error: {e.Message}");
            return "Review could not be completed due to a transient error. Please revise and resubmit the draft.";
        }
    }

    /// &lt;summary&gt;Node that reviews the draft.&lt;/summary&gt;
    public async Task&lt;ResearchState&gt; ReviewerNodeAsync(ResearchState state)
    {
        Console.WriteLine("\n&gt;&gt;REVIEWER");

        string review = await InvokeAsync(state);
        string preview = review.Length &gt; 100 ? review&#91;..100] : review;
        Console.WriteLine($"Review: {preview}...");

        bool isApproved = review.ToUpperInvariant().Contains("APPROVED");

        if (isApproved)
        {
            Console.WriteLine("\u2713 Draft APPROVED");
            state.ReviewNotes = "APPROVED";
            state.NextStep = "END";
        }
        else
        {
            Console.WriteLine("\u2717 Revisions needed");
            state.ReviewNotes = review;
            state.NextStep = "author";
        }

        return state;
    }
}
</code></pre>



<p>We&#8217;re ready to look at the changes to Blogger&#8230;</p>



<pre class="wp-block-code"><code>using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

namespace BlogMigration;

/// &lt;summary&gt;
/// Blogger decision chain.
///
/// MAF idiom change: the LLM fallback used to ask the model for raw JSON, then
/// manually strip ```-fences and call &lt;c&gt;JsonSerializer.Deserialize&lt;/c&gt;. That is
/// now replaced by MAF structured output — &lt;c&gt;RunAsync&amp;lt;BloggerDecision&amp;gt;&lt;/c&gt;
/// returns a typed, schema-validated &lt;see cref="BloggerDecision"/&gt; directly.
///
/// Routing note: the actual control flow is owned by the workflow edges in
/// &lt;see cref="BlogWorkflow"/&gt; (Blogger → Researcher → Author → Reviewer with a
/// bounded revision loop). The &lt;c&gt;NextStep&lt;/c&gt; this class computes is advisory;
/// its still-meaningful output is &lt;c&gt;CurrentSubTask&lt;/c&gt;, which seeds the
/// researcher. The deterministic rules below are kept because they faithfully
/// preserve the original LangGraph decision logic and avoid an LLM call in the
/// common cases.
/// &lt;/summary&gt;
public class BloggerChain : IBloggerChain
{
    // Built once and reused. Holds the static Blogger instructions; the volatile
    // state is passed per-turn as the user message.
    private readonly ChatClientAgent _agent;

    // Web-style options are sufficient: BloggerDecision carries explicit
    // &#91;JsonPropertyName] attributes (next_step / task_description) that drive the
    // generated schema regardless of naming policy.
    private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);

    public BloggerChain(IChatClient llm, ChatOptions chatOptions)
    {
        _agent = new ChatClientAgent(llm, new ChatClientAgentOptions
        {
            Name = "Blogger",
            ChatOptions = new ChatOptions
            {
                Instructions = Prompts.BloggerInstructions,
                Temperature = chatOptions.Temperature,
                MaxOutputTokens = chatOptions.MaxOutputTokens,
            },
        });
    }

    public async Task&lt;BloggerDecision&gt; InvokeAsync(ResearchState state)
    {
        List&lt;string&gt; research = state.ResearchFindings;
        string researchText = research.Count &gt; 0 ? string.Join("\n", research) : "No research yet.";
        int revision = state.RevisionNumber;
        bool hasResearch = research.Count &gt; 0;
        bool hasDraft = !string.IsNullOrWhiteSpace(state.Draft);
        string review = state.ReviewNotes;

        if (review.ToUpperInvariant().Contains("APPROVED") &amp;&amp; hasDraft)
        {
            Console.WriteLine("Blogger: Draft approved, ending workflow");
            return new BloggerDecision("END", "Report approved and complete");
        }

        if (!hasResearch)
        {
            Console.WriteLine("Blogger: No research yet, directing to researcher");
            return new BloggerDecision("researcher", $"Research the topic: {state.MainTask}");
        }

        if (hasResearch &amp;&amp; !hasDraft)
        {
            Console.WriteLine("Blogger: Have research, creating first draft");
            return new BloggerDecision("author", "Write the first draft based on research findings");
        }

        if (hasDraft &amp;&amp; string.IsNullOrEmpty(review))
        {
            Console.WriteLine("Blogger: Have draft, sending to reviewer");
            return new BloggerDecision("reviewer", "Prepare draft for review");
        }

        if (!string.IsNullOrEmpty(review) &amp;&amp; !review.ToUpperInvariant().Contains("APPROVED") &amp;&amp; revision &lt; ResearchState.MaxRevisions)
        {
            Console.WriteLine($"Blogger: Revision {revision}, sending back to author");
            return new BloggerDecision("author", "Revise the draft based on review feedback");
        }

        // Max revisions reached
        if (revision &gt;= ResearchState.MaxRevisions)
        {
            Console.WriteLine("Blogger: Max revisions reached! Ending");
            return new BloggerDecision("END", "Maximum revisions reached! Finalizing report");
        }

        // LLM decision as fallback. The dynamic state is the user message; the
        // static role lives in the agent's Instructions. MAF structured output
        // hands back a typed BloggerDecision — no fenced-block cleanup, no manual
        // JsonSerializer.Deserialize.
        string stateSummary = $"""
            Current Task: {state.MainTask}
            Research Findings: {researchText}
            Blog Draft: {(string.IsNullOrEmpty(state.Draft) ? "No draft yet." : state.Draft)}
            Reviewer Feedback: {(string.IsNullOrEmpty(review) ? "No review yet." : review)}
            Revision Number: {revision}
            """;

        try
        {
            AgentResponse&lt;BloggerDecision&gt; response =
                await _agent.RunAsync&lt;BloggerDecision&gt;(stateSummary, serializerOptions: _jsonOptions);

            BloggerDecision decision = response.Result;
            if (decision is not null &amp;&amp; !string.IsNullOrEmpty(decision.NextStep))
            {
                return decision;
            }
        }
        catch (Exception e)
        {
            Console.WriteLine($"LLM decision error: {e.Message}");
        }

        // Final fallback - continue with author
        Console.WriteLine("Blogger: Using final fallback - continuing with author");
        return new BloggerDecision("author", "Continue with draft creation");
    }

    /// &lt;summary&gt;Blogger decides the next step.&lt;/summary&gt;
    public async Task&lt;ResearchState&gt; BloggerNodeAsync(ResearchState state)
    {
        Console.WriteLine("\n&gt;&gt;&gt;Blogger");

        BloggerDecision decision = await InvokeAsync(state);

        string nextStep = string.IsNullOrEmpty(decision.NextStep) ? "researcher" : decision.NextStep;
        string taskDesc = string.IsNullOrEmpty(decision.TaskDescription) ? "Continue work" : decision.TaskDescription;

        Console.WriteLine($"Decision: {nextStep}");
        Console.WriteLine($"Task: {taskDesc}");

        state.NextStep = nextStep;
        state.CurrentSubTask = taskDesc;
        return state;
    }
}
</code></pre>



<p>We&#8217;re down to the BlogWorkflow and Program.cs. Let&#8217;s start with the former. BlogWorkflow switches to RunStreamingAsync and WatchStreamAsync to stream the executor lifecycle events, while maintaining the topology:</p>



<pre class="wp-block-code"><code>using Microsoft.Agents.AI.Workflows;

namespace BlogMigration;

/// &lt;summary&gt;
/// Blog creation workflow built on the Microsoft Agent Framework workflow engine.
///
/// Topology (faithful to the original LangGraph StateGraph):
///   Blogger → Researcher → Author → Reviewer
///   Reviewer ⇄ Author  (bounded revision loop)
///   Reviewer → Output  (on approval or revision cap)
///
/// The revision loop is bounded by &lt;see cref="ResearchState.MaxRevisions"/&gt;: the
/// loop-back edge only fires while the draft is unapproved AND the revision count
/// is below the cap, so the workflow is guaranteed to terminate even if the
/// reviewer never returns "APPROVED".
/// &lt;/summary&gt;
public class BlogWorkflow(
    IBloggerChain blogger,
    IResearcherAgent researcher,
    IAuthorChain author,
    IReviewerChain reviewer) : IBlogWorkflow
{
    public async Task&lt;ResearchState&gt; RunAsync(ResearchState state)
    {
        var bloggerExecutor = new BloggerExecutor(blogger);
        var researcherExecutor = new ResearcherExecutor(researcher);
        var authorExecutor = new AuthorExecutor(author);
        var reviewerExecutor = new ReviewerExecutor(reviewer);

        Workflow workflow = new WorkflowBuilder(bloggerExecutor)
            .AddEdge(bloggerExecutor, researcherExecutor)
            .AddEdge(researcherExecutor, authorExecutor)
            .AddEdge(authorExecutor, reviewerExecutor)
            // Bounded revision loop: route back to the author only while the draft
            // still needs work and the revision cap has not been reached. When the
            // condition is false the reviewer instead yields the final output.
            .AddEdge&lt;ResearchState&gt;(reviewerExecutor, authorExecutor, condition: s =&gt; s?.NeedsRevision == true)
            .WithOutputFrom(reviewerExecutor)
            .Build();

        // Stream execution instead of running to completion in one shot. The
        // topology is identical to before (proven terminating, MAF-Doctor grade A);
        // streaming simply surfaces each executor's lifecycle as it happens, giving
        // live progress and replacing the scattered Console.WriteLine tracing that
        // previously lived inside the node classes. The final ResearchState is
        // captured from the WorkflowOutputEvent emitted by the reviewer.
        StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, state);

        ResearchState? result = null;

        await foreach (WorkflowEvent evt in run.WatchStreamAsync())
        {
            switch (evt)
            {
                case ExecutorInvokedEvent invoked:
                    Console.WriteLine($"&#91;workflow] → {invoked.ExecutorId} started");
                    break;

                case ExecutorCompletedEvent completed:
                    Console.WriteLine($"&#91;workflow] ✓ {completed.ExecutorId} completed");
                    break;

                case ExecutorFailedEvent failed:
                    Console.WriteLine($"&#91;workflow] ✗ {failed.ExecutorId} failed: {(failed.Data as Exception)?.Message}");
                    break;

                case WorkflowOutputEvent { Data: ResearchState finalState }:
                    // The reviewer yielded the final, approved (or revision-capped) state.
                    result = finalState;
                    break;
            }
        }

        // Fall back to the input state only if no output event was ever produced.
        return result ?? state;
    }
}
</code></pre>



<p>Finally, we&#8217;re ready to update Program.cs. Here we add UseFunctionInvocation to the IChatClient pipeline which is required for the researcher&#8217;s tool calls to execute:</p>



<pre class="wp-block-code"><code>using System.ClientModel;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using BlogMigration;
using Microsoft.Extensions.AI;
using OpenAI;

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) =&gt;
    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")!)
    });

// Build the IChatClient pipeline once and share it across all agents.
// UseFunctionInvocation() adds the middleware that actually *executes* the tool
// calls the model requests — without it, attaching the Tavily tool to the
// Researcher agent would let the model ask for a search but nothing would run it.
// Middleware is applied inner-to-outer, so function invocation wraps the raw
// OpenAI client. (To add distributed tracing later, chain .UseOpenTelemetry()
// here and register the source with a TracerProvider.)
IChatClient llm = openAIClient
    .GetChatClient(modelName)
    .AsIChatClient()
    .AsBuilder()
    .UseFunctionInvocation()
    .Build();

var chatOptions = new ChatOptions
{
    Temperature = 0,
    MaxOutputTokens = 4096
};

var tavilyHttpClient = new HttpClient { BaseAddress = new Uri("https://api.tavily.com/") };
tavilyHttpClient.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TAVILY_API_KEY"));

AIFunction tavilyTool = AIFunctionFactory.Create(
    async (string query) =&gt;
    {
        var request = new
        {
            query,
            max_results = 5,
            topic = "general",
            include_answer = false,
            include_raw_content = false,
            search_depth = "basic"
        };

        using HttpResponseMessage response = await tavilyHttpClient.PostAsJsonAsync("search", request);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    },
    name: "tavily_search",
    description: "A search engine optimized for comprehensive, accurate, and trusted results.");

// Creating a callable object
var bloggerChain = new BloggerChain(llm, chatOptions);
var researcherAgent = new ResearcherAgent(llm, chatOptions, tavilyTool);
var authorChain = new AuthorChain(llm, chatOptions);
var reviewerChain = new ReviewerChain(llm, chatOptions);
var app = new BlogWorkflow(bloggerChain, researcherAgent, authorChain, reviewerChain);

// Run the workflow for a sample topic
var initialState = new ResearchState
{
    MainTask = "use of multiagents in writing a C# application"
};

ResearchState result = await app.RunAsync(initialState);

Console.WriteLine("\n========== RESULTS ==========");
Console.WriteLine($"Task: {result.MainTask}");

Console.WriteLine($"\nResearch Findings ({result.ResearchFindings.Count}):");
foreach (string finding in result.ResearchFindings)
{
    Console.WriteLine($"- {finding}");
}

Console.WriteLine($"\nDraft:\n{result.Draft}");
Console.WriteLine($"\nReview Notes: {result.ReviewNotes}");
Console.WriteLine($"Revision Number: {result.RevisionNumber}");
Console.WriteLine("=============================");
</code></pre>



<p>I&#8217;ll have a lot more to say about the Microsoft Agent Framework in coming blog posts, often pointing back to this code.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://jesseliberty.com/2026/06/28/migrating-c-microsoft-agent-framework/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Dependency Injection &amp; Agent Framework</title>
		<link>https://jesseliberty.com/2026/06/26/dependency-injection-agent-framework/</link>
					<comments>https://jesseliberty.com/2026/06/26/dependency-injection-agent-framework/#respond</comments>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Fri, 26 Jun 2026 13:33:30 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[Essentials]]></category>
		<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13373</guid>

					<description><![CDATA[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 &#8230; <a href="https://jesseliberty.com/2026/06/26/dependency-injection-agent-framework/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>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.</p>



<figure class="wp-block-image size-full"><img decoding="async" width="276" height="187" src="https://jesseliberty.com/wp-content/uploads/2026/06/dicartoon.jpg" alt="" class="wp-image-13375" srcset="https://jesseliberty.com/wp-content/uploads/2026/06/dicartoon.jpg 276w, https://jesseliberty.com/wp-content/uploads/2026/06/dicartoon-150x102.jpg 150w" sizes="(max-width: 276px) 100vw, 276px" /></figure>



<p>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.</p>



<p> For example, here is how you create a workflow</p>



<pre class="wp-block-code"><code>services.AddSingleton&lt;Workflow>(sp =>
{
    var blogger = sp.GetRequiredService&lt;BloggerExecutor>();
    var researcher = sp.GetRequiredService&lt;ResearcherExecutor>();
    var author = sp.GetRequiredService&lt;AuthorExecutor>();
    var reviewer = sp.GetRequiredService&lt;ReviewerExecutor>();

    return new WorkflowBuilder(blogger)
        .AddEdge(blogger, researcher)
        .AddEdge(researcher, author)
        .AddEdge(author, reviewer)
        .AddEdge&lt;ResearchState>(reviewer, author, s => s?.NeedsRevision == true)
        .WithOutputFrom(reviewer)
        .Build();
});
</code></pre>



<span id="more-13373"></span>



<p>In our console application we would then register the interfaces, agents, executors, and workflows in .configureServices:</p>



<pre class="wp-block-code"><code>using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        // Register your interfaces here
        services.AddSingleton&lt;IResearchService, ResearchService>();
        services.AddSingleton&lt;IAuthorTools, AuthorTools>();
        services.AddSingleton&lt;IReviewerLogic, ReviewerLogic>();

        // Register agents
        services.AddSingleton&lt;ResearchAgent>();
        services.AddSingleton&lt;AuthorAgent>();
        services.AddSingleton&lt;BloggerAgent>();
        services.AddSingleton&lt;ReviewerAgent>();

        // Register executors
        services.AddSingleton&lt;ResearcherExecutor>();
        services.AddSingleton&lt;AuthorExecutor>();
        services.AddSingleton&lt;BloggerExecutor>();
        services.AddSingleton&lt;ReviewerExecutor>();

        // Register workflow
        services.AddSingleton&lt;Workflow>(sp =>
        {
            var blogger = sp.GetRequiredService&lt;BloggerExecutor>();
            var researcher = sp.GetRequiredService&lt;ResearcherExecutor>();
            var author = sp.GetRequiredService&lt;AuthorExecutor>();
            var reviewer = sp.GetRequiredService&lt;ReviewerExecutor>();

            return new WorkflowBuilder(blogger)
                .AddEdge(blogger, researcher)
                .AddEdge(researcher, author)
                .AddEdge(author, reviewer)
                .AddEdge&lt;ResearchState>(reviewer, author, s => s?.NeedsRevision == true)
                .WithOutputFrom(reviewer)
                .Build();
        });
    })
    .Build();

// Resolve your entry point service
var workflow = host.Services.GetRequiredService&lt;Workflow>();
</code></pre>



<p>This has all the advantages of DI as used anywhere, principally decoupling objects from their dependencies. This allows for greater flexibility (e.g., swapping out executors, etc.) and especially for testing (where moq objects can be used).</p>



<p>DI will inject all the following relationships:</p>



<ul class="wp-block-list">
<li>Agents depend on services</li>



<li>Tools depend on services</li>



<li>Executors depend on agents</li>



<li>Workflows depend on executors</li>
</ul>



<p>And your console app resolves the workflow.</p>



<p>In an ASP application you do the same, but in the usual place (startup.cs for ASP.NET Core and in program.cs for Minimal API)</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://jesseliberty.com/2026/06/26/dependency-injection-agent-framework/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Migrating Agentic Code Python -&gt; C# Part 6 (final)</title>
		<link>https://jesseliberty.com/2026/06/22/migrating-agentic-code-python-c-part-6-final/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Mon, 22 Jun 2026 21:59:50 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13350</guid>

					<description><![CDATA[Note, the complete source code for the .NET version of this demo application is now available at https://github.com/JesseLiberty/blogMigration&#8212;public In the previous post we finished up creating our agents. You&#8217;ll remember that each of the agents declared nodes. We&#8217;re finally going &#8230; <a href="https://jesseliberty.com/2026/06/22/migrating-agentic-code-python-c-part-6-final/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Note, the complete source code for the .NET version of this demo application is now available at <a href="https://github.com/JesseLiberty/blogMigration---public">https://github.com/JesseLiberty/blogMigration&#8212;public</a></p>
</blockquote>



<p>In the <a href="https://jesseliberty.com/2026/06/22/migrating-agentic-code-python-c-part-5/">previous post</a> we finished up creating our agents. You&#8217;ll remember that each of the agents declared nodes. We&#8217;re finally going to put them to use in a class <em>BlogWorkflow</em>. However, up to now we&#8217;ve not fully taken advantage of the Microsoft Agent Framework (MAF). Let&#8217;s fix that up first. To do so we&#8217;ll add a class BlogExecutors. We&#8217;ll create MAF workflow executors that will wrap existing &#8220;node&#8221; chains so that the business logic is reused unchanged. </p>



<p><strong>Note: We&#8217;re only going to move <em>towards</em> Microsoft Agent Framework from where things stand. To see this fully migrated to an application that truly utilizes MAF navigate to </strong><a href="https://jesseliberty.com/2026/06/28/migrating-c-microsoft-agent-framework/">here</a></p>



<figure class="wp-block-image size-large is-resized"><img decoding="async" width="790" height="800" src="https://jesseliberty.com/wp-content/uploads/2026/06/iron-work-790x800.jpg" alt="" class="wp-image-13354" style="aspect-ratio:0.9875178480954355;width:303px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/06/iron-work-790x800.jpg 790w, https://jesseliberty.com/wp-content/uploads/2026/06/iron-work-296x300.jpg 296w, https://jesseliberty.com/wp-content/uploads/2026/06/iron-work-148x150.jpg 148w, https://jesseliberty.com/wp-content/uploads/2026/06/iron-work-768x777.jpg 768w, https://jesseliberty.com/wp-content/uploads/2026/06/iron-work.jpg 905w" sizes="(max-width: 790px) 100vw, 790px" /></figure>



<p><br />Let&#8217;s start with the BloggerExecutor which will allow the blogger to plan the task and seed the sub-task:</p>



<pre class="wp-block-code"><code>using Microsoft.Agents.AI.Workflows;

namespace BlogMigration;

/// &lt;summary&gt;Entry executor: lets the blogger plan the task and seed the sub-task.&lt;/summary&gt;
internal sealed partial class BloggerExecutor(IBloggerChain blogger) : Executor("Blogger")
{
    &#91;MessageHandler]
    private async ValueTask&lt;ResearchState&gt; HandleAsync(ResearchState state, IWorkflowContext context)
        =&gt; await blogger.BloggerNodeAsync(state);
}</code></pre>



<p>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.</p>



<p>The executor is just a thin wrapper around the Blogger agent. The handler calls the Blogger node and the node updates the state.</p>



<span id="more-13350"></span>



<p>Let&#8217;s go ahead and create the other Executors</p>



<pre class="wp-block-code"><code>/// &lt;summary&gt;Gathers research findings.&lt;/summary&gt;
internal sealed partial class ResearcherExecutor(IResearcherAgent researcher) : Executor("Researcher")
{
    &#91;MessageHandler]
    private async ValueTask&lt;ResearchState&gt; HandleAsync(ResearchState state, IWorkflowContext context)
        =&gt; await researcher.ResearchNodeAsync(state);
}

/// &lt;summary&gt;Writes or revises the draft (increments the revision counter).&lt;/summary&gt;
internal sealed partial class AuthorExecutor(IAuthorChain author) : Executor("Author")
{
    &#91;MessageHandler]
    private async ValueTask&lt;ResearchState&gt; HandleAsync(ResearchState state, IWorkflowContext context)
        =&gt; await author.AuthorNodeAsync(state);
}

/// &lt;summary&gt;
/// Reviews the draft and records approval / revision notes. Acts as the terminal
/// output node: when no further revision is needed it yields the final state.
/// &lt;/summary&gt;
internal sealed partial class ReviewerExecutor(IReviewerChain reviewer) : Executor("Reviewer")
{
    &#91;MessageHandler]
    private async ValueTask&lt;ResearchState&gt; HandleAsync(ResearchState state, IWorkflowContext context)
    {
        state = await reviewer.ReviewerNodeAsync(state);

        if (!state.NeedsRevision)
        {
            // Approved, or the revision cap was hit — emit the final result.
            await context.YieldOutputAsync(state);
        }

        // Returned state is routed back to the author only when the loop edge
        // condition (NeedsRevision) is satisfied; otherwise it goes nowhere.
        return state;
    }</code></pre>



<p>With that in place we can build the workflow.  We begin by creating the executors as wrappers around our agents,</p>



<pre class="wp-block-code"><code>/// &lt;summary&gt;
///   Blogger → Researcher → Author → Reviewer
///   Reviewer ⇄ Author  (bounded revision loop)
///   Reviewer → Output  (on approval or revision cap)
///
/// The revision loop is bounded by &lt;see cref="ResearchState.MaxRevisions"/&gt;: the
/// loop-back edge only fires while the draft is unapproved AND the revision count
/// is below the cap, so the workflow is guaranteed to terminate even if the
/// reviewer never returns "APPROVED".
/// &lt;/summary&gt;

public class BlogWorkflow(
    IBloggerChain blogger,
    IResearcherAgent researcher,
    IAuthorChain author,
    IReviewerChain reviewer) : IBlogWorkflow
{
    public async Task&lt;ResearchState&gt; RunAsync(ResearchState state)
    {
        var bloggerExecutor = new BloggerExecutor(blogger);
        var researcherExecutor = new ResearcherExecutor(researcher);
        var authorExecutor = new AuthorExecutor(author);
        var reviewerExecutor = new ReviewerExecutor(reviewer);</code></pre>



<p>Now we create a Workflow object and add edges between our nodes</p>



<pre class="wp-block-code"><code> Workflow workflow = new WorkflowBuilder(bloggerExecutor)
            .AddEdge(bloggerExecutor, researcherExecutor)
            .AddEdge(researcherExecutor, authorExecutor)
            .AddEdge(authorExecutor, reviewerExecutor)
            .AddEdge&lt;ResearchState&gt;(reviewerExecutor, authorExecutor, condition: s =&gt; s?.NeedsRevision == true)
            .WithOutputFrom(reviewerExecutor)
            .Build();</code></pre>



<p>Notice the conditional edge. This final AddEdge says &#8220;if the condition is not null and the draft needs revision (remember, the Reviewer caps the number of revisions at 4) then follow this edge. If NeedsRevision equals false, this edge is ignored.</p>



<p>We&#8217;re ready to let &#8216;er rip</p>



<pre class="wp-block-code"><code>     Run run = await InProcessExecution.RunAsync(workflow, state);

        foreach (WorkflowEvent evt in run.NewEvents)
        {
            if (evt is WorkflowOutputEvent { Data: ResearchState result })
            {
                return result;
            }
        }

        return state;
    }</code></pre>



<p>All that&#8217;s left is to create Program.cs. We&#8217;ll begin by initializing the environment we need</p>



<pre class="wp-block-code"><code>using System.ClientModel;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using BlogMigration;
using Microsoft.Extensions.AI;
using OpenAI;

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) =&gt;
    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")!)
    });</code></pre>



<p>Now let&#8217;s instantiate our IChatClient (the LLM) and set the options. We&#8217;ll set temperature to 0 to have minimum variability and we&#8217;ll set the maximum output tokens. </p>



<pre class="wp-block-code"><code>IChatClient llm = openAIClient.GetChatClient(modelName).AsIChatClient();

var chatOptions = new ChatOptions
{
    Temperature = 0,
    MaxOutputTokens = 4096
};</code></pre>



<p>Tavily is the library we&#8217;ll use for searching the web. You can learn more about it at https://tavily.com. </p>



<pre class="wp-block-code"><code>var tavilyHttpClient = new HttpClient { BaseAddress = new Uri("https://api.tavily.com/") };
tavilyHttpClient.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TAVILY_API_KEY"));

AIFunction tavilyTool = AIFunctionFactory.Create(
    async (string query) =&gt;
    {
        var request = new
        {
            query,
            max_results = 5,
            topic = "general",
            include_answer = false,
            include_raw_content = false,
            search_depth = "basic"
        };

        using HttpResponseMessage response = await tavilyHttpClient.PostAsJsonAsync("search", request);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    },
    name: "tavily_search",
    description: "A search engine optimized for comprehensive, accurate, and trusted results.");</code></pre>



<p>Let&#8217;s set up all the agents and the BlogWorkflow</p>



<pre class="wp-block-code"><code>var bloggerChain = new BloggerChain(llm, chatOptions);
var researcherAgent = new ResearcherAgent(llm, chatOptions, tavilyTool);
var authorChain = new AuthorChain(llm, chatOptions);
var reviewerChain = new ReviewerChain(llm, chatOptions);

var app = new BlogWorkflow(bloggerChain, researcherAgent, authorChain, reviewerChain);</code></pre>



<p>We&#8217;ll run the workflow for a sample topic</p>



<pre class="wp-block-code"><code>var initialState = new ResearchState
{
    MainTask = "use of multiagents in writing a C# application"
};

ResearchState result = await app.RunAsync(initialState);

Console.WriteLine("\n========== RESULTS ==========");
Console.WriteLine($"Task: {result.MainTask}");

Console.WriteLine($"\nResearch Findings ({result.ResearchFindings.Count}):");
foreach (string finding in result.ResearchFindings)
{
    Console.WriteLine($"- {finding}");
}

Console.WriteLine($"\nDraft:\n{result.Draft}");
Console.WriteLine($"\nReview Notes: {result.ReviewNotes}");
Console.WriteLine($"Revision Number: {result.RevisionNumber}");
Console.WriteLine("=============================");</code></pre>



<p>Before we run this, here&#8217;s a look at the csproj:</p>



<pre class="wp-block-code"><code>&lt;Project Sdk="Microsoft.NET.Sdk"&gt;

  &lt;PropertyGroup&gt;
    &lt;OutputType&gt;Exe&lt;/OutputType&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
  &lt;/PropertyGroup&gt;

  &lt;ItemGroup&gt;
    &lt;PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.10.0" /&gt;
    &lt;PackageReference Include="Microsoft.Agents.AI.Workflows.Generators" Version="1.10.0"&gt;
      &lt;IncludeAssets&gt;runtime; build; native; contentfiles; analyzers; buildtransitive&lt;/IncludeAssets&gt;
      &lt;PrivateAssets&gt;all&lt;/PrivateAssets&gt;
    &lt;/PackageReference&gt;
    &lt;PackageReference Include="Microsoft.Extensions.AI" Version="10.7.0" /&gt;
    &lt;PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.7.0" /&gt;
    &lt;PackageReference Include="OpenAI" Version="2.11.0" /&gt;
  &lt;/ItemGroup&gt;

&lt;/Project&gt;
</code></pre>



<p>That&#8217;s it. Easy Peasy.</p>



<p>Here is the output of running the above&#8230;</p>



<pre class="wp-block-code"><code>&gt;&gt;&gt;Blogger
Blogger: No research yet, directing to researcher
Decision: researcher
Task: Research the topic: use of multiagents in writing a C# application

&gt;&gt;&gt;RESEARCHER
Researching: Research the topic: use of multiagents in writing a C# application
Found: The search results highlight various resources and insights on developing multi-agent applications i...

&gt;&gt;&gt;Author
Draft created: 3140 characters

&gt;&gt;REVIEWER
Review: APPROVED - The draft effectively introduces the concept of multi-agent systems in C# and provides va...
✓ Draft APPROVED

========== RESULTS ==========
Task: use of multiagents in writing a C# application

Research Findings (1):
- The search results highlight various resources and insights on developing multi-agent applications in C#.

1. **Creating a Multi-Agent Application**: Jesse Liberty's blog post introduces the concept of a multi-agent application, showcasing a test run of an application designed to generate blog posts. The post promises a detailed breakdown of the application in subsequent entries, indicating a focus on practical implementation.

2. **Semantic Kernel Framework**: A YouTube video discusses building a multi-agent application using the Semantic Kernel framework in C#. This resource likely provides a hands-on approach to leveraging this framework for multi-agent systems, emphasizing its capabilities and features.

3. **Microsoft Agent Framework**: Another YouTube tutorial presents the Microsoft Agent Framework as a robust alternative for creating multi-agent architectures in .NET. This resource suggests that the framework offers significant tools and functionalities for developers looking to implement multi-agent systems.

Overall, these findings suggest a growing interest in multi-agent systems within the C# ecosystem, with various frameworks and tutorials available to assist developers in creating sophisticated applications.

Draft:
# Harnessing Multi-Agent Systems in C#: A Comprehensive Guide

In the evolving landscape of software development, multi-agent systems (MAS) have emerged as a powerful paradigm, particularly in the realm of C#. These systems consist of multiple interacting agents, each capable of autonomous decision-making, which can lead to more efficient and scalable applications. This post explores the key resources and frameworks available for developing multi-agent applications in C#, providing a roadmap for developers interested in this innovative approach.

## Understanding Multi-Agent Applications

Multi-agent applications are designed to solve complex problems by distributing tasks among various agents. Each agent operates independently, yet they can collaborate to achieve common goals. Jesse Liberty's blog post serves as an excellent introduction to this concept, showcasing a practical example of a multi-agent application that generates blog posts. Liberty promises a detailed breakdown of the application in future entries, making it a valuable resource for developers looking to implement similar functionalities.

## Leveraging the Semantic Kernel Framework

For those seeking a hands-on approach, the Semantic Kernel framework offers a robust environment for building multi-agent applications in C#. A recent YouTube video delves into the capabilities of this framework, highlighting its features that facilitate the development of intelligent agents. The Semantic Kernel is particularly beneficial for developers aiming to integrate natural language processing and machine learning into their applications, allowing agents to understand and respond to user inputs more effectively.

## Exploring the Microsoft Agent Framework

Another noteworthy resource is the Microsoft Agent Framework, which provides a comprehensive set of tools for creating multi-agent architectures within the .NET ecosystem. A tutorial available on YouTube outlines the framework's functionalities, demonstrating how it can be utilized to develop sophisticated multi-agent systems. This framework is particularly advantageous for developers who are already familiar with the Microsoft stack, as it seamlessly integrates with existing .NET applications.

## Conclusion

The interest in multi-agent systems within the C# ecosystem is on the rise, driven by the need for more intelligent and responsive applications. With resources like Jesse Liberty's blog, the Semantic Kernel framework, and the Microsoft Agent Framework, developers have access to a wealth of knowledge and tools to create innovative multi-agent applications. As you embark on your journey into multi-agent development, these resources will serve as invaluable guides, helping you harness the full potential of this exciting technology.

By embracing multi-agent systems, you can elevate your applications, making them more dynamic and capable of handling complex tasks with ease. Whether you're generating content, automating processes, or enhancing user interactions, the possibilities are endless. Start exploring today and unlock the future of intelligent software development in C#.

Review Notes: APPROVED
Revision Number: 1</code></pre>



<p><em>Note, I did not tell it to use my blog or what to say about it.</em> </p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>.NET Live Turns the Tables</title>
		<link>https://jesseliberty.com/2026/06/22/net-live-turns-the-tables/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Mon, 22 Jun 2026 17:11:31 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13347</guid>

					<description><![CDATA[I had the great privilege of being interviewed about AI on .NET Live. You can find the video on YouTube. Find my YouTube channel here.]]></description>
										<content:encoded><![CDATA[
<p>I had the great privilege of being interviewed about AI on .NET Live. You can find the video on <a href="https://www.youtube.com/watch?v=LzHo8StEJrg">YouTube</a>.</p>



<figure class="wp-block-image size-large is-resized"><a href="https://www.youtube.com/watch?v=LzHo8StEJrg"><img loading="lazy" decoding="async" width="800" height="450" src="https://jesseliberty.com/wp-content/uploads/2026/06/Dotnet-live-800x450.jpg" alt="" class="wp-image-13348" style="width:464px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/06/Dotnet-live-800x450.jpg 800w, https://jesseliberty.com/wp-content/uploads/2026/06/Dotnet-live-300x169.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/06/Dotnet-live-150x84.jpg 150w, https://jesseliberty.com/wp-content/uploads/2026/06/Dotnet-live-768x432.jpg 768w, https://jesseliberty.com/wp-content/uploads/2026/06/Dotnet-live.jpg 894w" sizes="auto, (max-width: 800px) 100vw, 800px" /></a></figure>



<p>Find <em>my </em>YouTube channel <a href="https://youtube.com/jesseliberty">here</a>.</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Migrating Agentic Code Python -&gt; C# Part 5</title>
		<link>https://jesseliberty.com/2026/06/22/migrating-agentic-code-python-c-part-5/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Mon, 22 Jun 2026 12:20:24 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13341</guid>

					<description><![CDATA[In the previous post we looked at implementing the Researcher in C#. In this, as promised, we&#8217;ll look at the Author and the Reviewer. The Author is handed two objects when instantiated: the llm (an IChatClient object) and the chatOptions. &#8230; <a href="https://jesseliberty.com/2026/06/22/migrating-agentic-code-python-c-part-5/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>In the <a href="https://jesseliberty.com/2026/06/22/migrating-agentic-code-python-c-part-5/">previous post </a>we looked at implementing the Researcher in C#. In this, as promised, we&#8217;ll look at the Author and the Reviewer.</p>



<p>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.</p>



<pre class="wp-block-code"><code>public class AuthorChain(IChatClient llm, ChatOptions chatOptions) : IAuthorChain
{
    public async Task&lt;string&gt; InvokeAsync(ResearchState state)
    {
        List&lt;string&gt; research = state.ResearchFindings;
        string researchText = research.Count &gt; 0 ? string.Join("\n\n", research) : "No research available.";</code></pre>



<p>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</p>



<span id="more-13341"></span>



<pre class="wp-block-code"><code>       string prompt = Prompts.AuthorPromptTemplate
            .Replace("{main_task}", state.MainTask)
            .Replace("{research_findings}", researchText)
            .Replace("{draft}", state.Draft)
            .Replace("{review_notes}", state.ReviewNotes);

        try
        {
            ChatResponse response = await llm.GetResponseAsync(prompt, chatOptions);
            string content = response.Text;
            return !string.IsNullOrEmpty(content) ? content : "Draft in progress...";
        }
        catch (Exception e)
        {
            Console.WriteLine($"Author error: {e.Message}");
            return "Error generating draft. Please try again.";
        }
    }</code></pre>



<p>All that&#8217;s left for the Author is to create its node</p>



<pre class="wp-block-code"><code>    public async Task&lt;ResearchState&gt; AuthorNodeAsync(ResearchState state)
    {
        Console.WriteLine("\n&gt;&gt;&gt;Author");

        string draft = await InvokeAsync(state);
        Console.WriteLine($"Draft created: {draft.Length} characters");

        state.Draft = draft;
        state.RevisionNumber += 1;
        return state;
    }</code></pre>



<p>That was short enough that we should look at the Reviewer while we&#8217;re here. </p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="800" height="736" src="https://jesseliberty.com/wp-content/uploads/2026/06/author-to-reviewer-1-800x736.jpg" alt="" class="wp-image-13345" style="aspect-ratio:1.0869730538163198;width:307px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/06/author-to-reviewer-1-800x736.jpg 800w, https://jesseliberty.com/wp-content/uploads/2026/06/author-to-reviewer-1-300x276.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/06/author-to-reviewer-1-150x138.jpg 150w, https://jesseliberty.com/wp-content/uploads/2026/06/author-to-reviewer-1-768x706.jpg 768w, https://jesseliberty.com/wp-content/uploads/2026/06/author-to-reviewer-1.jpg 924w" sizes="auto, (max-width: 800px) 100vw, 800px" /></figure>



<p>Like the Author, the Reviewer is passed the llm and the chatOptions. InvokeAsync gets the ResearchState and from the state it can get the current draft that the author created.</p>



<pre class="wp-block-code"><code>   public async Task&lt;string&gt; InvokeAsync(ResearchState state)
    {
        string draft = state.Draft;
        int revisionNum = state.RevisionNumber;

        if (draft.Trim().Length &lt; 100)
        {
            return "APPROVED - Draft is minimal but acceptable.";
        }

        if (revisionNum &gt;= 4)
        {
            return "APPROVED - Maximum revisions reached. The report is satisfactory.";
        }

        string prompt = Prompts.ReviewerPromptTemplate
            .Replace("{main_task}", state.MainTask)
            .Replace("{draft}", draft);

        try
        {
            ChatResponse response = await llm.GetResponseAsync(prompt, chatOptions);
            string content = response.Text;
            return !string.IsNullOrEmpty(content) ? content : "APPROVED";
        }
        catch (Exception e)
        {
            Console.WriteLine($"Review error: {e.Message}");
            return "APPROVED - Error in review, proceeding with current draft.";
        }
    }</code></pre>



<p>Notice that the Reviewer is quite generous in marking drafts as APPROVED. You may want to handle some conditions differently. </p>



<p>Let&#8217;s create the Reviewer&#8217;s node and in the next post we can finally see how these nodes are used.</p>



<pre class="wp-block-code"><code>   public async Task&lt;ResearchState&gt; ReviewerNodeAsync(ResearchState state)
    {
        Console.WriteLine("\n&gt;&gt;REVIEWER");

        string review = await InvokeAsync(state);
        string preview = review.Length &gt; 100 ? review&#91;..100] : review;
        Console.WriteLine($"Review: {preview}...");

        bool isApproved = review.ToUpperInvariant().Contains("APPROVED");

        if (isApproved)
        {
            Console.WriteLine("\u2713 Draft APPROVED");
            state.ReviewNotes = "APPROVED";
            state.NextStep = "END";
        }
        else
        {
            Console.WriteLine("\u2717 Revisions needed");
            state.ReviewNotes = review;
            state.NextStep = "author";
        }

        return state;
    }</code></pre>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Migrating Agentic Code Python -&gt; C# Part 4</title>
		<link>https://jesseliberty.com/2026/06/21/migrating-agentic-code-python-c-part-4/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Sun, 21 Jun 2026 13:08:56 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13338</guid>

					<description><![CDATA[In the previous blog post we looked at the Blogger (orchestrator) code in C#. Let&#8217;s move on to some of the other agents. The Blogger invokes the Researcher, so let&#8217;s go there next. The Researcher class is created with an &#8230; <a href="https://jesseliberty.com/2026/06/21/migrating-agentic-code-python-c-part-4/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>In the<a href="https://jesseliberty.com/2026/06/20/migrating-agentic-code-python-c-part-3/"> previous blog post</a> we looked at the Blogger (orchestrator) code in C#. Let&#8217;s move on to some of the other agents.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="800" height="766" src="https://jesseliberty.com/wp-content/uploads/2026/06/c-snake-800x766.jpg" alt="" class="wp-image-13339" style="aspect-ratio:1.0444016854616753;width:336px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/06/c-snake-800x766.jpg 800w, https://jesseliberty.com/wp-content/uploads/2026/06/c-snake-300x287.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/06/c-snake-150x144.jpg 150w, https://jesseliberty.com/wp-content/uploads/2026/06/c-snake-768x735.jpg 768w, https://jesseliberty.com/wp-content/uploads/2026/06/c-snake.jpg 921w" sizes="auto, (max-width: 800px) 100vw, 800px" /></figure>



<p>The Blogger invokes the Researcher, so let&#8217;s go there next.</p>



<span id="more-13338"></span>



<p>The Researcher class is created with an IChatClient (the principal object for llms), a set of options and the search tool (Tavily)</p>



<pre class="wp-block-code"><code>using System.Text.Json;
using Microsoft.Extensions.AI;

namespace BlogMigration;

/// &lt;summary>Creates a researcher agent that uses Tavily search.&lt;/summary>
public class ResearcherAgent(IChatClient llm, ChatOptions chatOptions, AIFunction tavilyTool) : IResearcherAgent
{</code></pre>



<p>The main method is InvokeAsync which gets a copy of the query and uses Tavily to search the web using that query. The results are JSON, and the next step is to extract a JsonDocument object by parsing these results.</p>



<pre class="wp-block-code"><code>try
        {
            object? searchResult = await tavilyTool.InvokeAsync(
                new AIFunctionArguments { &#91;"query"] = query });

            string searchJson = searchResult switch
            {
                JsonElement je => je.ValueKind == JsonValueKind.String ? je.GetString() ?? "{}" : je.GetRawText(),
                string s => s,
                _ => searchResult?.ToString() ?? "{}"
            };

            var formattedResults = new List&lt;string>();

            using (JsonDocument document = JsonDocument.Parse(searchJson))
            {
                if (document.RootElement.TryGetProperty("results", out JsonElement results)
                    &amp;&amp; results.ValueKind == JsonValueKind.Array)
                {
                    foreach (JsonElement result in results.EnumerateArray().Take(3))
                    {
                        string title = result.TryGetProperty("title", out JsonElement t) ? t.GetString() ?? "Untitled" : "Untitled";
                        string url = result.TryGetProperty("url", out JsonElement u) ? u.GetString() ?? "N/A" : "N/A";
                        string content = result.TryGetProperty("content", out JsonElement c) ? c.GetString() ?? "" : "";
                        string snippet = content.Length > 250 ? content&#91;..250] : content;
                        formattedResults.Add($">>{title}\nSource: {url}\n{snippet}...\n");
                    }
                }
            }

            string rawOutput = formattedResults.Count > 0
                ? string.Join("\n", formattedResults)
                : "No results found";</code></pre>



<p>We next instruct the llm using a system prompt, passing in the raw output we just created. We get back the summary of the findings and if that is not empty we return it.</p>



<pre class="wp-block-code"><code>           string summaryPrompt = $"""
                Based on these search results about '{query}',
                provide a concise summary of key findings:
                {rawOutput}
                """;

            ChatResponse summaryResponse = await llm.GetResponseAsync(summaryPrompt, chatOptions);
            string summary = summaryResponse.Text;

            return !string.IsNullOrEmpty(summary) ? summary : rawOutput;</code></pre>



<p>Finally, we handle any exceptions raised</p>



<pre class="wp-block-code"><code>       catch (Exception e)
        {
            Console.WriteLine($"Research error: {e.Message}");
            return $"Research completed on: {query}. Key information has been gathered from web sources.";
        }
 </code></pre>



<p>As we did with Blogger, we also create a Node, passing in the state and getting back the updated state.</p>



<pre class="wp-block-code"><code>   public async Task&lt;ResearchState> ResearchNodeAsync(ResearchState state)
    {
        Console.WriteLine("\n>>>RESEARCHER");

        string subTask = !string.IsNullOrEmpty(state.CurrentSubTask) ? state.CurrentSubTask : state.MainTask;
        Console.WriteLine($"Researching: {subTask}");

        string findings;
        try
        {
            findings = await InvokeAsync(subTask);
            string preview = findings.Length > 100 ? findings&#91;..100] : findings;
            Console.WriteLine($"Found: {preview}...");
        }
        catch (Exception e)
        {
            Console.WriteLine($"Research error: {e.Message}");
            findings = $"Research on {subTask} - information gathered";
        }

        state.ResearchFindings.Add(findings);
        return state;
    }</code></pre>



<p>In the next blog post we&#8217;ll look at the Author</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Migrating Agentic Code Python -&gt; C# Part 3</title>
		<link>https://jesseliberty.com/2026/06/20/migrating-agentic-code-python-c-part-3/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Sat, 20 Jun 2026 20:45:43 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13334</guid>

					<description><![CDATA[In the previous blog post (Part 2) we began the migration by setting up the configuration. In this post, we&#8217;ll tackle the Blogger, which acts as an orchestrator for the agents. In the python version of our program the blogger_prompt_template &#8230; <a href="https://jesseliberty.com/2026/06/20/migrating-agentic-code-python-c-part-3/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>In the previous blog post (<a href="https://jesseliberty.com/2026/06/20/migrating-agentic-code-python-c-part-2/">Part 2</a>) we began the migration by setting up the configuration. In this post, we&#8217;ll tackle the Blogger, which acts as an orchestrator for the agents.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="769" height="671" src="https://jesseliberty.com/wp-content/uploads/2026/06/cartoon.jpg" alt="" class="wp-image-13336" style="aspect-ratio:1.1460644126687158;width:319px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/06/cartoon.jpg 769w, https://jesseliberty.com/wp-content/uploads/2026/06/cartoon-300x262.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/06/cartoon-150x131.jpg 150w" sizes="auto, (max-width: 769px) 100vw, 769px" /></figure>



<p>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&#8217;s start with the Blogger prompt:<br /></p>



<pre class="wp-block-code"><code>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 &gt;= 4, choose "END"
""";</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>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&#8217;s more to it, and I&#8217;ll refer you to the C# documentation.</p>
</blockquote>



<span id="more-13334"></span>



<p>With that, we&#8217;re ready to create the BloggerChain. The structure matches the Python code closely, of course using C# syntax. </p>



<pre class="wp-block-code"><code>using System.Text.Json;
using Microsoft.Extensions.AI;

namespace BlogMigration;

/// &lt;summary&gt;Creates the blogger decision chain.&lt;/summary&gt;
public class BloggerChain(IChatClient llm, ChatOptions chatOptions) : IBloggerChain
{
    public async Task&lt;BloggerDecision&gt; InvokeAsync(ResearchState state)
    {
        List&lt;string&gt; research = state.ResearchFindings;
        string researchText = research.Count &gt; 0 ? string.Join("\n", research) : "No research yet.";
        int revision = state.RevisionNumber;
        bool hasResearch = research.Count &gt; 0;
        bool hasDraft = !string.IsNullOrWhiteSpace(state.Draft);
        string review = state.ReviewNotes;

        if (review.ToUpperInvariant().Contains("APPROVED") &amp;&amp; hasDraft)
        {
            Console.WriteLine("Blogger: Draft approved, ending workflow");
            return new BloggerDecision("END", "Report approved and complete");
        }

        if (!hasResearch)
        {
            Console.WriteLine("Blogger: No research yet, directing to researcher");
            return new BloggerDecision("researcher", $"Research the topic: {state.MainTask}");
        }

        if (hasResearch &amp;&amp; !hasDraft)
        {
            Console.WriteLine("Blogger: Have research, creating first draft");
            return new BloggerDecision("author", "Write the first draft based on research findings");
        }

        if (hasDraft &amp;&amp; string.IsNullOrEmpty(review))
        {
            Console.WriteLine("Blogger: Have draft, sending to reviewer");
            return new BloggerDecision("reviewer", "Prepare draft for review");
        }

        if (!string.IsNullOrEmpty(review) &amp;&amp; !review.ToUpperInvariant().Contains("APPROVED") &amp;&amp; revision &lt;= 4)
        {
            Console.WriteLine($"Blogger: Revision {revision}, sending back to author");
            return new BloggerDecision("author", "Revise the draft based on review feedback");
        }

        // Max revisions reached
        if (revision &gt;= 4)
        {
            Console.WriteLine("Blogger: Max revisions reached! Ending");
            return new BloggerDecision("END", "Maximum revisions reached! Finalizing report");
        }

        // LLM decision as fallback
        string prompt = Prompts.BloggerPromptTemplate
            .Replace("{main_task}", state.MainTask)
            .Replace("{research_findings}", researchText)
            .Replace("{draft}", string.IsNullOrEmpty(state.Draft) ? "No draft yet." : state.Draft)
            .Replace("{review_notes}", string.IsNullOrEmpty(review) ? "No review yet." : review)
            .Replace("{revision_number}", revision.ToString());

        try
        {
            ChatResponse response = await llm.GetResponseAsync(prompt, chatOptions);
            string content = response.Text;

            // Try to parse JSON
            string text = content.Trim();
            if (text.StartsWith("```"))
            {
                IEnumerable&lt;string&gt; lines = text.Split('\n').Where(l =&gt; !l.TrimStart().StartsWith("```"));
                text = string.Join("\n", lines);
            }
            text = text.Trim();

            BloggerDecision? decision = JsonSerializer.Deserialize&lt;BloggerDecision&gt;(text);

            if (decision is not null &amp;&amp; !string.IsNullOrEmpty(decision.NextStep))
            {
                return decision;
            }
        }
        catch (Exception e)
        {
            Console.WriteLine($"LLM parsing error: {e.Message}");
        }

        // Final fallback - continue with author
        Console.WriteLine("Blogger: Using final fallback - continuing with author");
        return new BloggerDecision("author", "Continue with draft creation");
    }</code></pre>



<p>It is in this class that we can create the BloggerNode, which will be used in the same way as it is in the Python example:</p>



<pre class="wp-block-code"><code>
    /// &lt;summary&gt;Blogger decides the next step.&lt;/summary&gt;
    public async Task&lt;ResearchState&gt; BloggerNodeAsync(ResearchState state)
    {
        Console.WriteLine("\n&gt;&gt;&gt;Blogger");

        BloggerDecision decision = await InvokeAsync(state);

        string nextStep = string.IsNullOrEmpty(decision.NextStep) ? "researcher" : decision.NextStep;
        string taskDesc = string.IsNullOrEmpty(decision.TaskDescription) ? "Continue work" : decision.TaskDescription;

        Console.WriteLine($"Decision: {nextStep}");
        Console.WriteLine($"Task: {taskDesc}");

        state.NextStep = nextStep;
        state.CurrentSubTask = taskDesc;
        return state;
    }
}</code></pre>



<p>Notice the use of a BloggerDecision object. Let&#8217;s create that in a file BloggerDecision.cs</p>



<pre class="wp-block-code"><code>using System.Text.Json.Serialization;

namespace BlogMigration;

public record BloggerDecision(
    &#91;property: JsonPropertyName("next_step")] string NextStep,
    &#91;property: JsonPropertyName("task_description")] string TaskDescription);</code></pre>



<p>Program.cs has more to do than we&#8217;ve seen so far. Among other things, it will instantiate a BloggerChain object, which we&#8217;ll see after we create the Author, Researcher, and Reviewer related classes.</p>



<p>We&#8217;ll tackle the Author files in the next installment.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Migrating Agentic Code Python -&gt; C# Part 2</title>
		<link>https://jesseliberty.com/2026/06/20/migrating-agentic-code-python-c-part-2/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Sat, 20 Jun 2026 15:19:06 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[Essentials]]></category>
		<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13330</guid>

					<description><![CDATA[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&#8217;m going to work my way down through my Python script and &#8230; <a href="https://jesseliberty.com/2026/06/20/migrating-agentic-code-python-c-part-2/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>In <a href="https://jesseliberty.com/2026/06/19/migrating-agentic-code-python-c-part-1/">Part 1</a> 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&#8217;m going to work my way down through my Python script and refactor it breaking out classes and refactoring to use Microsoft Agent Framework.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="800" height="793" src="https://jesseliberty.com/wp-content/uploads/2026/06/walking-to-csharp-800x793.jpg" alt="" class="wp-image-13368" style="aspect-ratio:1.0088462262375306;width:318px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/06/walking-to-csharp-800x793.jpg 800w, https://jesseliberty.com/wp-content/uploads/2026/06/walking-to-csharp-300x297.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/06/walking-to-csharp-150x150.jpg 150w, https://jesseliberty.com/wp-content/uploads/2026/06/walking-to-csharp-768x761.jpg 768w, https://jesseliberty.com/wp-content/uploads/2026/06/walking-to-csharp.jpg 866w" sizes="auto, (max-width: 800px) 100vw, 800px" /></figure>



<p>Note: to make sense of this code, you&#8217;ll want to start with the Python example. The code for that begins <a href="https://jesseliberty.com/2026/06/12/creating-a-multi-agent-application-part-2/">here</a>.</p>



<p>We begin with bringing in the config.json file. We&#8217;ll use the identical file, and bring it into Program.cs</p>



<pre class="wp-block-code"><code>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) =&gt;
    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")!)
    });
</code></pre>



<span id="more-13330"></span>



<p>Next, we set up the Tavily search tool:</p>



<pre class="wp-block-code"><code>var tavilyHttpClient = new HttpClient { BaseAddress = new Uri("https://api.tavily.com/") };
tavilyHttpClient.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TAVILY_API_KEY"));

AIFunction tavilyTool = AIFunctionFactory.Create(
    async (string query) =&gt;
    {
        var request = new
        {
            query,
            max_results = 5,
            topic = "general",
            include_answer = false,
            include_raw_content = false,
            search_depth = "basic"
        };

        using HttpResponseMessage response = await tavilyHttpClient.PostAsJsonAsync("search", request);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    },
    name: "tavily_search",
    description: "A search engine optimized for comprehensive, accurate, and trusted results.");</code></pre>



<p>A little more verbose, but we&#8217;re doing a bit of extra work along the way. Let&#8217;s go down to where we create the ResearchState. To do that, we&#8217;ll create ResearchState.cs:</p>



<pre class="wp-block-code"><code>namespace BlogMigration;

/// &lt;summary&gt;State for the research workflow.&lt;/summary&gt;
public class ResearchState
{
    public string MainTask { get; set; } = "";
    public List&lt;string&gt; ResearchFindings { get; set; } = &#91;];
    public string Draft { get; set; } = "";
    public string ReviewNotes { get; set; } = "";
    public int RevisionNumber { get; set; }
    public string NextStep { get; set; } = "";
    public string CurrentSubTask { get; set; } = "";
}
</code></pre>



<p>In the next blog post we&#8217;ll create the first of our agents: Blogger. We&#8217;ll mimic BloggerChain, passing in the llm (IChatClient) and the chat options.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Migrating Agentic Code Python -&gt; C# Part 1</title>
		<link>https://jesseliberty.com/2026/06/19/migrating-agentic-code-python-c-part-1/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Fri, 19 Jun 2026 20:41:25 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13327</guid>

					<description><![CDATA[In the last 5 posts we created an agentic application using Python. Let&#8217;s migrate that to C#. Here&#8217;s the set of files we&#8217;ll create: And here is the output after running it as a test using the prompt Use of &#8230; <a href="https://jesseliberty.com/2026/06/19/migrating-agentic-code-python-c-part-1/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>In the last 5 posts we created an agentic application using Python. Let&#8217;s migrate that to C#. </p>



<p>Here&#8217;s the set of files we&#8217;ll create:<br /></p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="302" height="800" src="https://jesseliberty.com/wp-content/uploads/2026/06/image-302x800.png" alt="" class="wp-image-13328" style="aspect-ratio:0.37748784155940546;width:120px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/06/image-302x800.png 302w, https://jesseliberty.com/wp-content/uploads/2026/06/image-113x300.png 113w, https://jesseliberty.com/wp-content/uploads/2026/06/image-57x150.png 57w, https://jesseliberty.com/wp-content/uploads/2026/06/image.png 378w" sizes="auto, (max-width: 302px) 100vw, 302px" /></figure>



<p>And here is the output after running it as a test using the prompt Use of multi-agents in writing a C# application:</p>



<span id="more-13327"></span>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Blogger Blogger: No research yet, directing to researcher Decision: researcher Task: Research the topic: use of multiagents in writing a C# application</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>RESEARCHER Researching: Research the topic: use of multiagents in writing a C# application Found: The search results highlight several key findings regarding the use of multi-agent systems in C# app&#8230;</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Author Draft created: 3066 characters</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>REVIEWER Review: APPROVED &#8211; The draft effectively introduces the concept of multi-agent systems in C# applications an&#8230; ✓ Draft APPROVED</p>
</blockquote>



<p>========== RESULTS ========== Task: use of multiagents in writing a C# application</p>



<p>Research Findings (1):</p>



<ul class="wp-block-list">
<li>The search results highlight several key findings regarding the use of multi-agent systems in C# applications:</li>
</ul>



<ol class="wp-block-list">
<li><strong>Creating Multi-Agent Applications</strong>: Jesse Liberty discusses the development of a multi-agent application designed to generate blog posts. The article suggests a step-by-step approach to understanding the application, indicating that the process involves both design and implementation phases.</li>



<li><strong>Creative Writing Assistant</strong>: A sample application showcased by Microsoft demonstrates the use of multi-agent systems in a creative writing context. This application utilizes the Semantic Kernel and .NET Aspire, illustrating practical applications of multi-agent frameworks in enhancing creative tasks.</li>



<li><strong>Challenges in Multi-Agent Systems</strong>: Elliot One emphasizes a critical perspective on multi-agent systems, warning that simply adding more agents does not necessarily improve the quality of outputs. Instead, it can complicate the identification of errors, suggesting that careful design and management of agents are crucial for effective outcomes.</li>
</ol>



<p>Overall, these findings suggest that while multi-agent systems can enhance functionality in applications like creative writing, they also present challenges that require careful consideration in their design and implementation.</p>



<p>Draft:</p>



<h1 class="wp-block-heading">Harnessing Multi-Agent Systems in C# Applications: A Guide to Creative Writing</h1>



<p>In the evolving landscape of software development, multi-agent systems (MAS) have emerged as a powerful paradigm, particularly in applications that require creativity and collaboration. This post explores the implementation of multi-agent systems in C# applications, focusing on their role in enhancing creative writing tasks.</p>



<h2 class="wp-block-heading">Understanding Multi-Agent Systems</h2>



<p>Multi-agent systems consist of multiple interacting agents, each capable of autonomous decision-making. These agents can collaborate, negotiate, and share information, making them ideal for complex tasks that benefit from diverse perspectives. In the context of writing applications, MAS can facilitate the generation of content, brainstorming ideas, and even editing drafts.</p>



<h2 class="wp-block-heading">Creating a Multi-Agent Application</h2>



<p>Jesse Liberty outlines a structured approach to developing a multi-agent application aimed at generating blog posts. The process involves two key phases: design and implementation. During the design phase, developers must define the roles and responsibilities of each agent, ensuring that they complement one another. The implementation phase focuses on coding the agents, integrating them into a cohesive system, and testing their interactions.</p>



<h3 class="wp-block-heading">Example Application: Creative Writing Assistant</h3>



<p>A practical example of multi-agent systems in action is a creative writing assistant developed using Microsoft’s Semantic Kernel and .NET Aspire. This application showcases how agents can work together to enhance the writing process. For instance, one agent might focus on generating ideas, while another refines the language and style. By leveraging the strengths of each agent, the application can produce high-quality content that resonates with readers.</p>



<h2 class="wp-block-heading">Challenges in Multi-Agent Systems</h2>



<p>While the potential of multi-agent systems is significant, it is essential to approach their implementation with caution. Elliot One highlights a critical challenge: simply adding more agents does not guarantee improved outcomes. In fact, an increase in agents can complicate error identification and management. Therefore, careful design and oversight are crucial to ensure that the agents work harmoniously and effectively.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p>The integration of multi-agent systems in C# applications, particularly for creative writing, offers exciting possibilities for enhancing productivity and creativity. By understanding the design and implementation processes, as well as the potential challenges, developers can create robust applications that leverage the strengths of multiple agents. As the field continues to evolve, the thoughtful application of these systems will undoubtedly lead to innovative solutions in various domains.</p>



<p>In summary, multi-agent systems represent a promising frontier in software development, particularly for applications that thrive on collaboration and creativity. Embracing this technology can empower developers to create more dynamic and engaging user experiences.</p>



<p>Review Notes: APPROVED Revision Number: 1</p>



<p>In the next post we&#8217;ll begin looking at each file and how it relates to what we had in Python.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Creating a multi-agent application – Part 5 (final)</title>
		<link>https://jesseliberty.com/2026/06/15/creating-a-multi-agent-application-part-5-final/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Mon, 15 Jun 2026 13:42:32 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13322</guid>

					<description><![CDATA[In part 4 of this series we created our final two agents. In this final part of the series we&#8217;ll review the workflow that we create with the StateGraph class of LangGraph. In the following code we pass the ResearchState &#8230; <a href="https://jesseliberty.com/2026/06/15/creating-a-multi-agent-application-part-5-final/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>In <a href="https://jesseliberty.com/2026/06/14/creating-a-multi-agent-application-part-4/">part 4</a> of this series we created our final two agents. In this final part of the series we&#8217;ll review the workflow that we create with the StateGraph class of LangGraph.</p>



<span id="more-13322"></span>



<p>In the following code we pass the ResearchState object to StateGraph and create a node for each agent and edges that represent transitions between the nodes. There is one conditional edge: if the reviewer rejects the draft, it goes back to the author</p>



<pre class="wp-block-code"><code>from langgraph.graph import StateGraph, END
workflow = StateGraph(ResearchState)

workflow.add_node("blogger", blogger_node)
workflow.add_node("researcher", research_node)
workflow.add_node("author", author_node)
workflow.add_node("reviewer", reviewer_node)

workflow.set_entry_point("blogger")

workflow.add_edge("blogger", "researcher")
workflow.add_edge("researcher", "author")
workflow.add_edge("author", "reviewer")

workflow.add_conditional_edges(
    "reviewer",
    lambda state: "author" if state.get("review_result") == "rejected" else "END",
    {
        "author": "author",
        "END": END
    }
)

# Compile the graph
app = workflow.compile()</code></pre>



<p>The conditional edge says, &#8220;Start with reviewer. Now examine the state. If the review_result is rejected, go to the author; otherwise, go to END. Finally, the dictionary identifies what each node is.</p>



<p>That&#8217;s it. All that&#8217;s left is to take it out for a spin.</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>