From c6811025ec8f29c788dba69bf438d15fc909c544 Mon Sep 17 00:00:00 2001 From: Jesse Liberty Date: Tue, 21 Jul 2026 14:53:43 -0400 Subject: [PATCH] Add ActivitySource and Logging --- AuthorAgent.cs | 16 +++++++++++++++- BlogWorkflow.cs | 8 ++++++++ BlogWriter.csproj | 2 ++ BloggerAgent.cs | 16 +++++++++++++++- Program.cs | 34 +++++++++++++++++++++++++++++----- ResearcherAgent.cs | 17 ++++++++++++++++- ReviewerAgent.cs | 16 +++++++++++++++- 7 files changed, 100 insertions(+), 9 deletions(-) diff --git a/AuthorAgent.cs b/AuthorAgent.cs index 4f1583b..b1b072f 100644 --- a/AuthorAgent.cs +++ b/AuthorAgent.cs @@ -1,5 +1,7 @@ +using System.Diagnostics; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; namespace BlogWriter; @@ -13,8 +15,16 @@ public class AuthorAgent : IAuthorAgent { private readonly ChatClientAgent _agent; - public AuthorAgent(IChatClient llm, ChatOptions chatOptions) + // Emits a span per draft creation/revision. Activated by the ActivityListener + // registered in Program.cs (or an OpenTelemetry TracerProvider). + private static readonly ActivitySource s_activitySource = new("BlogWriter.AuthorAgent"); + + private readonly ILogger _logger; + + public AuthorAgent(IChatClient llm, ChatOptions chatOptions, ILogger logger) { + _logger = logger; + _agent = new ChatClientAgent(llm, new ChatClientAgentOptions { Name = "Author", @@ -25,10 +35,14 @@ public AuthorAgent(IChatClient llm, ChatOptions chatOptions) MaxOutputTokens = chatOptions.MaxOutputTokens, }, }); + _logger.LogInformation("AuthorAgent initialized."); } public async Task InvokeAsync(ResearchState state) { + using Activity? activity = s_activitySource.StartActivity("Author.Invoke"); + activity?.SetTag("blog.revision", state.RevisionNumber); + List research = state.ResearchFindings; string researchText = research.Count > 0 ? string.Join("\n\n", research) : "No research available."; diff --git a/BlogWorkflow.cs b/BlogWorkflow.cs index e9175ff..bf25204 100644 --- a/BlogWorkflow.cs +++ b/BlogWorkflow.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Microsoft.Agents.AI.Workflows; namespace BlogWriter; @@ -16,8 +17,15 @@ public class BlogWorkflow( IAuthorAgent author, IReviewerAgent reviewer) : IBlogWorkflow { + // Emits the root span for a workflow run. Activated by the ActivityListener + // registered in Program.cs (or an OpenTelemetry TracerProvider). + private static readonly ActivitySource s_activitySource = new("BlogWriter.Workflow"); + public async Task RunAsync(ResearchState state) { + using Activity? activity = s_activitySource.StartActivity("Workflow.Run"); + activity?.SetTag("blog.topic", state.MainTask); + var bloggerExecutor = new BloggerExecutor(blogger); var researcherExecutor = new ResearcherExecutor(researcher); var authorExecutor = new AuthorExecutor(author); diff --git a/BlogWriter.csproj b/BlogWriter.csproj index e9fccea..8576a75 100644 --- a/BlogWriter.csproj +++ b/BlogWriter.csproj @@ -17,6 +17,8 @@ + + diff --git a/BloggerAgent.cs b/BloggerAgent.cs index 9459f8f..cb2419f 100644 --- a/BloggerAgent.cs +++ b/BloggerAgent.cs @@ -1,6 +1,8 @@ +using System.Diagnostics; using System.Text.Json; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; namespace BlogWriter; @@ -22,8 +24,16 @@ public class BloggerAgent : IBloggerAgent // generated schema regardless of naming policy. private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); - public BloggerAgent(IChatClient llm, ChatOptions chatOptions) + // 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 _logger; + + public BloggerAgent(IChatClient llm, ChatOptions chatOptions, ILogger logger) { + _logger = logger; + _agent = new ChatClientAgent(llm, new ChatClientAgentOptions { Name = "Blogger", @@ -34,10 +44,14 @@ public BloggerAgent(IChatClient llm, ChatOptions chatOptions) MaxOutputTokens = chatOptions.MaxOutputTokens, }, }); + _logger.LogInformation("BloggerAgent initialized."); } public async Task InvokeAsync(ResearchState state) { + using Activity? activity = s_activitySource.StartActivity("Blogger.Invoke"); + activity?.SetTag("blog.revision", state.RevisionNumber); + List research = state.ResearchFindings; string researchText = research.Count > 0 ? string.Join("\n", research) : "No research yet."; int revision = state.RevisionNumber; diff --git a/Program.cs b/Program.cs index de2fc22..0c82fd0 100644 --- a/Program.cs +++ b/Program.cs @@ -1,9 +1,11 @@ using System.ClientModel; +using System.Diagnostics; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using BlogWriter; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; using OpenAI; const string fileName = "config.json"; @@ -73,12 +75,29 @@ description: "A search engine optimized for comprehensive, accurate, and trusted results."); // Creating a callable object -var bloggerAgent = new BloggerAgent(llm, chatOptions); -var researcherAgent = new ResearcherAgent(llm, chatOptions, tavilyTool); -var authorAgent = new AuthorAgent(llm, chatOptions); -var reviewerAgent = new ReviewerAgent(llm, chatOptions); +using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); + +var bloggerAgent = new BloggerAgent(llm, chatOptions, loggerFactory.CreateLogger()); +var researcherAgent = new ResearcherAgent(llm, chatOptions, tavilyTool, loggerFactory.CreateLogger()); +var authorAgent = new AuthorAgent(llm, chatOptions, loggerFactory.CreateLogger()); +var reviewerAgent = new ReviewerAgent(llm, chatOptions, loggerFactory.CreateLogger()); var app = new BlogWorkflow(bloggerAgent, researcherAgent, authorAgent, reviewerAgent); +// Distributed tracing: an ActivityListener activates every "BlogWriter.*" +// ActivitySource in the app (agents + workflow) and writes span start/stop to +// the console. Swap this listener for OpenTelemetry's TracerProvider (and chain +// IChatClient.UseOpenTelemetry() above) to export the same spans instead. +var appActivitySource = new ActivitySource("BlogWriter.Program"); + +ActivitySource.AddActivityListener(new ActivityListener +{ + ShouldListenTo = source => source.Name.StartsWith("BlogWriter", StringComparison.Ordinal), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => Console.WriteLine($"[trace] \u2192 {activity.DisplayName}"), + ActivityStopped = activity => + Console.WriteLine($"[trace] \u2190 {activity.DisplayName} ({activity.Duration.TotalMilliseconds:F0} ms)") +}); + Console.Write("Enter your topic: "); string topic = Console.ReadLine() ?? string.Empty; @@ -88,7 +107,12 @@ MainTask = topic }; -ResearchState result = await app.RunAsync(initialState); +ResearchState result; +using (Activity? runActivity = appActivitySource.StartActivity("BlogWriter.Run")) +{ + runActivity?.SetTag("blog.topic", topic); + result = await app.RunAsync(initialState); +} Console.WriteLine("\n========== RESULTS =========="); Console.WriteLine($"Task: {result.MainTask}"); diff --git a/ResearcherAgent.cs b/ResearcherAgent.cs index f6d2762..7f434cc 100644 --- a/ResearcherAgent.cs +++ b/ResearcherAgent.cs @@ -1,5 +1,7 @@ +using System.Diagnostics; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; namespace BlogWriter; @@ -17,8 +19,16 @@ public class ResearcherAgent : IResearcherAgent // per-call behaviour while gaining tool-calling for free. private readonly ChatClientAgent _agent; - public ResearcherAgent(IChatClient llm, ChatOptions chatOptions, AIFunction tavilyTool) + // Emits a span per research turn. Activated by the ActivityListener + // registered in Program.cs (or an OpenTelemetry TracerProvider). + private static readonly ActivitySource s_activitySource = new("BlogWriter.ResearcherAgent"); + + private readonly ILogger _logger; + + public ResearcherAgent(IChatClient llm, ChatOptions chatOptions, AIFunction tavilyTool, ILogger logger) { + _logger = logger; + _agent = new ChatClientAgent(llm, new ChatClientAgentOptions { // Name surfaces in OpenTelemetry traces and agent logs. @@ -35,11 +45,16 @@ public ResearcherAgent(IChatClient llm, ChatOptions chatOptions, AIFunction tavi Tools = [tavilyTool], }, }); + + _logger.LogInformation("ResearcherAgent initialized with Tavily tool: {ToolName}", tavilyTool.Name); } /// Execute research by letting the agent search and summarise. public async Task InvokeAsync(string query) { + using Activity? activity = s_activitySource.StartActivity("Researcher.Invoke"); + activity?.SetTag("blog.query", query); + try { // A single agent run: the model may call tavily_search one or more diff --git a/ReviewerAgent.cs b/ReviewerAgent.cs index 9733d61..1c28bd2 100644 --- a/ReviewerAgent.cs +++ b/ReviewerAgent.cs @@ -1,5 +1,7 @@ +using System.Diagnostics; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; namespace BlogWriter; @@ -14,8 +16,16 @@ public class ReviewerAgent : IReviewerAgent { private readonly ChatClientAgent _agent; - public ReviewerAgent(IChatClient llm, ChatOptions chatOptions) + // Emits a span per review. Activated by the ActivityListener registered in + // Program.cs (or an OpenTelemetry TracerProvider). + private static readonly ActivitySource s_activitySource = new("BlogWriter.ReviewerAgent"); + + private readonly ILogger _logger; + + public ReviewerAgent(IChatClient llm, ChatOptions chatOptions, ILogger logger) { + _logger = logger; + _agent = new ChatClientAgent(llm, new ChatClientAgentOptions { Name = "Reviewer", @@ -26,10 +36,14 @@ public ReviewerAgent(IChatClient llm, ChatOptions chatOptions) MaxOutputTokens = chatOptions.MaxOutputTokens, }, }); + _logger.LogInformation("ReviewerAgent initialized."); } public async Task InvokeAsync(ResearchState state) { + using Activity? activity = s_activitySource.StartActivity("Reviewer.Invoke"); + activity?.SetTag("blog.revision", state.RevisionNumber); + string draft = state.Draft; int revisionNum = state.RevisionNumber;