db620d33df
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
134 lines
6.7 KiB
C#
134 lines
6.7 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.Projects;
|
|
using Azure.Identity;
|
|
using Microsoft.Agents.AI;
|
|
using Microsoft.Agents.AI.Tools.Shell;
|
|
using Microsoft.Extensions.AI;
|
|
|
|
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
|
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "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 aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
|
|
|
|
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 = aiProjectClient.AsAIAgent(new ChatClientAgentOptions
|
|
{
|
|
ChatOptions = new()
|
|
{
|
|
ModelId = deploymentName,
|
|
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 = aiProjectClient.AsAIAgent(new ChatClientAgentOptions
|
|
{
|
|
ChatOptions = new()
|
|
{
|
|
ModelId = deploymentName,
|
|
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)"}");
|
|
}
|
|
}
|