mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8ff0e344b | ||
|
|
668862caf9 | ||
|
|
1b7efad70e | ||
|
|
d6427838e3 | ||
|
|
78a975c98d | ||
|
|
573aa0048c | ||
|
|
f248be004d | ||
|
|
1554490e05 | ||
|
|
9b22ecd119 | ||
|
|
2eb0705ee0 | ||
|
|
dad3652f46 | ||
|
|
56fb634f0e | ||
|
|
56c3f8d825 |
@@ -6,8 +6,12 @@
|
||||
[](https://learn.microsoft.com/en-us/agent-framework/)
|
||||
[](https://pypi.org/project/agent-framework/)
|
||||
[](https://www.nuget.org/profiles/MicrosoftAgentFramework/)
|
||||
[](https://github.com/microsoft/agent-framework/stargazers)
|
||||
|
||||
Welcome to Microsoft's comprehensive multi-language framework for building, orchestrating, and deploying AI agents with support for both .NET and Python implementations. This framework provides everything from simple chat agents to complex multi-agent workflows with graph-based orchestration.
|
||||
|
||||
Microsoft Agent Framework (MAF) is an open, multi-language framework for building **production-grade AI agents and multi-agent workflows** in **.NET and Python**.
|
||||
|
||||
Microsoft Agent Framework is built for teams taking agents from prototype to production. It provides a consistent foundation for building, orchestrating, and operating agent systems across Python and .NET, while keeping architecture choices open as requirements evolve, and supports a broad ecosystem including Microsoft Foundry, Azure OpenAI, OpenAI, and the GitHub Copilot SDK, with samples and hosting patterns for both local development and cloud deployment.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=AAgdMhftj8w" title="Watch the full Agent Framework introduction (30 min)">
|
||||
@@ -21,10 +25,54 @@ Welcome to Microsoft's comprehensive multi-language framework for building, orch
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 📋 Getting Started
|
||||
## Is this the right framework for you?
|
||||
|
||||
### 📦 Installation
|
||||
MAF is a strong fit if you:
|
||||
- are building agents and workflows you expect to run in production,
|
||||
- need orchestration beyond a single prompt or stateless chat loop,
|
||||
- want graph-based patterns such as sequential, concurrent, handoff, and group collaboration,
|
||||
- care about durability, restartability, observability, governance, or human-in-the-loop control,
|
||||
- need provider flexibility so your architecture can evolve without major rewrites.
|
||||
|
||||
## Key Features
|
||||
Explore new MAF capabilities and real implementation patterns on the [official blog](https://devblogs.microsoft.com/agent-framework/).
|
||||
|
||||
- **Python and C#/.NET Support**: Full framework support for both Python and C#/.NET implementations with consistent APIs
|
||||
- [Python packages](./python/packages/) | [.NET source](./dotnet/src/)
|
||||
- **Multiple Agent Provider Support**: Support for various LLM providers with more being added continuously
|
||||
- [Python examples](./python/samples/02-agents/providers/) | [.NET examples](./dotnet/samples/02-agents/AgentProviders/)
|
||||
- **Middleware**: Flexible middleware system for request/response processing, exception handling, and custom pipelines
|
||||
- [Python middleware](./python/samples/02-agents/middleware/) | [.NET middleware](./dotnet/samples/02-agents/Agents/Agent_Step11_Middleware/)
|
||||
- **Orchestration Patterns & Workflows**: Build multi-agent systems with graph-based workflows supporting sequential, concurrent, handoff, and group collaboration patterns; includes checkpointing, streaming, human-in-the-loop, and time-travel
|
||||
- [Python workflows](./python/samples/03-workflows/) | [.NET workflows](./dotnet/samples/03-workflows/)
|
||||
- **Foundry Hosted Agents (new)**: Deploy and host your agents to Foundry-hosted infrastructure with just 2 additional lines of code
|
||||
- [Python samples](./python/samples/04-hosting/foundry-hosted-agents/) | [.NET samples](./dotnet/samples/04-hosting/FoundryHostedAgents/)
|
||||
- **Observability**: Built-in OpenTelemetry integration for distributed tracing, monitoring, and debugging
|
||||
- [Python observability](./python/samples/02-agents/observability/) | [.NET telemetry](./dotnet/samples/02-agents/AgentOpenTelemetry/)
|
||||
- **Declarative Agents**: Define agents using YAML for faster setup and versioning
|
||||
- [Declarative agent samples](./declarative-agents/)
|
||||
- **Agent Skills**: Build domain-specific knowledge bases from multiple sources—files, inline code, class libraries—for agents to discover and use
|
||||
- [Skills design](./docs/decisions/0021-agent-skills-design.md)
|
||||
- **AF Labs**: Experimental packages for cutting-edge features including benchmarking, reinforcement learning, and research initiatives
|
||||
- [Labs directory](./python/packages/lab/)
|
||||
- **DevUI**: Interactive developer UI for agent development, testing, and debugging workflows
|
||||
- [See the DevUI in action](https://www.youtube.com/watch?v=mOAaGY4WPvc)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Installation](#installation)
|
||||
- [Learning Resources](#learning-resources)
|
||||
- [Quickstart](#quickstart)
|
||||
- [Basic Agent - Python](#basic-agent---python)
|
||||
- [Basic Agent - .NET](#basic-agent---net)
|
||||
- [More Examples & Samples](#more-examples--samples)
|
||||
- [Community & Feedback](#community--feedback)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Contributor Resources](#contributor-resources)
|
||||
|
||||
## Getting Started
|
||||
### Installation
|
||||
Python
|
||||
|
||||
```bash
|
||||
@@ -37,9 +85,13 @@ pip install agent-framework
|
||||
|
||||
```bash
|
||||
dotnet add package Microsoft.Agents.AI
|
||||
# For Foundry integration (used in the .NET quickstart below):
|
||||
dotnet add package Microsoft.Agents.AI.Foundry
|
||||
dotnet add package Azure.AI.Projects
|
||||
dotnet add package Azure.Identity
|
||||
```
|
||||
|
||||
### 📚 Documentation
|
||||
### Learning Resources
|
||||
|
||||
- **[Overview](https://learn.microsoft.com/agent-framework/overview/agent-framework-overview)** - High level overview of the framework
|
||||
- **[Quick Start](https://learn.microsoft.com/agent-framework/tutorials/quick-start)** - Get started with a simple agent
|
||||
@@ -48,44 +100,9 @@ dotnet add package Microsoft.Agents.AI
|
||||
- **[Migration from Semantic Kernel](https://learn.microsoft.com/en-us/agent-framework/migration-guide/from-semantic-kernel)** - Guide to migrate from Semantic Kernel
|
||||
- **[Migration from AutoGen](https://learn.microsoft.com/en-us/agent-framework/migration-guide/from-autogen)** - Guide to migrate from AutoGen
|
||||
|
||||
Still have questions? Join our [weekly office hours](./COMMUNITY.md#public-community-office-hours) or ask questions in our [Discord channel](https://discord.gg/b5zjErwbQM) to get help from the team and other users.
|
||||
### Quickstart
|
||||
|
||||
### ✨ **Highlights**
|
||||
|
||||
- **Graph-based Workflows**: Connect agents and deterministic functions using data flows with streaming, checkpointing, human-in-the-loop, and time-travel capabilities
|
||||
- [Python workflows](./python/samples/03-workflows/) | [.NET workflows](./dotnet/samples/03-workflows/)
|
||||
- **AF Labs**: Experimental packages for cutting-edge features including benchmarking, reinforcement learning, and research initiatives
|
||||
- [Labs directory](./python/packages/lab/)
|
||||
- **DevUI**: Interactive developer UI for agent development, testing, and debugging workflows
|
||||
- [DevUI package](./python/packages/devui/)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=mOAaGY4WPvc">
|
||||
<img src="https://img.youtube.com/vi/mOAaGY4WPvc/hqdefault.jpg" alt="See the DevUI in action" width="480">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=mOAaGY4WPvc">
|
||||
See the DevUI in action (1 min)
|
||||
</a>
|
||||
</p>
|
||||
|
||||
- **Python and C#/.NET Support**: Full framework support for both Python and C#/.NET implementations with consistent APIs
|
||||
- [Python packages](./python/packages/) | [.NET source](./dotnet/src/)
|
||||
- **Observability**: Built-in OpenTelemetry integration for distributed tracing, monitoring, and debugging
|
||||
- [Python observability](./python/samples/02-agents/observability/) | [.NET telemetry](./dotnet/samples/02-agents/AgentOpenTelemetry/)
|
||||
- **Multiple Agent Provider Support**: Support for various LLM providers with more being added continuously
|
||||
- [Python examples](./python/samples/02-agents/providers/) | [.NET examples](./dotnet/samples/02-agents/AgentProviders/)
|
||||
- **Middleware**: Flexible middleware system for request/response processing, exception handling, and custom pipelines
|
||||
- [Python middleware](./python/samples/02-agents/middleware/) | [.NET middleware](./dotnet/samples/02-agents/Agents/Agent_Step11_Middleware/)
|
||||
|
||||
### 💬 **We want your feedback!**
|
||||
|
||||
- For bugs, please file a [GitHub issue](https://github.com/microsoft/agent-framework/issues).
|
||||
|
||||
## Quickstart
|
||||
|
||||
### Basic Agent - Python
|
||||
#### Basic Agent - Python
|
||||
|
||||
Create a simple Azure Responses Agent that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
@@ -109,7 +126,7 @@ async def main():
|
||||
# project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
# model=os.environ["FOUNDRY_MODEL_DEPLOYMENT_NAME"],
|
||||
),
|
||||
name="HaikuBot",
|
||||
name="HaikuAgent",
|
||||
instructions="You are an upbeat assistant that writes beautifully.",
|
||||
)
|
||||
|
||||
@@ -119,40 +136,24 @@ if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### Basic Agent - .NET
|
||||
Create a simple Agent, using Microsoft Foundry with token-based auth, that writes a haiku about the Microsoft Agent Framework
|
||||
#### Basic Agent - .NET
|
||||
Create a simple Agent, using Microsoft Foundry that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```c#
|
||||
// dotnet add package Microsoft.Agents.AI.Foundry
|
||||
// Use `az login` to authenticate with Azure CLI
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using System;
|
||||
// This sample shows how to create and run a basic agent with AIProjectClient.AsAIAgent(...).
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
var agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(model: deploymentName, name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
|
||||
Create a simple Agent, using OpenAI Responses, that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```c#
|
||||
// dotnet add package Microsoft.Agents.AI.OpenAI
|
||||
using System;
|
||||
using OpenAI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// Replace the <apikey> with your OpenAI API key.
|
||||
var agent = new OpenAIClient("<apikey>")
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(model: "gpt-5.4-mini", name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
AIAgent agent =
|
||||
new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(model: deploymentName, instructions: "You are an upbeat assistant that writes beautifully.", name: "HaikuAgent");
|
||||
|
||||
// Once you have the agent, you can invoke it like any other AIAgent.
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
|
||||
@@ -175,6 +176,12 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
|
||||
- [Hosting](./dotnet/samples/04-hosting): A2A, Durable Agents, Durable Workflows
|
||||
- [End-to-End](./dotnet/samples/05-end-to-end): full applications and demos
|
||||
|
||||
## Community & Feedback
|
||||
|
||||
- **Found a bug?** File a [GitHub issue](https://github.com/microsoft/agent-framework/issues) to help us improve.
|
||||
- **Enjoying MAF?** [](https://github.com/microsoft/agent-framework) to show your support and help others discover the project.
|
||||
- **Have questions?** Join our [Discord](https://discord.gg/b5zjErwbQM) or visit [weekly office hours](./COMMUNITY.md#public-community-office-hours).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication
|
||||
@@ -187,16 +194,7 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
|
||||
> **Tip:** `DefaultAzureCredential` is convenient for development but in production, consider using a specific credential (e.g., `ManagedIdentityCredential`) to avoid latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The samples typically read configuration from environment variables. Common required variables:
|
||||
|
||||
| Variable | Used by | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI samples | Your Azure OpenAI resource URL |
|
||||
| `AZURE_OPENAI_DEPLOYMENT_NAME` | Azure OpenAI samples | Model deployment name (e.g. `gpt-4o-mini`) |
|
||||
| `AZURE_AI_PROJECT_ENDPOINT` | Microsoft Foundry samples | Your Microsoft Foundry project endpoint |
|
||||
| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Microsoft Foundry samples | Model deployment name |
|
||||
| `OPENAI_API_KEY` | OpenAI (non-Azure) samples | Your OpenAI platform API key |
|
||||
For environment variable configuration specific to each sample, refer to the README in the sample directory ([Python samples](./python/samples/) | [.NET samples](./dotnet/samples/)).
|
||||
|
||||
## Contributor Resources
|
||||
|
||||
|
||||
@@ -56,15 +56,15 @@
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
|
||||
<!-- OpenTelemetry -->
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.15.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Api" Version="1.15.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.15.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.15.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.14.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.14.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.14.0" />
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.15.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Api" Version="1.15.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.15.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
|
||||
<!-- Microsoft.AspNetCore.* -->
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.0" />
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
// This is provided for demonstration purposes only.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Executes file-based skill scripts as local subprocesses.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This runner uses the script's absolute path, converts the arguments
|
||||
/// to CLI flags, and returns captured output. It is intended for
|
||||
/// demonstration purposes only.
|
||||
/// This runner uses the script's absolute path and converts the arguments
|
||||
/// to CLI arguments. When the LLM sends a JSON array, each element is used
|
||||
/// as a positional argument. It is intended for demonstration purposes only.
|
||||
/// </remarks>
|
||||
internal static class SubprocessScriptRunner
|
||||
{
|
||||
@@ -24,7 +24,8 @@ internal static class SubprocessScriptRunner
|
||||
public static async Task<object?> RunAsync(
|
||||
AgentFileSkill skill,
|
||||
AgentFileSkillScript script,
|
||||
AIFunctionArguments arguments,
|
||||
JsonElement? arguments,
|
||||
IServiceProvider? serviceProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(script.FullPath))
|
||||
@@ -61,24 +62,27 @@ internal static class SubprocessScriptRunner
|
||||
startInfo.FileName = script.FullPath;
|
||||
}
|
||||
|
||||
if (arguments is not null)
|
||||
if (arguments is { ValueKind: JsonValueKind.Array } json)
|
||||
{
|
||||
foreach (var (key, value) in arguments)
|
||||
// Positional CLI arguments
|
||||
foreach (var element in json.EnumerateArray())
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
if (element.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
if (boolValue)
|
||||
{
|
||||
startInfo.ArgumentList.Add(NormalizeKey(key));
|
||||
}
|
||||
}
|
||||
else if (value is not null)
|
||||
{
|
||||
startInfo.ArgumentList.Add(NormalizeKey(key));
|
||||
startInfo.ArgumentList.Add(value.ToString()!);
|
||||
throw new InvalidOperationException(
|
||||
$"File-based skill scripts only accept string CLI arguments but received a JSON element of kind '{element.ValueKind}'. " +
|
||||
"All array elements must be JSON strings.");
|
||||
}
|
||||
|
||||
startInfo.ArgumentList.Add(element.GetString()!);
|
||||
}
|
||||
}
|
||||
else if (arguments is not null && arguments.Value.ValueKind != JsonValueKind.Null && arguments.Value.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Expected a JSON array of CLI arguments but received {arguments.Value.ValueKind}. " +
|
||||
"File-based skill scripts expect positional arguments as a JSON array of strings.");
|
||||
}
|
||||
|
||||
Process? process = null;
|
||||
try
|
||||
@@ -128,10 +132,4 @@ internal static class SubprocessScriptRunner
|
||||
process?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a parameter key to a consistent --flag format.
|
||||
/// Models may return keys with or without leading dashes (e.g., "value" vs "--value").
|
||||
/// </summary>
|
||||
private static string NormalizeKey(string key) => "--" + key.TrimStart('-');
|
||||
}
|
||||
|
||||
+47
@@ -65,6 +65,53 @@ Workflow orchestration started for CancelOrder. Orchestration runId: abc123def45
|
||||
>
|
||||
> If not provided, a unique run ID is auto-generated.
|
||||
|
||||
### Wait for the Workflow Result
|
||||
|
||||
By default, the HTTP endpoint returns `202 Accepted` immediately with the run ID. If you want to wait for the workflow to complete and get the result in the response, add the `x-ms-wait-for-response: true` header:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/workflows/CancelOrder/run \
|
||||
-H "Content-Type: text/plain" \
|
||||
-H "x-ms-wait-for-response: true" \
|
||||
-d "12345"
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/workflows/CancelOrder/run `
|
||||
-ContentType text/plain `
|
||||
-Headers @{ "x-ms-wait-for-response" = "true" } `
|
||||
-Body "12345"
|
||||
```
|
||||
|
||||
The response will contain the workflow result as plain text (200 OK):
|
||||
|
||||
```text
|
||||
Cancellation email sent for order 12345 to jerry@example.com.
|
||||
```
|
||||
|
||||
To get the result as JSON, also include the `Accept: application/json` header:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/workflows/CancelOrder/run \
|
||||
-H "Content-Type: text/plain" \
|
||||
-H "x-ms-wait-for-response: true" \
|
||||
-H "Accept: application/json" \
|
||||
-d "12345"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"runId": "abc123def456",
|
||||
"workflowStatus": "Completed",
|
||||
"result": "Cancellation email sent for order 12345 to jerry@example.com."
|
||||
}
|
||||
```
|
||||
|
||||
In the function app logs, you will see the sequential execution of each executor:
|
||||
|
||||
```text
|
||||
|
||||
+22
@@ -7,6 +7,21 @@ Content-Type: text/plain
|
||||
|
||||
12345
|
||||
|
||||
### Cancel an order and wait for the result
|
||||
POST {{authority}}/api/workflows/CancelOrder/run
|
||||
Content-Type: text/plain
|
||||
x-ms-wait-for-response: true
|
||||
|
||||
12345
|
||||
|
||||
### Cancel an order and wait for the result (JSON response)
|
||||
POST {{authority}}/api/workflows/CancelOrder/run
|
||||
Content-Type: text/plain
|
||||
Accept: application/json
|
||||
x-ms-wait-for-response: true
|
||||
|
||||
12345
|
||||
|
||||
### Cancel an order with a custom run ID
|
||||
POST {{authority}}/api/workflows/CancelOrder/run?runId=my-custom-id-123
|
||||
Content-Type: text/plain
|
||||
@@ -19,6 +34,13 @@ Content-Type: text/plain
|
||||
|
||||
12345
|
||||
|
||||
### Get order status and wait for the result
|
||||
POST {{authority}}/api/workflows/OrderStatus/run
|
||||
Content-Type: text/plain
|
||||
x-ms-wait-for-response: true
|
||||
|
||||
12345
|
||||
|
||||
### Batch cancel orders with a complex JSON input
|
||||
POST {{authority}}/api/workflows/BatchCancelOrders/run
|
||||
Content-Type: application/json
|
||||
|
||||
+2
@@ -13,6 +13,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.AgentServer.Invocations" />
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
<PackageReference Include="OpenTelemetry.Api" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For contributors: uses ProjectReference to build against local source -->
|
||||
|
||||
+1
@@ -34,6 +34,7 @@
|
||||
<PackageReference Include="Azure.AI.Projects" VersionOverride="2.1.0-beta.1" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -21,6 +21,8 @@ internal static class BuiltInFunctions
|
||||
internal const string HttpPrefix = "http-";
|
||||
internal const string McpToolPrefix = "mcptool-";
|
||||
|
||||
private const string WaitForResponseHeaderName = "x-ms-wait-for-response";
|
||||
|
||||
internal static readonly string RunAgentHttpFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunAgentHttpAsync)}";
|
||||
internal static readonly string RunAgentEntityFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(InvokeAgentAsync)}";
|
||||
internal static readonly string RunAgentMcpToolFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunMcpToolAsync)}";
|
||||
@@ -62,6 +64,11 @@ internal static class BuiltInFunctions
|
||||
StartOrchestrationOptions? options = instanceId is not null ? new StartOrchestrationOptions(instanceId) : null;
|
||||
string resolvedInstanceId = await client.ScheduleNewOrchestrationInstanceAsync(orchestrationFunctionName, orchestrationInput, options);
|
||||
|
||||
if (ShouldWaitForResponse(req, defaultValue: false))
|
||||
{
|
||||
return await WaitForWorkflowCompletionAsync(req, client, context, resolvedInstanceId);
|
||||
}
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
|
||||
await response.WriteStringAsync($"Workflow orchestration started for {workflowName}. Orchestration runId: {resolvedInstanceId}");
|
||||
return response;
|
||||
@@ -304,15 +311,7 @@ internal static class BuiltInFunctions
|
||||
}
|
||||
|
||||
// Check if we should wait for response (default is true)
|
||||
bool waitForResponse = true;
|
||||
if (req.Headers.TryGetValues("x-ms-wait-for-response", out IEnumerable<string>? waitForResponseValues))
|
||||
{
|
||||
string? waitForResponseValue = waitForResponseValues.FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(waitForResponseValue) && bool.TryParse(waitForResponseValue, out bool parsedValue))
|
||||
{
|
||||
waitForResponse = parsedValue;
|
||||
}
|
||||
}
|
||||
bool waitForResponse = ShouldWaitForResponse(req, defaultValue: true);
|
||||
|
||||
AIAgent agentProxy = client.AsDurableAgentProxy(context, agentName);
|
||||
|
||||
@@ -428,6 +427,95 @@ internal static class BuiltInFunctions
|
||||
return metadata.ReadOutputAs<DurableWorkflowResult>()?.Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for a workflow orchestration to complete and returns an appropriate HTTP response.
|
||||
/// </summary>
|
||||
private static async Task<HttpResponseData> WaitForWorkflowCompletionAsync(
|
||||
HttpRequestData req,
|
||||
DurableTaskClient client,
|
||||
FunctionContext context,
|
||||
string instanceId)
|
||||
{
|
||||
bool acceptsJson = AcceptsJson(req);
|
||||
|
||||
OrchestrationMetadata? metadata = await client.WaitForInstanceCompletionAsync(
|
||||
instanceId,
|
||||
getInputsAndOutputs: true,
|
||||
cancellation: context.CancellationToken);
|
||||
|
||||
if (metadata is null)
|
||||
{
|
||||
return await CreateErrorResponseAsync(req, context, HttpStatusCode.NotFound,
|
||||
$"No workflow orchestration with ID '{instanceId}' was found.", acceptsJson);
|
||||
}
|
||||
|
||||
if (metadata.RuntimeStatus is OrchestrationRuntimeStatus.Failed)
|
||||
{
|
||||
string errorMessage = metadata.FailureDetails?.ErrorMessage ?? "Unknown error";
|
||||
HttpResponseData failedResponse = req.CreateResponse(HttpStatusCode.OK);
|
||||
|
||||
if (acceptsJson)
|
||||
{
|
||||
await failedResponse.WriteAsJsonAsync(
|
||||
new WorkflowRunResponse(instanceId, metadata.RuntimeStatus.ToString(), Result: null, Error: errorMessage),
|
||||
context.CancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
failedResponse.Headers.Add("Content-Type", "text/plain");
|
||||
await failedResponse.WriteStringAsync(errorMessage, context.CancellationToken);
|
||||
}
|
||||
|
||||
return failedResponse;
|
||||
}
|
||||
|
||||
if (metadata.RuntimeStatus is not OrchestrationRuntimeStatus.Completed)
|
||||
{
|
||||
return await CreateErrorResponseAsync(req, context, HttpStatusCode.InternalServerError,
|
||||
$"Workflow orchestration '{instanceId}' ended with unexpected status '{metadata.RuntimeStatus}'.", acceptsJson);
|
||||
}
|
||||
|
||||
string? result = metadata.ReadOutputAs<DurableWorkflowResult>()?.Result;
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
|
||||
|
||||
if (acceptsJson)
|
||||
{
|
||||
JsonElement? resultElement = null;
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
{
|
||||
try
|
||||
{
|
||||
using JsonDocument doc = JsonDocument.Parse(result);
|
||||
resultElement = doc.RootElement.Clone();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Result is a plain string (not valid JSON) — serialize it as a JSON string element.
|
||||
var buffer = new System.Buffers.ArrayBufferWriter<byte>();
|
||||
using (var writer = new Utf8JsonWriter(buffer))
|
||||
{
|
||||
writer.WriteStringValue(result);
|
||||
}
|
||||
|
||||
using JsonDocument fallbackDoc = JsonDocument.Parse(buffer.WrittenMemory);
|
||||
resultElement = fallbackDoc.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
await response.WriteAsJsonAsync(
|
||||
new WorkflowRunResponse(instanceId, metadata.RuntimeStatus.ToString(), resultElement),
|
||||
context.CancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
response.Headers.Add("Content-Type", "text/plain");
|
||||
await response.WriteStringAsync(result ?? string.Empty, context.CancellationToken);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an error response with the specified status code and error message.
|
||||
/// </summary>
|
||||
@@ -435,18 +523,18 @@ internal static class BuiltInFunctions
|
||||
/// <param name="context">The function context.</param>
|
||||
/// <param name="statusCode">The HTTP status code.</param>
|
||||
/// <param name="errorMessage">The error message.</param>
|
||||
/// <param name="acceptsJson">Optional pre-computed value indicating whether the client accepts JSON. When <see langword="null"/>, the value is determined from the request's <c>Accept</c> header.</param>
|
||||
/// <returns>The HTTP response data containing the error.</returns>
|
||||
private static async Task<HttpResponseData> CreateErrorResponseAsync(
|
||||
HttpRequestData req,
|
||||
FunctionContext context,
|
||||
HttpStatusCode statusCode,
|
||||
string errorMessage)
|
||||
string errorMessage,
|
||||
bool? acceptsJson = null)
|
||||
{
|
||||
HttpResponseData response = req.CreateResponse(statusCode);
|
||||
bool acceptsJson = req.Headers.TryGetValues("Accept", out IEnumerable<string>? acceptValues) &&
|
||||
acceptValues.Contains("application/json", StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (acceptsJson)
|
||||
if (acceptsJson ?? AcceptsJson(req))
|
||||
{
|
||||
ErrorResponse errorResponse = new((int)statusCode, errorMessage);
|
||||
await response.WriteAsJsonAsync(errorResponse, context.CancellationToken);
|
||||
@@ -479,10 +567,7 @@ internal static class BuiltInFunctions
|
||||
HttpResponseData response = req.CreateResponse(statusCode);
|
||||
response.Headers.Add("x-ms-thread-id", sessionId);
|
||||
|
||||
bool acceptsJson = req.Headers.TryGetValues("Accept", out IEnumerable<string>? acceptValues) &&
|
||||
acceptValues.Contains("application/json", StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (acceptsJson)
|
||||
if (AcceptsJson(req))
|
||||
{
|
||||
AgentRunSuccessResponse successResponse = new((int)statusCode, sessionId, agentResponse);
|
||||
await response.WriteAsJsonAsync(successResponse, context.CancellationToken);
|
||||
@@ -511,10 +596,7 @@ internal static class BuiltInFunctions
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
|
||||
response.Headers.Add("x-ms-thread-id", sessionId);
|
||||
|
||||
bool acceptsJson = req.Headers.TryGetValues("Accept", out IEnumerable<string>? acceptValues) &&
|
||||
acceptValues.Contains("application/json", StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (acceptsJson)
|
||||
if (AcceptsJson(req))
|
||||
{
|
||||
AgentRunAcceptedResponse acceptedResponse = new((int)HttpStatusCode.Accepted, sessionId);
|
||||
await response.WriteAsJsonAsync(acceptedResponse, context.CancellationToken);
|
||||
@@ -528,6 +610,34 @@ internal static class BuiltInFunctions
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> when the caller has requested waiting for the workflow/agent to complete,
|
||||
/// as indicated by the <c>x-ms-wait-for-response</c> header. Falls back to <paramref name="defaultValue"/>
|
||||
/// when the header is absent or not a valid boolean.
|
||||
/// </summary>
|
||||
private static bool ShouldWaitForResponse(HttpRequestData req, bool defaultValue)
|
||||
{
|
||||
if (req.Headers.TryGetValues(WaitForResponseHeaderName, out IEnumerable<string>? values) &&
|
||||
bool.TryParse(values.FirstOrDefault(), out bool parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> when the request accepts the <c>application/json</c> media type.
|
||||
/// </summary>
|
||||
private static bool AcceptsJson(HttpRequestData req)
|
||||
{
|
||||
return req.Headers.TryGetValues("Accept", out IEnumerable<string>? acceptValues) &&
|
||||
acceptValues
|
||||
.SelectMany(v => v.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
.Select(v => v.Split(';', 2)[0].Trim())
|
||||
.Contains("application/json", StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string GetAgentName(FunctionContext context)
|
||||
{
|
||||
// Check if the function name starts with the HttpPrefix
|
||||
@@ -591,6 +701,19 @@ internal static class BuiltInFunctions
|
||||
[property: JsonPropertyName("eventName")] string? EventName,
|
||||
[property: JsonPropertyName("response")] JsonElement Response);
|
||||
|
||||
/// <summary>
|
||||
/// Represents a workflow run response when waiting for completion.
|
||||
/// </summary>
|
||||
/// <param name="RunId">The orchestration run ID.</param>
|
||||
/// <param name="WorkflowStatus">The orchestration runtime status (e.g., "Completed", "Failed").</param>
|
||||
/// <param name="Result">The workflow result as a JSON element so POCOs serialize as nested objects rather than escaped strings.</param>
|
||||
/// <param name="Error">An optional error message when the workflow has failed.</param>
|
||||
private sealed record WorkflowRunResponse(
|
||||
[property: JsonPropertyName("runId")] string RunId,
|
||||
[property: JsonPropertyName("workflowStatus")] string WorkflowStatus,
|
||||
[property: JsonPropertyName("result")] JsonElement? Result,
|
||||
[property: JsonPropertyName("error"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? Error = null);
|
||||
|
||||
/// <summary>
|
||||
/// A service provider that combines the original service provider with an additional DurableTaskClient instance.
|
||||
/// </summary>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Support returning workflow results from HTTP trigger endpoint ([#5321](https://github.com/microsoft/agent-framework/pull/5321))
|
||||
- Added MCP tool trigger support for durable workflows ([#4768](https://github.com/microsoft/agent-framework/pull/4768))
|
||||
- Added Azure Functions hosting support for durable workflows ([#4436](https://github.com/microsoft/agent-framework/pull/4436))
|
||||
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
|
||||
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.BeginInvoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken,System.AsyncCallback,System.Object)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.Invoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
|
||||
@@ -29,6 +43,13 @@
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
|
||||
@@ -43,6 +64,20 @@
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.BeginInvoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken,System.AsyncCallback,System.Object)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.Invoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
|
||||
@@ -71,6 +106,13 @@
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
|
||||
@@ -85,6 +127,20 @@
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.BeginInvoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken,System.AsyncCallback,System.Object)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.Invoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
|
||||
@@ -113,6 +169,13 @@
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
|
||||
@@ -127,6 +190,20 @@
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.BeginInvoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken,System.AsyncCallback,System.Object)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.Invoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
|
||||
@@ -155,6 +232,13 @@
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
|
||||
@@ -169,6 +253,20 @@
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.BeginInvoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken,System.AsyncCallback,System.Object)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.Invoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
|
||||
@@ -197,6 +295,13 @@
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
|
||||
@@ -211,4 +316,39 @@
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0005</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,System.Nullable{System.Text.Json.JsonElement},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0005</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,System.Nullable{System.Text.Json.JsonElement},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0005</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,System.Nullable{System.Text.Json.JsonElement},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0005</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,System.Nullable{System.Text.Json.JsonElement},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0005</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,System.Nullable{System.Text.Json.JsonElement},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
</Suppressions>
|
||||
@@ -35,7 +35,8 @@ public abstract class AgentSkill
|
||||
/// Gets the full skill content.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For file-based skills this is the raw SKILL.md file content.
|
||||
/// For file-based skills this is the raw SKILL.md file content, optionally
|
||||
/// augmented with a synthesized scripts block when scripts are present.
|
||||
/// For code-defined skills this is a synthesized XML document
|
||||
/// containing name, description, and body (instructions, resources, scripts).
|
||||
/// </remarks>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
@@ -46,8 +46,9 @@ public abstract class AgentSkillScript
|
||||
/// Runs the script with the given arguments.
|
||||
/// </summary>
|
||||
/// <param name="skill">The skill that owns this script.</param>
|
||||
/// <param name="arguments">Arguments for script execution.</param>
|
||||
/// <param name="arguments">Raw JSON arguments for script execution, preserving the original format (object or array) sent by the caller.</param>
|
||||
/// <param name="serviceProvider">Optional service provider for dependency injection.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The script execution result.</returns>
|
||||
public abstract Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
|
||||
public abstract Task<object?> RunAsync(AgentSkill skill, JsonElement? arguments, IServiceProvider? serviceProvider, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -243,7 +244,7 @@ public sealed partial class AgentSkillsProvider : AIContextProvider
|
||||
}
|
||||
|
||||
AIFunction scriptFunction = AIFunctionFactory.Create(
|
||||
(string skillName, string scriptName, IDictionary<string, object?>? arguments = null, IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default) =>
|
||||
(string skillName, string scriptName, JsonElement? arguments = null, IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default) =>
|
||||
this.RunSkillScriptAsync(skills, skillName, scriptName, arguments, serviceProvider, cancellationToken),
|
||||
name: "run_skill_script",
|
||||
description: "Runs a script associated with a skill.");
|
||||
@@ -340,7 +341,7 @@ public sealed partial class AgentSkillsProvider : AIContextProvider
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<object?> RunSkillScriptAsync(IList<AgentSkill> skills, string skillName, string scriptName, IDictionary<string, object?>? arguments = null, IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
||||
private async Task<object?> RunSkillScriptAsync(IList<AgentSkill> skills, string skillName, string scriptName, JsonElement? arguments = null, IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(skillName))
|
||||
{
|
||||
@@ -366,7 +367,7 @@ public sealed partial class AgentSkillsProvider : AIContextProvider
|
||||
|
||||
try
|
||||
{
|
||||
return await script.RunAsync(skill, new AIFunctionArguments(arguments) { Services = serviceProvider }, cancellationToken).ConfigureAwait(false);
|
||||
return await script.RunAsync(skill, arguments, serviceProvider, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -15,6 +15,8 @@ public sealed class AgentFileSkill : AgentSkill
|
||||
{
|
||||
private readonly IReadOnlyList<AgentSkillResource> _resources;
|
||||
private readonly IReadOnlyList<AgentSkillScript> _scripts;
|
||||
private readonly string _originalContent;
|
||||
private string? _content;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentFileSkill"/> class.
|
||||
@@ -32,7 +34,7 @@ public sealed class AgentFileSkill : AgentSkill
|
||||
IReadOnlyList<AgentSkillScript>? scripts = null)
|
||||
{
|
||||
this.Frontmatter = Throw.IfNull(frontmatter);
|
||||
this.Content = Throw.IfNull(content);
|
||||
this._originalContent = Throw.IfNull(content);
|
||||
this.Path = Throw.IfNullOrWhitespace(path);
|
||||
this._resources = resources ?? [];
|
||||
this._scripts = scripts ?? [];
|
||||
@@ -42,7 +44,18 @@ public sealed class AgentFileSkill : AgentSkill
|
||||
public override AgentSkillFrontmatter Frontmatter { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Content { get; }
|
||||
/// <remarks>
|
||||
/// Returns the raw SKILL.md content. When the skill has scripts, a
|
||||
/// <c><scripts><script name="..."><parameters_schema>...</parameters_schema></script></scripts></c>
|
||||
/// block is appended with a per-script entry describing the expected argument format.
|
||||
/// The result is cached after the first access.
|
||||
/// </remarks>
|
||||
public override string Content
|
||||
{
|
||||
get => this._content ??= this._scripts is { Count: > 0 }
|
||||
? this._originalContent + AgentInlineSkillContentBuilder.BuildScriptsBlock(this._scripts)
|
||||
: this._originalContent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directory path where the skill was discovered.
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
@@ -16,6 +16,11 @@ namespace Microsoft.Agents.AI;
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed class AgentFileSkillScript : AgentSkillScript
|
||||
{
|
||||
/// <summary>
|
||||
/// Cached JSON schema element describing the expected argument format: a string array of CLI arguments.
|
||||
/// </summary>
|
||||
private static readonly JsonElement s_defaultSchema = CreateDefaultSchema();
|
||||
|
||||
private readonly AgentFileSkillScriptRunner? _runner;
|
||||
|
||||
/// <summary>
|
||||
@@ -37,7 +42,14 @@ public sealed class AgentFileSkillScript : AgentSkillScript
|
||||
public string FullPath { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default)
|
||||
/// <remarks>
|
||||
/// Returns a fixed schema describing a string array of CLI arguments:
|
||||
/// <c>{"type":"array","items":{"type":"string"}}</c>.
|
||||
/// </remarks>
|
||||
public override JsonElement? ParametersSchema => s_defaultSchema;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<object?> RunAsync(AgentSkill skill, JsonElement? arguments, IServiceProvider? serviceProvider, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (skill is not AgentFileSkill fileSkill)
|
||||
{
|
||||
@@ -51,6 +63,12 @@ public sealed class AgentFileSkillScript : AgentSkillScript
|
||||
$"Supply a script runner when constructing {nameof(AgentFileSkillsSource)} to enable script execution.");
|
||||
}
|
||||
|
||||
return await this._runner(fileSkill, this, arguments, cancellationToken).ConfigureAwait(false);
|
||||
return await this._runner(fileSkill, this, arguments, serviceProvider, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static JsonElement CreateDefaultSchema()
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse("""{"type":"array","items":{"type":"string"}}""");
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
@@ -13,15 +14,19 @@ namespace Microsoft.Agents.AI;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations determine the execution strategy (e.g., local subprocess, hosted code execution environment).
|
||||
/// The <paramref name="arguments"/> parameter preserves the raw JSON sent by the caller, in the shape
|
||||
/// described by <see cref="AgentFileSkillScript.ParametersSchema"/>.
|
||||
/// </remarks>
|
||||
/// <param name="skill">The skill that owns the script.</param>
|
||||
/// <param name="script">The file-based script to run.</param>
|
||||
/// <param name="arguments">Optional arguments for the script, provided by the agent/LLM.</param>
|
||||
/// <param name="arguments">Raw JSON arguments for the script, in the shape described by <see cref="AgentFileSkillScript.ParametersSchema"/>.</param>
|
||||
/// <param name="serviceProvider">Optional service provider for dependency injection.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The script execution result.</returns>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public delegate Task<object?> AgentFileSkillScriptRunner(
|
||||
AgentFileSkill skill,
|
||||
AgentFileSkillScript script,
|
||||
AIFunctionArguments arguments,
|
||||
JsonElement? arguments,
|
||||
IServiceProvider? serviceProvider,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
+49
-25
@@ -59,36 +59,60 @@ internal static class AgentInlineSkillContentBuilder
|
||||
|
||||
if (scripts is { Count: > 0 })
|
||||
{
|
||||
sb.Append("\n\n<scripts>\n");
|
||||
foreach (var script in scripts)
|
||||
{
|
||||
var parametersSchema = script.ParametersSchema;
|
||||
|
||||
if (script.Description is null && parametersSchema is null)
|
||||
{
|
||||
sb.Append($" <script name=\"{EscapeXmlString(script.Name)}\"/>\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(script.Description is not null
|
||||
? $" <script name=\"{EscapeXmlString(script.Name)}\" description=\"{EscapeXmlString(script.Description)}\">\n"
|
||||
: $" <script name=\"{EscapeXmlString(script.Name)}\">\n");
|
||||
|
||||
if (parametersSchema is not null)
|
||||
{
|
||||
sb.Append($" <parameters_schema>{EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}</parameters_schema>\n");
|
||||
}
|
||||
|
||||
sb.Append(" </script>\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("</scripts>");
|
||||
sb.Append('\n');
|
||||
sb.Append(BuildScriptsBlock(scripts));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <c><scripts>...</scripts></c> XML block for the given scripts.
|
||||
/// Each script is emitted as a <c><script name="..."></c> element with optional
|
||||
/// <c>description</c> attribute and <c><parameters_schema></c> child element.
|
||||
/// </summary>
|
||||
/// <param name="scripts">The scripts to include in the block.</param>
|
||||
/// <returns>An XML string starting with <c>\n<scripts></c>, or an empty string if the list is empty.</returns>
|
||||
public static string BuildScriptsBlock(IReadOnlyList<AgentSkillScript> scripts)
|
||||
{
|
||||
_ = Throw.IfNull(scripts);
|
||||
|
||||
if (scripts.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("\n<scripts>\n");
|
||||
|
||||
foreach (var script in scripts)
|
||||
{
|
||||
var parametersSchema = script.ParametersSchema;
|
||||
|
||||
if (script.Description is null && parametersSchema is null)
|
||||
{
|
||||
sb.Append($" <script name=\"{EscapeXmlString(script.Name)}\"/>\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(script.Description is not null
|
||||
? $" <script name=\"{EscapeXmlString(script.Name)}\" description=\"{EscapeXmlString(script.Description)}\">\n"
|
||||
: $" <script name=\"{EscapeXmlString(script.Name)}\">\n");
|
||||
|
||||
if (parametersSchema is not null)
|
||||
{
|
||||
sb.Append($" <parameters_schema>{EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}</parameters_schema>\n");
|
||||
}
|
||||
|
||||
sb.Append(" </script>\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("</scripts>");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes XML special characters: always escapes <c>&</c>, <c><</c>, <c>></c>,
|
||||
/// <c>"</c>, and <c>'</c>. When <paramref name="preserveQuotes"/> is <see langword="true"/>,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
@@ -67,8 +68,42 @@ internal sealed class AgentInlineSkillScript : AgentSkillScript
|
||||
public override JsonElement? ParametersSchema => this._function.JsonSchema;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default)
|
||||
public override async Task<object?> RunAsync(AgentSkill skill, JsonElement? arguments, IServiceProvider? serviceProvider, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await this._function.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
|
||||
var funcArgs = ConvertToFunctionArguments(arguments);
|
||||
funcArgs.Services = serviceProvider;
|
||||
|
||||
return await this._function.InvokeAsync(funcArgs, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a raw <see cref="JsonElement"/> to <see cref="AIFunctionArguments"/> for delegate invocation.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when <paramref name="arguments"/> is provided but is not a JSON object.
|
||||
/// Inline skill scripts expect arguments as a JSON object whose properties map to the delegate's parameters.
|
||||
/// </exception>
|
||||
private static AIFunctionArguments ConvertToFunctionArguments(JsonElement? arguments)
|
||||
{
|
||||
if (arguments is null ||
|
||||
arguments.Value.ValueKind == JsonValueKind.Null ||
|
||||
arguments.Value.ValueKind == JsonValueKind.Undefined)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (arguments.Value.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Inline skill scripts expect arguments as a JSON object but received a JSON element of kind '{arguments.Value.ValueKind}'.");
|
||||
}
|
||||
|
||||
var dict = new Dictionary<string, object?>();
|
||||
foreach (var property in arguments.Value.EnumerateObject())
|
||||
{
|
||||
dict[property.Name] = property.Value;
|
||||
}
|
||||
|
||||
return new AIFunctionArguments(dict);
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -3,6 +3,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Client;
|
||||
@@ -125,6 +126,45 @@ public sealed class WorkflowSamplesValidation(ITestOutputHelper outputHelper) :
|
||||
},
|
||||
message: "OrderStatus workflow completed",
|
||||
timeout: s_orchestrationTimeout);
|
||||
|
||||
// Test the CancelOrder workflow with x-ms-wait-for-response header
|
||||
this._outputHelper.WriteLine("Starting CancelOrder workflow with x-ms-wait-for-response: true...");
|
||||
|
||||
using HttpRequestMessage waitRequest = new(HttpMethod.Post, cancelOrderUri);
|
||||
waitRequest.Content = new StringContent("55555", Encoding.UTF8, "text/plain");
|
||||
waitRequest.Headers.Add("x-ms-wait-for-response", "true");
|
||||
using HttpResponseMessage waitResponse = await s_sharedHttpClient.SendAsync(waitRequest);
|
||||
|
||||
Assert.True(waitResponse.IsSuccessStatusCode, $"CancelOrder wait-for-response request failed with status: {waitResponse.StatusCode}");
|
||||
string waitResponseText = await waitResponse.Content.ReadAsStringAsync();
|
||||
this._outputHelper.WriteLine($"CancelOrder wait-for-response result: {waitResponseText}");
|
||||
|
||||
// The response should contain the workflow result (not just "started for CancelOrder")
|
||||
Assert.DoesNotContain("Workflow orchestration started", waitResponseText);
|
||||
Assert.Contains("55555", waitResponseText);
|
||||
|
||||
// Test the wait-for-response with Accept: application/json header
|
||||
this._outputHelper.WriteLine("Starting CancelOrder workflow with x-ms-wait-for-response and Accept: application/json...");
|
||||
|
||||
using HttpRequestMessage jsonWaitRequest = new(HttpMethod.Post, cancelOrderUri);
|
||||
jsonWaitRequest.Content = new StringContent("77777", Encoding.UTF8, "text/plain");
|
||||
jsonWaitRequest.Headers.Add("x-ms-wait-for-response", "true");
|
||||
jsonWaitRequest.Headers.Add("Accept", "application/json");
|
||||
|
||||
using CancellationTokenSource jsonWaitCts = new(s_orchestrationTimeout);
|
||||
using HttpResponseMessage jsonWaitResponse = await s_sharedHttpClient.SendAsync(jsonWaitRequest, jsonWaitCts.Token);
|
||||
|
||||
Assert.True(jsonWaitResponse.IsSuccessStatusCode, $"CancelOrder JSON wait-for-response request failed with status: {jsonWaitResponse.StatusCode}");
|
||||
string jsonWaitResponseText = await jsonWaitResponse.Content.ReadAsStringAsync();
|
||||
this._outputHelper.WriteLine($"CancelOrder JSON wait-for-response result: {jsonWaitResponseText}");
|
||||
|
||||
using JsonDocument jsonDoc = JsonDocument.Parse(jsonWaitResponseText);
|
||||
JsonElement root = jsonDoc.RootElement;
|
||||
Assert.True(root.TryGetProperty("runId", out _), "JSON response missing 'runId' property");
|
||||
Assert.True(root.TryGetProperty("workflowStatus", out JsonElement statusEl), "JSON response missing 'workflowStatus' property");
|
||||
Assert.Equal("Completed", statusEl.GetString());
|
||||
Assert.True(root.TryGetProperty("result", out JsonElement resultEl), "JSON response missing 'result' property");
|
||||
Assert.Contains("77777", resultEl.GetString());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
|
||||
@@ -128,8 +127,9 @@ public sealed class AgentClassSkillTests
|
||||
// Act — script with custom type deserialization
|
||||
var script = skill.Scripts![0];
|
||||
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "test", MaxResults = 5 }, jso);
|
||||
var args = new AIFunctionArguments { ["request"] = inputJson };
|
||||
var scriptResult = await script.RunAsync(skill, args, CancellationToken.None);
|
||||
using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
|
||||
var args = argsDoc.RootElement;
|
||||
var scriptResult = await script.RunAsync(skill, args, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(scriptResult);
|
||||
@@ -173,12 +173,14 @@ public sealed class AgentClassSkillTests
|
||||
|
||||
// Act & Assert — static method
|
||||
var doWorkScript = skill.Scripts!.First(s => s.Name == "do-work");
|
||||
var doWorkResult = await doWorkScript.RunAsync(skill, new AIFunctionArguments { ["input"] = "hello" }, CancellationToken.None);
|
||||
using var doWorkDoc = JsonDocument.Parse("""{"input":"hello"}""");
|
||||
var doWorkResult = await doWorkScript.RunAsync(skill, doWorkDoc.RootElement, null, CancellationToken.None);
|
||||
Assert.Equal("HELLO", doWorkResult?.ToString());
|
||||
|
||||
// Act & Assert — instance method
|
||||
var appendScript = skill.Scripts!.First(s => s.Name == "append");
|
||||
var appendResult = await appendScript.RunAsync(skill, new AIFunctionArguments { ["input"] = "test" }, CancellationToken.None);
|
||||
using var appendDoc = JsonDocument.Parse("""{"input":"test"}""");
|
||||
var appendResult = await appendScript.RunAsync(skill, appendDoc.RootElement, null, CancellationToken.None);
|
||||
Assert.Equal("test-suffix", appendResult?.ToString());
|
||||
}
|
||||
|
||||
@@ -367,7 +369,7 @@ public sealed class AgentClassSkillTests
|
||||
// Act & Assert — all scripts produce values
|
||||
foreach (var script in skill.Scripts!)
|
||||
{
|
||||
var result = await script.RunAsync(skill, new AIFunctionArguments(), CancellationToken.None);
|
||||
var result = await script.RunAsync(skill, null, null, CancellationToken.None);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
}
|
||||
@@ -382,8 +384,9 @@ public sealed class AgentClassSkillTests
|
||||
// Act & Assert — script with custom JSO
|
||||
var script = skill.Scripts![0];
|
||||
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "test", MaxResults = 3 }, jso);
|
||||
var args = new AIFunctionArguments { ["request"] = inputJson };
|
||||
var scriptResult = await script.RunAsync(skill, args, CancellationToken.None);
|
||||
using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
|
||||
var args = argsDoc.RootElement;
|
||||
var scriptResult = await script.RunAsync(skill, args, null, CancellationToken.None);
|
||||
Assert.NotNull(scriptResult);
|
||||
Assert.Contains("test", scriptResult!.ToString()!);
|
||||
Assert.Contains("3", scriptResult!.ToString()!);
|
||||
@@ -497,8 +500,9 @@ public sealed class AgentClassSkillTests
|
||||
var script = skill.Scripts!.First(s => s.Name == "Lookup");
|
||||
var jso = SkillTestJsonContext.Default.Options;
|
||||
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "fallback", MaxResults = 7 }, jso);
|
||||
var args = new AIFunctionArguments { ["request"] = inputJson };
|
||||
var result = await script.RunAsync(skill, args, CancellationToken.None);
|
||||
using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
|
||||
var args = argsDoc.RootElement;
|
||||
var result = await script.RunAsync(skill, args, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
@@ -531,8 +535,9 @@ public sealed class AgentClassSkillTests
|
||||
var script = skill.Scripts!.First(s => s.Name == "Lookup");
|
||||
var jso = SkillTestJsonContext.Default.Options;
|
||||
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "explicit", MaxResults = 2 }, jso);
|
||||
var args = new AIFunctionArguments { ["request"] = inputJson };
|
||||
var result = await script.RunAsync(skill, args, CancellationToken.None);
|
||||
using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
|
||||
var args = argsDoc.RootElement;
|
||||
var result = await script.RunAsync(skill, args, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
|
||||
+181
-10
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
|
||||
|
||||
@@ -16,13 +16,13 @@ public sealed class AgentFileSkillScriptTests
|
||||
public async Task RunAsync_SkillIsNotAgentFileSkill_ThrowsInvalidOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
static Task<object?> RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, AIFunctionArguments a, CancellationToken ct) => Task.FromResult<object?>("result");
|
||||
static Task<object?> RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, JsonElement? a, IServiceProvider? sp, CancellationToken ct) => Task.FromResult<object?>("result");
|
||||
var script = CreateScript("test-script", "/path/to/script.py", RunnerAsync);
|
||||
var nonFileSkill = new TestAgentSkill("my-skill", "A skill", "Instructions.");
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => script.RunAsync(nonFileSkill, new AIFunctionArguments(), CancellationToken.None));
|
||||
() => script.RunAsync(nonFileSkill, null, null, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -30,7 +30,7 @@ public sealed class AgentFileSkillScriptTests
|
||||
{
|
||||
// Arrange
|
||||
var runnerCalled = false;
|
||||
Task<object?> runnerAsync(AgentFileSkill skill, AgentFileSkillScript scriptArg, AIFunctionArguments args, CancellationToken ct)
|
||||
Task<object?> runnerAsync(AgentFileSkill skill, AgentFileSkillScript scriptArg, JsonElement? args, IServiceProvider? sp, CancellationToken ct)
|
||||
{
|
||||
runnerCalled = true;
|
||||
return Task.FromResult<object?>("executed");
|
||||
@@ -42,7 +42,7 @@ public sealed class AgentFileSkillScriptTests
|
||||
"/skills/my-skill");
|
||||
|
||||
// Act
|
||||
var result = await script.RunAsync(fileSkill, new AIFunctionArguments(), CancellationToken.None);
|
||||
var result = await script.RunAsync(fileSkill, null, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(runnerCalled);
|
||||
@@ -55,7 +55,7 @@ public sealed class AgentFileSkillScriptTests
|
||||
// Arrange
|
||||
AgentFileSkill? capturedSkill = null;
|
||||
AgentFileSkillScript? capturedScript = null;
|
||||
Task<object?> runnerAsync(AgentFileSkill skill, AgentFileSkillScript scriptArg, AIFunctionArguments args, CancellationToken ct)
|
||||
Task<object?> runnerAsync(AgentFileSkill skill, AgentFileSkillScript scriptArg, JsonElement? args, IServiceProvider? sp, CancellationToken ct)
|
||||
{
|
||||
capturedSkill = skill;
|
||||
capturedScript = scriptArg;
|
||||
@@ -68,7 +68,7 @@ public sealed class AgentFileSkillScriptTests
|
||||
"/skills/owner-skill");
|
||||
|
||||
// Act
|
||||
await script.RunAsync(fileSkill, new AIFunctionArguments(), CancellationToken.None);
|
||||
await script.RunAsync(fileSkill, null, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Same(fileSkill, capturedSkill);
|
||||
@@ -79,7 +79,7 @@ public sealed class AgentFileSkillScriptTests
|
||||
public void Script_HasCorrectNameAndPath()
|
||||
{
|
||||
// Arrange & Act
|
||||
static Task<object?> RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, AIFunctionArguments a, CancellationToken ct) => Task.FromResult<object?>(null);
|
||||
static Task<object?> RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, JsonElement? a, IServiceProvider? sp, CancellationToken ct) => Task.FromResult<object?>(null);
|
||||
var script = CreateScript("my-script", "/path/to/my-script.py", RunnerAsync);
|
||||
|
||||
// Assert
|
||||
@@ -87,10 +87,173 @@ public sealed class AgentFileSkillScriptTests
|
||||
Assert.Equal("/path/to/my-script.py", script.FullPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParametersSchema_ReturnsExpectedArraySchema()
|
||||
{
|
||||
// Arrange
|
||||
static Task<object?> RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, JsonElement? a, IServiceProvider? sp, CancellationToken ct) => Task.FromResult<object?>(null);
|
||||
var script = CreateScript("my-script", "/path/to/script.py", RunnerAsync);
|
||||
|
||||
// Act
|
||||
var schema = script.ParametersSchema;
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(schema);
|
||||
var raw = schema!.Value.GetRawText();
|
||||
Assert.Contains("\"type\":\"array\"", raw);
|
||||
Assert.Contains("\"items\":{\"type\":\"string\"}", raw);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Content_WithScripts_AppendsPerScriptEntries()
|
||||
{
|
||||
// Arrange
|
||||
static Task<object?> RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, JsonElement? a, IServiceProvider? sp, CancellationToken ct) => Task.FromResult<object?>(null);
|
||||
var script1 = CreateScript("build", "/scripts/build.sh", RunnerAsync);
|
||||
var script2 = CreateScript("deploy", "/scripts/deploy.sh", RunnerAsync);
|
||||
var fileSkill = new AgentFileSkill(
|
||||
new AgentSkillFrontmatter("my-skill", "A skill"),
|
||||
"Original content",
|
||||
"/skills/my-skill",
|
||||
scripts: [script1, script2]);
|
||||
|
||||
// Act
|
||||
var content = fileSkill.Content;
|
||||
|
||||
// Assert — content starts with original and appends per-script entries
|
||||
Assert.StartsWith("Original content", content);
|
||||
Assert.Contains("<scripts>", content);
|
||||
Assert.Contains("<script name=\"build\">", content);
|
||||
Assert.Contains("<script name=\"deploy\">", content);
|
||||
Assert.Contains("<parameters_schema>", content);
|
||||
Assert.Contains("</scripts>", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Content_WithoutScripts_ReturnsOriginalContent()
|
||||
{
|
||||
// Arrange
|
||||
var fileSkill = new AgentFileSkill(
|
||||
new AgentSkillFrontmatter("my-skill", "A skill"),
|
||||
"Original content only",
|
||||
"/skills/my-skill");
|
||||
|
||||
// Act
|
||||
var content = fileSkill.Content;
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Original content only", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Content_WithScripts_IsCached()
|
||||
{
|
||||
// Arrange
|
||||
static Task<object?> RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, JsonElement? a, IServiceProvider? sp, CancellationToken ct) => Task.FromResult<object?>(null);
|
||||
var script = CreateScript("test", "/scripts/test.sh", RunnerAsync);
|
||||
var fileSkill = new AgentFileSkill(
|
||||
new AgentSkillFrontmatter("my-skill", "A skill"),
|
||||
"Content",
|
||||
"/skills/my-skill",
|
||||
scripts: [script]);
|
||||
|
||||
// Act
|
||||
var content1 = fileSkill.Content;
|
||||
var content2 = fileSkill.Content;
|
||||
|
||||
// Assert
|
||||
Assert.Same(content1, content2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ForwardsJsonArrayArgumentsToRunnerAsync()
|
||||
{
|
||||
// Arrange
|
||||
JsonElement? capturedArgs = null;
|
||||
Task<object?> runnerAsync(AgentFileSkill skill, AgentFileSkillScript scriptArg, JsonElement? args, IServiceProvider? sp, CancellationToken ct)
|
||||
{
|
||||
capturedArgs = args;
|
||||
return Task.FromResult<object?>("done");
|
||||
}
|
||||
var script = CreateScript("array-test", "/scripts/test.sh", runnerAsync);
|
||||
var fileSkill = new AgentFileSkill(
|
||||
new AgentSkillFrontmatter("my-skill", "A skill"),
|
||||
"Content",
|
||||
"/skills/my-skill");
|
||||
using var arrayArgsDoc = JsonDocument.Parse("""["arg1","arg2","arg3"]""");
|
||||
var arrayArgs = arrayArgsDoc.RootElement;
|
||||
|
||||
// Act
|
||||
await script.RunAsync(fileSkill, arrayArgs, null, CancellationToken.None);
|
||||
|
||||
// Assert — the raw JSON array is forwarded unchanged
|
||||
Assert.NotNull(capturedArgs);
|
||||
Assert.Equal(JsonValueKind.Array, capturedArgs!.Value.ValueKind);
|
||||
Assert.Equal("""["arg1","arg2","arg3"]""", capturedArgs.Value.GetRawText());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ForwardsServiceProviderToRunnerAsync()
|
||||
{
|
||||
// Arrange
|
||||
IServiceProvider? capturedProvider = null;
|
||||
Task<object?> runnerAsync(AgentFileSkill skill, AgentFileSkillScript scriptArg, JsonElement? args, IServiceProvider? sp, CancellationToken ct)
|
||||
{
|
||||
capturedProvider = sp;
|
||||
return Task.FromResult<object?>("done");
|
||||
}
|
||||
var script = CreateScript("sp-test", "/scripts/test.sh", runnerAsync);
|
||||
var fileSkill = new AgentFileSkill(
|
||||
new AgentSkillFrontmatter("my-skill", "A skill"),
|
||||
"Content",
|
||||
"/skills/my-skill");
|
||||
var mockProvider = new TestServiceProvider();
|
||||
|
||||
// Act
|
||||
await script.RunAsync(fileSkill, null, mockProvider, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Same(mockProvider, capturedProvider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_NoRunner_ThrowsInvalidOperationExceptionAsync()
|
||||
{
|
||||
// Arrange — create script without a runner
|
||||
var script = CreateScript("no-runner", "/scripts/test.sh", runner: null);
|
||||
var fileSkill = new AgentFileSkill(
|
||||
new AgentSkillFrontmatter("my-skill", "A skill"),
|
||||
"Content",
|
||||
"/skills/my-skill");
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => script.RunAsync(fileSkill, null, null, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Content_WithScripts_ContainsDefaultParametersSchema()
|
||||
{
|
||||
// Arrange
|
||||
static Task<object?> RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, JsonElement? a, IServiceProvider? sp, CancellationToken ct) => Task.FromResult<object?>(null);
|
||||
var script = CreateScript("test", "/scripts/test.sh", RunnerAsync);
|
||||
var fileSkill = new AgentFileSkill(
|
||||
new AgentSkillFrontmatter("my-skill", "A skill"),
|
||||
"Original content",
|
||||
"/skills/my-skill",
|
||||
scripts: [script]);
|
||||
|
||||
// Act
|
||||
var content = fileSkill.Content;
|
||||
|
||||
// Assert — the appended block contains the actual default schema from AgentFileSkillScript
|
||||
Assert.Contains("""{"type":"array","items":{"type":"string"}}""", content);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to create an <see cref="AgentFileSkillScript"/> via reflection since the constructor is internal.
|
||||
/// </summary>
|
||||
private static AgentFileSkillScript CreateScript(string name, string fullPath, AgentFileSkillScriptRunner executor)
|
||||
private static AgentFileSkillScript CreateScript(string name, string fullPath, AgentFileSkillScriptRunner? runner)
|
||||
{
|
||||
var ctor = typeof(AgentFileSkillScript).GetConstructor(
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
|
||||
@@ -98,6 +261,14 @@ public sealed class AgentFileSkillScriptTests
|
||||
[typeof(string), typeof(string), typeof(AgentFileSkillScriptRunner)],
|
||||
null) ?? throw new InvalidOperationException("Could not find internal constructor.");
|
||||
|
||||
return (AgentFileSkillScript)ctor.Invoke([name, fullPath, executor]);
|
||||
return (AgentFileSkillScript)ctor.Invoke([name, fullPath, runner]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal <see cref="IServiceProvider"/> for testing service forwarding.
|
||||
/// </summary>
|
||||
private sealed class TestServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
}
|
||||
}
|
||||
|
||||
+13
-15
@@ -3,9 +3,9 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
|
||||
public sealed class AgentFileSkillsSourceScriptTests : IDisposable
|
||||
{
|
||||
private static readonly string[] s_rubyExtension = new[] { ".rb" };
|
||||
private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, ct) => Task.FromResult<object?>(null);
|
||||
private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, sp, ct) => Task.FromResult<object?>(null);
|
||||
|
||||
private readonly string _testRoot;
|
||||
|
||||
@@ -139,7 +139,7 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable
|
||||
var executorCalled = false;
|
||||
var source = new AgentFileSkillsSource(
|
||||
this._testRoot,
|
||||
(skill, script, args, ct) =>
|
||||
(skill, script, args, sp, ct) =>
|
||||
{
|
||||
executorCalled = true;
|
||||
Assert.Equal("exec-skill", skill.Frontmatter.Name);
|
||||
@@ -150,7 +150,7 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync(CancellationToken.None);
|
||||
var scriptResult = await skills[0].Scripts![0].RunAsync(skills[0], new AIFunctionArguments(), CancellationToken.None);
|
||||
var scriptResult = await skills[0].Scripts![0].RunAsync(skills[0], null, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(executorCalled);
|
||||
@@ -178,7 +178,7 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable
|
||||
var script = skills[0].Scripts![0];
|
||||
|
||||
// Assert — running the script throws because no runner was provided
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => script.RunAsync(skills[0], new AIFunctionArguments(), CancellationToken.None));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => script.RunAsync(skills[0], null, null, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -204,10 +204,10 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable
|
||||
{
|
||||
// Arrange
|
||||
CreateSkillWithScript(this._testRoot, "args-skill", "Args test", "Body.", "scripts/test.py", "print('ok')");
|
||||
AIFunctionArguments? capturedArgs = null;
|
||||
JsonElement? capturedArgs = null;
|
||||
var source = new AgentFileSkillsSource(
|
||||
this._testRoot,
|
||||
(skill, script, args, ct) =>
|
||||
(skill, script, args, sp, ct) =>
|
||||
{
|
||||
capturedArgs = args;
|
||||
return Task.FromResult<object?>("done");
|
||||
@@ -215,17 +215,15 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync(CancellationToken.None);
|
||||
var arguments = new AIFunctionArguments
|
||||
{
|
||||
["value"] = 26.2,
|
||||
["factor"] = 1.60934
|
||||
};
|
||||
await skills[0].Scripts![0].RunAsync(skills[0], arguments, CancellationToken.None);
|
||||
using var argumentsDoc = JsonDocument.Parse("""{"value":26.2,"factor":1.60934}""");
|
||||
var arguments = argumentsDoc.RootElement;
|
||||
await skills[0].Scripts![0].RunAsync(skills[0], arguments, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedArgs);
|
||||
Assert.Equal(26.2, capturedArgs["value"]);
|
||||
Assert.Equal(1.60934, capturedArgs["factor"]);
|
||||
Assert.Equal(JsonValueKind.Object, capturedArgs!.Value.ValueKind);
|
||||
Assert.Equal(26.2, capturedArgs.Value.GetProperty("value").GetDouble());
|
||||
Assert.Equal(1.60934, capturedArgs.Value.GetProperty("factor").GetDouble());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+72
-12
@@ -5,7 +5,6 @@ using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
|
||||
|
||||
@@ -22,7 +21,7 @@ public sealed class AgentInlineSkillScriptTests
|
||||
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
|
||||
|
||||
// Act
|
||||
var result = await script.RunAsync(skill, new AIFunctionArguments(), CancellationToken.None);
|
||||
var result = await script.RunAsync(skill, null, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("hello", result?.ToString());
|
||||
@@ -34,10 +33,11 @@ public sealed class AgentInlineSkillScriptTests
|
||||
// Arrange
|
||||
var script = new AgentInlineSkillScript("add", (int a, int b) => a + b);
|
||||
var skill = new AgentInlineSkill("calc-skill", "Calc.", "Instructions.");
|
||||
var args = new AIFunctionArguments { ["a"] = 3, ["b"] = 7 };
|
||||
using var argsDoc = JsonDocument.Parse("""{"a":3,"b":7}""");
|
||||
var args = argsDoc.RootElement;
|
||||
|
||||
// Act
|
||||
var result = await script.RunAsync(skill, args, CancellationToken.None);
|
||||
var result = await script.RunAsync(skill, args, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(10, int.Parse(result?.ToString()!));
|
||||
@@ -129,10 +129,11 @@ public sealed class AgentInlineSkillScriptTests
|
||||
}, serializerOptions: jso);
|
||||
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
|
||||
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "test", MaxResults = 5 }, jso);
|
||||
var args = new AIFunctionArguments { ["request"] = inputJson };
|
||||
using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
|
||||
var args = argsDoc.RootElement;
|
||||
|
||||
// Act
|
||||
var result = await script.RunAsync(skill, args, CancellationToken.None);
|
||||
var result = await script.RunAsync(skill, args, null, CancellationToken.None);
|
||||
|
||||
// Assert — the custom input type was deserialized and the response was produced
|
||||
Assert.NotNull(result);
|
||||
@@ -145,10 +146,11 @@ public sealed class AgentInlineSkillScriptTests
|
||||
// Arrange
|
||||
var script = new AgentInlineSkillScript("echo", (string message) => message);
|
||||
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
|
||||
var args = new AIFunctionArguments { ["message"] = "hello world" };
|
||||
using var argsDoc = JsonDocument.Parse("""{"message":"hello world"}""");
|
||||
var args = argsDoc.RootElement;
|
||||
|
||||
// Act
|
||||
var result = await script.RunAsync(skill, args, CancellationToken.None);
|
||||
var result = await script.RunAsync(skill, args, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("hello world", result?.ToString());
|
||||
@@ -175,10 +177,11 @@ public sealed class AgentInlineSkillScriptTests
|
||||
var method = typeof(AgentInlineSkillScriptTests).GetMethod(nameof(StaticScriptHelper), BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
var script = new AgentInlineSkillScript("static-method-script", method, target: null);
|
||||
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
|
||||
var args = new AIFunctionArguments { ["input"] = "hello" };
|
||||
using var argsDoc = JsonDocument.Parse("""{"input":"hello"}""");
|
||||
var args = argsDoc.RootElement;
|
||||
|
||||
// Act
|
||||
var result = await script.RunAsync(skill, args, CancellationToken.None);
|
||||
var result = await script.RunAsync(skill, args, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("HELLO", result?.ToString());
|
||||
@@ -191,10 +194,11 @@ public sealed class AgentInlineSkillScriptTests
|
||||
var method = typeof(AgentInlineSkillScriptTests).GetMethod(nameof(InstanceScriptHelper), BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||
var script = new AgentInlineSkillScript("instance-method-script", method, target: this);
|
||||
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
|
||||
var args = new AIFunctionArguments { ["input"] = "test" };
|
||||
using var argsDoc2 = JsonDocument.Parse("""{"input":"test"}""");
|
||||
var args2 = argsDoc2.RootElement;
|
||||
|
||||
// Act
|
||||
var result = await script.RunAsync(skill, args, CancellationToken.None);
|
||||
var result = await script.RunAsync(skill, args2, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test-suffix", result?.ToString());
|
||||
@@ -223,7 +227,63 @@ public sealed class AgentInlineSkillScriptTests
|
||||
Assert.Contains("input", schema!.Value.GetRawText());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WithNonObjectArguments_ThrowsInvalidOperationExceptionAsync()
|
||||
{
|
||||
// Arrange — inline scripts require a JSON object for arguments
|
||||
var script = new AgentInlineSkillScript("noop", () => "ok");
|
||||
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
|
||||
using var arrayArgsDoc = JsonDocument.Parse("""["a","b"]""");
|
||||
var arrayArgs = arrayArgsDoc.RootElement;
|
||||
|
||||
// Act & Assert — non-object JSON should fail fast rather than silently dropping arguments
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => script.RunAsync(skill, arrayArgs, null, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WithNullArguments_TreatsAsNoArgumentsAsync()
|
||||
{
|
||||
// Arrange — a parameterless delegate should succeed when given null arguments
|
||||
var script = new AgentInlineSkillScript("noop", () => "ok");
|
||||
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
|
||||
|
||||
// Act
|
||||
var result = await script.RunAsync(skill, null, null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("ok", result?.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ServiceProviderIsForwardedAsync()
|
||||
{
|
||||
// Arrange — delegate that resolves a service from the IServiceProvider
|
||||
IServiceProvider? capturedProvider = null;
|
||||
var script = new AgentInlineSkillScript("svc-test", (IServiceProvider sp) =>
|
||||
{
|
||||
capturedProvider = sp;
|
||||
return "done";
|
||||
});
|
||||
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
|
||||
var mockProvider = new TestServiceProvider();
|
||||
|
||||
// Act
|
||||
await script.RunAsync(skill, null, mockProvider, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Same(mockProvider, capturedProvider);
|
||||
}
|
||||
|
||||
private static string StaticScriptHelper(string input) => input.ToUpperInvariant();
|
||||
|
||||
private string InstanceScriptHelper(string input) => input + "-suffix";
|
||||
|
||||
/// <summary>
|
||||
/// Minimal <see cref="IServiceProvider"/> for testing service forwarding.
|
||||
/// </summary>
|
||||
private sealed class TestServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,10 +433,11 @@ public sealed class AgentInlineSkillTests
|
||||
TotalCount = request.MaxResults,
|
||||
});
|
||||
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "test", MaxResults = 3 }, jso);
|
||||
var args = new AIFunctionArguments { ["request"] = inputJson };
|
||||
using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
|
||||
var args = argsDoc.RootElement;
|
||||
|
||||
// Act
|
||||
var result = await skill.Scripts![0].RunAsync(skill, args, CancellationToken.None);
|
||||
var result = await skill.Scripts![0].RunAsync(skill, args, null, CancellationToken.None);
|
||||
|
||||
// Assert — the custom input was deserialized via skill-level JSO and response was produced
|
||||
Assert.NotNull(result);
|
||||
@@ -456,10 +457,11 @@ public sealed class AgentInlineSkillTests
|
||||
TotalCount = request.MaxResults,
|
||||
}, serializerOptions: scriptJso);
|
||||
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "override", MaxResults = 7 }, scriptJso);
|
||||
var args = new AIFunctionArguments { ["request"] = inputJson };
|
||||
using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
|
||||
var args = argsDoc.RootElement;
|
||||
|
||||
// Act
|
||||
var result = await skill.Scripts![0].RunAsync(skill, args, CancellationToken.None);
|
||||
var result = await skill.Scripts![0].RunAsync(skill, args, null, CancellationToken.None);
|
||||
|
||||
// Assert — per-script JSO takes effect and custom types are properly marshaled
|
||||
Assert.NotNull(result);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -15,7 +16,7 @@ namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
|
||||
/// </summary>
|
||||
public sealed class AgentSkillsProviderTests : IDisposable
|
||||
{
|
||||
private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, ct) => Task.FromResult<object?>(null);
|
||||
private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, sp, ct) => Task.FromResult<object?>(null);
|
||||
private readonly string _testRoot;
|
||||
private readonly TestAIAgent _agent = new();
|
||||
|
||||
@@ -462,7 +463,7 @@ public sealed class AgentSkillsProviderTests : IDisposable
|
||||
// Act — call UseFileScriptRunner AFTER UseFileSkill (the bug scenario)
|
||||
var provider = new AgentSkillsProviderBuilder()
|
||||
.UseFileSkill(this._testRoot)
|
||||
.UseFileScriptRunner((skill, script, args, ct) =>
|
||||
.UseFileScriptRunner((skill, script, args, sp, ct) =>
|
||||
{
|
||||
executorCalled = true;
|
||||
return Task.FromResult<object?>("executed");
|
||||
@@ -487,6 +488,62 @@ public sealed class AgentSkillsProviderTests : IDisposable
|
||||
Assert.True(executorCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunSkillScript_ForwardsJsonArgumentsAndServiceProviderToRunnerAsync()
|
||||
{
|
||||
// Arrange — create a skill with a script file
|
||||
string skillDir = Path.Combine(this._testRoot, "fwd-skill");
|
||||
Directory.CreateDirectory(Path.Combine(skillDir, "scripts"));
|
||||
File.WriteAllText(
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
"---\nname: fwd-skill\ndescription: Forwarding test\n---\nBody.");
|
||||
File.WriteAllText(
|
||||
Path.Combine(skillDir, "scripts", "run.py"),
|
||||
"print('ok')");
|
||||
|
||||
JsonElement? capturedArgs = null;
|
||||
IServiceProvider? capturedServiceProvider = null;
|
||||
|
||||
var provider = new AgentSkillsProviderBuilder()
|
||||
.UseFileSkill(this._testRoot)
|
||||
.UseFileScriptRunner((skill, script, args, sp, ct) =>
|
||||
{
|
||||
capturedArgs = args;
|
||||
capturedServiceProvider = sp;
|
||||
return Task.FromResult<object?>("executed");
|
||||
})
|
||||
.Build();
|
||||
|
||||
var mockServiceProvider = new TestServiceProvider();
|
||||
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
|
||||
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
var runScriptTool = result.Tools!.First(t => t.Name == "run_skill_script") as AIFunction;
|
||||
|
||||
// Act — invoke with JsonElement arguments and a service provider
|
||||
using var argsJsonDoc = JsonDocument.Parse("""["arg1","arg2"]""");
|
||||
var argsJson = argsJsonDoc.RootElement;
|
||||
await runScriptTool!.InvokeAsync(new AIFunctionArguments(new Dictionary<string, object?>
|
||||
{
|
||||
["skillName"] = "fwd-skill",
|
||||
["scriptName"] = "scripts/run.py",
|
||||
["arguments"] = argsJson,
|
||||
})
|
||||
{
|
||||
Services = mockServiceProvider,
|
||||
});
|
||||
|
||||
// Assert — JsonElement arguments and service provider are forwarded to the runner
|
||||
Assert.NotNull(capturedArgs);
|
||||
Assert.Equal(JsonValueKind.Array, capturedArgs!.Value.ValueKind);
|
||||
Assert.Equal("""["arg1","arg2"]""", capturedArgs.Value.GetRawText());
|
||||
Assert.Same(mockServiceProvider, capturedServiceProvider);
|
||||
}
|
||||
|
||||
private sealed class TestServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
}
|
||||
|
||||
private static void CreateSkillIn(string root, string name, string description, string body)
|
||||
{
|
||||
string skillDir = Path.Combine(root, name);
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
private static readonly string[] s_customExtensions = [".custom"];
|
||||
private static readonly string[] s_validExtensions = [".md", ".json", ".custom"];
|
||||
private static readonly string[] s_mixedValidInvalidExtensions = [".md", "json"];
|
||||
private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, ct) => Task.FromResult<object?>(null);
|
||||
private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, sp, ct) => Task.FromResult<object?>(null);
|
||||
|
||||
private readonly string _testRoot;
|
||||
|
||||
|
||||
@@ -247,6 +247,7 @@ from ._workflows._workflow_executor import (
|
||||
WorkflowExecutor,
|
||||
)
|
||||
from .exceptions import (
|
||||
AgentFrameworkException,
|
||||
MiddlewareException,
|
||||
UserInputRequiredException,
|
||||
WorkflowCheckpointException,
|
||||
@@ -280,6 +281,7 @@ __all__ = [
|
||||
"AgentExecutor",
|
||||
"AgentExecutorRequest",
|
||||
"AgentExecutorResponse",
|
||||
"AgentFrameworkException",
|
||||
"AgentMiddleware",
|
||||
"AgentMiddlewareLayer",
|
||||
"AgentMiddlewareTypes",
|
||||
|
||||
@@ -34,7 +34,8 @@ class AgentFrameworkException(Exception):
|
||||
logger.log(log_level, message, exc_info=inner_exception)
|
||||
if inner_exception:
|
||||
super().__init__(message, inner_exception, *args) # type: ignore
|
||||
super().__init__(message, *args) # type: ignore
|
||||
else:
|
||||
super().__init__(message, *args) # type: ignore
|
||||
|
||||
|
||||
# region Agent Exceptions
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Tests for AgentFrameworkException inner_exception handling."""
|
||||
|
||||
from agent_framework import AgentFrameworkException
|
||||
|
||||
|
||||
def test_exception_with_inner_exception():
|
||||
"""When inner_exception is provided, it should be set as the second arg."""
|
||||
inner = ValueError("inner error")
|
||||
exc = AgentFrameworkException("test message", inner_exception=inner)
|
||||
assert exc.args[0] == "test message"
|
||||
assert exc.args[1] is inner
|
||||
|
||||
|
||||
def test_exception_without_inner_exception():
|
||||
"""When inner_exception is None, args should only contain the message."""
|
||||
exc = AgentFrameworkException("test message")
|
||||
assert exc.args == ("test message",)
|
||||
assert len(exc.args) == 1
|
||||
|
||||
|
||||
def test_exception_inner_exception_none_explicit():
|
||||
"""When inner_exception is explicitly None, args should only contain the message."""
|
||||
exc = AgentFrameworkException("test message", inner_exception=None)
|
||||
assert exc.args == ("test message",)
|
||||
assert len(exc.args) == 1
|
||||
@@ -37,6 +37,8 @@ async def poem_workflow(topic: str) -> str:
|
||||
poem = (await writer.run(f"Write a poem about: {topic}")).text
|
||||
review = (await reviewer.run(f"Review this poem: {poem}")).text
|
||||
return f"Poem:\n{poem}\n\nReview: {review}"
|
||||
|
||||
|
||||
# </create_workflow>
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ async def text_workflow(text: str) -> str:
|
||||
"""Uppercase the text, then reverse it."""
|
||||
upper = await to_upper_case(text)
|
||||
return await reverse_text(upper)
|
||||
|
||||
|
||||
# </create_workflow>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Azure AI Foundry Configuration (required for a2a_server.py and a2a_agent_as_function_tools.py)
|
||||
FOUNDRY_PROJECT_ENDPOINT=https://your-project.services.ai.azure.com/api/projects/your-project
|
||||
FOUNDRY_MODEL=gpt-4o
|
||||
|
||||
# A2A Client Configuration (required for agent_with_a2a.py and a2a_agent_as_function_tools.py)
|
||||
# The function-tools flow uses http://localhost:5000/ in the sample docs/output.
|
||||
# Update this value if you start a different local A2A server instance on another port.
|
||||
A2A_AGENT_HOST=http://localhost:5000/
|
||||
@@ -42,8 +42,33 @@ All commands below should be run from this directory:
|
||||
cd python/samples/04-hosting/a2a
|
||||
```
|
||||
|
||||
### 0. Install Dependencies
|
||||
|
||||
Copy `.env.example` to `.env` and fill in your values:
|
||||
|
||||
```powershell
|
||||
copy .env.example .env
|
||||
```
|
||||
|
||||
**Option A — pip (standard):**
|
||||
|
||||
```powershell
|
||||
python -m venv .venv
|
||||
.venv\Scripts\Activate.ps1 # Windows
|
||||
# source .venv/bin/activate # macOS / Linux
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
**Option B — uv:**
|
||||
|
||||
```powershell
|
||||
uv run python a2a_server.py --agent-type policy
|
||||
```
|
||||
|
||||
### 1. Start the A2A Server
|
||||
|
||||
> **Note (Option A — pip users):** Replace `uv run python` with `python` in all `uv run` commands below (e.g. `python a2a_server.py ...`). `uv` is not required once the virtual environment is activated.
|
||||
|
||||
Pick an agent type and start the server (each in its own terminal):
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -51,7 +51,7 @@ if __name__ == "__main__":
|
||||
agent = Agent(
|
||||
client=OpenAIChatClient(),
|
||||
name="Europe Travel Agent",
|
||||
instructions="You are a helpful Europe Travel Agent. You can help users search and book flights and hotels across Europe."
|
||||
instructions="You are a helpful Europe Travel Agent. You can help users search and book flights and hotels across Europe.",
|
||||
)
|
||||
|
||||
request_handler = DefaultRequestHandler(
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Agent Framework packages
|
||||
# To use the deployed version, uncomment the lines below and comment out the local installation lines
|
||||
# agent-framework-a2a
|
||||
# agent-framework-foundry
|
||||
|
||||
# Local installation (for development and testing)
|
||||
# Each package must be listed explicitly because pip doesn't resolve uv workspace sources.
|
||||
# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source.
|
||||
-e ../../../packages/core # Core framework - base dependency for all packages
|
||||
-e ../../../packages/foundry # Foundry support - dependency for FoundryChatClient in a2a_server.py
|
||||
-e ../../../packages/a2a # A2A integration - provides A2AAgent and a2a-sdk
|
||||
|
||||
# Azure authentication
|
||||
azure-identity
|
||||
|
||||
# A2A server runtime
|
||||
uvicorn
|
||||
|
||||
# HTTP client used by A2A client samples
|
||||
httpx
|
||||
|
||||
# Environment variable loading
|
||||
python-dotenv
|
||||
Reference in New Issue
Block a user