Migrating Agentic Code Python -> C# Part 5

In the previous post we looked at implementing the Researcher in C#. In this, as promised, we’ll look at the Author and the Reviewer.

The Author is handed two objects when instantiated: the llm (an IChatClient object) and the chatOptions. Its primary method is InvokeAsync, which is passed the current ResearchState.

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

Its expectation is that the state object will have research information from the Researcher. It creates its prompt based on the state and then sends that prompt, along with its options, to the llm. What it gets back is its first draft which it will pass to the Reviewer

       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.";
        }
    }

All that’s left for the Author is to create its node

    public async Task<ResearchState> AuthorNodeAsync(ResearchState state)
    {
        Console.WriteLine("\n>>>Author");

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

        state.Draft = draft;
        state.RevisionNumber += 1;
        return state;
    }

That was short enough that we should look at the Reviewer while we’re here.

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.

   public async Task<string> InvokeAsync(ResearchState state)
    {
        string draft = state.Draft;
        int revisionNum = state.RevisionNumber;

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

        if (revisionNum >= 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.";
        }
    }

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

Let’s create the Reviewer’s node and in the next post we can finally see how these nodes are used.

   public async Task<ResearchState> ReviewerNodeAsync(ResearchState state)
    {
        Console.WriteLine("\n>>REVIEWER");

        string review = await InvokeAsync(state);
        string preview = review.Length > 100 ? review[..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;
    }

Unknown's avatar

About Jesse Liberty

** Note ** Jesse is currently looking for a new position. You can learn more about him at https://jesseliberty.bio Thank you. Jesse Liberty has three decades of experience writing and delivering software projects and is the author of 2 dozen books and a couple dozen online courses. His latest book, Building APIs with .NET, is now available wherever you buy your books. Liberty was a Team Lead and Senior Software Engineer for various corporations, a Senior Technical Evangelist for Microsoft, a Distinguished Software Engineer for AT&T, a VP for Information Services for Citibank and a Software Architect for PBS. He is a 13 year Microsoft MVP.
This entry was posted in AI, C#, Programming. Bookmark the permalink.