Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions AuthorAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public AuthorAgent(IChatClient llm, ChatOptions chatOptions, ILogger<AuthorAgent
_logger.LogInformation("AuthorAgent initialized.");
}

public async Task<string> InvokeAsync(ResearchState state, CancellationToken cancellationToken = default)
public async Task<string?> InvokeAsync(ResearchState state, CancellationToken cancellationToken = default)
{
using Activity? activity = s_activitySource.StartActivity("Author.Invoke");
activity?.SetTag("blog.revision", state.RevisionNumber);
Expand All @@ -63,6 +63,8 @@ public async Task<string> InvokeAsync(ResearchState state, CancellationToken can
Current Draft: {(string.IsNullOrEmpty(state.Draft) ? "(none — write the first draft)" : state.Draft)}

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

Target Word Count: {state.MinWords} to {state.MaxWords} words
""";

try
Expand All @@ -74,7 +76,13 @@ public async Task<string> InvokeAsync(ResearchState state, CancellationToken can
});
AgentResponse response = await _agent.RunAsync(message, options: runOptions, cancellationToken: cancellationToken);
string content = response.Text;
return !string.IsNullOrEmpty(content) ? content : "Draft in progress...";
if (!string.IsNullOrEmpty(content))
{
return content;
}

_logger.LogWarning("Author agent returned no content for revision {Revision}.", state.RevisionNumber);
return null;
}
catch (TokenCapExceededException)
{
Expand All @@ -84,7 +92,7 @@ public async Task<string> InvokeAsync(ResearchState state, CancellationToken can
catch (Exception e)
{
_logger.LogError(e, "Author agent failed to generate content.");
return "Error generating draft. Please try again.";
return null;
}
}

Expand All @@ -93,10 +101,20 @@ public async Task<ResearchState> AuthorNodeAsync(ResearchState state, Cancellati
{
_logger.LogInformation("Author stage started.");

string draft = await InvokeAsync(state, cancellationToken);
_logger.LogInformation("Draft created: {Length} characters", draft.Length);
string? draft = await InvokeAsync(state, cancellationToken);

if (string.IsNullOrEmpty(draft))
{
// Keep whatever draft already exists rather than clobbering it with a
// placeholder — an empty/failed generation shouldn't erase real content.
_logger.LogWarning("Author agent produced no draft; keeping the previous draft (if any).");
}
Comment on lines +106 to +111
else
{
state.Draft = draft;
_logger.LogInformation("Draft created: {Length} characters", draft.Length);
}

state.Draft = draft;
state.RevisionNumber += 1;
return state;
}
Expand Down
3 changes: 2 additions & 1 deletion IAuthorAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ namespace BlogWriter;

public interface IAuthorAgent
{
Task<string> InvokeAsync(ResearchState state, CancellationToken cancellationToken = default);
/// <summary>Returns the generated draft, or null if the agent produced no usable content.</summary>
Task<string?> InvokeAsync(ResearchState state, CancellationToken cancellationToken = default);

Task<ResearchState> AuthorNodeAsync(ResearchState state, CancellationToken cancellationToken = default);
}
44 changes: 42 additions & 2 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ string GetRequired(string key) =>

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

Expand Down Expand Up @@ -143,10 +143,44 @@ async Task<HttpResponseMessage> PostWithRetryAsync(string requestUri, object bod
Console.Write("Enter your topic: ");
string topic = Console.ReadLine() ?? string.Empty;

int minWords = ReadWordCount(
$"Enter minimum word count [{ResearchState.DefaultMinWords}]: ",
ResearchState.DefaultMinWords);
int maxWords = ReadWordCount(
$"Enter maximum word count [{ResearchState.DefaultMaxWords}]: ",
ResearchState.DefaultMaxWords,
minimum: minWords);

// Prompts for a positive word count, re-asking until a valid value (or blank
// for the default) is entered. `minimum`, when set, enforces max >= min.
int ReadWordCount(string prompt, int defaultValue, int? minimum = null)
{
while (true)
{
Console.Write(prompt);
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
return defaultValue;
}
Comment on lines +162 to +165

if (int.TryParse(input, out int value) && value > 0 && (minimum is null || value >= minimum))
{
return value;
}

Console.WriteLine(minimum is null
? "Please enter a positive whole number."
: $"Please enter a whole number greater than or equal to {minimum}.");
}
}

// Run the workflow for the entered topic
var initialState = new ResearchState
{
MainTask = topic
MainTask = topic,
MinWords = minWords,
MaxWords = maxWords
};

// Ctrl+C requests a graceful cancellation of the in-flight run instead of an
Expand Down Expand Up @@ -192,6 +226,12 @@ async Task<HttpResponseMessage> PostWithRetryAsync(string requestUri, object bod
Console.WriteLine($"\nDraft:\n{result.Draft}");
Console.WriteLine($"\nReview Notes: {result.ReviewNotes}");
Console.WriteLine($"Revision Number: {result.RevisionNumber}");
if (result.RevisionNumber >= ResearchState.MaxRevisions)
{
// The revision cap terminates the loop even if the reviewer never approved —
// call that out so the draft above isn't mistaken for a reviewer-approved one.
Console.WriteLine("Note: Maximum revision limit reached; draft above printed as-is.");
}
Comment on lines +229 to +234
Console.WriteLine("=============================");

if (tokenCapChatClient is not null)
Expand Down
18 changes: 9 additions & 9 deletions Prompts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,41 +49,41 @@ focused on .NET and AI with examples in C# and Python.
""";

/// <summary>
/// Author system prompt. The task, research findings, current draft and review
/// notes are supplied as the user message each turn.
/// Author system prompt. The task, research findings, current draft, review
/// notes, and target word count are supplied as the user message each turn.
/// </summary>
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.
draft (if any), any reviewer notes, and the target word count range.

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

- Aim for 1000 to 2000 words.
- Aim for the target word count range given in the user message.

Write the complete post.
""";

/// <summary>
/// Reviewer system prompt. The task and the draft to review are supplied as the
/// user message.
/// Reviewer system prompt. The task, the target word count range, and the draft
/// to review are supplied as the user message.
/// </summary>
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.
The user message contains the main task, the target word count range, 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?
6. Size – Is the post between 1000 and 2000 words?
6. Size – Is the post within the target word count range given in the user message?


Respond with one of:
Expand Down
12 changes: 12 additions & 0 deletions ResearchState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,19 @@ public class ResearchState
/// <summary>The single source-of-truth marker written to <see cref="ReviewNotes"/> on approval.</summary>
public const string ApprovedMarker = "APPROVED";

/// <summary>Default lower bound on the target word count for the blog post.</summary>
public const int DefaultMinWords = 1000;

/// <summary>Default upper bound on the target word count for the blog post.</summary>
public const int DefaultMaxWords = 2000;

public string MainTask { get; set; } = "";

/// <summary>Minimum target word count for the draft. Used by the author and reviewer stages.</summary>
public int MinWords { get; set; } = DefaultMinWords;

/// <summary>Maximum target word count for the draft. Used by the author and reviewer stages.</summary>
public int MaxWords { get; set; } = DefaultMaxWords;
public List<string> ResearchFindings { get; set; } = [];
public string Draft { get; set; } = "";
public string ReviewNotes { get; set; } = "";
Expand Down
8 changes: 2 additions & 6 deletions ReviewerAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,13 @@ public async Task<string> InvokeAsync(ResearchState state, CancellationToken can
activity?.SetTag("blog.revision", state.RevisionNumber);

string draft = state.Draft;
int revisionNum = state.RevisionNumber;

if (revisionNum >= ResearchState.MaxRevisions)
{
return "APPROVED - Maximum revisions reached.";
}

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

Target Word Count: {state.MinWords} to {state.MaxWords} words

Draft to Review:
{draft}
""";
Expand Down