diff --git a/BlogWriter.Tests/ResearchStateTests.cs b/BlogWriter.Tests/ResearchStateTests.cs index d090371..576c445 100644 --- a/BlogWriter.Tests/ResearchStateTests.cs +++ b/BlogWriter.Tests/ResearchStateTests.cs @@ -87,4 +87,22 @@ public void StartFollowUp_PreservesDraftAndResearchButResetsReviewCycle() Assert.Equal(0, state.RevisionNumber); Assert.Empty(state.NextStep); } + + [Fact] + public void BuildResearchQuery_IncludesOriginalTopicAndFollowUpRefinement() + { + var state = new ResearchState + { + MainTask = "How to build a blog app", + ResearchFindings = ["Concepts: context, prompts, and workflow.", "Drafting is done after research."], + CurrentSubTask = "Add a caching section." + }; + + string query = state.BuildResearchQuery(); + + Assert.Contains("How to build a blog app", query); + Assert.Contains("Add a caching section.", query); + Assert.Contains("Concepts: context, prompts, and workflow.", query); + Assert.Contains("Drafting is done after research.", query); + } } diff --git a/Program.cs b/Program.cs index eee65d8..99c79cd 100644 --- a/Program.cs +++ b/Program.cs @@ -194,7 +194,6 @@ void PrintResults(BlogSession session) { ResearchState result = session.State; Console.WriteLine("\n\n========== RESULTS =========="); - Console.WriteLine($"\nSession: {session.Id}"); Console.WriteLine($"\nTask: {result.MainTask}"); Console.WriteLine($"\n\n===Research Findings===\n ({result.ResearchFindings.Count}):"); foreach (string finding in result.ResearchFindings) @@ -202,14 +201,17 @@ void PrintResults(BlogSession session) Console.WriteLine($"- {finding}"); } - Console.WriteLine($"\n\n===Draft:===\n{result.Draft}"); Console.WriteLine($"\n\n===Review Notes:===\n {result.ReviewNotes}"); - Console.WriteLine($"\n\nRevision Number: {result.RevisionNumber}"); + Console.WriteLine($"\n\n===Draft:===\n{result.Draft}"); + if (result.RevisionLimitReached) { Console.WriteLine("Note: Maximum revision limit reached; draft above printed as-is."); } Console.WriteLine("============================="); + Console.WriteLine($"\n\nRevision Number: {result.RevisionNumber}"); + Console.WriteLine($"\nSession: {session.Id}\n"); + Console.WriteLine("============================="); } diff --git a/ResearchState.cs b/ResearchState.cs index 29514ec..e29a80a 100644 --- a/ResearchState.cs +++ b/ResearchState.cs @@ -23,17 +23,74 @@ public class ResearchState /// 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 List SearchRefinements { get; set; } = []; 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; } = ""; + /// + /// Builds the full research query for a follow-up refinement. It keeps the + /// original task, includes the new refinement request, and folds in the + /// previously discovered research context so the next search narrows the + /// earlier work instead of replacing it. + /// + public string BuildResearchQuery() + { + var parts = new List(); + + if (!string.IsNullOrWhiteSpace(MainTask)) + { + parts.Add($"Original task: {MainTask.Trim()}"); + } + + if (!string.IsNullOrWhiteSpace(CurrentSubTask)) + { + parts.Add($"Follow-up refinement: {CurrentSubTask.Trim()}"); + } + + if (SearchRefinements.Count > 0) + { + string uniqueRefinements = string.Join(" | ", SearchRefinements + .Where(r => !string.IsNullOrWhiteSpace(r)) + .Distinct(StringComparer.OrdinalIgnoreCase)); + + if (!string.IsNullOrWhiteSpace(uniqueRefinements)) + { + parts.Add($"Refinement history: {uniqueRefinements}"); + } + } + + if (ResearchFindings.Count > 0) + { + string context = string.Join("\n\n", ResearchFindings + .Where(f => !string.IsNullOrWhiteSpace(f)) + .Select(f => f.Trim())); + + if (!string.IsNullOrWhiteSpace(context)) + { + parts.Add($"Prior research context:\n{context}"); + } + } + + return string.Join("\n\n", parts); + } + /// Prepares an approved or revision-capped draft for a user-requested follow-up. public void StartFollowUp(string followUp) { ArgumentException.ThrowIfNullOrWhiteSpace(followUp); - CurrentSubTask = followUp.Trim(); + + string trimmed = followUp.Trim(); + if (!string.IsNullOrWhiteSpace(CurrentSubTask) && + !string.Equals(CurrentSubTask, trimmed, StringComparison.OrdinalIgnoreCase)) + { + SearchRefinements.Add(CurrentSubTask.Trim()); + } + + SearchRefinements.Add(trimmed); + CurrentSubTask = trimmed; ReviewNotes = ""; RevisionNumber = 0; NextStep = ""; diff --git a/ResearcherAgent.cs b/ResearcherAgent.cs index 05f41ff..e73dc6b 100644 --- a/ResearcherAgent.cs +++ b/ResearcherAgent.cs @@ -69,13 +69,17 @@ public async Task InvokeAsync(string query, CancellationToken cancellati /// Research node that gathers information. public async Task ResearchNodeAsync(ResearchState state, CancellationToken cancellationToken = default) { - string subTask = !string.IsNullOrEmpty(state.CurrentSubTask) ? state.CurrentSubTask : state.MainTask; - _logger.LogInformation("Researching: {SubTask}", subTask); + string query = state.BuildResearchQuery(); + string researchLabel = !string.IsNullOrWhiteSpace(state.CurrentSubTask) + ? state.CurrentSubTask + : state.MainTask; + + _logger.LogInformation("Researching: {SubTask}", researchLabel); string findings; try { - findings = await InvokeAsync(subTask, cancellationToken); + findings = await InvokeAsync(query, cancellationToken); string preview = findings.Length > 100 ? findings[..100] : findings; _logger.LogInformation("Found: {Preview}...", preview); } @@ -86,8 +90,8 @@ public async Task ResearchNodeAsync(ResearchState state, Cancella } catch (Exception e) { - _logger.LogError(e, "Research failed for sub-task '{SubTask}'.", subTask); - findings = $"Research on {subTask} - information gathered"; + _logger.LogError(e, "Research failed for query '{Query}'.", query); + findings = $"Research on {researchLabel} - information gathered"; } state.ResearchFindings.Add(findings);