mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
c79f886dc3
* dotnet: refresh Foundry sample guidance Carry forward the still-relevant sample guidance and Foundry-specific documentation fixes from the old stacked sample migration work, adapted to the current repo layout and policy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * dotnet: rename Foundry sample env vars Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * dotnet: remove persistent provider sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * dotnet: drop SAMPLE_GUIDELINES.md from this PR Defer the guidelines doc and its cross-link to a follow-on PR to avoid broken-link failures in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * dotnet: add DefaultAzureCredential warning to remaining samples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * dotnet: address PR review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
134 lines
6.6 KiB
C#
134 lines
6.6 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
// Shell tool with environment-aware system prompt
|
|
//
|
|
// WARNING: This sample uses LocalShellExecutor, which executes real commands
|
|
// against the shell on this machine. Approval gating is disabled here so
|
|
// the demo runs unattended; in any real application keep approval on
|
|
// (the default), or use DockerShellExecutor for container isolation. The
|
|
// commands the model emits below are read-only or scoped (echo, cd into
|
|
// a temp folder, set a process-local env var) but a different model or
|
|
// prompt could choose to do something destructive. Run this only in an
|
|
// environment where you are comfortable with the agent typing into your
|
|
// terminal.
|
|
//
|
|
// Demonstrates LocalShellExecutor in both modes paired with
|
|
// ShellEnvironmentProvider, an AIContextProvider that probes the live
|
|
// shell (OS, family, version, CWD, common CLIs) and injects authoritative
|
|
// system-prompt instructions so the agent emits commands in the right
|
|
// idiom (PowerShell vs POSIX).
|
|
//
|
|
// Two runs:
|
|
// 1) Stateless mode: each tool call runs in a fresh shell. Useful when
|
|
// commands are independent (read-only scripts, version checks, file
|
|
// listings) and you want strong isolation between calls. Side
|
|
// effects in one call (cd, exported variables) do NOT carry to the
|
|
// next.
|
|
// 2) Persistent mode: a single long-lived shell is reused across calls,
|
|
// so working directory and exported environment variables are
|
|
// preserved. Useful for multi-step workflows that build state
|
|
// (cd into a folder and run a sequence of commands there; set a
|
|
// token in one step and read it in the next).
|
|
|
|
using Azure.AI.OpenAI;
|
|
using Azure.Identity;
|
|
using Microsoft.Agents.AI;
|
|
using Microsoft.Agents.AI.Tools.Shell;
|
|
using Microsoft.Extensions.AI;
|
|
using OpenAI.Chat;
|
|
|
|
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
|
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
|
|
|
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
|
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
|
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
|
var chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
|
.GetChatClient(deploymentName);
|
|
|
|
const string Instructions = """
|
|
You are an agent with a single tool: run_shell. Use it to satisfy the
|
|
user's request. Do not describe what you would do — actually run the
|
|
commands. Reply with the final answer derived from real output.
|
|
""";
|
|
|
|
// --------------------------------------------------------------------
|
|
// 1. Stateless mode — each call gets a fresh shell.
|
|
// --------------------------------------------------------------------
|
|
Console.WriteLine("### Stateless mode\n");
|
|
await using (var statelessShell = new LocalShellExecutor(new() { Mode = ShellMode.Stateless, AcknowledgeUnsafe = true }))
|
|
{
|
|
var envProvider = new ShellEnvironmentProvider(statelessShell);
|
|
var statelessAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
|
{
|
|
ChatOptions = new()
|
|
{
|
|
Instructions = Instructions,
|
|
Tools = [statelessShell.AsAIFunction(requireApproval: false)],
|
|
},
|
|
AIContextProviders = [envProvider],
|
|
});
|
|
|
|
var statelessSession = await statelessAgent.CreateSessionAsync();
|
|
Console.WriteLine(await statelessAgent.RunAsync("Print the current working directory.", statelessSession));
|
|
Console.WriteLine();
|
|
|
|
// Show that side effects do NOT carry between stateless calls: ask the
|
|
// agent to cd into the system temp directory in one call, then ask
|
|
// for the CWD in a second call. Stateless mode means the cd is gone.
|
|
Console.WriteLine(await statelessAgent.RunAsync("Change directory into the system temp folder, then print the current working directory.", statelessSession));
|
|
Console.WriteLine();
|
|
Console.WriteLine(await statelessAgent.RunAsync("In a NEW shell call, print the current working directory again. Tell me whether it matches the temp folder from the previous call.", statelessSession));
|
|
Console.WriteLine();
|
|
|
|
PrintSnapshot(envProvider.CurrentSnapshot!);
|
|
}
|
|
|
|
// --------------------------------------------------------------------
|
|
// 2. Persistent mode — one shell, reused across calls. State carries.
|
|
// --------------------------------------------------------------------
|
|
Console.WriteLine("\n### Persistent mode\n");
|
|
await using (var persistentShell = new LocalShellExecutor(new() { Mode = ShellMode.Persistent, AcknowledgeUnsafe = true }))
|
|
{
|
|
var envProvider = new ShellEnvironmentProvider(persistentShell);
|
|
var persistentAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
|
{
|
|
ChatOptions = new()
|
|
{
|
|
Instructions = Instructions,
|
|
Tools = [persistentShell.AsAIFunction(requireApproval: false)],
|
|
},
|
|
AIContextProviders = [envProvider],
|
|
});
|
|
|
|
var persistentSession = await persistentAgent.CreateSessionAsync();
|
|
|
|
// State carries across calls in persistent mode: cd into temp, then
|
|
// verify the next call sees the new CWD.
|
|
Console.WriteLine(await persistentAgent.RunAsync("Change directory into the system temp folder, then print the current working directory.", persistentSession));
|
|
Console.WriteLine();
|
|
Console.WriteLine(await persistentAgent.RunAsync("In a NEW shell call, print the current working directory again. Tell me whether it still matches the temp folder.", persistentSession));
|
|
Console.WriteLine();
|
|
|
|
// Same idea with an exported variable: set in one call, read in the next.
|
|
Console.WriteLine(await persistentAgent.RunAsync("Set the environment variable DEMO_TOKEN to the value 'hello-world'.", persistentSession));
|
|
Console.WriteLine();
|
|
Console.WriteLine(await persistentAgent.RunAsync("Print the current value of DEMO_TOKEN. Tell me exactly what value the shell reports.", persistentSession));
|
|
Console.WriteLine();
|
|
|
|
PrintSnapshot(envProvider.CurrentSnapshot!);
|
|
}
|
|
|
|
static void PrintSnapshot(ShellEnvironmentSnapshot snap)
|
|
{
|
|
Console.WriteLine("--- Captured environment snapshot ---");
|
|
Console.WriteLine($" Family: {snap.Family}");
|
|
Console.WriteLine($" OS: {snap.OSDescription}");
|
|
Console.WriteLine($" Shell: {snap.ShellVersion ?? "(unknown)"}");
|
|
Console.WriteLine($" CWD: {snap.WorkingDirectory}");
|
|
foreach (var (tool, version) in snap.ToolVersions)
|
|
{
|
|
Console.WriteLine($" {tool,-8} {version ?? "(not installed)"}");
|
|
}
|
|
}
|