mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Adding a sample project demonstrating how to setup Agents and Workflows together.
This commit is contained in:
@@ -77,6 +77,7 @@
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/04_WorkflowMcpTool.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/">
|
||||
<File Path="samples/GettingStarted/README.md" />
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>WorkflowAndAgents</AssemblyName>
|
||||
<RootNamespace>WorkflowAndAgents</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Azure Functions packages -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace WorkflowAndAgents;
|
||||
|
||||
internal sealed class TranslateText() : Executor<string, TranslationResult>("TranslateText")
|
||||
{
|
||||
public override ValueTask<TranslationResult> HandleAsync(
|
||||
string message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine($"[Activity] TranslateText: '{message}'");
|
||||
return ValueTask.FromResult(new TranslationResult(message, message.ToUpperInvariant()));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FormatOutput() : Executor<TranslationResult, string>("FormatOutput")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(
|
||||
TranslationResult message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine("[Activity] FormatOutput: Formatting result");
|
||||
return ValueTask.FromResult($"Original: {message.Original} => Translated: {message.Translated}");
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record TranslationResult(string Original, string Translated);
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates using ConfigureDurableOptions to register BOTH agents AND workflows
|
||||
// in a single Azure Functions app. It uses a workflow to translate text and a standalone AI agent
|
||||
// accessible via HTTP and MCP tool triggers.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenAI.Chat;
|
||||
using WorkflowAndAgents;
|
||||
|
||||
// Get the Azure OpenAI endpoint and deployment name from environment variables.
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
ChatClient chatClient = client.GetChatClient(deploymentName);
|
||||
|
||||
// Define a standalone AI agent
|
||||
AIAgent assistant = chatClient.AsAIAgent(
|
||||
"You are a helpful assistant. Answer questions clearly and concisely.",
|
||||
"Assistant",
|
||||
description: "A general-purpose helpful assistant.");
|
||||
|
||||
// Define workflow executors
|
||||
TranslateText translateText = new();
|
||||
FormatOutput formatOutput = new();
|
||||
|
||||
// Build a workflow: TranslateText -> FormatOutput
|
||||
Workflow translateWorkflow = new WorkflowBuilder(translateText)
|
||||
.WithName("Translate")
|
||||
.WithDescription("Translate text to uppercase and format the result")
|
||||
.AddEdge(translateText, formatOutput)
|
||||
.Build();
|
||||
|
||||
// Use ConfigureDurableOptions to register both agents and workflows together
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableOptions(options =>
|
||||
{
|
||||
// Register the standalone agent with HTTP and MCP tool triggers
|
||||
options.Agents.AddAIAgent(assistant, enableHttpTrigger: true, enableMcpToolTrigger: true);
|
||||
|
||||
// Register the workflow with an HTTP endpoint and MCP tool trigger
|
||||
options.Workflows.AddWorkflow(translateWorkflow, exposeStatusEndpoint: false, exposeMcpToolTrigger: true);
|
||||
})
|
||||
.Build();
|
||||
app.Run();
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
# Workflow and Agents Sample
|
||||
|
||||
This sample demonstrates how to use `ConfigureDurableOptions` to register **both** AI agents **and** workflows in a single Azure Functions app. This is the recommended approach when your application needs both standalone agents and orchestrated workflows.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- **Unified Configuration**: Use `ConfigureDurableOptions` to register agents and workflows together
|
||||
- **Standalone Agent**: An AI agent accessible via HTTP and MCP tool triggers
|
||||
- **Workflow**: A simple text translation workflow also exposed as an MCP tool
|
||||
- **Mixed Triggers**: Both agents and workflows coexist in the same Functions host
|
||||
|
||||
## Sample Architecture
|
||||
|
||||
### Standalone Agent
|
||||
|
||||
| Agent | Description |
|
||||
|-------|-------------|
|
||||
| **Assistant** | A general-purpose AI assistant accessible via HTTP (`/agents/Assistant/run`) and as an MCP tool |
|
||||
|
||||
### Translate Workflow
|
||||
|
||||
| Executor | Input | Output | Description |
|
||||
|----------|-------|--------|-------------|
|
||||
| **TranslateText** | `string` | `TranslationResult` | Converts input text to uppercase |
|
||||
| **FormatOutput** | `TranslationResult` | `string` | Formats the result into a readable string |
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../../README.md) file in the parent directory for complete setup instructions, including:
|
||||
|
||||
- Prerequisites installation
|
||||
- Durable Task Scheduler setup
|
||||
- Storage emulator configuration
|
||||
|
||||
This sample also requires Azure OpenAI credentials. Set the following in `local.settings.json`:
|
||||
|
||||
- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint URL
|
||||
- `AZURE_OPENAI_DEPLOYMENT_NAME`: Your chat model deployment name
|
||||
- `AZURE_OPENAI_API_KEY` (optional): If not set, Azure CLI credential is used
|
||||
|
||||
## Running the Sample
|
||||
|
||||
1. **Start the Function App**:
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents
|
||||
func start
|
||||
```
|
||||
|
||||
2. **Expected Functions**: When the app starts, you should see functions for both the agent and the workflow:
|
||||
|
||||
- `dafx-Assistant` (entity trigger for the agent)
|
||||
- `http-Assistant` (HTTP trigger for the agent)
|
||||
- `mcptool-Assistant` (MCP tool trigger for the agent)
|
||||
- `wf-Translate` (orchestration trigger for the workflow)
|
||||
- `mcptool-wf-Translate` (MCP tool trigger for the workflow)
|
||||
|
||||
## Invoking the Agent via HTTP
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/agents/Assistant/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "What is the capital of France?"}'
|
||||
```
|
||||
|
||||
## Invoking via MCP Inspector
|
||||
|
||||
1. Install and run the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector):
|
||||
|
||||
```bash
|
||||
npx @modelcontextprotocol/inspector
|
||||
```
|
||||
|
||||
2. Connect to `http://localhost:7071/runtime/webhooks/mcp` using **Streamable HTTP** transport.
|
||||
|
||||
3. Click **List Tools** to see both the `Assistant` agent tool and the `Translate` workflow tool.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>"
|
||||
}
|
||||
}
|
||||
+8
@@ -67,6 +67,13 @@ public static class FunctionsApplicationBuilderExtensions
|
||||
|
||||
builder.Services.ConfigureDurableOptions(configure);
|
||||
|
||||
if (sharedOptions.Agents.GetAgentFactories().Count > 0)
|
||||
{
|
||||
builder.Services.TryAddSingleton<IFunctionsAgentOptionsProvider>(_ =>
|
||||
new DefaultFunctionsAgentOptionsProvider(DurableAgentsOptionsExtensions.GetAgentOptionsSnapshot()));
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IFunctionMetadataTransformer, DurableAgentFunctionMetadataTransformer>());
|
||||
}
|
||||
|
||||
if (sharedOptions.Workflows.Workflows.Count > 0)
|
||||
{
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IFunctionMetadataTransformer, DurableWorkflowsFunctionMetadataTransformer>());
|
||||
@@ -102,6 +109,7 @@ public static class FunctionsApplicationBuilderExtensions
|
||||
|
||||
builder.UseWhen<BuiltInFunctionExecutionMiddleware>(static context =>
|
||||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal) ||
|
||||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint, StringComparison.Ordinal) ||
|
||||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal) ||
|
||||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrchestrationHttpFunctionEntryPoint, StringComparison.Ordinal) ||
|
||||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrchestrationFunctionEntryPoint, StringComparison.Ordinal) ||
|
||||
|
||||
+52
@@ -293,6 +293,58 @@ public sealed class WorkflowSamplesValidation(ITestOutputHelper outputHelper) :
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WorkflowAndAgentsSampleValidationAsync()
|
||||
{
|
||||
string samplePath = Path.Combine(s_samplesPath, "05_WorkflowAndAgents");
|
||||
await this.RunSampleTestAsync(samplePath, requiresOpenAI: true, async (logs) =>
|
||||
{
|
||||
// Connect to the MCP endpoint exposed by the Azure Functions host
|
||||
IClientTransport clientTransport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new Uri($"http://localhost:{AzureFunctionsPort}/runtime/webhooks/mcp")
|
||||
});
|
||||
|
||||
await using McpClient mcpClient = await McpClient.CreateAsync(clientTransport);
|
||||
|
||||
// Verify both the agent and workflow tools are listed
|
||||
IList<McpClientTool> tools = await mcpClient.ListToolsAsync();
|
||||
this._outputHelper.WriteLine($"MCP tools found: {string.Join(", ", tools.Select(t => t.Name))}");
|
||||
|
||||
Assert.Single(tools, t => t.Name == "Assistant");
|
||||
Assert.Single(tools, t => t.Name == "Translate");
|
||||
|
||||
// Invoke the Translate workflow via MCP tool
|
||||
this._outputHelper.WriteLine("Invoking MCP tool 'Translate'...");
|
||||
CallToolResult translateResult = await mcpClient.CallToolAsync(
|
||||
"Translate",
|
||||
arguments: new Dictionary<string, object?> { { "input", "hello world" } });
|
||||
|
||||
Assert.NotEmpty(translateResult.Content);
|
||||
string translateResponse = Assert.IsType<TextContentBlock>(translateResult.Content[0]).Text;
|
||||
this._outputHelper.WriteLine($"Translate MCP tool response: {translateResponse}");
|
||||
Assert.Contains("HELLO WORLD", translateResponse);
|
||||
|
||||
// Invoke the Assistant agent via MCP tool
|
||||
this._outputHelper.WriteLine("Invoking MCP tool 'Assistant'...");
|
||||
CallToolResult assistantResult = await mcpClient.CallToolAsync(
|
||||
"Assistant",
|
||||
arguments: new Dictionary<string, object?> { { "query", "What is 2 + 2?" } });
|
||||
|
||||
Assert.NotEmpty(assistantResult.Content);
|
||||
string assistantResponse = Assert.IsType<TextContentBlock>(assistantResult.Content[0]).Text;
|
||||
this._outputHelper.WriteLine($"Assistant MCP tool response: {assistantResponse}");
|
||||
Assert.NotEmpty(assistantResponse);
|
||||
|
||||
// Verify workflow executor activities ran in the logs
|
||||
lock (logs)
|
||||
{
|
||||
Assert.True(logs.Any(log => log.Message.Contains("[Activity] TranslateText:")), "TranslateText activity not found in logs.");
|
||||
Assert.True(logs.Any(log => log.Message.Contains("[Activity] FormatOutput:")), "FormatOutput activity not found in logs.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConcurrentWorkflowSampleValidationAsync()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user