mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Add shell support to the HarnessAgent (#6005)
* Add shell support to the HarnessAgent * Address PR comments * Address PR comments
This commit is contained in:
committed by
GitHub
Unverified
parent
46ed66cfd5
commit
bda40ba0e1
@@ -1,6 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Moq;
|
||||
#if NET
|
||||
using Microsoft.Agents.AI.Tools.Shell;
|
||||
#endif
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests;
|
||||
|
||||
@@ -39,6 +42,10 @@ public class HarnessAgentOptionsTests
|
||||
Assert.Null(options.AgentSkillsSource);
|
||||
Assert.Null(options.BackgroundAgents);
|
||||
Assert.Null(options.BackgroundAgentsProviderOptions);
|
||||
#if NET
|
||||
Assert.Null(options.ShellExecutor);
|
||||
Assert.Null(options.ShellEnvironmentProviderOptions);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -56,6 +63,10 @@ public class HarnessAgentOptionsTests
|
||||
var skillsSource = new Mock<AgentSkillsSource>().Object;
|
||||
var backgroundAgents = new AIAgent[] { new Mock<AIAgent>().Object };
|
||||
var backgroundAgentsOptions = new BackgroundAgentsProviderOptions();
|
||||
#if NET
|
||||
var shellExecutor = new Mock<ShellExecutor>().Object;
|
||||
var shellEnvOptions = new ShellEnvironmentProviderOptions();
|
||||
#endif
|
||||
|
||||
// Act
|
||||
var options = new HarnessAgentOptions
|
||||
@@ -83,6 +94,10 @@ public class HarnessAgentOptionsTests
|
||||
OpenTelemetrySourceName = "custom-source",
|
||||
BackgroundAgents = backgroundAgents,
|
||||
BackgroundAgentsProviderOptions = backgroundAgentsOptions,
|
||||
#if NET
|
||||
ShellExecutor = shellExecutor,
|
||||
ShellEnvironmentProviderOptions = shellEnvOptions,
|
||||
#endif
|
||||
};
|
||||
|
||||
// Assert
|
||||
@@ -111,5 +126,9 @@ public class HarnessAgentOptionsTests
|
||||
Assert.Equal("custom-source", options.OpenTelemetrySourceName);
|
||||
Assert.Same(backgroundAgents, options.BackgroundAgents);
|
||||
Assert.Same(backgroundAgentsOptions, options.BackgroundAgentsProviderOptions);
|
||||
#if NET
|
||||
Assert.Same(shellExecutor, options.ShellExecutor);
|
||||
Assert.Same(shellEnvOptions, options.ShellEnvironmentProviderOptions);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
#if NET
|
||||
using Microsoft.Agents.AI.Tools.Shell;
|
||||
#endif
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
|
||||
@@ -1347,4 +1350,114 @@ public class HarnessAgentTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#if NET
|
||||
#region Feature: ShellEnvironmentProvider
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ShellEnvironmentProvider is included when ShellExecutor is provided.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ShellEnvironmentProvider_IncludedWhenExecutorProvided()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new Mock<IChatClient>().Object;
|
||||
var executorMock = new Mock<ShellExecutor>();
|
||||
executorMock.Setup(e => e.AsAIFunction(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<bool>()))
|
||||
.Returns(AIFunctionFactory.Create(() => "test", "run_shell"));
|
||||
var options = CreateAllDisabledOptions();
|
||||
options.ShellExecutor = executorMock.Object;
|
||||
|
||||
// Act
|
||||
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
||||
var innerAgent = agent.GetService<ChatClientAgent>();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(innerAgent?.AIContextProviders);
|
||||
Assert.Contains(innerAgent!.AIContextProviders!, p => p is ShellEnvironmentProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ShellEnvironmentProvider is not included when ShellExecutor is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ShellEnvironmentProvider_ExcludedWhenExecutorNull()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new Mock<IChatClient>().Object;
|
||||
var options = CreateAllDisabledOptions();
|
||||
options.ShellExecutor = null;
|
||||
|
||||
// Act
|
||||
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
||||
var innerAgent = agent.GetService<ChatClientAgent>();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(innerAgent);
|
||||
Assert.NotNull(innerAgent!.AIContextProviders);
|
||||
Assert.DoesNotContain(innerAgent.AIContextProviders!, p => p is ShellEnvironmentProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that the shell tool AIFunction is added to ChatOptions.Tools when ShellExecutor is provided.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ShellExecutor_ToolAddedToChatOptionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatOptions? capturedOptions = null;
|
||||
var chatClientMock = new Mock<IChatClient>();
|
||||
chatClientMock
|
||||
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
|
||||
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "done")));
|
||||
|
||||
var executorMock = new Mock<ShellExecutor>();
|
||||
executorMock.Setup(e => e.AsAIFunction(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<bool>()))
|
||||
.Returns(AIFunctionFactory.Create(() => "shell output", "run_shell"));
|
||||
|
||||
var options = CreateAllDisabledOptions();
|
||||
options.DisableWebSearch = true;
|
||||
options.ShellExecutor = executorMock.Object;
|
||||
|
||||
// Act
|
||||
var agent = new HarnessAgent(chatClientMock.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);
|
||||
|
||||
// Assert — the shell tool should be present
|
||||
Assert.NotNull(capturedOptions?.Tools);
|
||||
Assert.Contains(capturedOptions!.Tools!, t => t is AIFunction f && f.Name == "run_shell");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ShellEnvironmentProvider is present when ShellEnvironmentProviderOptions is also specified.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ShellEnvironmentProvider_PresentWhenOptionsProvided()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new Mock<IChatClient>().Object;
|
||||
var executorMock = new Mock<ShellExecutor>();
|
||||
executorMock.Setup(e => e.AsAIFunction(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<bool>()))
|
||||
.Returns(AIFunctionFactory.Create(() => "test", "run_shell"));
|
||||
var envOptions = new ShellEnvironmentProviderOptions
|
||||
{
|
||||
ProbeTools = ["git", "python"],
|
||||
};
|
||||
var options = CreateAllDisabledOptions();
|
||||
options.ShellExecutor = executorMock.Object;
|
||||
options.ShellEnvironmentProviderOptions = envOptions;
|
||||
|
||||
// Act
|
||||
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
||||
var innerAgent = agent.GetService<ChatClientAgent>();
|
||||
|
||||
// Assert — provider should exist (options wiring is validated by the provider's behavior)
|
||||
Assert.NotNull(innerAgent?.AIContextProviders);
|
||||
Assert.Contains(innerAgent!.AIContextProviders!, p => p is ShellEnvironmentProvider);
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endif
|
||||
}
|
||||
|
||||
+7
@@ -6,6 +6,7 @@ using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Tools.Shell.UnitTests;
|
||||
|
||||
@@ -226,6 +227,8 @@ public sealed class ShellEnvironmentProviderTests
|
||||
public override Task InitializeAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public override Task<ShellResult> RunAsync(string command, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(this.Responses.Dequeue());
|
||||
public override AIFunction AsAIFunction(string name = "run_shell", string? description = null, bool requireApproval = true) =>
|
||||
throw new NotSupportedException();
|
||||
public override ValueTask DisposeAsync() => default;
|
||||
}
|
||||
|
||||
@@ -280,6 +283,8 @@ public sealed class ShellEnvironmentProviderTests
|
||||
public override Task InitializeAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public override Task<ShellResult> RunAsync(string command, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(this._factory(cancellationToken));
|
||||
public override AIFunction AsAIFunction(string name = "run_shell", string? description = null, bool requireApproval = true) =>
|
||||
throw new NotSupportedException();
|
||||
public override ValueTask DisposeAsync() => default;
|
||||
}
|
||||
|
||||
@@ -372,6 +377,8 @@ public sealed class ShellEnvironmentProviderTests
|
||||
this.RunCount++;
|
||||
return Task.FromResult(this.NextResult);
|
||||
}
|
||||
public override AIFunction AsAIFunction(string name = "run_shell", string? description = null, bool requireApproval = true) =>
|
||||
throw new NotSupportedException();
|
||||
public override ValueTask DisposeAsync() => default;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user