mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Support hosted code interpreter for skill script execution (#4192)
* support script execution by code interpretor * improve the instruction prompt * Add DefaultAzureCredential production warning to AgentSkills samples Add the standard three-line WARNING comment about DefaultAzureCredential production considerations to both AgentSkills sample Program.cs files, matching the convention used in all other GettingStarted/Agents samples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * address pr review comments * address feedback * rename Skill* types to FileAgentSkill* prefix for consistency - Rename SkillFrontmatter -> FileAgentSkillFrontmatter - Rename SkillScriptExecutor -> FileAgentSkillScriptExecutor - Add FileAgentSkillScriptExecutionContext and FileAgentSkillScriptExecutionDetails - Update sample, provider, loader, and tests accordingly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * reorder usings * use set for props initialization instead of init * rename HostedCodeInterpreterSkillScriptExecutor --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
8e2cc4bedc
commit
c9cd067be6
@@ -82,6 +82,7 @@
|
||||
<Folder Name="/Samples/02-agents/AgentSkills/">
|
||||
<File Path="samples/02-agents/AgentSkills/README.md" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_BasicSkills/Agent_Step01_BasicSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step02_ScriptExecutionWithCodeInterpreter/Agent_Step02_ScriptExecutionWithCodeInterpreter.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
|
||||
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
|
||||
|
||||
@@ -22,6 +22,9 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYM
|
||||
var skillsProvider = new FileAgentSkillsProvider(skillPath: Path.Combine(AppContext.BaseDirectory, "skills"));
|
||||
|
||||
// --- Agent Setup ---
|
||||
// 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.
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MAAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Copy skills directory to output -->
|
||||
<ItemGroup>
|
||||
<None Include="skills\**\*.*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use Agent Skills with script execution via the hosted code interpreter.
|
||||
// When FileAgentSkillScriptExecutor.HostedCodeInterpreter() is configured, the agent can load and execute scripts
|
||||
// from skill resources using the LLM provider's built-in code interpreter.
|
||||
//
|
||||
// This sample includes the password-generator skill:
|
||||
// - A Python script for generating secure passwords
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// --- Configuration ---
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
// --- Skills Provider with Script Execution ---
|
||||
// Discovers skills and enables script execution via the hosted code interpreter
|
||||
var skillsProvider = new FileAgentSkillsProvider(
|
||||
skillPath: Path.Combine(AppContext.BaseDirectory, "skills"),
|
||||
options: new FileAgentSkillsProviderOptions
|
||||
{
|
||||
ScriptExecutor = FileAgentSkillScriptExecutor.HostedCodeInterpreter()
|
||||
});
|
||||
|
||||
// --- Agent Setup ---
|
||||
// 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.
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "SkillsAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant that can generate secure passwords.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
});
|
||||
|
||||
// --- Example: Password generation with script execution ---
|
||||
Console.WriteLine("Example: Generating a password with a skill script");
|
||||
Console.WriteLine("---------------------------------------------------");
|
||||
AgentResponse response = await agent.RunAsync("Generate a secure password for my database account.");
|
||||
Console.WriteLine($"Agent: {response.Text}\n");
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Script Execution with Code Interpreter
|
||||
|
||||
This sample demonstrates how to use **Agent Skills** with **script execution** via the hosted code interpreter.
|
||||
|
||||
## What's Different from Step01?
|
||||
|
||||
In the [basic skills sample](../Agent_Step01_BasicSkills/), skills only provide instructions and resources as text. This sample adds **script execution** — the agent can load Python scripts from skill resources and execute them using the LLM provider's built-in code interpreter.
|
||||
|
||||
This is enabled by configuring `FileAgentSkillScriptExecutor.HostedCodeInterpreter()` on the skills provider options:
|
||||
|
||||
```csharp
|
||||
var skillsProvider = new FileAgentSkillsProvider(
|
||||
skillPath: Path.Combine(AppContext.BaseDirectory, "skills"),
|
||||
options: new FileAgentSkillsProviderOptions
|
||||
{
|
||||
ScriptExecutor = FileAgentSkillScriptExecutor.HostedCodeInterpreter()
|
||||
});
|
||||
```
|
||||
|
||||
## Skills Included
|
||||
|
||||
### password-generator
|
||||
Generates secure passwords using a Python script with configurable length and complexity.
|
||||
- `scripts/generate.py` — Password generation script
|
||||
- `references/PASSWORD_GUIDELINES.md` — Recommended length and symbol sets by use case
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
Agent_Step02_ScriptExecutionWithCodeInterpreter/
|
||||
├── Program.cs
|
||||
├── Agent_Step02_ScriptExecutionWithCodeInterpreter.csproj
|
||||
└── skills/
|
||||
└── password-generator/
|
||||
├── SKILL.md
|
||||
├── scripts/
|
||||
│ └── generate.py
|
||||
└── references/
|
||||
└── PASSWORD_GUIDELINES.md
|
||||
```
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
- .NET 10.0 SDK
|
||||
- Azure OpenAI endpoint with a deployed model that supports code interpreter
|
||||
|
||||
### Setup
|
||||
1. Set environment variables:
|
||||
```bash
|
||||
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
|
||||
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
```
|
||||
|
||||
2. Run the sample:
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
The sample asks the agent to generate a secure password. The agent:
|
||||
1. Loads the password-generator skill
|
||||
2. Reads the `generate.py` script via `read_skill_resource`
|
||||
3. Executes the script using the code interpreter with appropriate parameters
|
||||
4. Returns the generated password
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Agent Skills Specification](https://agentskills.io/)
|
||||
- [Step01: Basic Skills](../Agent_Step01_BasicSkills/) — Skills without script execution
|
||||
- [Microsoft Agent Framework Documentation](../../../../../docs/)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: password-generator
|
||||
description: Generate secure passwords using a Python script. Use when asked to create passwords or credentials.
|
||||
---
|
||||
|
||||
# Password Generator
|
||||
|
||||
This skill generates secure passwords using a Python script.
|
||||
|
||||
## Usage
|
||||
|
||||
When the user requests a password:
|
||||
1. First, review `references/PASSWORD_GUIDELINES.md` to determine the recommended password length and character sets for the user's use case
|
||||
2. Load `scripts/generate.py` and adjust its parameters (length, character set) based on the guidelines and user's requirements
|
||||
3. Execute the script
|
||||
4. Present the generated password clearly
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# Password Generation Guidelines
|
||||
|
||||
## General Rules
|
||||
|
||||
- Never reuse passwords across services.
|
||||
- Always use cryptographically secure randomness (e.g., `random.SystemRandom()`).
|
||||
- Avoid dictionary words, keyboard patterns, and personal information.
|
||||
|
||||
## Recommended Settings by Use Case
|
||||
|
||||
| Use Case | Min Length | Character Set | Example |
|
||||
|-----------------------|-----------|----------------------------------------|--------------------------|
|
||||
| Web account | 16 | Upper + lower + digits + symbols | `G7!kQp@2xM#nW9$z` |
|
||||
| Database credential | 24 | Upper + lower + digits + symbols | `aR3$vK8!mN2@pQ7&xL5#wY` |
|
||||
| Wi-Fi / network key | 20 | Upper + lower + digits + symbols | `Ht4&jL9!rP2#mK7@xQ` |
|
||||
| API key / token | 32 | Upper + lower + digits (no symbols) | `k8Rm3xQ7nW2pL9vT4jH6yA` |
|
||||
| Encryption passphrase | 32 | Upper + lower + digits + symbols | `Xp4!kR8@mN2#vQ7&jL9$wT` |
|
||||
|
||||
## Symbol Sets
|
||||
|
||||
- **Standard symbols**: `!@#$%^&*()-_=+`
|
||||
- **Extended symbols**: `~`{}[]|;:'",.<>?/\`
|
||||
- **Safe symbols** (URL/shell-safe): `!@#$&*-_=+`
|
||||
- If the target system restricts symbols, use only the **safe** set.
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# Password generator script
|
||||
# Usage: Adjust 'length' as needed, then run
|
||||
|
||||
import random
|
||||
import string
|
||||
|
||||
length = 16 # desired length
|
||||
|
||||
pool = string.ascii_lowercase + string.ascii_uppercase + string.digits + string.punctuation
|
||||
password = "".join(random.SystemRandom().choice(pool) for _ in range(length))
|
||||
print(f"Generated password ({length} chars): {password}")
|
||||
@@ -5,3 +5,4 @@ Samples demonstrating Agent Skills capabilities.
|
||||
| Sample | Description |
|
||||
|--------|-------------|
|
||||
| [Agent_Step01_BasicSkills](Agent_Step01_BasicSkills/) | Using Agent Skills with a ChatClientAgent, including progressive disclosure and skill resources |
|
||||
| [Agent_Step02_ScriptExecutionWithCodeInterpreter](Agent_Step02_ScriptExecutionWithCodeInterpreter/) | Using Agent Skills with script execution via the hosted code interpreter |
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
@@ -13,7 +15,8 @@ namespace Microsoft.Agents.AI;
|
||||
/// and a markdown body with instructions. Resource files referenced in the body are validated at
|
||||
/// discovery time and read from disk on demand.
|
||||
/// </remarks>
|
||||
internal sealed class FileAgentSkill
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed class FileAgentSkill
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileAgentSkill"/> class.
|
||||
@@ -22,8 +25,8 @@ internal sealed class FileAgentSkill
|
||||
/// <param name="body">The SKILL.md content after the closing <c>---</c> delimiter.</param>
|
||||
/// <param name="sourcePath">Absolute path to the directory containing this skill.</param>
|
||||
/// <param name="resourceNames">Relative paths of resource files referenced in the skill body.</param>
|
||||
public FileAgentSkill(
|
||||
SkillFrontmatter frontmatter,
|
||||
internal FileAgentSkill(
|
||||
FileAgentSkillFrontmatter frontmatter,
|
||||
string body,
|
||||
string sourcePath,
|
||||
IReadOnlyList<string>? resourceNames = null)
|
||||
@@ -37,20 +40,20 @@ internal sealed class FileAgentSkill
|
||||
/// <summary>
|
||||
/// Gets the parsed YAML frontmatter (name and description).
|
||||
/// </summary>
|
||||
public SkillFrontmatter Frontmatter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SKILL.md body content (without the YAML frontmatter).
|
||||
/// </summary>
|
||||
public string Body { get; }
|
||||
public FileAgentSkillFrontmatter Frontmatter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directory path where the skill was discovered.
|
||||
/// </summary>
|
||||
public string SourcePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SKILL.md body content (without the YAML frontmatter).
|
||||
/// </summary>
|
||||
internal string Body { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the relative paths of resource files referenced in the skill body (e.g., "references/FAQ.md").
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> ResourceNames { get; }
|
||||
internal IReadOnlyList<string> ResourceNames { get; }
|
||||
}
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
@@ -7,14 +9,15 @@ namespace Microsoft.Agents.AI;
|
||||
/// <summary>
|
||||
/// Parsed YAML frontmatter from a SKILL.md file, containing the skill's name and description.
|
||||
/// </summary>
|
||||
internal sealed class SkillFrontmatter
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed class FileAgentSkillFrontmatter
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkillFrontmatter"/> class.
|
||||
/// Initializes a new instance of the <see cref="FileAgentSkillFrontmatter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">Skill name.</param>
|
||||
/// <param name="description">Skill description.</param>
|
||||
public SkillFrontmatter(string name, string description)
|
||||
internal FileAgentSkillFrontmatter(string name, string description)
|
||||
{
|
||||
this.Name = Throw.IfNullOrWhitespace(name);
|
||||
this.Description = Throw.IfNullOrWhitespace(description);
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -9,6 +10,7 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
@@ -20,7 +22,8 @@ namespace Microsoft.Agents.AI;
|
||||
/// Each file is validated for YAML frontmatter and resource integrity. Invalid skills are excluded
|
||||
/// with logged warnings. Resource paths are checked against path traversal and symlink escape attacks.
|
||||
/// </remarks>
|
||||
internal sealed partial class FileAgentSkillLoader
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed partial class FileAgentSkillLoader
|
||||
{
|
||||
private const string SkillFileName = "SKILL.md";
|
||||
private const int MaxSearchDepth = 2;
|
||||
@@ -33,13 +36,16 @@ internal sealed partial class FileAgentSkillLoader
|
||||
// Example: "---\nname: foo\n---\nBody" → Group 1: "name: foo\n"
|
||||
private static readonly Regex s_frontmatterRegex = new(@"\A\uFEFF?^---\s*$(.+?)^---\s*$", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled, TimeSpan.FromSeconds(5));
|
||||
|
||||
// Matches markdown links to local resource files. Group 1 = relative file path.
|
||||
// Matches resource file references in skill markdown. Group 1 = relative file path.
|
||||
// Supports two forms:
|
||||
// 1. Markdown links: [text](path/file.ext)
|
||||
// 2. Backtick-quoted paths: `path/file.ext`
|
||||
// Supports optional ./ or ../ prefixes; excludes URLs (no ":" in the path character class).
|
||||
// Intentionally conservative: only matches paths with word characters, hyphens, dots,
|
||||
// and forward slashes. Paths with spaces or special characters are not supported.
|
||||
// Examples: [doc](refs/FAQ.md) → "refs/FAQ.md", [s](./s.json) → "./s.json",
|
||||
// Examples: [doc](refs/FAQ.md) → "refs/FAQ.md", `./scripts/run.py` → "./scripts/run.py",
|
||||
// [p](../shared/doc.txt) → "../shared/doc.txt"
|
||||
private static readonly Regex s_resourceLinkRegex = new(@"\[.*?\]\((\.?\.?/?[\w][\w\-./]*\.\w+)\)", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
|
||||
private static readonly Regex s_resourceLinkRegex = new(@"(?:\[.*?\]\(|`)(\.?\.?/?[\w][\w\-./]*\.\w+)(?:\)|`)", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
|
||||
|
||||
// Matches YAML "key: value" lines. Group 1 = key, Group 2 = quoted value, Group 3 = unquoted value.
|
||||
// Accepts single or double quotes; the lazy quantifier trims trailing whitespace on unquoted values.
|
||||
@@ -111,7 +117,7 @@ internal sealed partial class FileAgentSkillLoader
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The resource is not registered, resolves outside the skill directory, or does not exist.
|
||||
/// </exception>
|
||||
internal async Task<string> ReadSkillResourceAsync(FileAgentSkill skill, string resourceName, CancellationToken cancellationToken = default)
|
||||
public async Task<string> ReadSkillResourceAsync(FileAgentSkill skill, string resourceName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
resourceName = NormalizeResourcePath(resourceName);
|
||||
|
||||
@@ -189,7 +195,7 @@ internal sealed partial class FileAgentSkillLoader
|
||||
|
||||
string content = File.ReadAllText(skillFilePath, Encoding.UTF8);
|
||||
|
||||
if (!this.TryParseSkillDocument(content, skillFilePath, out SkillFrontmatter frontmatter, out string body))
|
||||
if (!this.TryParseSkillDocument(content, skillFilePath, out FileAgentSkillFrontmatter frontmatter, out string body))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -208,7 +214,7 @@ internal sealed partial class FileAgentSkillLoader
|
||||
resourceNames: resourceNames);
|
||||
}
|
||||
|
||||
private bool TryParseSkillDocument(string content, string skillFilePath, out SkillFrontmatter frontmatter, out string body)
|
||||
private bool TryParseSkillDocument(string content, string skillFilePath, out FileAgentSkillFrontmatter frontmatter, out string body)
|
||||
{
|
||||
frontmatter = null!;
|
||||
body = null!;
|
||||
@@ -264,7 +270,7 @@ internal sealed partial class FileAgentSkillLoader
|
||||
return false;
|
||||
}
|
||||
|
||||
frontmatter = new SkillFrontmatter(name, description);
|
||||
frontmatter = new FileAgentSkillFrontmatter(name, description);
|
||||
body = content.Substring(match.Index + match.Length).TrimStart();
|
||||
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to loaded skills and the skill loader for use by <see cref="FileAgentSkillScriptExecutor"/> implementations.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed class FileAgentSkillScriptExecutionContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileAgentSkillScriptExecutionContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="skills">The loaded skills dictionary.</param>
|
||||
/// <param name="loader">The skill loader for reading resources.</param>
|
||||
internal FileAgentSkillScriptExecutionContext(Dictionary<string, FileAgentSkill> skills, FileAgentSkillLoader loader)
|
||||
{
|
||||
this.Skills = skills;
|
||||
this.Loader = loader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the loaded skills keyed by name.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, FileAgentSkill> Skills { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the skill loader for reading resources.
|
||||
/// </summary>
|
||||
public FileAgentSkillLoader Loader { get; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the tools and instructions contributed by a <see cref="FileAgentSkillScriptExecutor"/>.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed class FileAgentSkillScriptExecutionDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the additional instructions to provide to the agent for script execution.
|
||||
/// </summary>
|
||||
public string? Instructions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the additional tools to provide to the agent for script execution.
|
||||
/// </summary>
|
||||
public IReadOnlyList<AITool>? Tools { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the contract for skill script execution modes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A <see cref="FileAgentSkillScriptExecutor"/> provides the instructions and tools needed to enable
|
||||
/// script execution within an agent skill. Concrete implementations determine how scripts
|
||||
/// are executed (e.g., via the LLM's hosted code interpreter, an external executor, or a hybrid approach).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Use the static factory methods to create instances:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see cref="HostedCodeInterpreter"/> — executes scripts using the LLM provider's built-in code interpreter.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public abstract class FileAgentSkillScriptExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="FileAgentSkillScriptExecutor"/> that uses the LLM provider's hosted code interpreter for script execution.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="FileAgentSkillScriptExecutor"/> instance configured for hosted code interpreter execution.</returns>
|
||||
public static FileAgentSkillScriptExecutor HostedCodeInterpreter() => new HostedCodeInterpreterFileAgentSkillScriptExecutor();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the tools and instructions contributed by this executor.
|
||||
/// </summary>
|
||||
/// <param name="context">
|
||||
/// The execution context provided by the skills provider, containing the loaded skills
|
||||
/// and the skill loader for reading resources.
|
||||
/// </param>
|
||||
/// <returns>A <see cref="FileAgentSkillScriptExecutionDetails"/> containing the executor's tools and instructions.</returns>
|
||||
protected internal abstract FileAgentSkillScriptExecutionDetails GetExecutionDetails(FileAgentSkillScriptExecutionContext context);
|
||||
}
|
||||
@@ -48,21 +48,21 @@ public sealed partial class FileAgentSkillsProvider : AIContextProvider
|
||||
Each skill provides specialized instructions, reference documents, and assets for specific tasks.
|
||||
|
||||
<available_skills>
|
||||
{0}
|
||||
{skills}
|
||||
</available_skills>
|
||||
|
||||
When a task aligns with a skill's domain:
|
||||
1. Use `load_skill` to retrieve the skill's instructions
|
||||
2. Follow the provided guidance
|
||||
3. Use `read_skill_resource` to read any references or other files mentioned by the skill
|
||||
|
||||
- Use `load_skill` to retrieve the skill's instructions
|
||||
- Follow the provided guidance
|
||||
- Use `read_skill_resource` to read any references or other files mentioned by the skill, always using the full path as written (e.g. `references/FAQ.md`, not just `FAQ.md`)
|
||||
{executor_instructions}
|
||||
Only load what is needed, when it is needed.
|
||||
""";
|
||||
|
||||
private readonly Dictionary<string, FileAgentSkill> _skills;
|
||||
private readonly ILogger<FileAgentSkillsProvider> _logger;
|
||||
private readonly FileAgentSkillLoader _loader;
|
||||
private readonly AITool[] _tools;
|
||||
private readonly IEnumerable<AITool> _tools;
|
||||
private readonly string? _skillsInstructionPrompt;
|
||||
|
||||
/// <summary>
|
||||
@@ -91,9 +91,13 @@ public sealed partial class FileAgentSkillsProvider : AIContextProvider
|
||||
this._loader = new FileAgentSkillLoader(this._logger);
|
||||
this._skills = this._loader.DiscoverAndLoadSkills(skillPaths);
|
||||
|
||||
this._skillsInstructionPrompt = BuildSkillsInstructionPrompt(options, this._skills);
|
||||
var executionDetails = options?.ScriptExecutor is { } executor
|
||||
? executor.GetExecutionDetails(new(this._skills, this._loader))
|
||||
: null;
|
||||
|
||||
this._tools =
|
||||
this._skillsInstructionPrompt = BuildSkillsInstructionPrompt(options, this._skills, executionDetails?.Instructions);
|
||||
|
||||
AITool[] baseTools =
|
||||
[
|
||||
AIFunctionFactory.Create(
|
||||
this.LoadSkill,
|
||||
@@ -104,6 +108,10 @@ public sealed partial class FileAgentSkillsProvider : AIContextProvider
|
||||
name: "read_skill_resource",
|
||||
description: "Reads a file associated with a skill, such as references or assets."),
|
||||
];
|
||||
|
||||
this._tools = executionDetails?.Tools is { Count: > 0 } executorTools
|
||||
? baseTools.Concat(executorTools)
|
||||
: baseTools;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -117,7 +125,7 @@ public sealed partial class FileAgentSkillsProvider : AIContextProvider
|
||||
return new ValueTask<AIContext>(new AIContext
|
||||
{
|
||||
Instructions = this._skillsInstructionPrompt,
|
||||
Tools = this._tools
|
||||
Tools = this._tools,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -166,24 +174,9 @@ public sealed partial class FileAgentSkillsProvider : AIContextProvider
|
||||
}
|
||||
}
|
||||
|
||||
private static string? BuildSkillsInstructionPrompt(FileAgentSkillsProviderOptions? options, Dictionary<string, FileAgentSkill> skills)
|
||||
private static string? BuildSkillsInstructionPrompt(FileAgentSkillsProviderOptions? options, Dictionary<string, FileAgentSkill> skills, string? instructions)
|
||||
{
|
||||
string promptTemplate = DefaultSkillsInstructionPrompt;
|
||||
|
||||
if (options?.SkillsInstructionPrompt is { } optionsInstructions)
|
||||
{
|
||||
try
|
||||
{
|
||||
promptTemplate = string.Format(optionsInstructions, string.Empty);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The provided SkillsInstructionPrompt is not a valid format string. It must contain a '{0}' placeholder and escape any literal '{' or '}' by doubling them ('{{' or '}}').",
|
||||
nameof(options),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
string promptTemplate = options?.SkillsInstructionPrompt ?? DefaultSkillsInstructionPrompt;
|
||||
|
||||
if (skills.Count == 0)
|
||||
{
|
||||
@@ -202,7 +195,9 @@ public sealed partial class FileAgentSkillsProvider : AIContextProvider
|
||||
sb.AppendLine(" </skill>");
|
||||
}
|
||||
|
||||
return string.Format(promptTemplate, sb.ToString().TrimEnd());
|
||||
return promptTemplate
|
||||
.Replace("{skills}", sb.ToString().TrimEnd())
|
||||
.Replace("{executor_instructions}", instructions ?? "\n");
|
||||
}
|
||||
|
||||
[LoggerMessage(LogLevel.Information, "Loading skill: {SkillName}")]
|
||||
|
||||
@@ -13,8 +13,20 @@ public sealed class FileAgentSkillsProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a custom system prompt template for advertising skills.
|
||||
/// Use <c>{0}</c> as the placeholder for the generated skills list.
|
||||
/// Use <c>{skills}</c> as the placeholder for the generated skills list and
|
||||
/// <c>{executor_instructions}</c> for executor-provided instructions.
|
||||
/// When <see langword="null"/>, a default template is used.
|
||||
/// </summary>
|
||||
public string? SkillsInstructionPrompt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the skill executor that enables script execution for loaded skills.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When <see langword="null"/> (the default), script execution is disabled and skills only provide
|
||||
/// instructions and resources. Set this to a <see cref="FileAgentSkillScriptExecutor"/> instance (e.g.,
|
||||
/// <see cref="FileAgentSkillScriptExecutor.HostedCodeInterpreter()"/>) to enable script execution with
|
||||
/// mode-specific instructions and tools.
|
||||
/// </remarks>
|
||||
public FileAgentSkillScriptExecutor? ScriptExecutor { get; set; }
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="FileAgentSkillScriptExecutor"/> that uses the LLM provider's hosted code interpreter for script execution.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This executor directs the LLM to load scripts via <c>read_skill_resource</c> and execute them
|
||||
/// using the provider's built-in code interpreter. A <see cref="HostedCodeInterpreterTool"/> is
|
||||
/// registered to signal the provider to enable its code interpreter sandbox.
|
||||
/// </remarks>
|
||||
internal sealed class HostedCodeInterpreterFileAgentSkillScriptExecutor : FileAgentSkillScriptExecutor
|
||||
{
|
||||
private static readonly FileAgentSkillScriptExecutionDetails s_contribution = new()
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
|
||||
Some skills include executable scripts (e.g., Python files) in their resources.
|
||||
When a skill's instructions reference a script:
|
||||
1. Use `read_skill_resource` to load the script content
|
||||
2. Execute the script using the code interpreter
|
||||
|
||||
""",
|
||||
Tools = [new HostedCodeInterpreterTool()],
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable RCS1168 // Parameter name differs from base name
|
||||
protected internal override FileAgentSkillScriptExecutionDetails GetExecutionDetails(FileAgentSkillScriptExecutionContext _) => s_contribution;
|
||||
#pragma warning restore RCS1168 // Parameter name differs from base name
|
||||
}
|
||||
+49
-1
@@ -501,7 +501,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
}
|
||||
|
||||
// Manually construct a skill that bypasses discovery validation
|
||||
var frontmatter = new SkillFrontmatter("symlink-read-skill", "A skill");
|
||||
var frontmatter = new FileAgentSkillFrontmatter("symlink-read-skill", "A skill");
|
||||
var skill = new FileAgentSkill(
|
||||
frontmatter: frontmatter,
|
||||
body: "See [doc](refs/data.md).",
|
||||
@@ -532,6 +532,54 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
Assert.Equal("Body content.", skills["bom-skill"].Body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("No resource references.", new string[0])]
|
||||
[InlineData("Review `refs/FAQ.md` for details.", new[] { "refs/FAQ.md" })]
|
||||
[InlineData("See [guide](refs/guide.md) then run `scripts/run.py`.", new[] { "refs/guide.md", "scripts/run.py" })]
|
||||
public void DiscoverAndLoadSkills_ResourceReferences_ExtractsExpectedResourceNames(string body, string[] expectedResources)
|
||||
{
|
||||
// Arrange — create skill with resource files on disk so validation passes
|
||||
string skillDir = Path.Combine(this._testRoot, "res-skill");
|
||||
Directory.CreateDirectory(skillDir);
|
||||
foreach (string resource in expectedResources)
|
||||
{
|
||||
string resourcePath = Path.Combine(skillDir, resource.Replace('/', Path.DirectorySeparatorChar));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(resourcePath)!);
|
||||
File.WriteAllText(resourcePath, "content");
|
||||
}
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
$"---\nname: res-skill\ndescription: Resource test\n---\n{body}");
|
||||
|
||||
// Act
|
||||
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
|
||||
|
||||
// Assert
|
||||
Assert.Single(skills);
|
||||
var skill = skills["res-skill"];
|
||||
Assert.Equal(expectedResources.Length, skill.ResourceNames.Count);
|
||||
foreach (string expected in expectedResources)
|
||||
{
|
||||
Assert.Contains(expected, skill.ResourceNames);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadSkillResourceAsync_BacktickResourcePath_ReturnsContentAsync()
|
||||
{
|
||||
// Arrange — skill body uses backtick-quoted path
|
||||
_ = this.CreateSkillDirectoryWithResource("backtick-read", "A skill", "Load `refs/doc.md` first.", "refs/doc.md", "Backtick content.");
|
||||
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
|
||||
var skill = skills["backtick-read"];
|
||||
|
||||
// Act
|
||||
string content = await this._loader.ReadSkillResourceAsync(skill, "refs/doc.md");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Backtick content.", content);
|
||||
}
|
||||
|
||||
private string CreateSkillDirectory(string name, string description, string body)
|
||||
{
|
||||
string skillDir = Path.Combine(this._testRoot, name);
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="FileAgentSkillScriptExecutor"/> and its integration with <see cref="FileAgentSkillsProvider"/>.
|
||||
/// </summary>
|
||||
public sealed class FileAgentSkillScriptExecutorTests : IDisposable
|
||||
{
|
||||
private readonly string _testRoot;
|
||||
private readonly TestAIAgent _agent = new();
|
||||
private static readonly FileAgentSkillScriptExecutionContext s_emptyContext = new(
|
||||
new Dictionary<string, FileAgentSkill>(StringComparer.OrdinalIgnoreCase),
|
||||
new FileAgentSkillLoader(NullLogger.Instance));
|
||||
|
||||
public FileAgentSkillScriptExecutorTests()
|
||||
{
|
||||
this._testRoot = Path.Combine(Path.GetTempPath(), "skill-executor-tests-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(this._testRoot);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(this._testRoot))
|
||||
{
|
||||
Directory.Delete(this._testRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HostedCodeInterpreter_ReturnsNonNullInstance()
|
||||
{
|
||||
// Act
|
||||
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(executor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HostedCodeInterpreter_GetExecutionDetails_ReturnsNonNullInstructions()
|
||||
{
|
||||
// Arrange
|
||||
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
|
||||
|
||||
// Act
|
||||
var details = executor.GetExecutionDetails(s_emptyContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(details);
|
||||
Assert.NotNull(details.Instructions);
|
||||
Assert.NotEmpty(details.Instructions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HostedCodeInterpreter_GetExecutionDetails_ReturnsNonEmptyToolsList()
|
||||
{
|
||||
// Arrange
|
||||
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
|
||||
|
||||
// Act
|
||||
var details = executor.GetExecutionDetails(s_emptyContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(details);
|
||||
Assert.NotNull(details.Tools);
|
||||
Assert.NotEmpty(details.Tools);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Provider_WithExecutor_IncludesExecutorInstructionsInPromptAsync()
|
||||
{
|
||||
// Arrange
|
||||
CreateSkill(this._testRoot, "exec-skill", "Executor test", "Body.");
|
||||
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
|
||||
var options = new FileAgentSkillsProviderOptions { ScriptExecutor = executor };
|
||||
var provider = new FileAgentSkillsProvider(this._testRoot, options);
|
||||
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
|
||||
|
||||
// Act
|
||||
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert — executor instructions should be merged into the prompt
|
||||
Assert.NotNull(result.Instructions);
|
||||
Assert.Contains("code interpreter", result.Instructions, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Provider_WithExecutor_IncludesExecutorToolsAsync()
|
||||
{
|
||||
// Arrange
|
||||
CreateSkill(this._testRoot, "tools-exec-skill", "Executor tools test", "Body.");
|
||||
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
|
||||
var options = new FileAgentSkillsProviderOptions { ScriptExecutor = executor };
|
||||
var provider = new FileAgentSkillsProvider(this._testRoot, options);
|
||||
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
|
||||
|
||||
// Act
|
||||
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert — should have 3 tools: load_skill, read_skill_resource, and HostedCodeInterpreterTool
|
||||
Assert.NotNull(result.Tools);
|
||||
Assert.Equal(3, result.Tools!.Count());
|
||||
var toolNames = result.Tools!.Select(t => t.Name).ToList();
|
||||
Assert.Contains("load_skill", toolNames);
|
||||
Assert.Contains("read_skill_resource", toolNames);
|
||||
Assert.Single(result.Tools!, t => t is HostedCodeInterpreterTool);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Provider_WithoutExecutor_DoesNotIncludeExecutorToolsAsync()
|
||||
{
|
||||
// Arrange
|
||||
CreateSkill(this._testRoot, "no-exec-skill", "No executor test", "Body.");
|
||||
var provider = new FileAgentSkillsProvider(this._testRoot);
|
||||
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
|
||||
|
||||
// Act
|
||||
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert — should only have the two base tools
|
||||
Assert.NotNull(result.Tools);
|
||||
Assert.Equal(2, result.Tools!.Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Provider_WithHostedCodeInterpreter_MergesScriptInstructionsIntoPromptAsync()
|
||||
{
|
||||
// Arrange
|
||||
CreateSkill(this._testRoot, "merge-skill", "Merge test", "Body.");
|
||||
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
|
||||
var options = new FileAgentSkillsProviderOptions { ScriptExecutor = executor };
|
||||
var provider = new FileAgentSkillsProvider(this._testRoot, options);
|
||||
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
|
||||
|
||||
// Act
|
||||
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert — prompt should contain both the skill listing and the executor's script instructions
|
||||
Assert.NotNull(result.Instructions);
|
||||
string instructions = result.Instructions!;
|
||||
|
||||
// Skill listing is present
|
||||
Assert.Contains("merge-skill", instructions);
|
||||
Assert.Contains("Merge test", instructions);
|
||||
|
||||
// Hosted code interpreter script instructions are merged into the prompt
|
||||
Assert.Contains("executable scripts", instructions);
|
||||
Assert.Contains("read_skill_resource", instructions);
|
||||
Assert.Contains("Execute the script using the code interpreter", instructions);
|
||||
}
|
||||
|
||||
private static void CreateSkill(string root, string name, string description, string body)
|
||||
{
|
||||
string skillDir = Path.Combine(root, name);
|
||||
Directory.CreateDirectory(skillDir);
|
||||
File.WriteAllText(
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
$"---\nname: {name}\ndescription: {description}\n---\n{body}");
|
||||
}
|
||||
}
|
||||
+1
-16
@@ -96,7 +96,7 @@ public sealed class FileAgentSkillsProviderTests : IDisposable
|
||||
this.CreateSkill("custom-prompt-skill", "Custom prompt", "Body.");
|
||||
var options = new FileAgentSkillsProviderOptions
|
||||
{
|
||||
SkillsInstructionPrompt = "Custom template: {0}"
|
||||
SkillsInstructionPrompt = "Custom template: {skills}"
|
||||
};
|
||||
var provider = new FileAgentSkillsProvider(this._testRoot, options);
|
||||
var inputContext = new AIContext();
|
||||
@@ -110,21 +110,6 @@ public sealed class FileAgentSkillsProviderTests : IDisposable
|
||||
Assert.StartsWith("Custom template:", result.Instructions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPromptTemplate_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange — template with unescaped braces and no valid {0} placeholder
|
||||
var options = new FileAgentSkillsProviderOptions
|
||||
{
|
||||
SkillsInstructionPrompt = "Bad template with {unescaped} braces"
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<ArgumentException>(() => new FileAgentSkillsProvider(this._testRoot, options));
|
||||
Assert.Contains("SkillsInstructionPrompt", ex.Message);
|
||||
Assert.Equal("options", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingCoreAsync_SkillNamesAreXmlEscapedAsync()
|
||||
{
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="HostedCodeInterpreterFileAgentSkillScriptExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class HostedCodeInterpreterFileAgentSkillScriptExecutorTests
|
||||
{
|
||||
private static readonly FileAgentSkillScriptExecutionContext s_emptyContext = new(
|
||||
new Dictionary<string, FileAgentSkill>(StringComparer.OrdinalIgnoreCase),
|
||||
new FileAgentSkillLoader(NullLogger.Instance));
|
||||
|
||||
[Fact]
|
||||
public void GetExecutionDetails_ReturnsScriptExecutionGuidance()
|
||||
{
|
||||
// Arrange
|
||||
var executor = new HostedCodeInterpreterFileAgentSkillScriptExecutor();
|
||||
|
||||
// Act
|
||||
var details = executor.GetExecutionDetails(s_emptyContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(details.Instructions);
|
||||
Assert.Contains("read_skill_resource", details.Instructions);
|
||||
Assert.Contains("code interpreter", details.Instructions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetExecutionDetails_ReturnsSingleHostedCodeInterpreterTool()
|
||||
{
|
||||
// Arrange
|
||||
var executor = new HostedCodeInterpreterFileAgentSkillScriptExecutor();
|
||||
|
||||
// Act
|
||||
var details = executor.GetExecutionDetails(s_emptyContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(details.Tools);
|
||||
Assert.Single(details.Tools!);
|
||||
Assert.IsType<HostedCodeInterpreterTool>(details.Tools![0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetExecutionDetails_ReturnsSameInstanceOnMultipleCalls()
|
||||
{
|
||||
// Arrange
|
||||
var executor = new HostedCodeInterpreterFileAgentSkillScriptExecutor();
|
||||
|
||||
// Act
|
||||
var details1 = executor.GetExecutionDetails(s_emptyContext);
|
||||
var details2 = executor.GetExecutionDetails(s_emptyContext);
|
||||
|
||||
// Assert — static details should be reused
|
||||
Assert.Same(details1, details2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FactoryMethod_ReturnsHostedCodeInterpreterFileAgentSkillScriptExecutor()
|
||||
{
|
||||
// Act
|
||||
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
|
||||
|
||||
// Assert
|
||||
Assert.IsType<HostedCodeInterpreterFileAgentSkillScriptExecutor>(executor);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user