-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathBloggerAgent.cs
More file actions
152 lines (128 loc) · 6.17 KB
/
Copy pathBloggerAgent.cs
File metadata and controls
152 lines (128 loc) · 6.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
using System.Diagnostics;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
namespace BlogWriter;
/// <summary>
/// Determines the next workflow action and subtask based on the current
/// <see cref="ResearchState"/>.
///
/// The agent applies deterministic routing rules first, then uses structured
/// model output as fallback to produce a typed <see cref="BloggerDecision"/>.
/// </summary>
public class BloggerAgent : IBloggerAgent
{
// Built once and reused. Holds the static Blogger instructions; the volatile
// state is passed per-turn as the user message.
private readonly AIAgent _agent;
// Web-style options are sufficient: BloggerDecision carries explicit
// [JsonPropertyName] attributes (next_step / task_description) that drive the
// generated schema regardless of naming policy.
private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
// Emits a span per Blogger decision. Activated by the ActivityListener
// registered in Program.cs (or an OpenTelemetry TracerProvider).
private static readonly ActivitySource s_activitySource = new("BlogWriter.BloggerAgent");
private readonly ILogger<BloggerAgent> _logger;
public BloggerAgent(AIAgent agent, ILogger<BloggerAgent> logger)
{
_logger = logger;
_agent = agent;
_logger.LogInformation("BloggerAgent initialized.");
}
// Compatibility overload for callers that supply an in-process test client.
public BloggerAgent(IChatClient llm, ChatOptions chatOptions, ILogger<BloggerAgent> logger)
: this(new ChatClientAgent(llm, new ChatClientAgentOptions
{
Name = "Blogger",
ChatOptions = chatOptions,
}), logger)
{
}
public async Task<BloggerDecision> InvokeAsync(ResearchState state, CancellationToken cancellationToken = default)
{
using Activity? activity = s_activitySource.StartActivity("Blogger.Invoke");
activity?.SetTag("blog.revision", state.RevisionNumber);
List<string> research = state.ResearchFindings;
string researchText = research.Count > 0 ? string.Join("\n", research) : "No research yet.";
int revision = state.RevisionNumber;
bool hasResearch = research.Count > 0;
bool hasDraft = !string.IsNullOrWhiteSpace(state.Draft);
string review = state.ReviewNotes;
if (ResearchState.IsApproved(review) && hasDraft)
{
_logger.LogInformation("Blogger: Draft approved, ending workflow");
return new BloggerDecision("END", "Report approved and complete");
}
if (!hasResearch)
{
_logger.LogInformation("Blogger: No research yet, directing to researcher");
return new BloggerDecision("researcher", $"Research the topic: {state.MainTask}");
}
if (hasResearch && !hasDraft)
{
_logger.LogInformation("Blogger: Have research, creating first draft");
return new BloggerDecision("author", "Write the first draft based on research findings");
}
if (hasDraft && string.IsNullOrEmpty(review))
{
_logger.LogInformation("Blogger: Have draft, sending to reviewer");
return new BloggerDecision("reviewer", "Prepare draft for review");
}
if (!string.IsNullOrEmpty(review) && !ResearchState.IsApproved(review) && revision < ResearchState.MaxRevisions)
{
_logger.LogInformation("Blogger: Revision {Revision}, sending back to author", revision);
return new BloggerDecision("author", "Revise the draft based on review feedback");
}
// Max revisions reached
if (revision >= ResearchState.MaxRevisions)
{
_logger.LogInformation("Blogger: Max revisions reached! Ending");
return new BloggerDecision("END", "Maximum revisions reached! Finalizing report");
}
// LLM decision as fallback. The dynamic state is the user message; the
// static role lives in the agent's Instructions. MAF structured output
// hands back a typed BloggerDecision — no fenced-block cleanup, no manual
// JsonSerializer.Deserialize.
string stateSummary = $"""
Current Task: {state.MainTask}
Research Findings: {researchText}
Blog Draft: {(string.IsNullOrEmpty(state.Draft) ? "No draft yet." : state.Draft)}
Reviewer Feedback: {(string.IsNullOrEmpty(review) ? "No review yet." : review)}
Revision Number: {revision}
""";
try
{
AgentResponse<BloggerDecision> response =
await _agent.RunAsync<BloggerDecision>(stateSummary, serializerOptions: _jsonOptions, cancellationToken: cancellationToken);
BloggerDecision decision = response.Result;
if (decision is not null && !string.IsNullOrEmpty(decision.NextStep))
{
return decision;
}
}
catch (TokenCapExceededException)
{
// Budget breach is fatal — let it propagate so the app can shut down.
throw;
}
catch (Exception e)
{
_logger.LogError(e, "Blogger LLM decision failed.");
}
// Final fallback - continue with author
_logger.LogInformation("Blogger: Using final fallback - continuing with author");
return new BloggerDecision("author", "Continue with draft creation");
}
/// <summary>Blogger decides the next step.</summary>
public async Task<ResearchState> BloggerNodeAsync(ResearchState state, CancellationToken cancellationToken = default)
{
BloggerDecision decision = await InvokeAsync(state, cancellationToken);
string nextStep = string.IsNullOrEmpty(decision.NextStep) ? "researcher" : decision.NextStep;
string taskDesc = string.IsNullOrEmpty(decision.TaskDescription) ? "Continue work" : decision.TaskDescription;
_logger.LogInformation("Blogger decision: {NextStep}, Task: {TaskDescription}", nextStep, taskDesc);
state.NextStep = nextStep;
state.CurrentSubTask = taskDesc;
return state;
}
}