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
5 changes: 5 additions & 0 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
"MAF_DOCTOR_INIT_VERSION": "1.14.0",
"MAF_DOCTOR_WORKSPACE_ROOTS": "C:\\Users\\jesseliberty\\ai\\.net\\blogWriter;E:\\ai\\.net\\blog\\blogMigration---public"
}
},
"microsoft-learn": {
"type": "http",
"url": "https://learn.microsoft.com/api/mcp"
}
}
}

1 change: 1 addition & 0 deletions BlogWriter.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.10" />
<PackageReference Include="ModelContextProtocol" Version="2.2.0" />
<PackageReference Include="OpenAI" Version="2.11.0" />
</ItemGroup>

Expand Down
15 changes: 14 additions & 1 deletion Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Client;
using OpenAI;

// Secrets come from the .NET user-secrets store and from
Expand Down Expand Up @@ -115,11 +116,23 @@ async Task<HttpResponseMessage> PostWithRetryAsync(string requestUri, object bod
name: "tavily_search",
description: "A search engine optimized for comprehensive, accurate, and trusted results.");

// Microsoft Learn's remote MCP server exposes docs search/fetch tools the
// Researcher can call alongside Tavily for authoritative Microsoft/Azure content.
await using McpClient microsoftLearnMcp = await McpClient.CreateAsync(
new HttpClientTransport(new HttpClientTransportOptions
{
Endpoint = new Uri("https://learn.microsoft.com/api/mcp"),
Name = "microsoft-learn",
}));
IList<McpClientTool> microsoftLearnTools = await microsoftLearnMcp.ListToolsAsync();

List<AIFunction> researcherTools = [tavilyTool, .. microsoftLearnTools];

Comment on lines +119 to +130
// Creating a callable object
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());

var bloggerAgent = new BloggerAgent(llm, chatOptions, loggerFactory.CreateLogger<BloggerAgent>());
var researcherAgent = new ResearcherAgent(llm, chatOptions, tavilyTool, loggerFactory.CreateLogger<ResearcherAgent>());
var researcherAgent = new ResearcherAgent(llm, chatOptions, researcherTools, loggerFactory.CreateLogger<ResearcherAgent>());
var authorAgent = new AuthorAgent(llm, chatOptions, loggerFactory.CreateLogger<AuthorAgent>());
var reviewerAgent = new ReviewerAgent(llm, chatOptions, loggerFactory.CreateLogger<ReviewerAgent>());
var app = new BlogWorkflow(bloggerAgent, researcherAgent, authorAgent, reviewerAgent, loggerFactory.CreateLogger<BlogWorkflow>());
Expand Down
30 changes: 16 additions & 14 deletions ResearcherAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ namespace BlogWriter;
/// Performs research tasks with a <see cref="ChatClientAgent"/> and returns
/// concise findings.
///
/// The agent can call the configured Tavily tool during execution and summarize
/// results for use in later drafting stages.
/// The agent can call the configured tools (e.g. Tavily web search, Microsoft
/// Learn MCP) during execution and summarize results for use in later drafting
/// stages.
/// </summary>
public class ResearcherAgent : IResearcherAgent
{
Expand All @@ -28,11 +29,13 @@ public class ResearcherAgent : IResearcherAgent
// Per-call output-token cap, applied on each RunAsync to bound cost.
private readonly int? _maxOutputTokens;

public ResearcherAgent(IChatClient llm, ChatOptions chatOptions, AIFunction tavilyTool, ILogger<ResearcherAgent> logger)
public ResearcherAgent(IChatClient llm, ChatOptions chatOptions, IEnumerable<AIFunction> tools, ILogger<ResearcherAgent> logger)
{
_logger = logger;
_maxOutputTokens = chatOptions.MaxOutputTokens;

List<AITool> toolList = [.. tools];

_agent = new ChatClientAgent(llm, new ChatClientAgentOptions
{
// Name surfaces in OpenTelemetry traces and agent logs.
Expand All @@ -45,29 +48,28 @@ public ResearcherAgent(IChatClient llm, ChatOptions chatOptions, AIFunction tavi
// Preserve the original sampling/cost settings.
Temperature = chatOptions.Temperature,
MaxOutputTokens = chatOptions.MaxOutputTokens,
// Attaching the tool lets the model call it autonomously.
Tools = [tavilyTool],
// Attaching the tools lets the model call them autonomously.
Tools = toolList,
},
})
.AsBuilder()
// Function-invocation middleware: fires around every tool call the agent
// makes. We log each time the model invokes the Tavily search tool.
// makes. We log each time the model invokes one of the attached tools.
.Use(async (agent, context, next, cancellationToken) =>
{
if (context.Function.Name == tavilyTool.Name)
{
_logger.LogInformation(
"Researcher invoking Tavily tool '{Tool}' with arguments {Arguments}",
context.Function.Name,
context.Arguments);
}
_logger.LogInformation(
"Researcher invoking tool '{Tool}' with arguments {Arguments}",
context.Function.Name,
context.Arguments);
Comment on lines +60 to +63

return await next(context, cancellationToken);
})
.UseOpenTelemetry(sourceName: "BlogWriter.Agents")
.Build();

_logger.LogInformation("ResearcherAgent initialized with Tavily tool: {ToolName}", tavilyTool.Name);
_logger.LogInformation(
"ResearcherAgent initialized with tools: {ToolNames}",
string.Join(", ", toolList.Select(t => t.Name)));
}

/// <summary>Execute research by letting the agent search and summarise.</summary>
Expand Down