diff --git a/AuthorAgent.cs b/AuthorAgent.cs index 2c9a056..b1d8c52 100644 --- a/AuthorAgent.cs +++ b/AuthorAgent.cs @@ -45,7 +45,7 @@ public AuthorAgent(IChatClient llm, ChatOptions chatOptions, ILogger InvokeAsync(ResearchState state, CancellationToken cancellationToken = default) + public async Task InvokeAsync(ResearchState state, CancellationToken cancellationToken = default) { using Activity? activity = s_activitySource.StartActivity("Author.Invoke"); activity?.SetTag("blog.revision", state.RevisionNumber); @@ -63,6 +63,8 @@ public async Task 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 @@ -74,7 +76,13 @@ public async Task 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) { @@ -84,7 +92,7 @@ public async Task 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; } } @@ -93,10 +101,20 @@ public async Task 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)."); + } + else + { + state.Draft = draft; + _logger.LogInformation("Draft created: {Length} characters", draft.Length); + } - state.Draft = draft; state.RevisionNumber += 1; return state; } diff --git a/IAuthorAgent.cs b/IAuthorAgent.cs index 73e9bf8..a9e4ce9 100644 --- a/IAuthorAgent.cs +++ b/IAuthorAgent.cs @@ -2,7 +2,8 @@ namespace BlogWriter; public interface IAuthorAgent { - Task InvokeAsync(ResearchState state, CancellationToken cancellationToken = default); + /// Returns the generated draft, or null if the agent produced no usable content. + Task InvokeAsync(ResearchState state, CancellationToken cancellationToken = default); Task AuthorNodeAsync(ResearchState state, CancellationToken cancellationToken = default); } diff --git a/Program.cs b/Program.cs index b81a769..8469b11 100644 --- a/Program.cs +++ b/Program.cs @@ -68,7 +68,7 @@ string GetRequired(string key) => var chatOptions = new ChatOptions { - Temperature = 0, + Temperature = 1, MaxOutputTokens = maxOutputTokens }; @@ -143,10 +143,44 @@ async Task 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; + } + + 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 @@ -192,6 +226,12 @@ async Task 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."); +} Console.WriteLine("============================="); if (tokenCapChatClient is not null) diff --git a/Prompts.cs b/Prompts.cs index bb4f73e..86fbb89 100644 --- a/Prompts.cs +++ b/Prompts.cs @@ -49,33 +49,33 @@ focused on .NET and AI with examples in C# and Python. """; /// - /// 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. /// 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. """; /// - /// 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. /// 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? @@ -83,7 +83,7 @@ 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: diff --git a/ResearchState.cs b/ResearchState.cs index a56072f..4de0afd 100644 --- a/ResearchState.cs +++ b/ResearchState.cs @@ -9,7 +9,19 @@ public class ResearchState /// The single source-of-truth marker written to on approval. public const string ApprovedMarker = "APPROVED"; + /// Default lower bound on the target word count for the blog post. + public const int DefaultMinWords = 1000; + + /// Default upper bound on the target word count for the blog post. + public const int DefaultMaxWords = 2000; + public string MainTask { get; set; } = ""; + + /// Minimum target word count for the draft. Used by the author and reviewer stages. + public int MinWords { get; set; } = DefaultMinWords; + + /// Maximum target word count for the draft. Used by the author and reviewer stages. + public int MaxWords { get; set; } = DefaultMaxWords; public List ResearchFindings { get; set; } = []; public string Draft { get; set; } = ""; public string ReviewNotes { get; set; } = ""; diff --git a/ReviewerAgent.cs b/ReviewerAgent.cs index fa02546..82edd1b 100644 --- a/ReviewerAgent.cs +++ b/ReviewerAgent.cs @@ -52,17 +52,13 @@ public async Task 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} """;