Skip to content

Commit eb0873d

Browse files
committed
Add micro agent service demo to ExampleTool
1 parent a42f664 commit eb0873d

5 files changed

Lines changed: 104 additions & 6 deletions

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using Azure.AI.OpenAI;
44
using Azure.Sdk.Tools.Cli.Microagents;
55
using Azure.Sdk.Tools.Cli.Microagents.Tools;
6+
using Azure.Sdk.Tools.Cli.Helpers;
7+
using NUnit.Framework;
68
using Moq;
79
using OpenAI.Chat;
810

@@ -23,7 +25,8 @@ public void Setup()
2325
chatClientMock = new Mock<ChatClient>();
2426
openAIClientMock.Setup(client => client.GetChatClient(It.IsAny<string>()))
2527
.Returns(chatClientMock.Object);
26-
microagentHostService = new MicroagentHostService(openAIClientMock.Object, loggerMock.Object);
28+
var tokenUsageHelper = new TokenUsageHelper(Mock.Of<Azure.Sdk.Tools.Cli.Helpers.IOutputHelper>());
29+
microagentHostService = new MicroagentHostService(openAIClientMock.Object, loggerMock.Object, tokenUsageHelper);
2730
}
2831

2932
[Test]

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33
using Moq;
4+
using NUnit.Framework;
45
using NUnit.Framework.Internal;
56
using Azure.Core;
67
using Azure.Sdk.Tools.Cli.Helpers;
@@ -22,6 +23,7 @@ internal class ExampleToolTests
2223
private MockGitHubService? mockGitHubService;
2324
private Mock<IProcessHelper>? mockProcessHelper;
2425
private Mock<IPowershellHelper>? mockPowershellHelper;
26+
private Mock<Azure.Sdk.Tools.Cli.Microagents.IMicroagentHostService>? mockMicroagentHostService;
2527

2628
[SetUp]
2729
public void Setup()
@@ -33,6 +35,7 @@ public void Setup()
3335
mockGitHubService = new MockGitHubService();
3436
mockProcessHelper = new Mock<IProcessHelper>();
3537
mockPowershellHelper = new Mock<IPowershellHelper>();
38+
mockMicroagentHostService = new Mock<Azure.Sdk.Tools.Cli.Microagents.IMicroagentHostService>();
3639

3740
// Set up Azure service mock to return a mock credential
3841
var mockCredential = new Mock<TokenCredential>();
@@ -59,6 +62,7 @@ public void Setup()
5962
mockProcessHelper.Object,
6063
mockPowershellHelper.Object,
6164
tokenUsageHelper: new TokenUsageHelper(mockOutput.Object),
65+
mockMicroagentHostService.Object,
6266
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
6367
null
6468
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
@@ -154,7 +158,7 @@ public void GetCommand_ReturnsCommandWithCorrectSubCommands()
154158

155159
Assert.That(command.Name, Is.EqualTo("demo"));
156160
Assert.That(command.Description, Does.Contain("Comprehensive demonstration"));
157-
Assert.That(command.Subcommands.Count, Is.EqualTo(7));
161+
Assert.That(command.Subcommands.Count, Is.EqualTo(8));
158162

159163
var subCommandNames = command.Subcommands.Select(sc => sc.Name).ToList();
160164
Assert.That(subCommandNames, Does.Contain("azure"));
@@ -164,6 +168,7 @@ public void GetCommand_ReturnsCommandWithCorrectSubCommands()
164168
Assert.That(subCommandNames, Does.Contain("error"));
165169
Assert.That(subCommandNames, Does.Contain("process"));
166170
Assert.That(subCommandNames, Does.Contain("powershell"));
171+
Assert.That(subCommandNames, Does.Contain("microagent"));
167172
}
168173

169174
[Test]
@@ -237,7 +242,6 @@ public async Task DemonstratePowershellExecution_Success()
237242
{
238243
mockPowershellHelper!.Setup(p => p.Run(It.IsAny<PowershellOptions>(), It.IsAny<CancellationToken>()))
239244
.ReturnsAsync(new ProcessResult { ExitCode = 0, });
240-
241245
var result = await tool.DemonstratePowershellExecution("foobar");
242246

243247
Assert.That(result.ResponseError, Is.Null);
@@ -246,4 +250,16 @@ public async Task DemonstratePowershellExecution_Success()
246250
Assert.That(result.Result, Is.Empty);
247251
Assert.That(result.Details?["exit_code"], Is.EqualTo("0"));
248252
}
253+
254+
[Test]
255+
public async Task DemonstrateMicroagentFibonacci_Success()
256+
{
257+
mockMicroagentHostService!.Setup(m => m.RunAgentToCompletion(It.IsAny<Azure.Sdk.Tools.Cli.Microagents.Microagent<int>>(), It.IsAny<CancellationToken>()))
258+
.ReturnsAsync(13);
259+
260+
var response = await tool.DemonstrateMicroagentFibonacci(7);
261+
262+
Assert.That(response.ResponseError, Is.Null);
263+
Assert.That(response.Result as string, Does.Contain("Fibonacci(7) = 13"));
264+
}
249265
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ public async Task<TResult> RunAgentToCompletion<TResult>(Microagent<TResult> age
6161
// Request the chat completion
6262
logger.LogDebug("Sending conversation history with {MessageCount} messages to model '{Model}'", conversationHistory.Count, agentDefinition.Model);
6363
var response = await chatClient.CompleteChatAsync(conversationHistory, chatCompletionOptions, ct);
64-
tokenUsageHelper.Add(agentDefinition.Model, response.Value.Usage.InputTokenCount, response.Value.Usage.OutputTokenCount);
64+
if (null != response.Value.Usage)
65+
{
66+
tokenUsageHelper.Add(agentDefinition.Model, response.Value.Usage.InputTokenCount, response.Value.Usage.OutputTokenCount);
67+
}
6568

6669
var toolCall = response.Value.ToolCalls.Single();
6770
logger.LogInformation("Model called tool '{ToolName}'", toolCall.FunctionName);
@@ -106,7 +109,7 @@ public async Task<TResult> RunAgentToCompletion<TResult>(Microagent<TResult> age
106109
conversationHistory.Add(ChatMessage.CreateToolMessage(toolCall.Id, toolResult));
107110
}
108111

109-
throw new Exception("Agent did not return a result within the maximum number of iterations");
112+
throw new Exception($"Agent did not return a result within the maximum number of {agentDefinition.MaxToolCalls} iterations");
110113
}
111114

112115
/// <summary>

tools/azsdk-cli/Azure.Sdk.Tools.Cli/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public static WebApplicationBuilder CreateAppBuilder(string[] args)
6666
// Skip azure client logging noise
6767
builder.Logging.AddFilter((category, level) =>
6868
{
69-
if (null == category) { return level >= logLevel; }
69+
if (debug || null == category) { return level >= logLevel; }
7070
var isAzureClient = category.StartsWith("Azure.", StringComparison.Ordinal);
7171
var isToolsClient = category.StartsWith("Azure.Sdk.Tools.", StringComparison.Ordinal);
7272
if (isAzureClient && !isToolsClient) { return level >= LogLevel.Error; }

tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Example/ExampleTool.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Azure.Sdk.Tools.Cli.Helpers;
1212
using ModelContextProtocol.Server;
1313
using OpenAI.Chat;
14+
using Azure.Sdk.Tools.Cli.Microagents;
1415

1516
namespace Azure.Sdk.Tools.Cli.Tools.Example;
1617

@@ -26,6 +27,7 @@ public class ExampleTool : MCPTool
2627
private const string ErrorSubCommand = "error";
2728
private const string ProcessSubCommand = "process";
2829
private const string PowershellSubCommand = "powershell";
30+
private const string MicroagentSubCommand = "microagent";
2931

3032
// Dependencies injected via constructor
3133
private readonly ILogger<ExampleTool> logger;
@@ -37,6 +39,7 @@ public class ExampleTool : MCPTool
3739
private readonly IProcessHelper processHelper;
3840
private readonly IPowershellHelper powershellHelper;
3941
private readonly TokenUsageHelper tokenUsageHelper;
42+
private readonly IMicroagentHostService microagentHostService;
4043

4144
// CLI Options and Arguments
4245
private readonly Argument<string> aiInputArg = new(
@@ -68,6 +71,11 @@ public class ExampleTool : MCPTool
6871
description: "Message to pass to the PowerShell script via parameter")
6972
{ Arity = ArgumentArity.ExactlyOne };
7073

74+
private readonly Option<int> fibonacciIndexOption = new(
75+
name: "--fibonacci",
76+
description: "Index (0-based) of Fibonacci number to compute using micro-agent")
77+
{ IsRequired = true };
78+
7179
private readonly Option<string> tenantOption = new(["--tenant", "-t"], "Tenant ID");
7280
private readonly Option<string> languageOption = new(["--language", "-l"], "Programming language of the repository");
7381
private readonly Option<string> promptOption = new(["--prompt", "-p"], "AI prompt text");
@@ -83,6 +91,7 @@ public ExampleTool(
8391
IProcessHelper processHelper,
8492
IPowershellHelper powershellHelper,
8593
TokenUsageHelper tokenUsageHelper,
94+
IMicroagentHostService microagentHostService,
8695
AzureOpenAIClient openAIClient
8796
) : base()
8897
{
@@ -95,6 +104,7 @@ AzureOpenAIClient openAIClient
95104
this.processHelper = processHelper;
96105
this.powershellHelper = powershellHelper;
97106
this.tokenUsageHelper = tokenUsageHelper;
107+
this.microagentHostService = microagentHostService;
98108

99109
// Set command hierarchy - results in: azsdk example
100110
CommandHierarchy = [
@@ -142,13 +152,19 @@ public override Command GetCommand()
142152
powershellCmd.AddArgument(powershellMessageArg);
143153
powershellCmd.SetHandler(async ctx => { await HandleCommand(ctx, ctx.GetCancellationToken()); });
144154

155+
// Microagent Fibonacci demo sub-command
156+
var microagentCmd = new Command(MicroagentSubCommand, "Demonstrate micro-agent looping tool calls to compute Fibonacci");
157+
microagentCmd.AddOption(fibonacciIndexOption);
158+
microagentCmd.SetHandler(async ctx => { await HandleCommand(ctx, ctx.GetCancellationToken()); });
159+
145160
parentCommand.Add(azureCmd);
146161
parentCommand.Add(devopsCmd);
147162
parentCommand.Add(githubCmd);
148163
parentCommand.Add(aiCmd);
149164
parentCommand.Add(errorCmd);
150165
parentCommand.Add(processCmd);
151166
parentCommand.Add(powershellCmd);
167+
parentCommand.Add(microagentCmd);
152168

153169
return parentCommand;
154170
}
@@ -166,6 +182,7 @@ public override async Task HandleCommand(InvocationContext ctx, CancellationToke
166182
ErrorSubCommand => await DemonstrateErrorHandling(ctx.ParseResult.GetValueForArgument(errorInputArg), ctx.ParseResult.GetValueForOption(forceFailureOption), ct),
167183
ProcessSubCommand => await DemonstrateProcessExecution(ctx.ParseResult.GetValueForArgument(processSleepArg), ct),
168184
PowershellSubCommand => await DemonstratePowershellExecution(ctx.ParseResult.GetValueForArgument(powershellMessageArg), ct),
185+
MicroagentSubCommand => await DemonstrateMicroagentFibonacci(ctx.ParseResult.GetValueForOption(fibonacciIndexOption), ct),
169186
_ => new ExampleServiceResponse { ResponseError = $"Unknown command: {commandName}" }
170187
};
171188

@@ -486,5 +503,64 @@ public async Task<ExampleServiceResponse> DemonstratePowershellExecution(string
486503
}
487504
}
488505
}
506+
507+
public record Fibonacci
508+
{
509+
public int Index { get; set; }
510+
public int Previous { get; set; }
511+
public int Current { get; set; }
512+
}
513+
514+
[McpServerTool(Name = "azsdk_example_microagent_fibonacci"), Description("Demonstrates micro-agent computing Nth Fibonacci number via iterative tool calls")]
515+
public async Task<DefaultCommandResponse> DemonstrateMicroagentFibonacci(int n, CancellationToken ct = default)
516+
{
517+
try
518+
{
519+
if (n < 2)
520+
{
521+
SetFailure();
522+
return new DefaultCommandResponse { ResponseError = "--fibonacci must be >= 2 to exercise the micro-agent (trivial cases are disallowed)" };
523+
}
524+
525+
var advanceTool = AgentTool<Fibonacci, Fibonacci>.FromFunc(
526+
name: "advance_state",
527+
description: "Advances state by one step",
528+
invokeHandler: (input, ct) =>
529+
{
530+
return Task.FromResult(new Fibonacci
531+
{
532+
Index = input.Index + 1,
533+
Previous = input.Current,
534+
Current = input.Previous + input.Current
535+
});
536+
});
537+
538+
// Avoid mentioning 'fibonacci' in the instructions so the LLM doesn't try to calculate it directly
539+
var instructions = $"""
540+
Call advance_state repeatedly until the returned index == {n}.
541+
Return the '{nameof(Fibonacci.Current)}' value when index == {n}.
542+
Initial state is {nameof(Fibonacci.Index)}=1, {nameof(Fibonacci.Previous)}=0, {nameof(Fibonacci.Current)}=1.
543+
""";
544+
545+
var agent = new Microagent<int>
546+
{
547+
Instructions = instructions,
548+
MaxToolCalls = 7,
549+
Tools = [advanceTool],
550+
};
551+
552+
var resultValue = await microagentHostService.RunAgentToCompletion(agent, ct);
553+
554+
tokenUsageHelper.LogCost();
555+
return new DefaultCommandResponse { Result = $"Fibonacci({n}) = {resultValue}" };
556+
}
557+
catch (Exception ex)
558+
{
559+
tokenUsageHelper.LogCost();
560+
logger.LogError(ex, "Error demonstrating micro-agent Fibonacci for n={n}", n);
561+
SetFailure();
562+
return new DefaultCommandResponse { ResponseError = $"Failed to compute Fibonacci({n}): {ex.Message}" };
563+
}
564+
}
489565
}
490566
#endif

0 commit comments

Comments
 (0)