Skip to content

Commit 24121c1

Browse files
richardpark-msftdanieljurek
authored andcommitted
Changing the readme tool to use Timo's MicroAgent framework (#11796)
* Changing the readme tool to use Timo's MicroAgent framework and taking the chance to refactor and thin out the ReadmeGeneratorTool class. * Adding in a function adapter for agent tool * Adding some more tests for readme validation.
1 parent 8292767 commit 24121c1

3 files changed

Lines changed: 246 additions & 136 deletions

File tree

tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/Generators/ReadMeGeneratorToolTests.cs

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
using System.ClientModel;
2-
using System.ClientModel.Primitives;
31
using System.CommandLine;
42
using System.CommandLine.Parsing;
5-
using Microsoft.Extensions.DependencyInjection;
6-
using Microsoft.Extensions.Logging;
7-
using Moq;
83
using Azure.AI.OpenAI;
9-
using OpenAI.Chat;
104
using Azure.Sdk.Tools.Cli.Helpers;
5+
using Azure.Sdk.Tools.Cli.Microagents;
116
using Azure.Sdk.Tools.Cli.Services;
127
using Azure.Sdk.Tools.Cli.Tests.Mocks.Helpers;
138
using Azure.Sdk.Tools.Cli.Tests.TestHelpers;
149
using Azure.Sdk.Tools.Cli.Tools.Package;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Logging;
12+
using Moq;
13+
using OpenAI.Chat;
1514

1615
namespace Azure.Sdk.Tools.Cli.Tests.Tools.Generators
1716
{
@@ -20,7 +19,7 @@ internal class ReadMeGeneratorToolTests
2019
[Test]
2120
public async Task TestReadmeGeneratorTool()
2221
{
23-
var testClients = SetupOpenAIMocks();
22+
var testClients = SetupMocks();
2423
(DirectoryInfo root, string packagePath) = await CreateFakeLanguageRepo();
2524

2625
var readmeOutputPath = Path.GetTempFileName();
@@ -84,6 +83,51 @@ public void TestReadmeGeneratorToolLive()
8483
});
8584
}
8685

86+
/// <summary>
87+
/// These tests all correspond to "LLM needs to do more work" type of returns from the tool, like removing
88+
/// hardcoded locales, like 'en-us', or having it regenerate if some of the templates tokens make it through
89+
/// and weren't replaced.
90+
/// </summary>
91+
/// <returns></returns>
92+
[TestCase(
93+
"Bad link, still has locale in it: https://learn.microsoft.com/fr-fr/blahblah",
94+
"The readme contains links with locales. Keep the link, but remove these locales from links: (fr-fr).", TestName = "Links with locales in them")]
95+
[TestCase(
96+
"Still referencing aztemplate and (package path)",
97+
"The readme contains placeholders (aztemplate,(package path)) that should be removed and replaced with a proper package name")]
98+
[TestCase(
99+
"Still referencing placeholder (package path)",
100+
"The readme contains placeholders ((package path)) that should be removed and replaced with a proper package name")]
101+
public async Task TestBadReadmeContent(string readmeContent, string expectedFeedback)
102+
{
103+
var testClients = SetupMocks(readmeContent);
104+
(DirectoryInfo root, string packagePath) = await CreateFakeLanguageRepo();
105+
106+
var readmeOutputPath = Path.GetTempFileName();
107+
var readmeTemplatePath = Path.Combine(AppContext.BaseDirectory, "TestAssets", "README-template.go.md");
108+
109+
try
110+
{
111+
var tool = ActivatorUtilities.CreateInstance<ReadMeGeneratorTool>(testClients.ServiceProvider);
112+
var command = tool.GetCommand();
113+
114+
int exitCode = command.Invoke($"--output-path \"{readmeOutputPath}\" --service-url \"https://learn.microsoft.com/azure/service-bus-messaging\" --template-path {readmeTemplatePath} --package-path {packagePath}");
115+
116+
Assert.Multiple(() =>
117+
{
118+
Assert.That(exitCode, Is.EqualTo(1), "Command should fail, as the final readme doesn't pass validation");
119+
Assert.That(testClients.OutputHelper.Outputs.First().Method, Is.EqualTo("OutputError"));
120+
Assert.That(testClients.OutputHelper.Outputs.First().OutputValue, Is.EqualTo($"ReadmeGenerator failed with validation errors: {expectedFeedback}"));
121+
});
122+
123+
Assert.That(File.Exists(readmeOutputPath), Is.True, "Readme output file should be created");
124+
}
125+
finally
126+
{
127+
Directory.Delete(root.FullName, true);
128+
}
129+
}
130+
87131
/// <summary>
88132
/// Creates a service provider in the same way as the normal program. Can be used to instantiate real clients.
89133
/// </summary>
@@ -107,34 +151,19 @@ private static (ServiceProvider, MockOutputHelper) CreateServiceProvider(Action<
107151
return (sp, OutputHelper);
108152
}
109153

110-
private static TestClients SetupOpenAIMocks()
154+
private static TestClients SetupMocks(string readmeContents = "This is a test response for the readme generation.")
111155
{
112156
var (openAIClientMock, chatClientMock) = OpenAIMockHelper.Create("gpt-4.1");
113157

114-
// basically - create a model using the appropriate OpenAI*ModelFactory
115-
// then return it, wrapped in a ClientResult. This is really similar to the online samples
116-
// except it's ClientResult (instead of Response).
117-
var chatCompletion = OpenAIChatModelFactory.ChatCompletion(
118-
content: new ChatMessageContent("This is a test response for the readme generation.")
119-
);
120-
121-
chatClientMock
122-
.Setup(ccm => ccm.CompleteChatAsync(It.IsAny<ChatMessage[]>())) // NOTE: I'm not checking the chat message input - I already know what I'm sending.
123-
.Returns(() =>
124-
{
125-
return Task.FromResult(
126-
ClientResult.FromValue(chatCompletion, Mock.Of<PipelineResponse>())
127-
);
128-
});
158+
var serviceMock = new Mock<IMicroagentHostService>();
159+
serviceMock.Setup(svc => svc.RunAgentToCompletion(
160+
It.IsAny<Microagent<ReadmeGenerator.ReadmeContents>>(), It.IsAny<CancellationToken>())
161+
).Returns(() => Task.FromResult(new ReadmeGenerator.ReadmeContents(readmeContents)));
129162

130163
var (serviceProvider, OutputHelper) = CreateServiceProvider((sc) =>
131164
{
132165
sc.AddLogging((lb) => lb.AddConsole());
133-
sc.AddSingleton(openAIClientMock.Object);
134-
135-
// register the mocks too, if you want to grab them later.
136-
sc.AddSingleton(chatClientMock);
137-
sc.AddSingleton(openAIClientMock);
166+
sc.AddSingleton(serviceMock.Object);
138167
});
139168

140169
return new TestClients(openAIClientMock, chatClientMock, serviceProvider, OutputHelper);

tools/azsdk-cli/Azure.Sdk.Tools.Cli/Microagents/AgentTool.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,37 @@ public async Task<string> Invoke(string input, CancellationToken ct = default)
2424
}
2525

2626
public abstract Task<TOutput> Invoke(TInput input, CancellationToken ct);
27+
28+
/// <summary>
29+
/// Creates an AgentTool that wraps a function to invoke, rather than requiring a class.
30+
/// </summary>
31+
/// <param name="name">The name of the tool</param>
32+
/// <param name="description">The description of the tool</param>
33+
/// <param name="invokeHandler">The function to invoke when this tool is used.</param>
34+
/// <returns></returns>
35+
public static AgentTool<TInput, TOutput> FromFunc(string name, string description, Func<TInput, CancellationToken, Task<TOutput>> invokeHandler)
36+
{
37+
return new FuncAgentTool<TInput, TOutput>(name, description, invokeHandler);
38+
}
39+
40+
/// <summary>
41+
/// An AgentTool that wraps a function to invoke, rather than requiring a class.
42+
/// </summary>
43+
/// <typeparam name="TInput">Input schema type</typeparam>
44+
/// <typeparam name="TOutput">Output schema type</typeparam>
45+
/// <param name="name">The name of the tool</param>
46+
/// <param name="description">The description of the tool</param>
47+
/// <param name="invokeHandler">The function to invoke when this tool is used.</param>
48+
private class FuncAgentTool<ToolInputT, ToolOutputT>(string name, string description, Func<ToolInputT, CancellationToken, Task<ToolOutputT>> invokeHandler) : AgentTool<ToolInputT, ToolOutputT>
49+
{
50+
private readonly Func<ToolInputT, CancellationToken, Task<ToolOutputT>> invoke = invokeHandler;
51+
52+
public override string Name { get; init; } = name;
53+
public override string Description { get; init; } = description;
54+
55+
public override Task<ToolOutputT> Invoke(ToolInputT input, CancellationToken ct)
56+
{
57+
return invoke(input, ct);
58+
}
59+
}
2760
}

0 commit comments

Comments
 (0)