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

<channel>
	<title>Jesse Liberty</title>
	<atom:link href="https://jesseliberty.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://jesseliberty.com</link>
	<description>.NET, AI, C#, Python, Software Engineer, Tech Evangelist, Podcaster</description>
	<lastBuildDate>Sun, 21 Jun 2026 13:08:58 +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 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 fetchpriority="high" 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="(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 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="(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>



<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#. Here&#8217;s the set of files we&#8217;ll create:<br /></p>



<figure class="wp-block-image size-large is-resized"><img 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="(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 &#8211; 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>
		<item>
		<title>Creating a multi-agent application &#8211; Part 4</title>
		<link>https://jesseliberty.com/2026/06/14/creating-a-multi-agent-application-part-4/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 18:02:36 +0000</pubDate>
				<category><![CDATA[Agents]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13319</guid>

					<description><![CDATA[In part 3 we looked at creating the researcher. As promised, today we&#8217;ll look at the author. You&#8217;ll notice in the following code a great deal of similarity to what we&#8217;ve seen before. The goal is to create a code &#8230; <a href="https://jesseliberty.com/2026/06/14/creating-a-multi-agent-application-part-4/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>In <a href="https://jesseliberty.com/2026/06/12/creating-a-multi-agent-application-part-3/">part 3</a> we looked at creating the researcher. As promised, today we&#8217;ll look at the author.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="770" height="800" src="https://jesseliberty.com/wp-content/uploads/2026/06/author-770x800.jpg" alt="" class="wp-image-13320" style="aspect-ratio:0.9625140291806958;width:284px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/06/author-770x800.jpg 770w, https://jesseliberty.com/wp-content/uploads/2026/06/author-289x300.jpg 289w, https://jesseliberty.com/wp-content/uploads/2026/06/author-144x150.jpg 144w, https://jesseliberty.com/wp-content/uploads/2026/06/author-768x798.jpg 768w, https://jesseliberty.com/wp-content/uploads/2026/06/author.jpg 925w" sizes="auto, (max-width: 770px) 100vw, 770px" /></figure>



<p>You&#8217;ll notice in the following code a great deal of similarity to what we&#8217;ve seen before. The goal is to create a code &#8220;template&#8221; that we can follow as we create any agent; departing only for the agent&#8217;s special requirements and abilities.</p>



<p>As usual, we start with the factory method:</p>



<pre class="wp-block-code"><code>def create_author_chain():
    """Creates the author chain."""
    def author_invoke(state):
        research = state.get("research_findings", &#91;])
        research_text = "\n\n".join(research) if research else "No research available."

        prompt = author_prompt_template.format(
            main_task=state.get("main_task", ""),
            research_findings=research_text,
            draft=state.get("draft", ""),
            review_notes=state.get("review_notes", "")
        )

        try:
            response = llm.invoke(prompt)
            content = response.content if hasattr(response, 'content') else str(response)
            return content if content else "Draft in progress..."
        except Exception as e:
            print(f"Author error: {e}")
            return "Error generating draft. Please try again."

    return author_invoke

# Creating a callable object
author_chain = create_author_chain()</code></pre>



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



<p>We start by getting the research findings as a collection. We join all the entries into a single text string, or if there are no findings, we create the string &#8220;No research available.&#8221;</p>



<p>A local variable, prompt, is created from the author_prompt_template and passed to the invoke method of the LLM. The local variable content will contain the metadata or just the response from the LLM if there is no metadata.</p>



<p>The structure, so far, is identical to what we&#8217;ve seen before.</p>



<p>Next, we create the author_prompt_template used above,</p>



<pre class="wp-block-code"><code>author_prompt_template = """
You are a professional blogger.

Main Task: {main_task}

Research Findings:
{research_findings}

Current Draft: {draft}

Review Notes: {review_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 professional tone
- Make the post consise (aim for 250-500 words)

Write the complete post now:
"""</code></pre>



<p>This is pretty self-explanatory. The interpolation variables (e.g., {draft}) will be filled in when the create_author_chain runs.</p>



<p>Finally, we create the author_node,</p>



<pre class="wp-block-code"><code>def author_node(state: ResearchState) -> dict:
    """Author node that creates or revises draft."""
    print("\n>>>Author")

    draft = author_chain(state)
    print(f"Draft created: {len(draft)} characters")

    return {
        "draft": draft,
        "revision_number": state.get("revision_number", 0) + 1
    }</code></pre>



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



<p>The reviewer follows the same pattern, so we might as well look at it here. We start with the factory</p>



<pre class="wp-block-code"><code>def create_reviewer_chain():
    """Creates the reviewer chain."""
    def reviewer_invoke(state):
        draft = state.get("draft", "")
        revision_num = state.get("revision_number", 0)

        if len(draft.strip()) &lt; 100:
            return "APPROVED - Draft is minimal but acceptable."

        if revision_num >= 4:
            return "APPROVED - Maximum revisions reached. The report is satisfactory."

        prompt = reviewer_prompt_template.format(
            main_task=state.get("main_task", ""),
            draft=draft
        )

        try:
            response = llm.invoke(prompt)
            content = response.content if hasattr(response, 'content') else str(response)
            return content if content else "APPROVED"
        except Exception as e:
            print(f"Review error: {e}")
            return "APPROVED - Error in review, proceeding with current draft."

    return reviewer_invoke

# Creating a callable object
reviewer_chain = create_reviewer_chain()</code></pre>



<p>The reviewer will either approve the draft or kick it back directly to the author for revision. The only other part here is creating the node for the reviewer.</p>



<pre class="wp-block-code"><code>def reviewer_node(state: ResearchState) -> dict:
    """Node that reviews the draft."""
    print("\n>>REVIEWER")

    review = reviewer_chain(state)
    print(f"Review: {review&#91;:100]}...")

    is_approved = "APPROVED" in review.upper()

    if is_approved:
        print("✓ Draft APPROVED")
        return {
            "review_notes": "APPROVED",
            "next_step": "END"
        }
    else:
        print("✗ Revisions needed")
        return {
            "review_notes": review,
            "next_step": "author"
        }</code></pre>



<p>In the next post we&#8217;ll finally see how the nodes are used in creating a workflow.</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Creating a multi-agent application &#8212; Part 3</title>
		<link>https://jesseliberty.com/2026/06/12/creating-a-multi-agent-application-part-3/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Fri, 12 Jun 2026 21:16:22 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13316</guid>

					<description><![CDATA[In the previous post, we examined how to load the libraries we need and how to create the Blogger agent. In this post, we&#8217;ll examine the Research agent. You&#8217;ll no doubt notice the pattern of defining the template, the agent &#8230; <a href="https://jesseliberty.com/2026/06/12/creating-a-multi-agent-application-part-3/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>In the <a href="https://jesseliberty.com/2026/06/12/creating-a-multi-agent-application-part-2/">previous post</a>, we examined how to load the libraries we need and how to create the Blogger agent. In this post, we&#8217;ll examine the Research agent. You&#8217;ll no doubt notice the pattern of defining the template, the agent and the node. This will carry through for all the agents we&#8217;ll create.</p>



<pre class="wp-block-code"><code>researcher_prompt_template = """You are a researcher for a technical blog 
focused on .NET and AI with examples in C# and Python

Research Topic: {task}

Your goal is to find relevant, up-to-date insights for developers. Focus on:
- Key trends, challenges, or innovations
- Real-world use cases
- Supporting data or quotes from credible sources
- Simple explanations
- Short code examples in C# or Python

Summarize your findings concisely.
"""</code></pre>



<p>In this template we start by telling the researcher what role it will play. We then provide a goal and narrow that goal to a series of topics to focus on and how to present that data.</p>



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



<pre class="wp-block-code"><code>def create_researcher_agent():
    """Creates a researcher agent that uses Tavily search."""

    def researcher_invoke(input_dict):
        """Execute research using Tavily search."""
        query = input_dict.get("input", "")

        try:
            search_response = tavily_tool.invoke({"query": query})
            results = search_response.get('results', &#91;])
            formatted_results = &#91;]

            if results:
                for result in results&#91;:3]:
                    title = result.get('title', 'Untitled')
                    url = result.get('url', 'N/A')
                    content = result.get('content', '')
                    formatted_results.append(f">>{title}\nSource: {url}\n{content&#91;:300]}...\n")

                raw_output = "\n".join(formatted_results)
            elif not raw_output:
                raw_output = "No results found"

            # Summarize with LLM
            summary_prompt = f"""Based on these search results about '{query}',
            provide a concise summary of key findings:
            {raw_output}
            """

            summary_response = llm.invoke(summary_prompt)
            summary = summary_response.content if hasattr(summary_response, 'content') else str(summary_response)

            return {
                "output": summary if summary else raw_output,
                "input": query
            }

        except Exception as e:
            print(f"Research error: {e}")
            return {
                "output": f"Research completed on: {query}. Key information has been gathered from web sources.",
                "input": query
            }

    return researcher_invoke

researcher_agent = create_researcher_agent()</code></pre>



<p>Using the same pattern we used with the Blogger we create a factory to create the researcher agent. Note that we flag that we&#8217;ll be using Tavily for searching the web. </p>



<p>We take the raw information obtained and feed it to the LLM asking for a concise summary. This line:</p>



<pre class="wp-block-code"><code>summary = summary_response.content if hasattr(summary_response, 'content') else str(summary_response)</code></pre>



<p>&#8230;attempts to get the content attribute if it exists. Otherwise, it returns the summary_response.</p>



<p>Don&#8217;t be confused by the inner and outer methods. Remember, indentation is critical in Python.</p>



<p>Finally, we&#8217;ll create the research node, just as we did with Blogger. </p>



<pre class="wp-block-code"><code>def research_node(state: ResearchState) -> dict:
    """Research node that gathers information."""
    print("\n>>>RESEARCHER")

    sub_task = state.get("current_sub_task", state.get("main_task"))
    print(f"Researching: {sub_task}")

    try:
        result = researcher_agent({"input": sub_task})
        findings = result.get("output", "Research completed")
        print(f"Found: {str(findings)&#91;:100]}...")
    except Exception as e:
        print(f"Research error: {e}")
        findings = f"Research on {sub_task} - information gathered"

    return {
        "research_findings": &#91;findings]
    }</code></pre>



<p>In the next post, we&#8217;ll examine the Author agent.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Creating a multi-agent application. Part 2</title>
		<link>https://jesseliberty.com/2026/06/12/creating-a-multi-agent-application-part-2/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Fri, 12 Jun 2026 07:00:00 +0000</pubDate>
				<category><![CDATA[Essentials]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13308</guid>

					<description><![CDATA[In my previous post, I showed the output of a multi-agent application I wrote to create blog posts (not to worry, it is for demonstration purposes only). In this post, I will begin the process of working through the code, &#8230; <a href="https://jesseliberty.com/2026/06/12/creating-a-multi-agent-application-part-2/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>In my <a href="https://jesseliberty.com/2026/06/11/creating-a-multi-agent-application/">previous post,</a> I showed the output of a multi-agent application I wrote to create blog posts (not to worry, it is for demonstration purposes only). In this post, I will begin the process of working through the code, line by line.</p>



<p>This application is written in Python, in a Colab notebook, using (among other things) LangChain and LangGraph. To follow along you will need to obtain an API key from OpenAI and a key from <a href="https://www.tavily.com/">Tavily</a>. </p>



<p>If you are a C# programmer with little or no Python experience, <em>don&#8217;t panic!</em> Python is pretty readable, and I&#8217;ll explain any part that is potentially obscure or confusing.</p>



<p>This will be a multi-agent application. The agents we&#8217;ll create will be:</p>



<ul class="wp-block-list">
<li><strong>Blogger</strong> which will orchestrate the others</li>



<li><strong>Researcher</strong>, which will search the web for relevant information</li>



<li><strong>Author</strong>, which will write drafts of the blog post</li>



<li><strong>Reviewer</strong>, which will evaluate the drafts and suggest improvements</li>
</ul>



<p>As a general rule, I try to limit the number of agents to 3-5. Any more than that can get terribly complicated with diminishing returns. Your mileage may vary.</p>



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



<h2 class="wp-block-heading">Set Up</h2>



<p>The program begins by importing the necessary libraries.</p>



<pre class="wp-block-code"><code>!pip install -q openai==1.66.3 \
                langchain==0.3.20 \
                langchain-openai==0.3.9 \
                langchain_experimental==0.3.4 \
                langchain-tavily==0.2.4 \
                tavily-python==0.5.0 \
                langgraph==0.3.21 \
                langgraph-supervisor==0.0.18
</code></pre>



<p>You then load your OpenAI API key and Tavily API key from either a configuration file (which is what I do here) or from the environment.</p>



<pre class="wp-block-code"><code>import json
import os
from pprint import pprint

file_name = 'config.json'
with open(file_name, 'r') as file:
    config = json.load(file)
    os.environ&#91;'OPENAI_API_KEY'] = config.get("API_KEY")
    os.environ&#91;"OPENAI_BASE_URL"] = config.get("OPENAI_API_BASE")
    os.environ&#91;"TAVILY_API_KEY"] = config.get("TAVILY_API_KEY")</code></pre>



<p>Next, we set up the model. </p>



<pre class="wp-block-code"><code>from langchain_openai import ChatOpenAI

model_name = 'gpt-4o-mini'

llm = ChatOpenAI(
    model = model_name,
    temperature=0,
    max_tokens=4096
)</code></pre>



<p>I&#8217;ve opted for the gpt-4o-mini model, as it&#8217;s the most cost-effective option. I&#8217;ve set the temperature to 0 to get the most consistent results. </p>



<p>Tavily is a tool for searching the web, so let&#8217;s set that up as well</p>



<pre class="wp-block-code"><code>from langchain_tavily import TavilySearch

tavily_tool = TavilySearch(
    max_results=5,
    topic="general",
    include_answer=False,
    include_raw_content=False,
    search_depth="basic"
)</code></pre>



<p>You can find all the options for this on the <a href="https://tavily.com">Tavily website</a>.</p>



<p>I&#8217;m going to want a state object so that I can pass it among the agents. </p>



<pre class="wp-block-code"><code>from typing import TypedDict, Annotated, List
from langgraph.graph import StateGraph, END
import operator

class ResearchState(TypedDict):
    """State for the research workflow."""
    main_task: str
    research_findings: Annotated&#91;List&#91;str], operator.add]
    draft: str
    review_notes: str
    revision_number: int
    next_step: str
    current_sub_task: str</code></pre>



<p><strong>Note</strong>, the line <em>research_findings: Annotated [List(str), operator.add) </em>is a Python type annotation. In short, an annotated list has metadata, in this case the function operator.add. This operator, in this context, is used to update state, though it can also be used to reduce the prompt. </p>



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



<p>To get started, we&#8217;ll create the (non-trivial) blogger agent. This is actually the most powerful and thus the most complex of the agents.</p>



<p>We begin with the prompt template (this is, essentially, the system prompt)</p>



<pre class="wp-block-code"><code>blogger_prompt_template = """You are a blogger managing a blog post creation workflow.

Current Task: {main_task}

Current State:
- Research Findings: {research_findings}
- Blog Draft: {draft}
- Reviewer Feedback: {review_notes}
- Revision Number: {revision_number}

Your goal is to ensure a clear, engaging, and valuable blog post targeted at software developers.

Decide the next step and respond only with a JSON object (no extra text):
{
  "next_step": "researcher" or "author" or "END",
  "task_description": "Brief description of what needs to be done next"
}

Decision Rules:
- If no research exists, choose "researcher"
- If research exists but no draft, choose "author"
- If draft exists and reviewer says "APPROVED", choose "END"
- If draft needs revision, choose "author"
- If revision_number >= 4, choose "END"
"""</code></pre>



<p>Take a moment to read this over; it sets the parameters and goals of the program. The decision rules are critical, they control the flow, and they set a limit on revisions (in this case 4). </p>



<p>Having told the Blogger what we are trying to accomplish and the general tone of the output we&#8217;re ready to create the decision tree that constitutes the workflow for the Blogger. This is pretty long, but much of it is self-explanatory.</p>



<pre class="wp-block-code"><code>def create_blogger_chain():
    """Creates the bloger decision chain."""

    def blogger_invoke(state):
        research = state.get("research_findings", &#91;])
        research_text = "\n".join(research) if research else "No research yet."
        revision = state.get("revision_number", 0)
        has_research = len(research) > 0
        has_draft = bool(state.get("draft", "").strip())
        review = state.get("review_notes", "")

        if "APPROVED" in review.upper() and has_draft:
            print("Blogger: Draft approved, ending workflow")
            return {
                "next_step": "END",
                "task_description": "Report approved and complete"
            }

        if not has_research:
            print("Blogger: No research yet, directing to researcher")
            return {
                "next_step": "researcher",
                "task_description": f"Research the topic: {state.get('main_task', '')}"
            }


        if has_research and not has_draft:
            print("Blogger: Have research, creating first draft")
            return {
                "next_step": "author",
                "task_description": "Write the first draft based on research findings"
            }

        if has_draft and not review:
            print("Blogger: Have draft, sending to reviewer")
            return {
                "next_step": "reviewer",
                "task_description": "Prepare draft for review"
            }

        if review and "APPROVED" not in review.upper() and revision &lt;= 4:
            print(f"Blogger: Revision {revision}, sending back to author")
            return {
                "next_step": "author",
                "task_description": "Revise the draft based on review feedback"
            }

        if revision >= 4:
            print("Blogger: Max revisions reached! Ending")
            return {
                "next_step": "END",
                "task_description": "Maximum revisions reached! Finalizing report"
            }

        # LLM as fallback
        prompt = blogger_prompt_template.format(
            main_task=state.get("main_task", ""),
            research_findings=research_text,
            draft=state.get("draft", "No draft yet."),
            review_notes=review if review else "No review yet.",
            revision_number=revision
        )

        try:
            response = llm.invoke(prompt)
            content = response.content if hasattr(response, 'content') else str(response)

            text = content.strip()
            if text.startswith("```"):
                lines = text.split("\n")
                text = "\n".join(&#91;l for l in lines if not l.strip().startswith("```")])
            text = text.strip()

            decision = json.loads(text)

            if "next_step" in decision:
                return decision

        except Exception as e:
            print(f"LLM parsing error: {e}")

        # Final fallback 
        print("Blogger: Using final fallback - continuing with author")
        return {
            "next_step": "author",
            "task_description": "Continue with draft creation"
        }

    return blogger_invoke

# Creating a callable object
blogger_chain = create_blogger_chain()</code></pre>



<p>We begin with a nested function.</p>



<pre class="wp-block-code"><code>def create_blogger_chain():<br />    """Creates the blogger decision chain."""<br /><br />    def blogger_invoke(state):<br />        ...</code></pre>



<h3 class="wp-block-heading">When called, Python executes the code inside it, that is, the inner function. In this case, the inner function does the real work.</h3>



<p>We define a second function <strong>inside</strong> <code>create_blogger_chain</code>. We do this to create a closure, that is the inner function can access variables from the outer function. This is a common construct when working with langchain. The inner function has access to the llm without it being passed every time. So, in short, <em>create_blogger_chain</em> is actually a factory function that constructs and configures a callable function and then returns it.</p>



<p>We next set up our variables based on starting values in the state object. With that done, we&#8217;re ready to progress through a series of possible conditions. These follow the rules established in the template.</p>



<p>All that&#8217;s left for the blogger is to create a node where the decision will be implemented. We&#8217;ll use this node, and the others we&#8217;ll create, when we implement the workflow (after we define all the agents).</p>



<pre class="wp-block-code"><code>def blogger_node(state: ResearchState) -&gt; dict:
    """Blogger decides the next step."""
    print("\n&gt;&gt;&gt;Blogger")

    decision = blogger_chain(state)

    next_step = decision.get("next_step", "researcher")
    task_desc = decision.get("task_description", "Continue work")

    print(f"Decision: {next_step}")
    print(f"Task: {task_desc}")

    return {
        "next_step": next_step,
        "current_sub_task": task_desc,
    }</code></pre>



<p><strong>Note</strong> The variable <em>decision</em> is assigned as a result of calling the code we just reviewed. With that in hand, we call <em>get</em> on the <em>decision,</em> asking for the next step. If none is returned, we use researcher.</p>



<p>That&#8217;s it for blogger. We&#8217;ll review the other agents in the next posting.</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Creating a multi-agent application &#8211; Part 1</title>
		<link>https://jesseliberty.com/2026/06/11/creating-a-multi-agent-application/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Thu, 11 Jun 2026 16:24:42 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13301</guid>

					<description><![CDATA[The following text was created by a multi-agent application designed to create blog posts. In my next post we&#8217;ll take the application apart, step by step. For now, here is a test run with the prompt Use of multiagents in &#8230; <a href="https://jesseliberty.com/2026/06/11/creating-a-multi-agent-application/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p><strong>The following text was created by a multi-agent application designed to create blog posts. In my next post we&#8217;ll take the application apart, step by step. For now, here is a test run with the prompt <em>Use of multiagents in writing a C# application</em>.&#8221;</strong></p>



<p>Draft created: 2653 characters<br />{&#8216;author&#8217;: {&#8216;draft&#8217;: &#8216;# Harnessing Multi-Agent Systems in C# Applications\n&#8217;<br />&#8216;\n&#8217;<br />&#8216;In the evolving landscape of software development, &#8216;<br />&#8216;multi-agent systems (MAS) have emerged as a powerful &#8216;<br />&#8216;paradigm, particularly in enhancing the functionality of &#8216;<br />&#8216;applications. However, the integration of these systems &#8216;<br />&#8216;into C# applications comes with its own set of &#8216;<br />&#8216;challenges and considerations. This post explores the &#8216;<br />&#8216;key aspects of implementing multi-agent systems in C#, &#8216;<br />&#8216;drawing from recent research findings.\n&#8217;<br />&#8216;\n&#8217;</p>



<p><br />&#8216;## Understanding Multi-Agent Systems\n&#8217;<br />&#8216;\n&#8217;<br />&#8216;At its core, a multi-agent system consists of multiple &#8216;<br />&#8216;autonomous agents that interact with one another to &#8216;<br />&#8216;achieve specific goals. These agents can be designed to &#8216;<br />&#8216;perform tasks collaboratively, leading to improved &#8216;<br />&#8216;efficiency and problem-solving capabilities. However, as &#8216;<br />&#8216;highlighted by Elliot One, simply increasing the number &#8216;<br />&#8216;of agents does not guarantee better outcomes. In fact, &#8216;<br />&#8216;it can complicate the debugging process, making it more &#8216;<br />&#8216;difficult to trace failures and understand system &#8216;<br />&#8216;behavior. This underscores the importance of thoughtful &#8216;<br />&#8216;design and implementation when developing multi-agent &#8216;<br />&#8216;systems.\n&#8217;<br />&#8216;\n&#8217;</p>



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



<p><br />&#8216;## Multi-Agent Architecture in .NET\n&#8217;<br />&#8216;\n&#8217;<br />&#8216;For developers looking to implement multi-agent systems &#8216;<br />&#8216;in C#, the Microsoft Agent Framework provides a robust &#8216;<br />&#8216;foundation. A recent tutorial video introduces the &#8216;<br />&#8216;concept of multi-agent orchestration and workflows, &#8216;<br />&#8216;offering insights into how these systems can be &#8216;<br />&#8216;structured within .NET applications. This resource is &#8216;<br />&#8216;invaluable for developers seeking to grasp the &#8216;<br />&#8216;architectural considerations necessary for effective &#8216;<br />&#8216;multi-agent implementation.\n&#8217;<br />&#8216;\n&#8217;<br />&#8216;## Practical Implementation\n&#8217;<br />&#8216;\n&#8217;<br />&#8216;Building a multi-agent system requires a structured &#8216;<br />&#8216;approach. A Codelabs resource outlines a step-by-step &#8216;<br />&#8216;guide for developers interested in practical &#8216;<br />&#8216;implementation. This guide covers prerequisites, &#8216;<br />&#8216;essential components, and best practices for creating a &#8216;<br />&#8216;functional multi-agent system. By following this &#8216;<br />&#8216;structured methodology, developers can mitigate common &#8216;<br />&#8216;pitfalls and enhance the overall quality of their &#8216;<br />&#8216;applications.\n&#8217;<br />&#8216;\n&#8217;<br />&#8216;## Conclusion\n&#8217;<br />&#8216;\n&#8217;<br />&#8216;While multi-agent systems hold significant potential for &#8216;<br />&#8216;enhancing C# applications, developers must navigate the &#8216;<br />&#8216;complexities associated with their implementation. By &#8216;<br />&#8216;leveraging available resources and adhering to best &#8216;<br />&#8216;practices, it is possible to create robust multi-agent &#8216;<br />&#8216;systems that improve application functionality without &#8216;<br />&#8216;succumbing to the common challenges of increased &#8216;<br />&#8216;complexity. As the field continues to evolve, staying &#8216;<br />&#8216;informed and educated on the latest developments will be &#8216;<br />&#8216;crucial for developers aiming to harness the full power &#8216;<br />&#8216;of multi-agent systems in their applications.&#8217;</p>



<p>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212; <br />&gt;&gt;REVIEWER Review: APPROVED &#8211; The draft effectively introduces the concept of multi-agent systems in C# applications an&#8230; ✓ Draft APPROVED {&#8216;reviewer&#8217;: {&#8216;next_step&#8217;: &#8216;END&#8217;, &#8216;review_notes&#8217;: &#8216;APPROVED&#8217;}}<br /> &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212; <br />&gt;&gt;&gt;Blogger Blogger: Draft approved, ending workflow Decision: END Task: Report approved and complete {&#8216;blogger&#8217;: {&#8216;current_sub_task&#8217;: &#8216;Report approved and complete&#8217;, &#8216;next_step&#8217;: &#8216;END&#8217;}}</p>



<p>We take this apart beginning in <a href="https://jesseliberty.com/2026/06/12/creating-a-multi-agent-application-part-2/">part 2</a>.</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>ReAct and Agents in AI</title>
		<link>https://jesseliberty.com/2026/05/22/react-and-agents-in-ai/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Fri, 22 May 2026 11:22:31 +0000</pubDate>
				<category><![CDATA[Agents]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[CoT]]></category>
		<category><![CDATA[ReAct]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13295</guid>

					<description><![CDATA[In the previous post, we looked at the use of Chain of Thought (CoT) reasoning in the context of LLMs. For an LLM to take action in the world, however, it needs agents. The paradigm for this is called ReAct—that &#8230; <a href="https://jesseliberty.com/2026/05/22/react-and-agents-in-ai/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>In the <a href="https://jesseliberty.com/2026/05/18/ai-reasoning-and-planning/">previous post,</a> we looked at the use of Chain of Thought (CoT) reasoning in the context of LLMs. For an LLM to take action in the world, however, it needs agents. The paradigm for this is called ReAct—that is, REason and ACT. </p>



<p>In order to interact with the world, the agent will use tools (such as code that accesses APIs, searches the Internet, etc.). This creates a dynamic cycle: </p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="257" height="211" src="https://jesseliberty.com/wp-content/uploads/2026/05/loop.jpg" alt="" class="wp-image-13296" srcset="https://jesseliberty.com/wp-content/uploads/2026/05/loop.jpg 257w, https://jesseliberty.com/wp-content/uploads/2026/05/loop-150x123.jpg 150w" sizes="auto, (max-width: 257px) 100vw, 257px" /></figure>



<p><strong>Think</strong>—the LLM reasons and decides what tool to use<br /><strong>ACT</strong>—the LLM uses the tool to take action in the world<br /><strong>Observe</strong>—the LLM observes the result of the action and adjusts accordingly, refining its plan</p>



<p>The cycle ends when the LLM has its final answer.</p>



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



<h2 class="wp-block-heading">Prompts for ReAct</h2>



<p>The prompt must specify the available tools <em>and their descriptions</em>. It will instruct the LLM to use the cycle above and provide examples of the cycle. Finally, it will constrain the output into a <em>machine-readable format</em>. </p>



<p>The prompt must be very clear and precise. The basic template is to tell the agent what it is (&#8220;you are a medical assistant&#8221;), what tools it has access to, how to use the tool, what the returned observation will look like (structured data such as JSON) and how to provide the interim and final answers.</p>



<p>The output after each tool use is machine-readable so that it can be fed back into the cycle or to a tool or internal prompt.</p>



<p>With CoT (Chain of Thought), the LLM was limited to its existing knowledge and could not deviate from its initial planning. With ReAct the LLM can adapt to the observed results and use tools to extend its knowledge. If an error is encountered, the LLM can adjust its plan to avoid the failure (e.g., using a different tool).</p>



<p>We can tell the LLM to output its plan and observations as it goes. This can be enormously helpful for debugging.</p>



<p>When we look at the &#8220;think&#8221; phase, we return to the use of CoT. It is here that an initial plan is generated and the LLM uses the tools to execute the plan after which the LLM will observe the results and use them to adjust the plan.</p>



<p>Interacting with the tools brings us to the Model Context Protocol (MCP), a topic for an upcoming blog post.</p>



<p></p>



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