mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'feature-foundry-agents' into feature-declarative-agents-dotnet
This commit is contained in:
@@ -39,7 +39,7 @@ jobs:
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
|
||||
- name: Pytest coverage comment
|
||||
id: coverageComment
|
||||
uses: MishaKav/pytest-coverage-comment@v1.1.57
|
||||
uses: MishaKav/pytest-coverage-comment@v1.1.59
|
||||
with:
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
issue-number: ${{ env.PR_NUMBER }}
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Aspire -->
|
||||
<AspireAppHostSdkVersion>9.5.2</AspireAppHostSdkVersion>
|
||||
<AspireAppHostSdkVersion>13.0.0</AspireAppHostSdkVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="9.5.1-preview.1.25502.11" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.9.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0-beta.435" />
|
||||
<!-- Azure.* -->
|
||||
<PackageVersion Include="Azure.AI.Agents" Version="2.0.0-alpha.20251107.3" />
|
||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.7" />
|
||||
@@ -23,18 +23,18 @@
|
||||
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
|
||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
|
||||
<!-- System.* -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.7.0" />
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="9.0.10" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.8.0" />
|
||||
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="10.0.0" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="System.Net.Http.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<!-- OpenTelemetry -->
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.13.1" />
|
||||
@@ -45,39 +45,39 @@
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
|
||||
<!-- Microsoft.AspNetCore.* -->
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
|
||||
<!-- Microsoft.Extensions.* -->
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.10.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.10.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.10.0-preview.1.25513.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.10.2-preview.1.25552.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.10.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="10.0.0-preview.1.25559.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.0-preview.1.25559.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
|
||||
<!-- Semantic Kernel -->
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.66.0" />
|
||||
<!-- Vector Stores -->
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.67.0-preview" />
|
||||
<!-- Semantic Kernel -->
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.67.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.67.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
|
||||
<!-- Agent SDKs -->
|
||||
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.2.41" />
|
||||
<!-- A2A -->
|
||||
@@ -102,7 +102,7 @@
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<!-- Test -->
|
||||
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageVersion Include="Moq" Version="[4.18.4]" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
@@ -114,7 +114,7 @@
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||
<!-- Toolset -->
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -134,7 +134,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageVersion Include="Roslynator.Analyzers" Version="[4.14.0]" />
|
||||
<PackageVersion Include="Roslynator.Analyzers" Version="[4.14.1]" />
|
||||
<PackageReference Include="Roslynator.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Agent_Step16_ChatReduction.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Agent_Step17_BackgroundResponses.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step18_Declarative/Agent_Step18_Declarative.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Agent_Step18_DeepResearch.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/DevUI/">
|
||||
<File Path="samples/GettingStarted/DevUI/README.md" />
|
||||
@@ -123,6 +124,7 @@
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/FunctionTools/FunctionTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/GenerateCode/GenerateCode.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/InputArguments/InputArguments.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/Marketing/Marketing.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/StudentTeacher/StudentTeacher.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ToolApproval/ToolApproval.csproj" />
|
||||
@@ -175,7 +177,6 @@
|
||||
<Folder Name="/Samples/HostedAgents/">
|
||||
<Project Path="samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj" />
|
||||
<Project Path="samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
|
||||
<Project Path="samples/HostedAgents/DeepResearchAgent/DeepResearchAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/">
|
||||
<File Path=".editorconfig" />
|
||||
@@ -313,6 +314,7 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251110.2</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251110.2</PackageVersion>
|
||||
<GitTag>1.0.0-preview.251110.2</GitTag>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251111.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251111.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.251111.1</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Agents.Persistent" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create an Azure AI Foundry Agent with the Deep Research Tool.
|
||||
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deepResearchDeploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEEP_RESEARCH_DEPLOYMENT_NAME") ?? "o3-deep-research";
|
||||
var modelDeploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o";
|
||||
var bingConnectionId = Environment.GetEnvironmentVariable("BING_CONNECTION_ID") ?? throw new InvalidOperationException("BING_CONNECTION_ID is not set.");
|
||||
|
||||
// Configure extended network timeout for long-running Deep Research tasks.
|
||||
PersistentAgentsAdministrationClientOptions persistentAgentsClientOptions = new();
|
||||
persistentAgentsClientOptions.Retry.NetworkTimeout = TimeSpan.FromMinutes(20);
|
||||
|
||||
// Get a client to create/retrieve server side agents with.
|
||||
PersistentAgentsClient persistentAgentsClient = new(endpoint, new AzureCliCredential(), persistentAgentsClientOptions);
|
||||
|
||||
// Define and configure the Deep Research tool.
|
||||
DeepResearchToolDefinition deepResearchTool = new(new DeepResearchDetails(
|
||||
bingGroundingConnections: [new(bingConnectionId)],
|
||||
model: deepResearchDeploymentName)
|
||||
);
|
||||
|
||||
// Create an agent with the Deep Research tool on the Azure AI agent service.
|
||||
AIAgent agent = await persistentAgentsClient.CreateAIAgentAsync(
|
||||
model: modelDeploymentName,
|
||||
name: "DeepResearchAgent",
|
||||
instructions: "You are a helpful Agent that assists in researching scientific topics.",
|
||||
tools: [deepResearchTool]);
|
||||
|
||||
const string Task = "Research the current state of studies on orca intelligence and orca language, " +
|
||||
"including what is currently known about orcas' cognitive capabilities and communication systems.";
|
||||
|
||||
Console.WriteLine($"# User: '{Task}'");
|
||||
Console.WriteLine();
|
||||
|
||||
try
|
||||
{
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
await foreach (var response in agent.RunStreamingAsync(Task, thread))
|
||||
{
|
||||
Console.Write(response.Text);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
# What this sample demonstrates
|
||||
|
||||
This sample demonstrates how to create an Azure AI Agent with the Deep Research Tool, which leverages the o3-deep-research reasoning model to perform comprehensive research on complex topics.
|
||||
|
||||
Key features:
|
||||
- Configuring and using the Deep Research Tool with Bing grounding
|
||||
- Creating a persistent AI agent with deep research capabilities
|
||||
- Executing deep research queries and retrieving results
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running this sample, ensure you have:
|
||||
|
||||
1. An Azure AI Foundry project set up
|
||||
2. A deep research model deployment (e.g., o3-deep-research)
|
||||
3. A model deployment (e.g., gpt-4o)
|
||||
4. A Bing Connection configured in your Azure AI Foundry project
|
||||
5. Azure CLI installed and authenticated
|
||||
|
||||
**Important**: Please visit the following documentation for detailed setup instructions:
|
||||
- [Deep Research Tool Documentation](https://aka.ms/agents-deep-research)
|
||||
- [Research Tool Setup](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/deep-research#research-tool-setup)
|
||||
|
||||
Pay special attention to the purple `Note` boxes in the Azure documentation.
|
||||
|
||||
**Note**: The Bing Connection ID must be from the **project**, not the resource. It has the following format:
|
||||
|
||||
```
|
||||
/subscriptions/<sub_id>/resourceGroups/<rg_name>/providers/<provider_name>/accounts/<account_name>/projects/<project_name>/connections/<connection_name>
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
# Replace with your Azure AI Foundry project endpoint
|
||||
$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/"
|
||||
|
||||
# Replace with your Bing connection ID from the project
|
||||
$env:BING_CONNECTION_ID="/subscriptions/.../connections/your-bing-connection"
|
||||
|
||||
# Optional, defaults to o3-deep-research
|
||||
$env:AZURE_FOUNDRY_PROJECT_DEEP_RESEARCH_DEPLOYMENT_NAME="o3-deep-research"
|
||||
|
||||
# Optional, defaults to gpt-4o
|
||||
$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="gpt-4o"
|
||||
@@ -44,6 +44,7 @@ Before you begin, ensure you have the following prerequisites:
|
||||
|[Using plugins with an agent](./Agent_Step15_Plugins/)|This sample demonstrates how to use plugins with an agent|
|
||||
|[Reducing chat history size](./Agent_Step16_ChatReduction/)|This sample demonstrates how to reduce the chat history to constrain its size, where chat history is maintained locally|
|
||||
|[Background responses](./Agent_Step17_BackgroundResponses/)|This sample demonstrates how to use background responses for long-running operations with polling and resumption support|
|
||||
|[Deep research with an agent](./Agent_Step18_DeepResearch/)|This sample demonstrates how to use the Deep Research Tool to perform comprehensive research on complex topics|
|
||||
|
||||
## Running the samples from the console
|
||||
|
||||
|
||||
+10
-25
@@ -16,7 +16,7 @@ internal static class WorkflowFactory
|
||||
internal static Workflow BuildWorkflow(IChatClient chatClient)
|
||||
{
|
||||
// Create executors
|
||||
var startExecutor = new ConcurrentStartExecutor();
|
||||
var startExecutor = new ChatForwardingExecutor("Start");
|
||||
var aggregationExecutor = new ConcurrentAggregationExecutor();
|
||||
AIAgent frenchAgent = GetLanguageAgent("French", chatClient);
|
||||
AIAgent englishAgent = GetLanguageAgent("English", chatClient);
|
||||
@@ -38,33 +38,11 @@ internal static class WorkflowFactory
|
||||
private static ChatClientAgent GetLanguageAgent(string targetLanguage, IChatClient chatClient) =>
|
||||
new(chatClient, instructions: $"You're a helpful assistant who always responds in {targetLanguage}.", name: $"{targetLanguage}Agent");
|
||||
|
||||
/// <summary>
|
||||
/// Executor that starts the concurrent processing by sending messages to the agents.
|
||||
/// </summary>
|
||||
private sealed class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor")
|
||||
{
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
|
||||
{
|
||||
return routeBuilder
|
||||
.AddHandler<List<ChatMessage>>(this.RouteMessages)
|
||||
.AddHandler<TurnToken>(this.RouteTurnTokenAsync);
|
||||
}
|
||||
|
||||
private ValueTask RouteMessages(List<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return context.SendMessageAsync(messages, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private ValueTask RouteTurnTokenAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return context.SendMessageAsync(token, cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that aggregates the results from the concurrent agents.
|
||||
/// </summary>
|
||||
private sealed class ConcurrentAggregationExecutor() : Executor<List<ChatMessage>>("ConcurrentAggregationExecutor")
|
||||
private sealed class ConcurrentAggregationExecutor() :
|
||||
Executor<List<ChatMessage>>("ConcurrentAggregationExecutor"), IResettableExecutor
|
||||
{
|
||||
private readonly List<ChatMessage> _messages = [];
|
||||
|
||||
@@ -85,5 +63,12 @@ internal static class WorkflowFactory
|
||||
await context.YieldOutputAsync(formattedMessages, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask ResetAsync()
|
||||
{
|
||||
this._messages.Clear();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Demo.Workflows.Declarative.ConfirmInput;
|
||||
/// and confirm it matches the original input.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../Declarative/README.md) for detailed
|
||||
/// See the README.md file in the parent folder (../README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Demo.Workflows.Declarative.DeepResearch;
|
||||
/// using the Magentic orchestration pattern developed by AutoGen.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../Declarative/README.md) for detailed
|
||||
/// See the README.md file in the parent folder (../README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+1
@@ -28,6 +28,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Demo.Workflows.Declarative.FunctionTools;
|
||||
/// with function tools assigned. Exits the loop when the user enters "exit".
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../Declarative/README.md) for detailed
|
||||
/// See the README.md file in the parent folder (../README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
|
||||
@@ -42,7 +42,7 @@ internal sealed class Program
|
||||
Console.WriteLine(code);
|
||||
}
|
||||
|
||||
private const string DefaultWorkflow = "HelloWorld.yaml";
|
||||
private const string DefaultWorkflow = "Marketing.yaml";
|
||||
|
||||
private string WorkflowFile { get; }
|
||||
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
|
||||
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
|
||||
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="InputArguments.yaml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
#
|
||||
# This workflow demonstrates providing input arguments to an agent.
|
||||
#
|
||||
# Example input:
|
||||
# I'd like to go on vacation.
|
||||
#
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
# Capture the original user message for input to the location-aware agent
|
||||
- kind: SetVariable
|
||||
id: set_count_increment
|
||||
variable: Local.InputMessage
|
||||
value: =System.LastMessage
|
||||
|
||||
# Invoke the triage agent to determine location requirements
|
||||
- kind: InvokeAzureAgent
|
||||
id: solicit_input
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: LocationTriageAgent
|
||||
input:
|
||||
messages: =Local.ActionMessage
|
||||
output:
|
||||
messages: Local.TriageResponse
|
||||
|
||||
# Request input from the user based on the triage response
|
||||
- kind: RequestExternalInput
|
||||
id: request_requirements
|
||||
variable: Local.NextInput
|
||||
|
||||
# Capture the most recent interaction for evaluation
|
||||
- kind: SetTextVariable
|
||||
id: set_status_message
|
||||
variable: Local.LocationStatusInput
|
||||
value: |-
|
||||
AGENT - {MessageText(Local.TriageResponse)}
|
||||
|
||||
USER - {MessageText(Local.NextInput)}
|
||||
|
||||
# Evaluate the status of the location triage
|
||||
- kind: InvokeAzureAgent
|
||||
id: evaluate_location
|
||||
agent:
|
||||
name: LocationCaptureAgent
|
||||
input:
|
||||
messages: =UserMessage(Local.LocationStatusInput)
|
||||
output:
|
||||
responseObject: Local.LocationResponse
|
||||
|
||||
# Determine if the location information is complete
|
||||
- kind: ConditionGroup
|
||||
id: check_completion
|
||||
conditions:
|
||||
|
||||
- condition: |-
|
||||
=Local.LocationResponse.is_location_defined = false Or
|
||||
Local.LocationResponse.is_location_confirmed = false
|
||||
id: check_done
|
||||
actions:
|
||||
|
||||
# Capture the action message for input to the triage agent
|
||||
- kind: SetVariable
|
||||
id: set_next_message
|
||||
variable: Local.ActionMessage
|
||||
value: =AgentMessage(Local.LocationResponse.action)
|
||||
|
||||
- kind: GotoAction
|
||||
id: goto_solicit_input
|
||||
actionId: solicit_input
|
||||
|
||||
elseActions:
|
||||
|
||||
# Create a new conversation so the prior context does not interfere
|
||||
- kind: CreateConversation
|
||||
id: conversation_location
|
||||
conversationId: Local.LocationConversationId
|
||||
|
||||
# Invoke the location-aware agent with the location argument
|
||||
# and loop until the user types "EXIT"
|
||||
- kind: InvokeAzureAgent
|
||||
id: location_response
|
||||
conversationId: =Local.LocationConversationId
|
||||
agent:
|
||||
name: LocationAwareAgent
|
||||
input:
|
||||
messages: =Local.InputMessage
|
||||
arguments:
|
||||
location: =Local.LocationResponse.place
|
||||
externalLoop:
|
||||
when: =Upper(System.LastMessage.Text) <> "EXIT"
|
||||
output:
|
||||
autoSend: true
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using OpenAI.Responses;
|
||||
using Shared.Foundry;
|
||||
using Shared.Workflows;
|
||||
|
||||
namespace Demo.Workflows.Declarative.InputArguments;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate a workflow that consumes input arguments to dynamically enhance the agent
|
||||
/// instructions. Exits the loop when the user enters "exit".
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// Initialize configuration
|
||||
IConfiguration configuration = Application.InitializeConfig();
|
||||
Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
|
||||
|
||||
// Ensure sample agents exist in Foundry.
|
||||
await CreateAgentAsync(foundryEndpoint, configuration);
|
||||
|
||||
// Get input from command line or console
|
||||
string workflowInput = Application.GetInput(args);
|
||||
|
||||
// Create the workflow factory. This class demonstrates how to initialize a
|
||||
// declarative workflow from a YAML file. Once the workflow is created, it
|
||||
// can be executed just like any regular workflow.
|
||||
WorkflowFactory workflowFactory = new("InputArguments.yaml", foundryEndpoint);
|
||||
|
||||
// Execute the workflow: The WorkflowRunner demonstrates how to execute
|
||||
// a workflow, handle the workflow events, and providing external input.
|
||||
// This also includes the ability to checkpoint workflow state and how to
|
||||
// resume execution.
|
||||
WorkflowRunner runner = new();
|
||||
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
|
||||
}
|
||||
|
||||
private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration)
|
||||
{
|
||||
AgentClient agentsClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "LocationTriageAgent",
|
||||
agentDefinition: DefineLocationTriageAgent(configuration),
|
||||
agentDescription: "Chats with the user to solicit a location of interest.");
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "LocationCaptureAgent",
|
||||
agentDefinition: DefineLocationCaptureAgent(configuration),
|
||||
agentDescription: "Evaluate the status of soliciting the location.");
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "LocationAwareAgent",
|
||||
agentDefinition: DefineLocationAwareAgent(configuration),
|
||||
agentDescription: "Chats with the user with location awareness.");
|
||||
}
|
||||
|
||||
private static PromptAgentDefinition DefineLocationTriageAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
Your only job is to solicit a location from the user.
|
||||
|
||||
Always repeat back the location when addressing the user, except when it is not known.
|
||||
"""
|
||||
};
|
||||
|
||||
private static PromptAgentDefinition DefineLocationCaptureAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
Request a location from the user. This location could be their own location
|
||||
or perhaps a location they are interested in.
|
||||
|
||||
City level precision is sufficient.
|
||||
|
||||
If extrapolating region and country, confirm you have it right.
|
||||
""",
|
||||
TextOptions =
|
||||
new ResponseTextOptions
|
||||
{
|
||||
TextFormat =
|
||||
ResponseTextFormat.CreateJsonSchemaFormat(
|
||||
"TaskEvaluation",
|
||||
BinaryData.FromString(
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"place": {
|
||||
"type": "string",
|
||||
"description": "Captures only your understanding of the location specified by the user without explanation, or 'unknown' if not yet defined."
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "The instruction for the next action to take regarding the need for additional detail or confirmation."
|
||||
},
|
||||
"is_location_defined": {
|
||||
"type": "boolean",
|
||||
"description": "True if the user location is understood."
|
||||
},
|
||||
"is_location_confirmed": {
|
||||
"type": "boolean",
|
||||
"description": "True if the user location is confirmed. An unambiguous location may be implicitly confirmed without explicit user confirmation."
|
||||
}
|
||||
},
|
||||
"required": ["place", "action", "is_location_defined", "is_location_confirmed"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
"""),
|
||||
jsonSchemaFormatDescription: null,
|
||||
jsonSchemaIsStrict: true),
|
||||
}
|
||||
};
|
||||
|
||||
private static PromptAgentDefinition DefineLocationAwareAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
// Parameterized instructions reference the "location" input argument.
|
||||
Instructions =
|
||||
"""
|
||||
Talk to the user about their request.
|
||||
Their request is related to a specific location: {{location}}.
|
||||
""",
|
||||
StructuredInputs =
|
||||
{
|
||||
["location"] =
|
||||
new StructuredInputDefinition
|
||||
{
|
||||
IsRequired = false,
|
||||
DefaultValue = BinaryData.FromString(@"""unknown"""),
|
||||
Description = "The user's location",
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Demo.Workflows.Declarative.Marketing;
|
||||
/// sequentially engaging in a task.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../Declarative/README.md) for detailed
|
||||
/// See the README.md file in the parent folder (../README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
|
||||
@@ -86,12 +86,14 @@ To run the sampes from the command line:
|
||||
1. From the root of the repository, navigate the console to the project folder:
|
||||
|
||||
```sh
|
||||
cd dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher
|
||||
cd dotnet/samples/GettingStarted/Workflows/Declarative/Marketing
|
||||
dotnet run Marketing
|
||||
```
|
||||
|
||||
2. Run the demo and optionally provided input:
|
||||
|
||||
```sh
|
||||
dotnet run "How would you compute the value of PI?"
|
||||
dotnet run "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours."
|
||||
dotnet run c:/myworkflows/Marketing.yaml
|
||||
```
|
||||
> The sample will allow for interactive input in the absence of an input argument.
|
||||
@@ -13,7 +13,7 @@ namespace Demo.Workflows.Declarative.StudentTeacher;
|
||||
/// in an iterative conversation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../Declarative/README.md) for detailed
|
||||
/// See the README.md file in the parent folder (../README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
|
||||
+1
@@ -28,6 +28,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Demo.Workflows.Declarative.ToolApproval;
|
||||
/// has an MCP tool that requires approval. Exits the loop when the user enters "exit".
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../Declarative/README.md) for detailed
|
||||
/// See the README.md file in the parent folder (../README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
@@ -32,6 +33,7 @@ public class AgentRunOptions
|
||||
_ = Throw.IfNull(options);
|
||||
this.ContinuationToken = options.ContinuationToken;
|
||||
this.AllowBackgroundResponses = options.AllowBackgroundResponses;
|
||||
this.AdditionalProperties = options.AdditionalProperties?.Clone();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,4 +76,18 @@ public class AgentRunOptions
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool? AllowBackgroundResponses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional properties associated with these options.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// An <see cref="AdditionalPropertiesDictionary"/> containing custom properties,
|
||||
/// or <see langword="null"/> if no additional properties are present.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Additional properties provide a way to include custom metadata or provider-specific
|
||||
/// information that doesn't fit into the standard options schema. This is useful for
|
||||
/// preserving implementation-specific details or extending the options with custom data.
|
||||
/// </remarks>
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ public static class AgentClientExtensions
|
||||
/// </summary>
|
||||
/// <param name="agentClient">The client used to interact with Azure AI Agents. Cannot be <see langword="null"/>.</param>
|
||||
/// <param name="agentVersion">The agent version to be converted. Cannot be <see langword="null"/>.</param>
|
||||
/// <param name="tools">The tools to use when interacting with the agent. This is required when using prompt agent definitions with tools.</param>
|
||||
/// <param name="tools">In-process invocable tools to be provided. If no tools are provided manual handling will be necessary to invoke in-process tools.</param>
|
||||
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
|
||||
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
|
||||
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
|
||||
@@ -156,7 +156,7 @@ public static class AgentClientExtensions
|
||||
Throw.IfNull(agentClient);
|
||||
Throw.IfNull(agentVersion);
|
||||
|
||||
ValidateUsingToolsParameter(agentVersion, tools);
|
||||
var allowDeclarativeMode = tools is not { Count: > 0 };
|
||||
|
||||
return CreateChatClientAgent(
|
||||
agentClient,
|
||||
@@ -164,7 +164,7 @@ public static class AgentClientExtensions
|
||||
tools,
|
||||
clientFactory,
|
||||
openAIClientOptions,
|
||||
requireInvocableTools: true,
|
||||
!allowDeclarativeMode,
|
||||
services);
|
||||
}
|
||||
|
||||
@@ -293,7 +293,6 @@ public static class AgentClientExtensions
|
||||
new AgentVersionCreationOptions(new PromptAgentDefinition(model) { Instructions = instructions }) { Description = description },
|
||||
clientFactory,
|
||||
openAIClientOptions,
|
||||
requireInvocableTools: true,
|
||||
services,
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -339,7 +338,6 @@ public static class AgentClientExtensions
|
||||
new AgentVersionCreationOptions(new PromptAgentDefinition(model) { Instructions = instructions }) { Description = description },
|
||||
clientFactory,
|
||||
openAIClientOptions,
|
||||
requireInvocableTools: true,
|
||||
services,
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -381,7 +379,7 @@ public static class AgentClientExtensions
|
||||
Instructions = options.Instructions,
|
||||
};
|
||||
|
||||
ApplyToolsToAgentDefinition(agentDefinition, options.ChatOptions?.Tools, RequireInvocableTools);
|
||||
ApplyToolsToAgentDefinition(agentDefinition, options.ChatOptions?.Tools);
|
||||
|
||||
AgentVersionCreationOptions? creationOptions = new(agentDefinition);
|
||||
if (!string.IsNullOrWhiteSpace(options.Description))
|
||||
@@ -440,7 +438,7 @@ public static class AgentClientExtensions
|
||||
Instructions = options.Instructions,
|
||||
};
|
||||
|
||||
ApplyToolsToAgentDefinition(agentDefinition, options.ChatOptions?.Tools, RequireInvocableTools);
|
||||
ApplyToolsToAgentDefinition(agentDefinition, options.ChatOptions?.Tools);
|
||||
|
||||
AgentVersionCreationOptions? creationOptions = new(agentDefinition);
|
||||
if (!string.IsNullOrWhiteSpace(options.Description))
|
||||
@@ -489,16 +487,13 @@ public static class AgentClientExtensions
|
||||
Throw.IfNullOrWhitespace(name);
|
||||
Throw.IfNull(creationOptions);
|
||||
|
||||
var tools = (creationOptions.Definition as PromptAgentDefinition)?.Tools.Select(t => t.AsAITool()).ToList();
|
||||
|
||||
return CreateAIAgent(
|
||||
agentClient,
|
||||
name,
|
||||
tools,
|
||||
tools: null,
|
||||
creationOptions,
|
||||
clientFactory,
|
||||
openAIClientOptions,
|
||||
requireInvocableTools: false,
|
||||
services: null,
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -531,16 +526,13 @@ public static class AgentClientExtensions
|
||||
Throw.IfNull(agentClient);
|
||||
Throw.IfNull(creationOptions);
|
||||
|
||||
var tools = (creationOptions.Definition as PromptAgentDefinition)?.Tools.Select(t => t.AsAITool()).ToList();
|
||||
|
||||
return CreateAIAgentAsync(
|
||||
agentClient,
|
||||
name,
|
||||
tools,
|
||||
tools: null,
|
||||
creationOptions,
|
||||
clientFactory,
|
||||
openAIClientOptions,
|
||||
requireInvocableTools: false,
|
||||
services: null,
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -594,7 +586,6 @@ public static class AgentClientExtensions
|
||||
AgentVersionCreationOptions creationOptions,
|
||||
Func<IChatClient, IChatClient>? clientFactory,
|
||||
OpenAIClientOptions? openAIClientOptions,
|
||||
bool requireInvocableTools,
|
||||
IServiceProvider? services,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -602,9 +593,12 @@ public static class AgentClientExtensions
|
||||
Throw.IfNullOrWhitespace(name);
|
||||
Throw.IfNull(creationOptions);
|
||||
|
||||
tools ??= (creationOptions.Definition as PromptAgentDefinition)?.Tools.Select(t => t.AsAITool()).ToList();
|
||||
var allowDeclarativeMode = tools is not { Count: > 0 };
|
||||
|
||||
ApplyToolsToAgentDefinition(creationOptions.Definition, tools, requireInvocableTools);
|
||||
if (!allowDeclarativeMode)
|
||||
{
|
||||
ApplyToolsToAgentDefinition(creationOptions.Definition, tools);
|
||||
}
|
||||
|
||||
AgentVersion agentVersion = CreateAgentVersionWithProtocol(agentClient, name, creationOptions, cancellationToken);
|
||||
|
||||
@@ -614,7 +608,7 @@ public static class AgentClientExtensions
|
||||
tools,
|
||||
clientFactory,
|
||||
openAIClientOptions,
|
||||
requireInvocableTools,
|
||||
!allowDeclarativeMode,
|
||||
services);
|
||||
}
|
||||
|
||||
@@ -625,7 +619,6 @@ public static class AgentClientExtensions
|
||||
AgentVersionCreationOptions creationOptions,
|
||||
Func<IChatClient, IChatClient>? clientFactory,
|
||||
OpenAIClientOptions? openAIClientOptions,
|
||||
bool requireInvocableTools,
|
||||
IServiceProvider? services,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -633,9 +626,12 @@ public static class AgentClientExtensions
|
||||
Throw.IfNull(agentClient);
|
||||
Throw.IfNull(creationOptions);
|
||||
|
||||
tools ??= (creationOptions.Definition as PromptAgentDefinition)?.Tools.Select(t => t.AsAITool()).ToList();
|
||||
var allowDeclarativeMode = tools is not { Count: > 0 };
|
||||
|
||||
ApplyToolsToAgentDefinition(creationOptions.Definition, tools, requireInvocableTools);
|
||||
if (!allowDeclarativeMode)
|
||||
{
|
||||
ApplyToolsToAgentDefinition(creationOptions.Definition, tools);
|
||||
}
|
||||
|
||||
AgentVersion agentVersion = await CreateAgentVersionWithProtocolAsync(agentClient, name, creationOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -645,7 +641,7 @@ public static class AgentClientExtensions
|
||||
tools,
|
||||
clientFactory,
|
||||
openAIClientOptions,
|
||||
requireInvocableTools,
|
||||
!allowDeclarativeMode,
|
||||
services);
|
||||
}
|
||||
|
||||
@@ -719,14 +715,11 @@ public static class AgentClientExtensions
|
||||
// Check function tools
|
||||
foreach (ResponseTool responseTool in definitionTools)
|
||||
{
|
||||
if (responseTool is FunctionTool functionTool)
|
||||
if (requireInvocableTools && responseTool is FunctionTool functionTool)
|
||||
{
|
||||
// Check if a tool with the same type and name exists in the provided tools.
|
||||
var matchingTool = chatOptions?.Tools?.FirstOrDefault(t =>
|
||||
requireInvocableTools
|
||||
? t is AIFunction tf && functionTool.FunctionName == tf.Name // When invocable tools are required, match only AIFunction.
|
||||
: (t is AIFunctionDeclaration tfd && functionTool.FunctionName == tfd.Name) ? true // When not required, match AIFunctionDeclaration OR
|
||||
: (t.GetService<FunctionTool>() is FunctionTool ft && functionTool.FunctionName == ft.FunctionName)); // Match a FunctionTool converted AsAITool.
|
||||
// When invocable tools are required, match only AIFunction.
|
||||
var matchingTool = chatOptions?.Tools?.FirstOrDefault(t => t is AIFunction tf && functionTool.FunctionName == tf.Name);
|
||||
|
||||
if (matchingTool is null)
|
||||
{
|
||||
@@ -742,7 +735,7 @@ public static class AgentClientExtensions
|
||||
(agentTools ??= []).Add(responseTool.AsAITool());
|
||||
}
|
||||
|
||||
if (missingTools is { Count: > 0 })
|
||||
if (requireInvocableTools && missingTools is { Count: > 0 })
|
||||
{
|
||||
throw new InvalidOperationException($"The following prompt agent definition required tools were not provided: {string.Join(", ", missingTools)}");
|
||||
}
|
||||
@@ -796,35 +789,14 @@ public static class AgentClientExtensions
|
||||
return agentOptions;
|
||||
}
|
||||
|
||||
/// <summary>For already created agent versions, retrieve the definition and validate the tools parameter.</summary>
|
||||
/// <exception cref="ArgumentException"><see cref="PromptAgentDefinition.Tools"/> cannot be used. The <paramref name="tools"/> parameter should be used instead.</exception>
|
||||
/// <remarks>
|
||||
/// Because <see cref="PromptAgentDefinition.Tools"/> doesn't support in-proc tools (only declarative/definitions),
|
||||
/// the <paramref name="tools"/> parameter needs to be the single source of truth for tools, and must be provided when using tools.
|
||||
/// </remarks>
|
||||
private static void ValidateUsingToolsParameter(AgentVersion agentVersion, IList<AITool>? tools)
|
||||
{
|
||||
if (agentVersion.Definition is PromptAgentDefinition { Tools.Count: > 0 } && tools is null or { Count: 0 })
|
||||
{
|
||||
throw new ArgumentException("When retrieving prompt agents with tools the tools parameter needs to be provided with the necessary tools.", nameof(tools));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified AI tools to a prompt agent definition, ensuring that all tools are compatible and, if required, invocable.
|
||||
/// Adds the specified AI tools to a prompt agent definition, while also ensuring that all invocable tools are provided.
|
||||
/// </summary>
|
||||
/// <remarks>This method ensures that only compatible and properly constructed tools are added to the agent definition.
|
||||
/// When <paramref name="requireInvocableTools"/> is <see langword="true"/>, all tools must be
|
||||
/// invocable AIFunctions, which can be created using AIFunctionFactory.Create. Tools are converted to ResponseTool
|
||||
/// instances before being added.</remarks>
|
||||
/// <param name="agentDefinition">The agent definition to which the tools will be applied. Must be a PromptAgentDefinition to support tools.</param>
|
||||
/// <param name="tools">A list of AI tools to add to the agent definition. If null or empty, no tools are added.</param>
|
||||
/// <param name="requireInvocableTools">Indicates whether all provided tools must be invocable AI functions. If set to <see langword="true"/>, only
|
||||
/// invocable AIFunctions are accepted.</param>
|
||||
/// <exception cref="ArgumentException">Thrown if <paramref name="agentDefinition"/> is not a <see cref="PromptAgentDefinition"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown if <paramref name="requireInvocableTools"/> is <see langword="true"/> and a tool is an
|
||||
/// <see cref="AIFunctionDeclaration"/> that is not invocable, or if a tool cannot be converted to a <see cref="ResponseTool"/>.</exception>
|
||||
private static void ApplyToolsToAgentDefinition(AgentDefinition agentDefinition, IList<AITool>? tools, bool requireInvocableTools)
|
||||
/// <exception cref="ArgumentException">Thrown if tools were provided but <paramref name="agentDefinition"/> is not a <see cref="PromptAgentDefinition"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">When providing functions, they need to be invokable AIFunctions.</exception>
|
||||
private static void ApplyToolsToAgentDefinition(AgentDefinition agentDefinition, IList<AITool>? tools)
|
||||
{
|
||||
if (tools is { Count: > 0 })
|
||||
{
|
||||
@@ -833,10 +805,14 @@ public static class AgentClientExtensions
|
||||
throw new ArgumentException("Only prompt agent definitions support tools.", nameof(agentDefinition));
|
||||
}
|
||||
|
||||
// When tools are provided, those should represent the complete set of tools for the agent definition.
|
||||
// This is particularly important for existing agents so no duplication happens for what was already defined.
|
||||
promptAgentDefinition.Tools.Clear();
|
||||
|
||||
foreach (var tool in tools)
|
||||
{
|
||||
// Ensure that any AIFunctions provided are In-Proc, not just the declarations.
|
||||
if (requireInvocableTools && tool is not AIFunction && (
|
||||
if (tool is not AIFunction && (
|
||||
tool.GetService<FunctionTool>() is not null // Declarative FunctionTool converted as AsAITool()
|
||||
|| tool is AIFunctionDeclaration)) // AIFunctionDeclaration type
|
||||
{
|
||||
|
||||
@@ -160,5 +160,5 @@ internal sealed record CustomToolFormat
|
||||
/// Additional format properties (schema definition).
|
||||
/// </summary>
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, object?>? AdditionalProperties { get; init; }
|
||||
public Dictionary<string, object?>? AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
+20
-14
@@ -7,6 +7,7 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents;
|
||||
@@ -32,6 +33,11 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
private AgentClient? _agentClient;
|
||||
private ConversationClient? _conversationClient;
|
||||
|
||||
/// <summary>
|
||||
/// Optional options used when creating the <see cref="AgentClient"/>.
|
||||
/// </summary>
|
||||
public AgentClientOptions? ClientOptions { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<string> CreateConversationAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -78,10 +84,11 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
string? agentVersion,
|
||||
string? conversationId,
|
||||
IEnumerable<ChatMessage>? messages,
|
||||
IDictionary<string, object?>? inputArguments,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
AgentVersion agentDefinition = await this.QueryAgentAsync(agentId, agentVersion, cancellationToken).ConfigureAwait(false);
|
||||
AIAgent agent = await this.GetAgentAsync(agentDefinition, cancellationToken).ConfigureAwait(false);
|
||||
AgentVersion agentVersionResult = await this.QueryAgentAsync(agentId, agentVersion, cancellationToken).ConfigureAwait(false);
|
||||
AIAgent agent = await this.GetAgentAsync(agentVersionResult, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
ChatOptions chatOptions =
|
||||
new()
|
||||
@@ -90,6 +97,14 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
AllowMultipleToolCalls = this.AllowMultipleToolCalls,
|
||||
};
|
||||
|
||||
if (inputArguments is not null)
|
||||
{
|
||||
JsonNode jsonNode = ConvertDictionaryToJson(inputArguments);
|
||||
ResponseCreationOptions responseCreationOptions = new();
|
||||
responseCreationOptions.SetStructuredInputs(BinaryData.FromString(jsonNode.ToJsonString()));
|
||||
chatOptions.RawRepresentationFactory = (_) => responseCreationOptions;
|
||||
}
|
||||
|
||||
ChatClientAgentRunOptions runOptions = new(chatOptions);
|
||||
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentResponse =
|
||||
@@ -99,7 +114,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
|
||||
await foreach (AgentRunResponseUpdate update in agentResponse.ConfigureAwait(false))
|
||||
{
|
||||
update.AuthorName = agentDefinition.Name;
|
||||
update.AuthorName = agentVersionResult.Name;
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
@@ -146,16 +161,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
|
||||
AgentClient client = this.GetAgentClient();
|
||||
|
||||
IList<AITool>? tools = null;
|
||||
if (agentVersion.Definition is PromptAgentDefinition promptAgent)
|
||||
{
|
||||
tools =
|
||||
promptAgent.Tools
|
||||
.Select(tool => tool.AsAITool())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
agent = client.GetAIAgent(agentVersion, tools, clientFactory: null, openAIClientOptions: null, services: null, cancellationToken);
|
||||
agent = client.GetAIAgent(agentVersion, tools: null, clientFactory: null, openAIClientOptions: null, services: null, cancellationToken);
|
||||
|
||||
FunctionInvokingChatClient? functionInvokingClient = agent.GetService<FunctionInvokingChatClient>();
|
||||
if (functionInvokingClient is not null)
|
||||
@@ -215,7 +221,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
{
|
||||
if (this._agentClient is null)
|
||||
{
|
||||
AgentClientOptions clientOptions = new();
|
||||
AgentClientOptions clientOptions = this.ClientOptions ?? new();
|
||||
|
||||
if (httpClient is not null)
|
||||
{
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<NoWarn>$(NoWarn);MEAI001;OPENAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Microsoft Agent Framework Declarative Workflows Azure AI</Title>
|
||||
<Description>Provides Microsoft Agent Framework support for declarative workflows for Azure AI Agents.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.Workflows.Declarative.UnitTests" />
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-1
@@ -17,9 +17,10 @@ internal static class AgentProviderExtensions
|
||||
string? conversationId,
|
||||
bool autoSend,
|
||||
IEnumerable<ChatMessage>? inputMessages = null,
|
||||
IDictionary<string, object?>? inputArguments = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentUpdates = agentProvider.InvokeAgentAsync(agentName, null, conversationId, inputMessages, cancellationToken);
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentUpdates = agentProvider.InvokeAgentAsync(agentName, null, conversationId, inputMessages, inputArguments, cancellationToken);
|
||||
|
||||
// Enable "autoSend" behavior if this is the workflow conversation.
|
||||
bool isWorkflowConversation = context.IsWorkflowConversation(conversationId, out string? workflowConversationId);
|
||||
|
||||
+18
-17
@@ -163,6 +163,24 @@ internal static class FormulaValueExtensions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static JsonNode ToJson(this FormulaValue value) =>
|
||||
value switch
|
||||
{
|
||||
BooleanValue booleanValue => JsonValue.Create(booleanValue.Value),
|
||||
DecimalValue decimalValue => JsonValue.Create(decimalValue.Value),
|
||||
NumberValue numberValue => JsonValue.Create(numberValue.Value),
|
||||
DateValue dateValue => JsonValue.Create(dateValue.GetConvertedValue(TimeZoneInfo.Utc)),
|
||||
DateTimeValue datetimeValue => JsonValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)),
|
||||
TimeValue timeValue => JsonValue.Create($"{timeValue.Value}"),
|
||||
StringValue stringValue => JsonValue.Create(stringValue.Value),
|
||||
GuidValue guidValue => JsonValue.Create(guidValue.Value),
|
||||
RecordValue recordValue => recordValue.ToJson(),
|
||||
TableValue tableValue => tableValue.ToJson(),
|
||||
BlankValue => JsonValue.Create(string.Empty),
|
||||
_ => $"[{value.GetType().Name}]",
|
||||
};
|
||||
|
||||
public static RecordValue ToRecord(this Dictionary<string, PortableValue> value) =>
|
||||
FormulaValue.NewRecordFromFields(
|
||||
value.Select(
|
||||
@@ -256,23 +274,6 @@ internal static class FormulaValueExtensions
|
||||
|
||||
private static KeyValuePair<string, DataValue> GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.ToDataValue());
|
||||
|
||||
private static JsonNode ToJson(this FormulaValue value) =>
|
||||
value switch
|
||||
{
|
||||
BooleanValue booleanValue => JsonValue.Create(booleanValue.Value),
|
||||
DecimalValue decimalValue => JsonValue.Create(decimalValue.Value),
|
||||
NumberValue numberValue => JsonValue.Create(numberValue.Value),
|
||||
DateValue dateValue => JsonValue.Create(dateValue.GetConvertedValue(TimeZoneInfo.Utc)),
|
||||
DateTimeValue datetimeValue => JsonValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)),
|
||||
TimeValue timeValue => JsonValue.Create($"{timeValue.Value}"),
|
||||
StringValue stringValue => JsonValue.Create(stringValue.Value),
|
||||
GuidValue guidValue => JsonValue.Create(guidValue.Value),
|
||||
RecordValue recordValue => recordValue.ToJson(),
|
||||
TableValue tableValue => tableValue.ToJson(),
|
||||
BlankValue => JsonValue.Create(string.Empty),
|
||||
_ => $"[{value.GetType().Name}]",
|
||||
};
|
||||
|
||||
private static JsonArray ToJson(this TableValue value)
|
||||
{
|
||||
return new([.. GetJsonElements()]);
|
||||
|
||||
@@ -33,5 +33,5 @@ public abstract class AgentExecutor(string id, FormulaSession session, WorkflowA
|
||||
bool autoSend,
|
||||
IEnumerable<ChatMessage>? inputMessages = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, inputMessages, cancellationToken);
|
||||
=> agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, inputMessages, inputArguments: null, cancellationToken);
|
||||
}
|
||||
|
||||
-2
@@ -22,7 +22,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
|
||||
@@ -34,7 +33,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+19
-2
@@ -60,8 +60,8 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
string? conversationId = this.GetConversationId();
|
||||
string agentName = this.GetAgentName();
|
||||
bool autoSend = this.GetAutoSendValue();
|
||||
|
||||
AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, messages, cancellationToken).ConfigureAwait(false);
|
||||
Dictionary<string, object?>? inputParameters = this.GetStructuredInputs();
|
||||
AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, messages, inputParameters, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
ChatMessage[] actionableMessages = FilterActionableContent(agentResponse).ToArray();
|
||||
if (actionableMessages.Length > 0)
|
||||
@@ -107,6 +107,23 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Dictionary<string, object?>? GetStructuredInputs()
|
||||
{
|
||||
Dictionary<string, object?>? inputs = null;
|
||||
|
||||
if (this.AgentInput?.Arguments is not null)
|
||||
{
|
||||
inputs = [];
|
||||
|
||||
foreach (KeyValuePair<string, ValueExpression> argument in this.AgentInput.Arguments)
|
||||
{
|
||||
inputs[argument.Key] = this.Evaluator.GetValue(argument.Value).Value.ToObject();
|
||||
}
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
private IEnumerable<ChatMessage>? GetInputMessages()
|
||||
{
|
||||
DataValue? userInput = null;
|
||||
|
||||
+2
-5
@@ -8,7 +8,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
|
||||
@@ -17,17 +16,15 @@ internal sealed class SetVariableExecutor(SetVariable model, WorkflowFormulaStat
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
PropertyPath variablePath = Throw.IfNull(this.Model.Variable?.Path, $"{nameof(this.Model)}.{nameof(model.Variable)}");
|
||||
|
||||
if (this.Model.Value is null)
|
||||
{
|
||||
await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false);
|
||||
await this.AssignAsync(this.Model.Variable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
EvaluationResult<DataValue> expressionResult = this.Evaluator.GetValue(this.Model.Value);
|
||||
|
||||
await this.AssignAsync(variablePath, expressionResult.Value.ToFormula(), context).ConfigureAwait(false);
|
||||
await this.AssignAsync(this.Model.Variable?.Path, expressionResult.Value.ToFormula(), context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
|
||||
|
||||
internal sealed class AgentMessage : MessageFunction
|
||||
{
|
||||
public const string FunctionName = nameof(AgentMessage);
|
||||
|
||||
public AgentMessage() : base(FunctionName) { }
|
||||
|
||||
public static FormulaValue Execute(StringValue input) => Create(ChatRole.Assistant, input);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
|
||||
|
||||
internal abstract class MessageFunction : ReflectionFunction
|
||||
{
|
||||
protected MessageFunction(string functionName)
|
||||
: base(functionName, FormulaType.String, FormulaType.String)
|
||||
{ }
|
||||
|
||||
protected static FormulaValue Create(ChatRole role, StringValue input) =>
|
||||
string.IsNullOrEmpty(input.Value) ?
|
||||
FormulaValue.NewBlank(RecordType.Empty()) :
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue(TypeSchema.Discriminator, nameof(ChatMessage).ToFormula()),
|
||||
new NamedValue(TypeSchema.Message.Fields.Role, FormulaValue.New(role.Value)),
|
||||
new NamedValue(
|
||||
TypeSchema.Message.Fields.Content,
|
||||
FormulaValue.NewTable(
|
||||
RecordType.Empty()
|
||||
.Add(TypeSchema.Message.Fields.ContentType, FormulaType.String)
|
||||
.Add(TypeSchema.Message.Fields.ContentValue, FormulaType.String),
|
||||
[
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue(TypeSchema.Message.Fields.ContentType, FormulaValue.New(TypeSchema.Message.ContentTypes.Text)),
|
||||
new NamedValue(TypeSchema.Message.Fields.ContentValue, input))
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
|
||||
|
||||
internal static class MessageText
|
||||
{
|
||||
public const string FunctionName = nameof(MessageText);
|
||||
|
||||
public sealed class StringInput()
|
||||
: ReflectionFunction(FunctionName, FormulaType.String, FormulaType.String)
|
||||
{
|
||||
public static FormulaValue Execute(StringValue input) => input;
|
||||
}
|
||||
|
||||
public sealed class RecordInput() : ReflectionFunction(FunctionName, FormulaType.String, RecordType.Empty())
|
||||
{
|
||||
public static FormulaValue Execute(RecordValue input) => FormulaValue.New(GetTextFromRecord(input));
|
||||
}
|
||||
|
||||
public sealed class TableInput() : ReflectionFunction(FunctionName, FormulaType.String, TableType.Empty())
|
||||
{
|
||||
public static FormulaValue Execute(TableValue tableValue)
|
||||
{
|
||||
return FormulaValue.New(string.Join("\n", GetText()));
|
||||
|
||||
IEnumerable<string> GetText()
|
||||
{
|
||||
foreach (DValue<RecordValue> row in tableValue.Rows)
|
||||
{
|
||||
string text = GetTextFromRecord(row.Value);
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
yield return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetTextFromRecord(RecordValue recordValue)
|
||||
{
|
||||
FormulaValue textValue = recordValue.GetField(TypeSchema.Message.Fields.Text);
|
||||
|
||||
return textValue switch
|
||||
{
|
||||
StringValue stringValue => stringValue.Value.Trim(),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
+3
-26
@@ -1,38 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
|
||||
|
||||
internal sealed class UserMessage : ReflectionFunction
|
||||
internal sealed class UserMessage : MessageFunction
|
||||
{
|
||||
public const string FunctionName = nameof(UserMessage);
|
||||
|
||||
public UserMessage()
|
||||
: base(FunctionName, FormulaType.String, FormulaType.String)
|
||||
{ }
|
||||
public UserMessage() : base(FunctionName) { }
|
||||
|
||||
public static FormulaValue Execute(StringValue input) =>
|
||||
string.IsNullOrEmpty(input.Value) ?
|
||||
FormulaValue.NewBlank(RecordType.Empty()) :
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue(TypeSchema.Discriminator, nameof(ChatMessage).ToFormula()),
|
||||
new NamedValue(TypeSchema.Message.Fields.Role, FormulaValue.New(ChatRole.User.Value)),
|
||||
new NamedValue(
|
||||
TypeSchema.Message.Fields.Content,
|
||||
FormulaValue.NewTable(
|
||||
RecordType.Empty()
|
||||
.Add(TypeSchema.Message.Fields.ContentType, FormulaType.String)
|
||||
.Add(TypeSchema.Message.Fields.ContentValue, FormulaType.String),
|
||||
[
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue(TypeSchema.Message.Fields.ContentType, FormulaValue.New(TypeSchema.Message.ContentTypes.Text)),
|
||||
new NamedValue(TypeSchema.Message.Fields.ContentValue, input))
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
public static FormulaValue Execute(StringValue input) => Create(ChatRole.User, input);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,11 @@ internal static class RecalcEngineFactory
|
||||
}
|
||||
|
||||
config.EnableSetFunction();
|
||||
config.AddFunction(new AgentMessage());
|
||||
config.AddFunction(new UserMessage());
|
||||
config.AddFunction(new MessageText.StringInput());
|
||||
config.AddFunction(new MessageText.RecordInput());
|
||||
config.AddFunction(new MessageText.TableInput());
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative;
|
||||
@@ -87,9 +89,16 @@ public abstract class WorkflowAgentProvider
|
||||
/// <param name="agentVersion">An optional agent version.</param>
|
||||
/// <param name="conversationId">Optional identifier of the target conversation.</param>
|
||||
/// <param name="messages">The messages to include in the invocation.</param>
|
||||
/// <param name="inputArguments">Optional input arguments for agents that provide support.</param>
|
||||
/// <param name="cancellationToken">A token that propagates notification when operation should be canceled.</param>
|
||||
/// <returns>Asynchronous set of <see cref="AgentRunResponseUpdate"/>.</returns>
|
||||
public abstract IAsyncEnumerable<AgentRunResponseUpdate> InvokeAgentAsync(string agentId, string? agentVersion, string? conversationId, IEnumerable<ChatMessage>? messages, CancellationToken cancellationToken = default);
|
||||
public abstract IAsyncEnumerable<AgentRunResponseUpdate> InvokeAgentAsync(
|
||||
string agentId,
|
||||
string? agentVersion,
|
||||
string? conversationId,
|
||||
IEnumerable<ChatMessage>? messages,
|
||||
IDictionary<string, object?>? inputArguments,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a set of messages from a conversation.
|
||||
@@ -108,4 +117,14 @@ public abstract class WorkflowAgentProvider
|
||||
string? before = null,
|
||||
bool newestFirst = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Utility method to convert a dictionary of input arguments to a JsonNode.
|
||||
/// </summary>
|
||||
/// <param name="inputArguments">The dictionary of input arguments.</param>
|
||||
/// <returns>A JsonNode representing the input arguments.</returns>
|
||||
protected static JsonNode ConvertDictionaryToJson(IDictionary<string, object?> inputArguments)
|
||||
{
|
||||
return inputArguments.ToFormula().ToJson();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration options for <see cref="ChatForwardingExecutor"/>.
|
||||
/// </summary>
|
||||
public class ChatForwardingExecutorOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the chat role to use when converting string messages to <see cref="ChatMessage"/> instances.
|
||||
/// If set, the executor will accept string messages and convert them to chat messages with this role.
|
||||
/// </summary>
|
||||
public ChatRole? StringMessageChatRole { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A ChatProtocol executor that forwards all messages it receives. Useful for splitting inputs into parallel
|
||||
/// processing paths.
|
||||
/// </summary>
|
||||
/// <remarks>This executor is designed to be cross-run shareable and can be reset to its initial state. It handles
|
||||
/// multiple chat-related types, enabling flexible message forwarding scenarios. Thread safety and reusability are
|
||||
/// ensured by its design.</remarks>
|
||||
/// <param name="id">The unique identifier for the executor instance. Used to distinguish this executor within the system.</param>
|
||||
/// <param name="options">Optional configuration settings for the executor. If null, default options are used.</param>
|
||||
public sealed class ChatForwardingExecutor(string id, ChatForwardingExecutorOptions? options = null) : Executor(id, declareCrossRunShareable: true), IResettableExecutor
|
||||
{
|
||||
private readonly ChatRole? _stringMessageChatRole = options?.StringMessageChatRole;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
|
||||
{
|
||||
if (this._stringMessageChatRole.HasValue)
|
||||
{
|
||||
routeBuilder = routeBuilder.AddHandler<string>(
|
||||
(message, context) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message)));
|
||||
}
|
||||
|
||||
return routeBuilder.AddHandler<ChatMessage>(ForwardMessageAsync)
|
||||
.AddHandler<IEnumerable<ChatMessage>>(ForwardMessagesAsync)
|
||||
.AddHandler<ChatMessage[]>(ForwardMessagesAsync)
|
||||
.AddHandler<List<ChatMessage>>(ForwardMessagesAsync)
|
||||
.AddHandler<TurnToken>(ForwardTurnTokenAsync);
|
||||
}
|
||||
|
||||
private static ValueTask ForwardMessageAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(message, cancellationToken);
|
||||
|
||||
// Note that this can be used to split a turn into multiple parallel turns taken, which will cause streaming ChatMessages
|
||||
// to overlap.
|
||||
private static ValueTask ForwardTurnTokenAsync(TurnToken message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(message, cancellationToken);
|
||||
|
||||
// TODO: This is not ideal, but until we have a way of guaranteeing correct routing of interfaces across serialization
|
||||
// boundaries, we need to do type unification. It behaves better when used as a handler in ChatProtocolExecutor because
|
||||
// it is a strictly contravariant use, whereas this forces invariance on the type because it is directly forwarded.
|
||||
private static ValueTask ForwardMessagesAsync(IEnumerable<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(messages is List<ChatMessage> messageList ? messageList : messages.ToList(), cancellationToken);
|
||||
|
||||
private static ValueTask ForwardMessagesAsync(ChatMessage[] messages, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(messages, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask ResetAsync() => default;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Specialized;
|
||||
|
||||
/// <summary>Executor that forwards all messages.</summary>
|
||||
internal sealed class ChatForwardingExecutor(string id) : Executor(id, declareCrossRunShareable: true), IResettableExecutor
|
||||
{
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
|
||||
routeBuilder
|
||||
.AddHandler<string>((message, context, cancellationToken) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message), cancellationToken: cancellationToken))
|
||||
.AddHandler<ChatMessage>((message, context, cancellationToken) => context.SendMessageAsync(message, cancellationToken: cancellationToken))
|
||||
.AddHandler<List<ChatMessage>>((messages, context, cancellationToken) => context.SendMessageAsync(messages, cancellationToken: cancellationToken))
|
||||
.AddHandler<TurnToken>((turnToken, context, cancellationToken) => context.SendMessageAsync(turnToken, cancellationToken: cancellationToken));
|
||||
|
||||
public ValueTask ResetAsync() => default;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
launchSettings.json
|
||||
@@ -18,7 +18,12 @@ public class AgentRunOptionsTests
|
||||
var options = new AgentRunOptions
|
||||
{
|
||||
ContinuationToken = new object(),
|
||||
AllowBackgroundResponses = true
|
||||
AllowBackgroundResponses = true,
|
||||
AdditionalProperties = new AdditionalPropertiesDictionary
|
||||
{
|
||||
["key1"] = "value1",
|
||||
["key2"] = 42
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -28,6 +33,10 @@ public class AgentRunOptionsTests
|
||||
Assert.NotNull(clone);
|
||||
Assert.Same(options.ContinuationToken, clone.ContinuationToken);
|
||||
Assert.Equal(options.AllowBackgroundResponses, clone.AllowBackgroundResponses);
|
||||
Assert.NotNull(clone.AdditionalProperties);
|
||||
Assert.NotSame(options.AdditionalProperties, clone.AdditionalProperties);
|
||||
Assert.Equal("value1", clone.AdditionalProperties["key1"]);
|
||||
Assert.Equal(42, clone.AdditionalProperties["key2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -42,7 +51,12 @@ public class AgentRunOptionsTests
|
||||
var options = new AgentRunOptions
|
||||
{
|
||||
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
|
||||
AllowBackgroundResponses = true
|
||||
AllowBackgroundResponses = true,
|
||||
AdditionalProperties = new AdditionalPropertiesDictionary
|
||||
{
|
||||
["key1"] = "value1",
|
||||
["key2"] = 42
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -54,5 +68,13 @@ public class AgentRunOptionsTests
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), deserialized!.ContinuationToken);
|
||||
Assert.Equal(options.AllowBackgroundResponses, deserialized.AllowBackgroundResponses);
|
||||
Assert.NotNull(deserialized.AdditionalProperties);
|
||||
Assert.Equal(2, deserialized.AdditionalProperties.Count);
|
||||
Assert.True(deserialized.AdditionalProperties.TryGetValue("key1", out object? value1));
|
||||
Assert.IsType<JsonElement>(value1);
|
||||
Assert.Equal("value1", ((JsonElement)value1!).GetString());
|
||||
Assert.True(deserialized.AdditionalProperties.TryGetValue("key2", out object? value2));
|
||||
Assert.IsType<JsonElement>(value2);
|
||||
Assert.Equal(42, ((JsonElement)value2!).GetInt32());
|
||||
}
|
||||
}
|
||||
|
||||
+141
-13
@@ -504,10 +504,10 @@ public sealed class AgentClientExtensionsTests
|
||||
#region GetAIAgent(AgentClient, AgentRecord) with tools Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verify that GetAIAgent with tools parameter passes tools to the agent.
|
||||
/// Verify that GetAIAgent with additional tools when the definition has no tools does not throw and results in an agent with no tools.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetAIAgent_WithAgentRecordAndTools_PassesToolsToAgent()
|
||||
public void GetAIAgent_WithAgentRecordAndAdditionalTools_WhenDefinitionHasNoTools_ShouldNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
AgentClient client = this.CreateTestAgentClient();
|
||||
@@ -527,6 +527,8 @@ public sealed class AgentClientExtensionsTests
|
||||
Assert.NotNull(chatClient);
|
||||
var agentVersion = chatClient.GetService<AgentVersion>();
|
||||
Assert.NotNull(agentVersion);
|
||||
var definition = Assert.IsType<PromptAgentDefinition>(agentVersion.Definition);
|
||||
Assert.Empty(definition.Tools);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -974,6 +976,88 @@ public sealed class AgentClientExtensionsTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgentAsync when AI Tools are provided, uses them for the definition via http request.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CreateAIAgentAsync_WithNameAndAITools_SendsToolDefinitionViaHttpAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var httpHandler = new HttpHandlerAssert(async (request) =>
|
||||
{
|
||||
if (request.Content is not null)
|
||||
{
|
||||
var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
|
||||
Assert.Contains("required_tool", requestBody);
|
||||
}
|
||||
|
||||
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AgentVersionTestJsonObject, Encoding.UTF8, "application/json") };
|
||||
});
|
||||
|
||||
#pragma warning disable CA5399
|
||||
using var httpClient = new HttpClient(httpHandler);
|
||||
#pragma warning restore CA5399
|
||||
|
||||
var client = new AgentClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
|
||||
|
||||
// Act
|
||||
var agent = await client.CreateAIAgentAsync(
|
||||
name: "test-agent",
|
||||
model: "test-model",
|
||||
instructions: "Test",
|
||||
tools: [AIFunctionFactory.Create(() => true, "required_tool")]);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.IsType<ChatClientAgent>(agent);
|
||||
var agentVersion = agent.GetService<AgentVersion>();
|
||||
Assert.NotNull(agentVersion);
|
||||
Assert.IsType<PromptAgentDefinition>(agentVersion.Definition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent when AI Tools are provided, uses them for the definition via http request.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithNameAndAITools_SendsToolDefinitionViaHttp()
|
||||
{
|
||||
// Arrange
|
||||
using var httpHandler = new HttpHandlerAssert((request) =>
|
||||
{
|
||||
if (request.Content is not null)
|
||||
{
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
var requestBody = request.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
|
||||
Assert.Contains("required_tool", requestBody);
|
||||
}
|
||||
|
||||
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AgentVersionTestJsonObject, Encoding.UTF8, "application/json") };
|
||||
});
|
||||
|
||||
#pragma warning disable CA5399
|
||||
using var httpClient = new HttpClient(httpHandler);
|
||||
#pragma warning restore CA5399
|
||||
|
||||
var client = new AgentClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
|
||||
|
||||
// Act
|
||||
var agent = client.CreateAIAgent(
|
||||
name: "test-agent",
|
||||
model: "test-model",
|
||||
instructions: "Test",
|
||||
tools: [AIFunctionFactory.Create(() => true, "required_tool")]);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.IsType<ChatClientAgent>(agent);
|
||||
var agentVersion = agent.GetService<AgentVersion>();
|
||||
Assert.NotNull(agentVersion);
|
||||
Assert.IsType<PromptAgentDefinition>(agentVersion.Definition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent without tools creates an agent successfully.
|
||||
/// </summary>
|
||||
@@ -997,10 +1081,10 @@ public sealed class AgentClientExtensionsTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that GetAIAgent with inline tools in agent definition throws ArgumentException.
|
||||
/// Verify that when providing AITools with GetAIAgent, any additional tool that doesn't match the tools in agent definition are ignored.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetAIAgent_WithInlineToolsInDefinition_ThrowsArgumentException()
|
||||
public void GetAIAgent_AdditionalAITools_WhenNotInTheDefinitionAreIgnored()
|
||||
{
|
||||
// Arrange
|
||||
AgentClient client = this.CreateTestAgentClient();
|
||||
@@ -1012,11 +1096,20 @@ public sealed class AgentClientExtensionsTests
|
||||
promptDef.Tools.Add(ResponseTool.CreateFunctionTool("inline_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
|
||||
}
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentException>(() =>
|
||||
client.GetAIAgent(agentVersion));
|
||||
var invocableInlineAITool = AIFunctionFactory.Create(() => "test", "inline_tool", "An invocable AIFunction for the inline function");
|
||||
var shouldBeIgnoredTool = AIFunctionFactory.Create(() => "test", "additional_tool", "An additional test function that should be ignored");
|
||||
|
||||
Assert.Contains("tools parameter", exception.Message);
|
||||
// Act & Assert
|
||||
var agent = client.GetAIAgent(agentVersion, tools: [invocableInlineAITool, shouldBeIgnoredTool]);
|
||||
Assert.NotNull(agent);
|
||||
var version = agent.GetService<AgentVersion>();
|
||||
Assert.NotNull(version);
|
||||
var definition = Assert.IsType<PromptAgentDefinition>(version.Definition);
|
||||
Assert.NotEmpty(definition.Tools);
|
||||
Assert.NotNull(GetAgentChatOptions(agent));
|
||||
Assert.NotNull(GetAgentChatOptions(agent)!.Tools);
|
||||
Assert.Single(GetAgentChatOptions(agent)!.Tools!);
|
||||
Assert.Equal("inline_tool", (definition.Tools.First() as FunctionTool)?.FunctionName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -2174,19 +2267,54 @@ public sealed class AgentClientExtensionsTests
|
||||
|
||||
#endregion
|
||||
|
||||
private sealed class HttpHandlerAssert(Func<HttpRequestMessage, HttpResponseMessage> assertion) : HttpClientHandler
|
||||
private sealed class HttpHandlerAssert : HttpClientHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
private readonly Func<HttpRequestMessage, HttpResponseMessage>? _assertion;
|
||||
private readonly Func<HttpRequestMessage, Task<HttpResponseMessage>>? _assertionAsync;
|
||||
|
||||
public HttpHandlerAssert(Func<HttpRequestMessage, HttpResponseMessage> assertion)
|
||||
{
|
||||
var response = assertion(request);
|
||||
return Task.FromResult(response);
|
||||
this._assertion = assertion;
|
||||
}
|
||||
public HttpHandlerAssert(Func<HttpRequestMessage, Task<HttpResponseMessage>> assertionAsync)
|
||||
{
|
||||
this._assertionAsync = assertionAsync;
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this._assertionAsync is not null)
|
||||
{
|
||||
return await this._assertionAsync.Invoke(request);
|
||||
}
|
||||
|
||||
return this._assertion!.Invoke(request);
|
||||
}
|
||||
|
||||
#if NET
|
||||
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
return assertion(request);
|
||||
return this._assertion!(request);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to access internal ChatOptions property via reflection.
|
||||
/// </summary>
|
||||
private static ChatOptions? GetAgentChatOptions(ChatClientAgent agent)
|
||||
{
|
||||
if (agent is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var chatOptionsProperty = typeof(ChatClientAgent).GetProperty(
|
||||
"ChatOptions",
|
||||
System.Reflection.BindingFlags.Public |
|
||||
System.Reflection.BindingFlags.NonPublic |
|
||||
System.Reflection.BindingFlags.Instance);
|
||||
|
||||
return chatOptionsProperty?.GetValue(agent) as ChatOptions;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -15,6 +15,7 @@ internal abstract class AgentProvider(IConfiguration configuration)
|
||||
public const string FunctionTool = "FUNCTIONTOOL";
|
||||
public const string Marketing = "MARKETING";
|
||||
public const string MathChat = "MATHCHAT";
|
||||
public const string InputArguments = "INPUTARGUMENTS";
|
||||
}
|
||||
|
||||
public static class Settings
|
||||
@@ -31,6 +32,7 @@ internal abstract class AgentProvider(IConfiguration configuration)
|
||||
Names.FunctionTool => new FunctionToolAgentProvider(configuration),
|
||||
Names.Marketing => new MarketingAgentProvider(configuration),
|
||||
Names.MathChat => new MathChatAgentProvider(configuration),
|
||||
Names.InputArguments => new PoemAgentProvider(configuration),
|
||||
_ => new TestAgentProvider(configuration),
|
||||
};
|
||||
|
||||
@@ -40,7 +42,7 @@ internal abstract class AgentProvider(IConfiguration configuration)
|
||||
|
||||
await foreach (AgentVersion agent in this.CreateAgentsAsync(foundryEndpoint))
|
||||
{
|
||||
Console.WriteLine($"Created agent: {agent.Name}:{agent.Version})");
|
||||
Console.WriteLine($"Created agent: {agent.Name}:{agent.Version}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shared.Foundry;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
|
||||
|
||||
internal sealed class PoemAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
|
||||
{
|
||||
protected override async IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
|
||||
{
|
||||
AgentClient agentClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
|
||||
yield return
|
||||
await agentClient.CreateAgentAsync(
|
||||
agentName: "PoemAgent",
|
||||
agentDefinition: this.DefinePoemAgent(),
|
||||
agentDescription: "Authors original poems");
|
||||
}
|
||||
|
||||
private PromptAgentDefinition DefinePoemAgent() =>
|
||||
new(this.GetSetting(Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
Write a one verse poem on the requested topic in the style of: {{style}}.
|
||||
""",
|
||||
StructuredInputs =
|
||||
{
|
||||
["style"] =
|
||||
new StructuredInputDefinition
|
||||
{
|
||||
IsRequired = false,
|
||||
DefaultValue = BinaryData.FromString(@"""haiku"""),
|
||||
Description = "The style of poem to write",
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+4
-3
@@ -17,11 +17,12 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("CheckSystem.yaml", "CheckSystem.json")]
|
||||
[InlineData("SendActivity.yaml", "SendActivity.json")]
|
||||
[InlineData("InvokeAgent.yaml", "InvokeAgent.json")]
|
||||
[InlineData("InvokeAgent.yaml", "InvokeAgent.json", true)]
|
||||
[InlineData("ConversationMessages.yaml", "ConversationMessages.json")]
|
||||
[InlineData("ConversationMessages.yaml", "ConversationMessages.json", true)]
|
||||
[InlineData("InputArguments.yaml", "InputArguments.json")]
|
||||
[InlineData("InvokeAgent.yaml", "InvokeAgent.json")]
|
||||
[InlineData("InvokeAgent.yaml", "InvokeAgent.json", true)]
|
||||
[InlineData("SendActivity.yaml", "SendActivity.json")]
|
||||
public Task ValidateCaseAsync(string workflowFileName, string testcaseFileName, bool externalConveration = false) =>
|
||||
this.RunWorkflowAsync(GetWorkflowPath(workflowFileName, isSample: false), testcaseFileName, externalConveration);
|
||||
|
||||
|
||||
+2
@@ -5,6 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectSharedBuildTestCode>true</InjectSharedBuildTestCode>
|
||||
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
|
||||
<InjectSharedIntegrationTestCode>true</InjectSharedIntegrationTestCode>
|
||||
@@ -12,6 +13,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"description": "Authors a poem in the style specified by the input argument.",
|
||||
"setup": {
|
||||
"input": {
|
||||
"type": "String",
|
||||
"value": "Why is the sky blue?"
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"conversation_count": 1,
|
||||
"min_action_count": 1,
|
||||
"min_response_count": 1,
|
||||
"min_message_count": 3,
|
||||
"actions": {
|
||||
"start": [
|
||||
"invoke_poem"
|
||||
],
|
||||
"final": [
|
||||
"invoke_poem"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
"min_response_count": 2,
|
||||
"max_response_count": 8,
|
||||
"min_message_count": 4,
|
||||
"max_message_count": 17,
|
||||
"max_message_count": -1,
|
||||
"actions": {
|
||||
"start": [
|
||||
],
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_test
|
||||
actions:
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_poem
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: PoemAgent
|
||||
input:
|
||||
arguments:
|
||||
style: "ee cummings"
|
||||
+1
@@ -14,6 +14,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx.Functions;
|
||||
|
||||
public sealed class AgentMessageTests
|
||||
{
|
||||
[Fact]
|
||||
public void Construct_Function()
|
||||
{
|
||||
AgentMessage function = new();
|
||||
Assert.NotNull(function);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_ReturnsBlank_ForEmptyInput()
|
||||
{
|
||||
// Arrange
|
||||
StringValue sourceValue = FormulaValue.New(string.Empty);
|
||||
|
||||
// Act
|
||||
FormulaValue result = AgentMessage.Execute(sourceValue);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<BlankValue>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_ReturnsExpectedRecord_ForNonEmptyInput()
|
||||
{
|
||||
const string Text = "Hello";
|
||||
FormulaValue sourceValue = FormulaValue.New(Text);
|
||||
StringValue stringValue = Assert.IsType<StringValue>(sourceValue);
|
||||
|
||||
FormulaValue result = AgentMessage.Execute(stringValue);
|
||||
|
||||
RecordValue recordResult = Assert.IsType<RecordValue>(result, exactMatch: false);
|
||||
|
||||
// Discriminator
|
||||
FormulaValue discriminator = recordResult.GetField(TypeSchema.Discriminator);
|
||||
StringValue discriminatorValue = Assert.IsType<StringValue>(discriminator);
|
||||
Assert.Equal(nameof(ChatMessage), discriminatorValue.Value);
|
||||
|
||||
// Role
|
||||
FormulaValue role = recordResult.GetField(TypeSchema.Message.Fields.Role);
|
||||
StringValue roleValue = Assert.IsType<StringValue>(role);
|
||||
Assert.Equal(ChatRole.Assistant.Value, roleValue.Value);
|
||||
|
||||
// Content table
|
||||
FormulaValue content = recordResult.GetField(TypeSchema.Message.Fields.Content);
|
||||
TableValue table = Assert.IsType<TableValue>(content, exactMatch: false);
|
||||
|
||||
List<RecordValue> rows = table.Rows.Select(value => value.Value).ToList();
|
||||
Assert.Single(rows);
|
||||
|
||||
StringValue contentType = Assert.IsType<StringValue>(rows[0].GetField(TypeSchema.Message.Fields.ContentType));
|
||||
Assert.Equal(TypeSchema.Message.ContentTypes.Text, contentType.Value);
|
||||
|
||||
StringValue contentValue = Assert.IsType<StringValue>(rows[0].GetField(TypeSchema.Message.Fields.ContentValue));
|
||||
Assert.Equal(Text, contentValue.Value);
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx.Functions;
|
||||
|
||||
public sealed class MessageTextTests
|
||||
{
|
||||
[Fact]
|
||||
public void Construct_Function()
|
||||
{
|
||||
MessageText.StringInput function1 = new();
|
||||
Assert.NotNull(function1);
|
||||
|
||||
MessageText.RecordInput function2 = new();
|
||||
Assert.NotNull(function2);
|
||||
|
||||
MessageText.TableInput function3 = new();
|
||||
Assert.NotNull(function3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_ReturnsEmpty_ForEmptyInput()
|
||||
{
|
||||
// Arrange
|
||||
StringValue sourceValue = FormulaValue.New(string.Empty);
|
||||
|
||||
// Act
|
||||
FormulaValue result = MessageText.StringInput.Execute(sourceValue);
|
||||
|
||||
// Assert
|
||||
StringValue stringResult = Assert.IsType<StringValue>(result);
|
||||
Assert.Empty(stringResult.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_ReturnsText_ForStringInput()
|
||||
{
|
||||
// Arrange
|
||||
StringValue sourceValue = FormulaValue.New("wowsie");
|
||||
|
||||
// Act
|
||||
FormulaValue result = MessageText.StringInput.Execute(sourceValue);
|
||||
|
||||
// Assert
|
||||
StringValue stringResult = Assert.IsType<StringValue>(result);
|
||||
Assert.Equal(sourceValue.Value, stringResult.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_ReturnsText_ForMessageInput()
|
||||
{
|
||||
// Arrange
|
||||
RecordValue sourceValue = new ChatMessage(ChatRole.User, "test message").ToRecord();
|
||||
|
||||
// Act
|
||||
FormulaValue result = MessageText.RecordInput.Execute(sourceValue);
|
||||
|
||||
// Assert
|
||||
StringValue stringResult = Assert.IsType<StringValue>(result);
|
||||
Assert.Equal("test message", stringResult.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_ReturnsEmpty_ForUnknownInput()
|
||||
{
|
||||
// Arrange
|
||||
RecordValue sourceValue = FormulaValue.NewRecordFromFields(new NamedValue("Anything", FormulaValue.New(333)));
|
||||
|
||||
// Act
|
||||
FormulaValue result = MessageText.RecordInput.Execute(sourceValue);
|
||||
|
||||
// Assert
|
||||
StringValue stringResult = Assert.IsType<StringValue>(result);
|
||||
Assert.Empty(stringResult.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_ReturnsText_ForMessagesInput()
|
||||
{
|
||||
// Arrange
|
||||
TableValue sourceValue = new ChatMessage[]
|
||||
{
|
||||
new(ChatRole.User, "test message 1"),
|
||||
new(ChatRole.User, "test message 2"),
|
||||
}.ToTable();
|
||||
|
||||
// Act
|
||||
FormulaValue result = MessageText.TableInput.Execute(sourceValue);
|
||||
|
||||
// Assert
|
||||
StringValue stringResult = Assert.IsType<StringValue>(result);
|
||||
Assert.Equal("test message 1\ntest message 2", stringResult.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_ReturnsEmpty_ForEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
TableValue sourceValue = Array.Empty<ChatMessage>().ToTable();
|
||||
|
||||
// Act
|
||||
FormulaValue result = MessageText.TableInput.Execute(sourceValue);
|
||||
|
||||
// Assert
|
||||
StringValue stringResult = Assert.IsType<StringValue>(result);
|
||||
Assert.Empty(stringResult.Value);
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -22,11 +22,10 @@ public class UserMessageTests
|
||||
public void Execute_ReturnsBlank_ForEmptyInput()
|
||||
{
|
||||
// Arrange
|
||||
FormulaValue sourceValue = FormulaValue.New(string.Empty);
|
||||
StringValue stringValue = Assert.IsType<StringValue>(sourceValue);
|
||||
StringValue sourceValue = FormulaValue.New(string.Empty);
|
||||
|
||||
// Act
|
||||
FormulaValue result = UserMessage.Execute(stringValue);
|
||||
FormulaValue result = UserMessage.Execute(sourceValue);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<BlankValue>(result);
|
||||
|
||||
+17
-1
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.0b251111] - 2025-11-11
|
||||
|
||||
### Added
|
||||
|
||||
- **agent-framework-core**: Add OpenAI Responses Image Generation Stream Support with partial images and unit tests ([#1853](https://github.com/microsoft/agent-framework/pull/1853))
|
||||
- **agent-framework-ag-ui**: Add concrete AGUIChatClient implementation ([#2072](https://github.com/microsoft/agent-framework/pull/2072))
|
||||
|
||||
### Fixed
|
||||
|
||||
- **agent-framework-a2a**: Use the last entry in the task history to avoid empty responses ([#2101](https://github.com/microsoft/agent-framework/pull/2101))
|
||||
- **agent-framework-core**: Fix MCP Tool Parameter Descriptions not propagated to LLMs ([#1978](https://github.com/microsoft/agent-framework/pull/1978))
|
||||
- **agent-framework-core**: Handle agent user input request in AgentExecutor ([#2022](https://github.com/microsoft/agent-framework/pull/2022))
|
||||
- **agent-framework-core**: Fix Model ID attribute not showing up in `invoke_agent` span ([#2061](https://github.com/microsoft/agent-framework/pull/2061))
|
||||
- **agent-framework-core**: Fix underlying tool choice bug and enable return to previous Handoff subagent ([#2037](https://github.com/microsoft/agent-framework/pull/2037))
|
||||
|
||||
## [1.0.0b251108] - 2025-11-08
|
||||
|
||||
### Added
|
||||
@@ -189,7 +204,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/).
|
||||
|
||||
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251108...HEAD
|
||||
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251111...HEAD
|
||||
[1.0.0b251111]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251108...python-1.0.0b251111
|
||||
[1.0.0b251108]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251106.post1...python-1.0.0b251108
|
||||
[1.0.0b251106.post1]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251106...python-1.0.0b251106.post1
|
||||
[1.0.0b251106]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251105...python-1.0.0b251106
|
||||
|
||||
@@ -388,6 +388,17 @@ class A2AAgent(BaseAgent):
|
||||
if task.artifacts is not None:
|
||||
for artifact in task.artifacts:
|
||||
messages.append(self._artifact_to_chat_message(artifact))
|
||||
elif task.history is not None and len(task.history) > 0:
|
||||
# Include the last history item as the agent response
|
||||
history_item = task.history[-1]
|
||||
contents = self._a2a_parts_to_contents(history_item.parts)
|
||||
messages.append(
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT if history_item.role == A2ARole.agent else Role.USER,
|
||||
contents=contents,
|
||||
raw_representation=history_item,
|
||||
)
|
||||
)
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "agent-framework-ag-ui"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
description = "AG-UI protocol integration for Agent Framework"
|
||||
readme = "README.md"
|
||||
license-files = ["LICENSE"]
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Azure AI Foundry integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "OpenAI ChatKit integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -19,7 +19,7 @@ from mcp.client.websocket import websocket_client
|
||||
from mcp.shared.context import RequestContext
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.session import RequestResponder
|
||||
from pydantic import BaseModel, create_model
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
|
||||
from ._tools import AIFunction, HostedMCPSpecificApproval
|
||||
from ._types import ChatMessage, Contents, DataContent, Role, TextContent, UriContent
|
||||
@@ -224,13 +224,20 @@ def _get_input_model_from_mcp_tool(tool: types.Tool) -> type[BaseModel]:
|
||||
prop_details = json.loads(prop_details) if isinstance(prop_details, str) else prop_details
|
||||
|
||||
python_type = resolve_type(prop_details)
|
||||
description = prop_details.get("description", "")
|
||||
|
||||
# Create field definition for create_model
|
||||
if prop_name in required:
|
||||
field_definitions[prop_name] = (python_type, ...)
|
||||
field_definitions[prop_name] = (
|
||||
(python_type, Field(description=description)) if description else (python_type, ...)
|
||||
)
|
||||
else:
|
||||
default_value = prop_details.get("default", None)
|
||||
field_definitions[prop_name] = (python_type, default_value)
|
||||
field_definitions[prop_name] = (
|
||||
(python_type, Field(default=default_value, description=description))
|
||||
if description
|
||||
else (python_type, default_value)
|
||||
)
|
||||
|
||||
return create_model(f"{tool.name}_input", **field_definitions)
|
||||
|
||||
|
||||
@@ -1050,6 +1050,50 @@ class DataContent(BaseContent):
|
||||
def has_top_level_media_type(self, top_level_media_type: Literal["application", "audio", "image", "text"]) -> bool:
|
||||
return _has_top_level_media_type(self.media_type, top_level_media_type)
|
||||
|
||||
@staticmethod
|
||||
def detect_image_format_from_base64(image_base64: str) -> str:
|
||||
"""Detect image format from base64 data by examining the binary header.
|
||||
|
||||
Args:
|
||||
image_base64: Base64 encoded image data
|
||||
|
||||
Returns:
|
||||
Image format as string (png, jpeg, webp, gif) with png as fallback
|
||||
"""
|
||||
try:
|
||||
# Constants for image format detection
|
||||
# ~75 bytes of binary data should be enough to detect most image formats
|
||||
FORMAT_DETECTION_BASE64_CHARS = 100
|
||||
|
||||
# Decode a small portion to detect format
|
||||
decoded_data = base64.b64decode(image_base64[:FORMAT_DETECTION_BASE64_CHARS])
|
||||
if decoded_data.startswith(b"\x89PNG"):
|
||||
return "png"
|
||||
if decoded_data.startswith(b"\xff\xd8\xff"):
|
||||
return "jpeg"
|
||||
if decoded_data.startswith(b"RIFF") and b"WEBP" in decoded_data[:12]:
|
||||
return "webp"
|
||||
if decoded_data.startswith(b"GIF87a") or decoded_data.startswith(b"GIF89a"):
|
||||
return "gif"
|
||||
return "png" # Default fallback
|
||||
except Exception:
|
||||
return "png" # Fallback if decoding fails
|
||||
|
||||
@classmethod
|
||||
def create_data_uri_from_base64(cls, image_base64: str) -> tuple[str, str]:
|
||||
"""Create a data URI and media type from base64 image data.
|
||||
|
||||
Args:
|
||||
image_base64: Base64 encoded image data
|
||||
|
||||
Returns:
|
||||
Tuple of (data_uri, media_type)
|
||||
"""
|
||||
format_type = cls.detect_image_format_from_base64(image_base64)
|
||||
uri = f"data:image/{format_type};base64,{image_base64}"
|
||||
media_type = f"image/{format_type}"
|
||||
return uri, media_type
|
||||
|
||||
|
||||
class UriContent(BaseContent):
|
||||
"""Represents a URI content.
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from agent_framework import FunctionApprovalRequestContent, FunctionApprovalResponseContent
|
||||
|
||||
from .._agents import AgentProtocol, ChatAgent
|
||||
from .._threads import AgentThread
|
||||
from .._types import AgentRunResponse, AgentRunResponseUpdate, ChatMessage
|
||||
from ._checkpoint_encoding import decode_checkpoint_value, encode_checkpoint_value
|
||||
from ._conversation_state import encode_chat_messages
|
||||
from ._events import (
|
||||
AgentRunEvent,
|
||||
@@ -14,6 +17,7 @@ from ._events import (
|
||||
)
|
||||
from ._executor import Executor, handler
|
||||
from ._message_utils import normalize_messages_input
|
||||
from ._request_info_mixin import response_handler
|
||||
from ._workflow_context import WorkflowContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -83,6 +87,8 @@ class AgentExecutor(Executor):
|
||||
super().__init__(exec_id)
|
||||
self._agent = agent
|
||||
self._agent_thread = agent_thread or self._agent.get_new_thread()
|
||||
self._pending_agent_requests: dict[str, FunctionApprovalRequestContent] = {}
|
||||
self._pending_responses_to_agent: list[FunctionApprovalResponseContent] = []
|
||||
self._output_response = output_response
|
||||
self._cache: list[ChatMessage] = []
|
||||
|
||||
@@ -93,50 +99,6 @@ class AgentExecutor(Executor):
|
||||
return [AgentRunResponse]
|
||||
return []
|
||||
|
||||
async def _run_agent_and_emit(self, ctx: WorkflowContext[AgentExecutorResponse, AgentRunResponse]) -> None:
|
||||
"""Execute the underlying agent, emit events, and enqueue response.
|
||||
|
||||
Checks ctx.is_streaming() to determine whether to emit incremental AgentRunUpdateEvent
|
||||
events (streaming mode) or a single AgentRunEvent (non-streaming mode).
|
||||
"""
|
||||
if ctx.is_streaming():
|
||||
# Streaming mode: emit incremental updates
|
||||
updates: list[AgentRunResponseUpdate] = []
|
||||
async for update in self._agent.run_stream(
|
||||
self._cache,
|
||||
thread=self._agent_thread,
|
||||
):
|
||||
updates.append(update)
|
||||
await ctx.add_event(AgentRunUpdateEvent(self.id, update))
|
||||
|
||||
if isinstance(self._agent, ChatAgent):
|
||||
response_format = self._agent.chat_options.response_format
|
||||
response = AgentRunResponse.from_agent_run_response_updates(
|
||||
updates,
|
||||
output_format_type=response_format,
|
||||
)
|
||||
else:
|
||||
response = AgentRunResponse.from_agent_run_response_updates(updates)
|
||||
else:
|
||||
# Non-streaming mode: use run() and emit single event
|
||||
response = await self._agent.run(
|
||||
self._cache,
|
||||
thread=self._agent_thread,
|
||||
)
|
||||
await ctx.add_event(AgentRunEvent(self.id, response))
|
||||
|
||||
if self._output_response:
|
||||
await ctx.yield_output(response)
|
||||
|
||||
# Always construct a full conversation snapshot from inputs (cache)
|
||||
# plus agent outputs (agent_run_response.messages). Do not mutate
|
||||
# response.messages so AgentRunEvent remains faithful to the raw output.
|
||||
full_conversation: list[ChatMessage] = list(self._cache) + list(response.messages)
|
||||
|
||||
agent_response = AgentExecutorResponse(self.id, response, full_conversation=full_conversation)
|
||||
await ctx.send_message(agent_response)
|
||||
self._cache.clear()
|
||||
|
||||
@handler
|
||||
async def run(
|
||||
self, request: AgentExecutorRequest, ctx: WorkflowContext[AgentExecutorResponse, AgentRunResponse]
|
||||
@@ -192,6 +154,31 @@ class AgentExecutor(Executor):
|
||||
self._cache = normalize_messages_input(messages)
|
||||
await self._run_agent_and_emit(ctx)
|
||||
|
||||
@response_handler
|
||||
async def handle_user_input_response(
|
||||
self,
|
||||
original_request: FunctionApprovalRequestContent,
|
||||
response: FunctionApprovalResponseContent,
|
||||
ctx: WorkflowContext[AgentExecutorResponse, AgentRunResponse],
|
||||
) -> None:
|
||||
"""Handle user input responses for function approvals during agent execution.
|
||||
|
||||
This will hold the executor's execution until all pending user input requests are resolved.
|
||||
|
||||
Args:
|
||||
original_request: The original function approval request sent by the agent.
|
||||
response: The user's response to the function approval request.
|
||||
ctx: The workflow context for emitting events and outputs.
|
||||
"""
|
||||
self._pending_responses_to_agent.append(response)
|
||||
self._pending_agent_requests.pop(original_request.id, None)
|
||||
|
||||
if not self._pending_agent_requests:
|
||||
# All pending requests have been resolved; resume agent execution
|
||||
self._cache = normalize_messages_input(ChatMessage(role="user", contents=self._pending_responses_to_agent))
|
||||
self._pending_responses_to_agent.clear()
|
||||
await self._run_agent_and_emit(ctx)
|
||||
|
||||
async def snapshot_state(self) -> dict[str, Any]:
|
||||
"""Capture current executor state for checkpointing.
|
||||
|
||||
@@ -226,6 +213,8 @@ class AgentExecutor(Executor):
|
||||
return {
|
||||
"cache": encode_chat_messages(self._cache),
|
||||
"agent_thread": serialized_thread,
|
||||
"pending_agent_requests": encode_checkpoint_value(self._pending_agent_requests),
|
||||
"pending_responses_to_agent": encode_checkpoint_value(self._pending_responses_to_agent),
|
||||
}
|
||||
|
||||
async def restore_state(self, state: dict[str, Any]) -> None:
|
||||
@@ -258,7 +247,109 @@ class AgentExecutor(Executor):
|
||||
else:
|
||||
self._agent_thread = self._agent.get_new_thread()
|
||||
|
||||
pending_requests_payload = state.get("pending_agent_requests")
|
||||
if pending_requests_payload:
|
||||
self._pending_agent_requests = decode_checkpoint_value(pending_requests_payload)
|
||||
|
||||
pending_responses_payload = state.get("pending_responses_to_agent")
|
||||
if pending_responses_payload:
|
||||
self._pending_responses_to_agent = decode_checkpoint_value(pending_responses_payload)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the internal cache of the executor."""
|
||||
logger.debug("AgentExecutor %s: Resetting cache", self.id)
|
||||
self._cache.clear()
|
||||
|
||||
async def _run_agent_and_emit(self, ctx: WorkflowContext[AgentExecutorResponse, AgentRunResponse]) -> None:
|
||||
"""Execute the underlying agent, emit events, and enqueue response.
|
||||
|
||||
Checks ctx.is_streaming() to determine whether to emit incremental AgentRunUpdateEvent
|
||||
events (streaming mode) or a single AgentRunEvent (non-streaming mode).
|
||||
"""
|
||||
if ctx.is_streaming():
|
||||
# Streaming mode: emit incremental updates
|
||||
response = await self._run_agent_streaming(cast(WorkflowContext, ctx))
|
||||
else:
|
||||
# Non-streaming mode: use run() and emit single event
|
||||
response = await self._run_agent(cast(WorkflowContext, ctx))
|
||||
|
||||
if response is None:
|
||||
# Agent did not complete (e.g., waiting for user input); do not emit response
|
||||
logger.info("AgentExecutor %s: Agent did not complete, awaiting user input", self.id)
|
||||
return
|
||||
|
||||
if self._output_response:
|
||||
await ctx.yield_output(response)
|
||||
|
||||
# Always construct a full conversation snapshot from inputs (cache)
|
||||
# plus agent outputs (agent_run_response.messages). Do not mutate
|
||||
# response.messages so AgentRunEvent remains faithful to the raw output.
|
||||
full_conversation: list[ChatMessage] = list(self._cache) + list(response.messages)
|
||||
|
||||
agent_response = AgentExecutorResponse(self.id, response, full_conversation=full_conversation)
|
||||
await ctx.send_message(agent_response)
|
||||
self._cache.clear()
|
||||
|
||||
async def _run_agent(self, ctx: WorkflowContext) -> AgentRunResponse | None:
|
||||
"""Execute the underlying agent in non-streaming mode.
|
||||
|
||||
Args:
|
||||
ctx: The workflow context for emitting events.
|
||||
|
||||
Returns:
|
||||
The complete AgentRunResponse, or None if waiting for user input.
|
||||
"""
|
||||
response = await self._agent.run(
|
||||
self._cache,
|
||||
thread=self._agent_thread,
|
||||
)
|
||||
await ctx.add_event(AgentRunEvent(self.id, response))
|
||||
|
||||
# Handle any user input requests
|
||||
if response.user_input_requests:
|
||||
for user_input_request in response.user_input_requests:
|
||||
self._pending_agent_requests[user_input_request.id] = user_input_request
|
||||
await ctx.request_info(user_input_request, FunctionApprovalResponseContent)
|
||||
return None
|
||||
|
||||
return response
|
||||
|
||||
async def _run_agent_streaming(self, ctx: WorkflowContext) -> AgentRunResponse | None:
|
||||
"""Execute the underlying agent in streaming mode and collect the full response.
|
||||
|
||||
Args:
|
||||
ctx: The workflow context for emitting events.
|
||||
|
||||
Returns:
|
||||
The complete AgentRunResponse, or None if waiting for user input.
|
||||
"""
|
||||
updates: list[AgentRunResponseUpdate] = []
|
||||
user_input_requests: list[FunctionApprovalRequestContent] = []
|
||||
async for update in self._agent.run_stream(
|
||||
self._cache,
|
||||
thread=self._agent_thread,
|
||||
):
|
||||
updates.append(update)
|
||||
await ctx.add_event(AgentRunUpdateEvent(self.id, update))
|
||||
|
||||
if update.user_input_requests:
|
||||
user_input_requests.extend(update.user_input_requests)
|
||||
|
||||
# Build the final AgentRunResponse from the collected updates
|
||||
if isinstance(self._agent, ChatAgent):
|
||||
response_format = self._agent.chat_options.response_format
|
||||
response = AgentRunResponse.from_agent_run_response_updates(
|
||||
updates,
|
||||
output_format_type=response_format,
|
||||
)
|
||||
else:
|
||||
response = AgentRunResponse.from_agent_run_response_updates(updates)
|
||||
|
||||
# Handle any user input requests after the streaming completes
|
||||
if user_input_requests:
|
||||
for user_input_request in user_input_requests:
|
||||
self._pending_agent_requests[user_input_request.id] = user_input_request
|
||||
await ctx.request_info(user_input_request, FunctionApprovalResponseContent)
|
||||
return None
|
||||
|
||||
return response
|
||||
|
||||
@@ -293,6 +293,14 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
# Map the parameter name and remove the old one
|
||||
mapped_tool[api_param] = mapped_tool.pop(user_param)
|
||||
|
||||
# Validate partial_images parameter for streaming image generation
|
||||
# OpenAI API requires partial_images to be between 0-3 (inclusive) for image_generation tool
|
||||
# Reference: https://platform.openai.com/docs/api-reference/responses/create#responses_create-tools-image_generation_tool-partial_images
|
||||
if "partial_images" in mapped_tool:
|
||||
partial_images = mapped_tool["partial_images"]
|
||||
if not isinstance(partial_images, int) or partial_images < 0 or partial_images > 3:
|
||||
raise ValueError("partial_images must be an integer between 0 and 3 (inclusive).")
|
||||
|
||||
response_tools.append(mapped_tool)
|
||||
else:
|
||||
response_tools.append(tool_dict)
|
||||
@@ -695,29 +703,8 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
uri = item.result
|
||||
media_type = None
|
||||
if not uri.startswith("data:"):
|
||||
# Raw base64 string - convert to proper data URI format
|
||||
# Detect format from base64 data
|
||||
import base64
|
||||
|
||||
try:
|
||||
# Decode a small portion to detect format
|
||||
decoded_data = base64.b64decode(uri[:100]) # First ~75 bytes should be enough
|
||||
if decoded_data.startswith(b"\x89PNG"):
|
||||
format_type = "png"
|
||||
elif decoded_data.startswith(b"\xff\xd8\xff"):
|
||||
format_type = "jpeg"
|
||||
elif decoded_data.startswith(b"RIFF") and b"WEBP" in decoded_data[:12]:
|
||||
format_type = "webp"
|
||||
elif decoded_data.startswith(b"GIF87a") or decoded_data.startswith(b"GIF89a"):
|
||||
format_type = "gif"
|
||||
else:
|
||||
# Default to png if format cannot be detected
|
||||
format_type = "png"
|
||||
except Exception:
|
||||
# Fallback to png if decoding fails
|
||||
format_type = "png"
|
||||
uri = f"data:image/{format_type};base64,{uri}"
|
||||
media_type = f"image/{format_type}"
|
||||
# Raw base64 string - convert to proper data URI format using helper
|
||||
uri, media_type = DataContent.create_data_uri_from_base64(uri)
|
||||
else:
|
||||
# Parse media type from existing data URI
|
||||
try:
|
||||
@@ -933,6 +920,25 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
raw_representation=event,
|
||||
)
|
||||
)
|
||||
case "response.image_generation_call.partial_image":
|
||||
# Handle streaming partial image generation
|
||||
image_base64 = event.partial_image_b64
|
||||
partial_index = event.partial_image_index
|
||||
|
||||
# Use helper function to create data URI from base64
|
||||
uri, media_type = DataContent.create_data_uri_from_base64(image_base64)
|
||||
|
||||
contents.append(
|
||||
DataContent(
|
||||
uri=uri,
|
||||
media_type=media_type,
|
||||
additional_properties={
|
||||
"partial_image_index": partial_index,
|
||||
"is_partial_image": True,
|
||||
},
|
||||
raw_representation=event,
|
||||
)
|
||||
)
|
||||
case _:
|
||||
logger.debug("Unparsed event of type: %s: %s", event.type, event)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import base64
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import Any
|
||||
|
||||
@@ -166,6 +167,57 @@ def test_data_content_empty():
|
||||
DataContent(uri="")
|
||||
|
||||
|
||||
def test_data_content_detect_image_format_from_base64():
|
||||
"""Test the detect_image_format_from_base64 static method."""
|
||||
# Test each supported format
|
||||
png_data = b"\x89PNG\r\n\x1a\n" + b"fake_data"
|
||||
assert DataContent.detect_image_format_from_base64(base64.b64encode(png_data).decode()) == "png"
|
||||
|
||||
jpeg_data = b"\xff\xd8\xff\xe0" + b"fake_data"
|
||||
assert DataContent.detect_image_format_from_base64(base64.b64encode(jpeg_data).decode()) == "jpeg"
|
||||
|
||||
webp_data = b"RIFF" + b"1234" + b"WEBP" + b"fake_data"
|
||||
assert DataContent.detect_image_format_from_base64(base64.b64encode(webp_data).decode()) == "webp"
|
||||
|
||||
gif_data = b"GIF89a" + b"fake_data"
|
||||
assert DataContent.detect_image_format_from_base64(base64.b64encode(gif_data).decode()) == "gif"
|
||||
|
||||
# Test fallback behavior
|
||||
unknown_data = b"UNKNOWN_FORMAT"
|
||||
assert DataContent.detect_image_format_from_base64(base64.b64encode(unknown_data).decode()) == "png"
|
||||
|
||||
# Test error handling
|
||||
assert DataContent.detect_image_format_from_base64("invalid_base64!") == "png"
|
||||
assert DataContent.detect_image_format_from_base64("") == "png"
|
||||
|
||||
|
||||
def test_data_content_create_data_uri_from_base64():
|
||||
"""Test the create_data_uri_from_base64 class method."""
|
||||
# Test with PNG data
|
||||
png_data = b"\x89PNG\r\n\x1a\n" + b"fake_data"
|
||||
png_base64 = base64.b64encode(png_data).decode()
|
||||
uri, media_type = DataContent.create_data_uri_from_base64(png_base64)
|
||||
|
||||
assert uri == f"data:image/png;base64,{png_base64}"
|
||||
assert media_type == "image/png"
|
||||
|
||||
# Test with different format
|
||||
jpeg_data = b"\xff\xd8\xff\xe0" + b"fake_data"
|
||||
jpeg_base64 = base64.b64encode(jpeg_data).decode()
|
||||
uri, media_type = DataContent.create_data_uri_from_base64(jpeg_base64)
|
||||
|
||||
assert uri == f"data:image/jpeg;base64,{jpeg_base64}"
|
||||
assert media_type == "image/jpeg"
|
||||
|
||||
# Test fallback for unknown format
|
||||
unknown_data = b"UNKNOWN_FORMAT"
|
||||
unknown_base64 = base64.b64encode(unknown_data).decode()
|
||||
uri, media_type = DataContent.create_data_uri_from_base64(unknown_base64)
|
||||
|
||||
assert uri == f"data:image/png;base64,{unknown_base64}"
|
||||
assert media_type == "image/png"
|
||||
|
||||
|
||||
# region UriContent
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -111,6 +111,10 @@ async def test_agent_executor_checkpoint_stores_and_restores_state() -> None:
|
||||
chat_store_state = thread_state["chat_message_store_state"] # type: ignore[index]
|
||||
assert "messages" in chat_store_state, "Message store state should include messages"
|
||||
|
||||
# Verify checkpoint contains pending requests from agents and responses to be sent
|
||||
assert "pending_agent_requests" in executor_state
|
||||
assert "pending_responses_to_agent" in executor_state
|
||||
|
||||
# Create a new agent and executor for restoration
|
||||
# This simulates starting from a fresh state and restoring from checkpoint
|
||||
restored_agent = _CountingAgent(id="test_agent", name="TestAgent")
|
||||
|
||||
@@ -5,19 +5,32 @@
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Never
|
||||
|
||||
from agent_framework import (
|
||||
AgentExecutor,
|
||||
AgentExecutorResponse,
|
||||
AgentRunResponse,
|
||||
AgentRunResponseUpdate,
|
||||
AgentRunUpdateEvent,
|
||||
AgentThread,
|
||||
BaseAgent,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
RequestInfoEvent,
|
||||
Role,
|
||||
TextContent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
ai_function,
|
||||
executor,
|
||||
use_function_invocation,
|
||||
)
|
||||
|
||||
|
||||
@@ -120,3 +133,235 @@ async def test_agent_executor_emits_tool_calls_in_streaming_mode() -> None:
|
||||
assert events[3].data is not None
|
||||
assert isinstance(events[3].data.contents[0], TextContent)
|
||||
assert "sunny" in events[3].data.contents[0].text
|
||||
|
||||
|
||||
@ai_function(approval_mode="always_require")
|
||||
def mock_tool_requiring_approval(query: str) -> str:
|
||||
"""Mock tool that requires approval before execution."""
|
||||
return f"Executed tool with query: {query}"
|
||||
|
||||
|
||||
@use_function_invocation
|
||||
class MockChatClient:
|
||||
"""Simple implementation of a chat client."""
|
||||
|
||||
def __init__(self, parallel_request: bool = False) -> None:
|
||||
self.additional_properties: dict[str, Any] = {}
|
||||
self._iteration: int = 0
|
||||
self._parallel_request: bool = parallel_request
|
||||
|
||||
async def get_response(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage],
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
if self._iteration == 0:
|
||||
if self._parallel_request:
|
||||
response = ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
call_id="1", name="mock_tool_requiring_approval", arguments='{"query": "test"}'
|
||||
),
|
||||
FunctionCallContent(
|
||||
call_id="2", name="mock_tool_requiring_approval", arguments='{"query": "test"}'
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
response = ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
call_id="1", name="mock_tool_requiring_approval", arguments='{"query": "test"}'
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
response = ChatResponse(messages=ChatMessage(role="assistant", text="Tool executed successfully."))
|
||||
|
||||
self._iteration += 1
|
||||
return response
|
||||
|
||||
async def get_streaming_response(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage],
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
if self._iteration == 0:
|
||||
if self._parallel_request:
|
||||
yield ChatResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
call_id="1", name="mock_tool_requiring_approval", arguments='{"query": "test"}'
|
||||
),
|
||||
FunctionCallContent(
|
||||
call_id="2", name="mock_tool_requiring_approval", arguments='{"query": "test"}'
|
||||
),
|
||||
],
|
||||
role="assistant",
|
||||
)
|
||||
else:
|
||||
yield ChatResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
call_id="1", name="mock_tool_requiring_approval", arguments='{"query": "test"}'
|
||||
)
|
||||
],
|
||||
role="assistant",
|
||||
)
|
||||
else:
|
||||
yield ChatResponseUpdate(text=TextContent(text="Tool executed "), role="assistant")
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="successfully.")], role="assistant")
|
||||
|
||||
self._iteration += 1
|
||||
|
||||
|
||||
@executor(id="test_executor")
|
||||
async def test_executor(agent_executor_response: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:
|
||||
await ctx.yield_output(agent_executor_response.agent_run_response.text)
|
||||
|
||||
|
||||
async def test_agent_executor_tool_call_with_approval() -> None:
|
||||
"""Test that AgentExecutor handles tool calls requiring approval."""
|
||||
# Arrange
|
||||
agent = ChatAgent(
|
||||
chat_client=MockChatClient(),
|
||||
name="ApprovalAgent",
|
||||
tools=[mock_tool_requiring_approval],
|
||||
)
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(agent).add_edge(agent, test_executor).build()
|
||||
|
||||
# Act
|
||||
events = await workflow.run("Invoke tool requiring approval")
|
||||
|
||||
# Assert
|
||||
assert len(events.get_request_info_events()) == 1
|
||||
approval_request = events.get_request_info_events()[0]
|
||||
assert isinstance(approval_request.data, FunctionApprovalRequestContent)
|
||||
assert approval_request.data.function_call.name == "mock_tool_requiring_approval"
|
||||
assert approval_request.data.function_call.arguments == '{"query": "test"}'
|
||||
|
||||
# Act
|
||||
events = await workflow.send_responses({approval_request.request_id: approval_request.data.create_response(True)})
|
||||
|
||||
# Assert
|
||||
final_response = events.get_outputs()
|
||||
assert len(final_response) == 1
|
||||
assert final_response[0] == "Tool executed successfully."
|
||||
|
||||
|
||||
async def test_agent_executor_tool_call_with_approval_streaming() -> None:
|
||||
"""Test that AgentExecutor handles tool calls requiring approval in streaming mode."""
|
||||
# Arrange
|
||||
agent = ChatAgent(
|
||||
chat_client=MockChatClient(),
|
||||
name="ApprovalAgent",
|
||||
tools=[mock_tool_requiring_approval],
|
||||
)
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(agent).add_edge(agent, test_executor).build()
|
||||
|
||||
# Act
|
||||
request_info_events: list[RequestInfoEvent] = []
|
||||
async for event in workflow.run_stream("Invoke tool requiring approval"):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
request_info_events.append(event)
|
||||
|
||||
# Assert
|
||||
assert len(request_info_events) == 1
|
||||
approval_request = request_info_events[0]
|
||||
assert isinstance(approval_request.data, FunctionApprovalRequestContent)
|
||||
assert approval_request.data.function_call.name == "mock_tool_requiring_approval"
|
||||
assert approval_request.data.function_call.arguments == '{"query": "test"}'
|
||||
|
||||
# Act
|
||||
output: str | None = None
|
||||
async for event in workflow.send_responses_streaming({
|
||||
approval_request.request_id: approval_request.data.create_response(True)
|
||||
}):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
output = event.data
|
||||
|
||||
# Assert
|
||||
assert output is not None
|
||||
assert output == "Tool executed successfully."
|
||||
|
||||
|
||||
async def test_agent_executor_parallel_tool_call_with_approval() -> None:
|
||||
"""Test that AgentExecutor handles parallel tool calls requiring approval."""
|
||||
# Arrange
|
||||
agent = ChatAgent(
|
||||
chat_client=MockChatClient(parallel_request=True),
|
||||
name="ApprovalAgent",
|
||||
tools=[mock_tool_requiring_approval],
|
||||
)
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(agent).add_edge(agent, test_executor).build()
|
||||
|
||||
# Act
|
||||
events = await workflow.run("Invoke tool requiring approval")
|
||||
|
||||
# Assert
|
||||
assert len(events.get_request_info_events()) == 2
|
||||
for approval_request in events.get_request_info_events():
|
||||
assert isinstance(approval_request.data, FunctionApprovalRequestContent)
|
||||
assert approval_request.data.function_call.name == "mock_tool_requiring_approval"
|
||||
assert approval_request.data.function_call.arguments == '{"query": "test"}'
|
||||
|
||||
# Act
|
||||
responses = {
|
||||
approval_request.request_id: approval_request.data.create_response(True) # type: ignore
|
||||
for approval_request in events.get_request_info_events()
|
||||
}
|
||||
events = await workflow.send_responses(responses)
|
||||
|
||||
# Assert
|
||||
final_response = events.get_outputs()
|
||||
assert len(final_response) == 1
|
||||
assert final_response[0] == "Tool executed successfully."
|
||||
|
||||
|
||||
async def test_agent_executor_parallel_tool_call_with_approval_streaming() -> None:
|
||||
"""Test that AgentExecutor handles parallel tool calls requiring approval in streaming mode."""
|
||||
# Arrange
|
||||
agent = ChatAgent(
|
||||
chat_client=MockChatClient(parallel_request=True),
|
||||
name="ApprovalAgent",
|
||||
tools=[mock_tool_requiring_approval],
|
||||
)
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(agent).add_edge(agent, test_executor).build()
|
||||
|
||||
# Act
|
||||
request_info_events: list[RequestInfoEvent] = []
|
||||
async for event in workflow.run_stream("Invoke tool requiring approval"):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
request_info_events.append(event)
|
||||
|
||||
# Assert
|
||||
assert len(request_info_events) == 2
|
||||
for approval_request in request_info_events:
|
||||
assert isinstance(approval_request.data, FunctionApprovalRequestContent)
|
||||
assert approval_request.data.function_call.name == "mock_tool_requiring_approval"
|
||||
assert approval_request.data.function_call.arguments == '{"query": "test"}'
|
||||
|
||||
# Act
|
||||
responses = {
|
||||
approval_request.request_id: approval_request.data.create_response(True) # type: ignore
|
||||
for approval_request in request_info_events
|
||||
}
|
||||
|
||||
output: str | None = None
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
output = event.data
|
||||
|
||||
# Assert
|
||||
assert output is not None
|
||||
assert output == "Tool executed successfully."
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://github.com/microsoft/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Experimental modules for Microsoft Agent Framework"
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Mem0 integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Microsoft Purview (Graph dataSecurityAndGovernance) integration f
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://github.com/microsoft/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Redis integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -281,6 +281,7 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [`getting_started/workflows/human-in-the-loop/guessing_game_with_human_input.py`](./getting_started/workflows/human-in-the-loop/guessing_game_with_human_input.py) | Sample: Human in the loop guessing game |
|
||||
| [`getting_started/workflows/human-in-the-loop/agents_with_approval_requests.py`](./getting_started/workflows/human-in-the-loop/agents_with_approval_requests.py) | Sample: Agents with Approval Requests in Workflows |
|
||||
|
||||
### Observability
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"typescript": "^5.4.0",
|
||||
"vite": "^7.1.9"
|
||||
"vite": "^7.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18",
|
||||
@@ -1328,9 +1328,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
|
||||
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
|
||||
"version": "7.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -22,6 +22,6 @@
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"typescript": "^5.4.0",
|
||||
"vite": "^7.1.9"
|
||||
"vite": "^7.1.12"
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ This folder contains examples demonstrating different ways to create and use age
|
||||
| [`openai_responses_client_image_analysis.py`](openai_responses_client_image_analysis.py) | Demonstrates how to use vision capabilities with agents to analyze images. |
|
||||
| [`openai_responses_client_image_generation.py`](openai_responses_client_image_generation.py) | Demonstrates how to use image generation capabilities with OpenAI agents to create images based on text descriptions. Requires PIL (Pillow) for image display. |
|
||||
| [`openai_responses_client_reasoning.py`](openai_responses_client_reasoning.py) | Demonstrates how to use reasoning capabilities with OpenAI agents, showing how the agent can provide detailed reasoning for its responses. |
|
||||
| [`openai_responses_client_streaming_image_generation.py`](openai_responses_client_streaming_image_generation.py) | Demonstrates streaming image generation with partial images for real-time image creation feedback and improved user experience. |
|
||||
| [`openai_responses_client_with_code_interpreter.py`](openai_responses_client_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with OpenAI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. |
|
||||
| [`openai_responses_client_with_explicit_settings.py`](openai_responses_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific responses client, configuring settings explicitly including API key and model ID. |
|
||||
| [`openai_responses_client_with_file_search.py`](openai_responses_client_with_file_search.py) | Demonstrates how to use file search capabilities with OpenAI agents, allowing the agent to search through uploaded files to answer questions. |
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
|
||||
import anyio
|
||||
from agent_framework import DataContent
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
"""OpenAI Responses Client Streaming Image Generation Example
|
||||
|
||||
Demonstrates streaming partial image generation using OpenAI's image generation tool.
|
||||
Shows progressive image rendering with partial images for improved user experience.
|
||||
|
||||
Note: The number of partial images received depends on generation speed:
|
||||
- High quality/complex images: More partials (generation takes longer)
|
||||
- Low quality/simple images: Fewer partials (generation completes quickly)
|
||||
- You may receive fewer partial images than requested if generation is fast
|
||||
|
||||
Important: The final partial image IS the complete, full-quality image. Each partial
|
||||
represents a progressive refinement, with the last one being the finished result.
|
||||
"""
|
||||
|
||||
|
||||
async def save_image_from_data_uri(data_uri: str, filename: str) -> None:
|
||||
"""Save an image from a data URI to a file."""
|
||||
try:
|
||||
if data_uri.startswith("data:image/"):
|
||||
# Extract base64 data
|
||||
base64_data = data_uri.split(",", 1)[1]
|
||||
image_bytes = base64.b64decode(base64_data)
|
||||
|
||||
# Save to file
|
||||
await anyio.Path(filename).write_bytes(image_bytes)
|
||||
print(f" Saved: {filename} ({len(image_bytes) / 1024:.1f} KB)")
|
||||
except Exception as e:
|
||||
print(f" Error saving {filename}: {e}")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Demonstrate streaming image generation with partial images."""
|
||||
print("=== OpenAI Streaming Image Generation Example ===\n")
|
||||
|
||||
# Create agent with streaming image generation enabled
|
||||
agent = OpenAIResponsesClient().create_agent(
|
||||
instructions="You are a helpful agent that can generate images.",
|
||||
tools=[
|
||||
{
|
||||
"type": "image_generation",
|
||||
"size": "1024x1024",
|
||||
"quality": "high",
|
||||
"partial_images": 3,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
query = "Draw a beautiful sunset over a calm ocean with sailboats"
|
||||
print(f" User: {query}")
|
||||
print()
|
||||
|
||||
# Track partial images
|
||||
image_count = 0
|
||||
|
||||
# Create output directory
|
||||
output_dir = anyio.Path("generated_images")
|
||||
await output_dir.mkdir(exist_ok=True)
|
||||
|
||||
print(" Streaming response:")
|
||||
async for update in agent.run_stream(query):
|
||||
for content in update.contents:
|
||||
# Handle partial images
|
||||
# The final partial image IS the complete, full-quality image. Each partial
|
||||
# represents a progressive refinement, with the last one being the finished result.
|
||||
if isinstance(content, DataContent) and content.additional_properties.get("is_partial_image"):
|
||||
print(f" Image {image_count} received")
|
||||
|
||||
# Extract file extension from media_type (e.g., "image/png" -> "png")
|
||||
extension = "png" # Default fallback
|
||||
if content.media_type and "/" in content.media_type:
|
||||
extension = content.media_type.split("/")[-1]
|
||||
|
||||
# Save images with correct extension
|
||||
filename = output_dir / f"image{image_count}.{extension}"
|
||||
await save_image_from_data_uri(content.uri, str(filename))
|
||||
|
||||
image_count += 1
|
||||
|
||||
# Summary
|
||||
print("\n Summary:")
|
||||
print(f" Images received: {image_count}")
|
||||
print(" Output directory: generated_images")
|
||||
print("\n Streaming image generation completed!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -78,6 +78,7 @@ Once comfortable with these, explore the rest of the samples below.
|
||||
|---|---|---|
|
||||
| Human-In-The-Loop (Guessing Game) | [human-in-the-loop/guessing_game_with_human_input.py](./human-in-the-loop/guessing_game_with_human_input.py) | Interactive request/response prompts with a human |
|
||||
| Azure Agents Tool Feedback Loop | [agents/azure_chat_agents_tool_calls_with_feedback.py](./agents/azure_chat_agents_tool_calls_with_feedback.py) | Two-agent workflow that streams tool calls and pauses for human guidance between passes |
|
||||
| Agents with Approval Requests in Workflows | [human-in-the-loop/agents_with_approval_requests.py](./human-in-the-loop/agents_with_approval_requests.py) | Agents that create approval requests during workflow execution and wait for human approval to proceed |
|
||||
|
||||
### observability
|
||||
|
||||
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Annotated, Never
|
||||
|
||||
from agent_framework import (
|
||||
AgentExecutorResponse,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionApprovalResponseContent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
ai_function,
|
||||
executor,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
"""
|
||||
Sample: Agents in a workflow with AI functions requiring approval
|
||||
|
||||
This sample creates a workflow that automatically replies to incoming emails.
|
||||
If historical email data is needed, it uses an AI function to read the data,
|
||||
which requires human approval before execution.
|
||||
|
||||
This sample works as follows:
|
||||
1. An incoming email is received by the workflow.
|
||||
2. The EmailPreprocessor executor preprocesses the email, adding special notes if the sender is important.
|
||||
3. The preprocessed email is sent to the Email Writer agent, which generates a response.
|
||||
4. If the agent needs to read historical email data, it calls the read_historical_email_data AI function,
|
||||
which triggers an approval request.
|
||||
5. The sample automatically approves the request for demonstration purposes.
|
||||
6. Once approved, the AI function executes and returns the historical email data to the agent.
|
||||
7. The agent uses the historical data to compose a comprehensive email response.
|
||||
8. The response is sent to the conclude_workflow_executor, which yields the final response.
|
||||
|
||||
Purpose:
|
||||
Show how to integrate AI functions with approval requests into a workflow.
|
||||
|
||||
Demonstrate:
|
||||
- Creating AI functions that require approval before execution.
|
||||
- Building a workflow that includes an agent and executors.
|
||||
- Handling approval requests during workflow execution.
|
||||
|
||||
Prerequisites:
|
||||
- Azure AI Agent Service configured, along with the required environment variables.
|
||||
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
|
||||
- Basic familiarity with WorkflowBuilder, edges, events, RequestInfoEvent, and streaming runs.
|
||||
"""
|
||||
|
||||
|
||||
@ai_function
|
||||
def get_current_date() -> str:
|
||||
"""Get the current date in YYYY-MM-DD format."""
|
||||
# For demonstration purposes, we return a fixed date.
|
||||
return "2025-11-07"
|
||||
|
||||
|
||||
@ai_function
|
||||
def get_team_members_email_addresses() -> list[dict[str, str]]:
|
||||
"""Get the email addresses of team members."""
|
||||
# In a real implementation, this might query a database or directory service.
|
||||
return [
|
||||
{
|
||||
"name": "Alice",
|
||||
"email": "alice@contoso.com",
|
||||
"position": "Software Engineer",
|
||||
"manager": "John Doe",
|
||||
},
|
||||
{
|
||||
"name": "Bob",
|
||||
"email": "bob@contoso.com",
|
||||
"position": "Product Manager",
|
||||
"manager": "John Doe",
|
||||
},
|
||||
{
|
||||
"name": "Charlie",
|
||||
"email": "charlie@contoso.com",
|
||||
"position": "Senior Software Engineer",
|
||||
"manager": "John Doe",
|
||||
},
|
||||
{
|
||||
"name": "Mike",
|
||||
"email": "mike@contoso.com",
|
||||
"position": "Principal Software Engineer Manager",
|
||||
"manager": "VP of Engineering",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@ai_function
|
||||
def get_my_information() -> dict[str, str]:
|
||||
"""Get my personal information."""
|
||||
return {
|
||||
"name": "John Doe",
|
||||
"email": "john@contoso.com",
|
||||
"position": "Software Engineer Manager",
|
||||
"manager": "Mike",
|
||||
}
|
||||
|
||||
|
||||
@ai_function(approval_mode="always_require")
|
||||
async def read_historical_email_data(
|
||||
email_address: Annotated[str, "The email address to read historical data from"],
|
||||
start_date: Annotated[str, "The start date in YYYY-MM-DD format"],
|
||||
end_date: Annotated[str, "The end date in YYYY-MM-DD format"],
|
||||
) -> list[dict[str, str]]:
|
||||
"""Read historical email data for a given email address and date range."""
|
||||
historical_data = {
|
||||
"alice@contoso.com": [
|
||||
{
|
||||
"from": "alice@contoso.com",
|
||||
"to": "john@contoso.com",
|
||||
"date": "2025-11-05",
|
||||
"subject": "Bug Bash Results",
|
||||
"body": "We just completed the bug bash and found a few issues that need immediate attention.",
|
||||
},
|
||||
{
|
||||
"from": "alice@contoso.com",
|
||||
"to": "john@contoso.com",
|
||||
"date": "2025-11-03",
|
||||
"subject": "Code Freeze",
|
||||
"body": "We are entering code freeze starting tomorrow.",
|
||||
},
|
||||
],
|
||||
"bob@contoso.com": [
|
||||
{
|
||||
"from": "bob@contoso.com",
|
||||
"to": "john@contoso.com",
|
||||
"date": "2025-11-04",
|
||||
"subject": "Team Outing",
|
||||
"body": "Don't forget about the team outing this Friday!",
|
||||
},
|
||||
{
|
||||
"from": "bob@contoso.com",
|
||||
"to": "john@contoso.com",
|
||||
"date": "2025-11-02",
|
||||
"subject": "Requirements Update",
|
||||
"body": "The requirements for the new feature have been updated. Please review them.",
|
||||
},
|
||||
],
|
||||
"charlie@contoso.com": [
|
||||
{
|
||||
"from": "charlie@contoso.com",
|
||||
"to": "john@contoso.com",
|
||||
"date": "2025-11-05",
|
||||
"subject": "Project Update",
|
||||
"body": "The bug bash went well. A few critical bugs but should be fixed by the end of the week.",
|
||||
},
|
||||
{
|
||||
"from": "charlie@contoso.com",
|
||||
"to": "john@contoso.com",
|
||||
"date": "2025-11-06",
|
||||
"subject": "Code Review",
|
||||
"body": "Please review my latest code changes.",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
emails = historical_data.get(email_address, [])
|
||||
return [email for email in emails if start_date <= email["date"] <= end_date]
|
||||
|
||||
|
||||
@ai_function(approval_mode="always_require")
|
||||
async def send_email(
|
||||
to: Annotated[str, "The recipient email address"],
|
||||
subject: Annotated[str, "The email subject"],
|
||||
body: Annotated[str, "The email body"],
|
||||
) -> str:
|
||||
"""Send an email."""
|
||||
await asyncio.sleep(1) # Simulate sending email
|
||||
return "Email successfully sent."
|
||||
|
||||
|
||||
@dataclass
|
||||
class Email:
|
||||
sender: str
|
||||
subject: str
|
||||
body: str
|
||||
|
||||
|
||||
class EmailPreprocessor(Executor):
|
||||
def __init__(self, special_email_addresses: set[str]) -> None:
|
||||
super().__init__(id="email_preprocessor")
|
||||
self.special_email_addresses = special_email_addresses
|
||||
|
||||
@handler
|
||||
async def preprocess(self, email: Email, ctx: WorkflowContext[str]) -> None:
|
||||
"""Preprocess the incoming email."""
|
||||
message = str(email)
|
||||
if email.sender in self.special_email_addresses:
|
||||
note = (
|
||||
"Pay special attention to this sender. This email is very important. "
|
||||
"Gather relevant information from all previous emails within my team before responding."
|
||||
)
|
||||
message = f"{note}\n\n{message}"
|
||||
|
||||
await ctx.send_message(message)
|
||||
|
||||
|
||||
@executor(id="conclude_workflow_executor")
|
||||
async def conclude_workflow(
|
||||
email_response: AgentExecutorResponse,
|
||||
ctx: WorkflowContext[Never, str],
|
||||
) -> None:
|
||||
"""Conclude the workflow by yielding the final email response."""
|
||||
await ctx.yield_output(email_response.agent_run_response.text)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# Create the agent and executors
|
||||
chat_client = OpenAIChatClient()
|
||||
email_writer = chat_client.create_agent(
|
||||
name="Email Writer",
|
||||
instructions=("You are an excellent email assistant. You respond to incoming emails."),
|
||||
# tools with `approval_mode="always_require"` will trigger approval requests
|
||||
tools=[
|
||||
read_historical_email_data,
|
||||
send_email,
|
||||
get_current_date,
|
||||
get_team_members_email_addresses,
|
||||
get_my_information,
|
||||
],
|
||||
)
|
||||
email_preprocessor = EmailPreprocessor(special_email_addresses={"mike@contoso.com"})
|
||||
|
||||
# Build the workflow
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(email_preprocessor)
|
||||
.add_edge(email_preprocessor, email_writer)
|
||||
.add_edge(email_writer, conclude_workflow)
|
||||
.build()
|
||||
)
|
||||
|
||||
# Simulate an incoming email
|
||||
incoming_email = Email(
|
||||
sender="mike@contoso.com",
|
||||
subject="Important: Project Update",
|
||||
body="Please provide your team's status update on the project since last week.",
|
||||
)
|
||||
|
||||
responses: dict[str, FunctionApprovalResponseContent] = {}
|
||||
output: list[ChatMessage] | None = None
|
||||
while True:
|
||||
if responses:
|
||||
events = await workflow.send_responses(responses)
|
||||
responses.clear()
|
||||
else:
|
||||
events = await workflow.run(incoming_email)
|
||||
|
||||
request_info_events = events.get_request_info_events()
|
||||
for request_info_event in request_info_events:
|
||||
# We should only expect FunctionApprovalRequestContent in this sample
|
||||
if not isinstance(request_info_event.data, FunctionApprovalRequestContent):
|
||||
raise ValueError(f"Unexpected request info content type: {type(request_info_event.data)}")
|
||||
|
||||
# Pretty print the function call details
|
||||
arguments = json.dumps(request_info_event.data.function_call.parse_arguments(), indent=2)
|
||||
print(
|
||||
f"Received approval request for function: {request_info_event.data.function_call.name} "
|
||||
f"with args:\n{arguments}"
|
||||
)
|
||||
|
||||
# For demo purposes, we automatically approve the request
|
||||
# The expected response type of the request is `FunctionApprovalResponseContent`,
|
||||
# which can be created via `create_response` method on the request content
|
||||
print("Performing automatic approval for demo purposes...")
|
||||
responses[request_info_event.request_id] = request_info_event.data.create_response(approved=True)
|
||||
|
||||
# Once we get an output event, we can conclude the workflow
|
||||
# Outputs can only be produced by the conclude_workflow_executor in this sample
|
||||
if outputs := events.get_outputs():
|
||||
# We expect only one output from the conclude_workflow_executor
|
||||
output = outputs[0]
|
||||
break
|
||||
|
||||
if not output:
|
||||
raise RuntimeError("Workflow did not produce any output event.")
|
||||
|
||||
print("Final email response conversation:")
|
||||
print(output)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
Received approval request for function: read_historical_email_data with args:
|
||||
{
|
||||
"email_address": "alice@contoso.com",
|
||||
"start_date": "2025-10-31",
|
||||
"end_date": "2025-11-07"
|
||||
}
|
||||
Performing automatic approval for demo purposes...
|
||||
Received approval request for function: read_historical_email_data with args:
|
||||
{
|
||||
"email_address": "bob@contoso.com",
|
||||
"start_date": "2025-10-31",
|
||||
"end_date": "2025-11-07"
|
||||
}
|
||||
Performing automatic approval for demo purposes...
|
||||
Received approval request for function: read_historical_email_data with args:
|
||||
{
|
||||
"email_address": "charlie@contoso.com",
|
||||
"start_date": "2025-10-31",
|
||||
"end_date": "2025-11-07"
|
||||
}
|
||||
Performing automatic approval for demo purposes...
|
||||
Received approval request for function: send_email with args:
|
||||
{
|
||||
"to": "mike@contoso.com",
|
||||
"subject": "Team's Status Update on the Project",
|
||||
"body": "
|
||||
Hi Mike,
|
||||
|
||||
Here's the status update from our team:
|
||||
- **Bug Bash and Code Freeze:**
|
||||
- We recently completed a bug bash, during which several issues were identified. Alice and Charlie are working on fixing these critical bugs, and we anticipate resolving them by the end of this week.
|
||||
- We have entered a code freeze as of November 4, 2025.
|
||||
|
||||
- **Requirements Update:**
|
||||
- Bob has updated the requirements for a new feature, and all team members are reviewing these changes to ensure alignment.
|
||||
|
||||
- **Ongoing Reviews:**
|
||||
- Charlie has submitted his latest code changes for review to ensure they meet our quality standards.
|
||||
|
||||
Please let me know if you need more detailed information or have any questions.
|
||||
|
||||
Best regards,
|
||||
John"
|
||||
}
|
||||
Performing automatic approval for demo purposes...
|
||||
Final email response conversation:
|
||||
I've sent the status update to Mike with the relevant information from the team. Let me know if there's anything else you need
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Generated
+13
-13
@@ -87,7 +87,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-a2a", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -178,7 +178,7 @@ docs = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-a2a"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { editable = "packages/a2a" }
|
||||
dependencies = [
|
||||
{ name = "a2a-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -193,7 +193,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-ag-ui"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { editable = "packages/ag-ui" }
|
||||
dependencies = [
|
||||
{ name = "ag-ui-protocol", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -223,7 +223,7 @@ provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-anthropic"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { editable = "packages/anthropic" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -238,7 +238,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-azure-ai"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { editable = "packages/azure-ai" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -257,7 +257,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-chatkit"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { editable = "packages/chatkit" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -272,7 +272,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-copilotstudio"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { editable = "packages/copilotstudio" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -287,7 +287,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-core"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { editable = "packages/core" }
|
||||
dependencies = [
|
||||
{ name = "azure-identity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -343,7 +343,7 @@ provides-extras = ["all"]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-devui"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { editable = "packages/devui" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -377,7 +377,7 @@ provides-extras = ["dev", "all"]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-lab"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { editable = "packages/lab" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -468,7 +468,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-mem0"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { editable = "packages/mem0" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -483,7 +483,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-purview"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { editable = "packages/purview" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -500,7 +500,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-redis"
|
||||
version = "1.0.0b251108"
|
||||
version = "1.0.0b251111"
|
||||
source = { editable = "packages/redis" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
|
||||
@@ -119,13 +119,13 @@ trigger:
|
||||
# FACTS
|
||||
Consider this initial fact sheet:
|
||||
|
||||
{Trim(Last(Local.TaskFacts).Text)}
|
||||
{MessageText(Local.TaskFacts)}
|
||||
|
||||
|
||||
# PLAN
|
||||
Here is the plan to follow as best as possible:
|
||||
|
||||
{Last(Local.Plan).Text}
|
||||
{MessageText(Local.Plan)}
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_bwNZiM
|
||||
@@ -247,7 +247,7 @@ trigger:
|
||||
|
||||
Here is the old fact sheet:
|
||||
|
||||
{Local.TaskFacts}"
|
||||
{MessageText(Local.TaskFacts)}"
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_dsBaJU
|
||||
@@ -291,13 +291,13 @@ trigger:
|
||||
# FACTS
|
||||
Consider this initial fact sheet:
|
||||
|
||||
{Local.TaskFacts.Text}
|
||||
{MessageText(Local.TaskFacts)}
|
||||
|
||||
|
||||
# PLAN
|
||||
Here is the plan to follow as best as possible:
|
||||
|
||||
{Local.Plan.Text}
|
||||
{MessageText(Local.Plan)}
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_6J2snP
|
||||
@@ -356,7 +356,7 @@ trigger:
|
||||
- kind: SetVariable
|
||||
id: setVariable_XzNrdM
|
||||
variable: Local.AgentResponseText
|
||||
value: =Last(Local.AgentResponse).Text
|
||||
value: =MessageText(Local.AgentResponse)
|
||||
|
||||
- kind: ResetVariable
|
||||
id: setVariable_8eIx2A
|
||||
|
||||
@@ -35,7 +35,7 @@ trigger:
|
||||
id: check_completion
|
||||
conditions:
|
||||
|
||||
- condition: =!IsBlank(Find("CONGRATULATIONS", Upper(Last(Local.TeacherResponse).Text)))
|
||||
- condition: =!IsBlank(Find("CONGRATULATIONS", Upper(MessageText(Local.TeacherResponse))))
|
||||
id: check_turn_done
|
||||
actions:
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ may be executed locally no different from any regular `Workflow` that is defined
|
||||
The difference is that the workflow definition is loaded from a YAML file instead of being defined in code:
|
||||
|
||||
```c#
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>("HelloWorld.yaml", options);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build("Marketing.yaml", options);
|
||||
```
|
||||
|
||||
These example workflows may be executed by the workflow
|
||||
|
||||
Reference in New Issue
Block a user