mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef1e33619a | ||
|
|
44bb9e9d1a | ||
|
|
e5644c4955 | ||
|
|
1d86b6a105 | ||
|
|
cafcc53483 | ||
|
|
9e18c60745 | ||
|
|
2b10c2a0bc |
@@ -242,7 +242,6 @@
|
||||
<Project Path="samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InputArguments/InputArguments.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/InvokeFoundryToolboxMcp.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InvokeHttpRequest/InvokeHttpRequest.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InvokeMcpTool/InvokeMcpTool.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/Marketing/Marketing.csproj" />
|
||||
|
||||
@@ -478,17 +478,6 @@ internal static class WorkflowSamples
|
||||
ExpectedOutputDescription = ["The output should show a workflow invoking a function tool (e.g. a menu plugin) to answer a question about the soup of the day."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_InvokeFoundryToolboxMcp",
|
||||
ProjectPath = "samples/03-workflows/Declarative/InvokeFoundryToolboxMcp",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME", "FOUNDRY_TOOLBOX_NAME", "FOUNDRY_AGENT_TOOLSET_API_VERSION"],
|
||||
Inputs = ["How do I use Azure OpenAI with my data?"],
|
||||
InputDelayMs = 3000,
|
||||
ExpectedOutputDescription = ["The output should show a workflow using Foundry Toolbox MCP tools to search Microsoft Learn documentation and web search to provide a summary of results."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_InvokeMcpTool",
|
||||
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</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="Azure.AI.Projects" />
|
||||
<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" />
|
||||
<PackageReference Include="OpenAI" />
|
||||
<PackageReference Include="System.ClientModel" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.Foundry\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.Mcp\Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="InvokeFoundryToolboxMcp.yaml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
#
|
||||
# This workflow demonstrates invoking MCP tools through a Foundry toolbox MCP proxy.
|
||||
#
|
||||
# The toolbox is provisioned with TWO different tool types:
|
||||
# 1. A Foundry built-in web_search tool
|
||||
# 2. A Microsoft Learn MCP server (microsoft_docs)
|
||||
# Both are surfaced through the same MCP-compatible toolbox endpoint.
|
||||
#
|
||||
# The workflow:
|
||||
# 1. Accepts a documentation/web search query as input
|
||||
# 2. Lists the tools exposed by the Foundry toolbox using reserved toolName: tools/list
|
||||
# 3. Invokes the microsoft_docs_search MCP tool
|
||||
# 4. Invokes the built-in web_search tool against the same toolbox endpoint
|
||||
# 5. Uses an agent to summarize and combine both result sets
|
||||
#
|
||||
# Example input:
|
||||
# How do I use Azure OpenAI with my data?
|
||||
#
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_invoke_foundry_toolbox_mcp
|
||||
actions:
|
||||
|
||||
# Set the search query from user input.
|
||||
- kind: SetVariable
|
||||
id: set_search_query
|
||||
variable: Local.SearchQuery
|
||||
value: =System.LastMessage.Text
|
||||
|
||||
# List tools exposed by the Foundry toolbox MCP proxy.
|
||||
- kind: InvokeMcpTool
|
||||
id: list_toolbox_tools
|
||||
serverUrl: =Env.FOUNDRY_TOOLBOX_MCP_SERVER_URL
|
||||
serverLabel: foundry_toolbox
|
||||
toolName: tools/list
|
||||
conversationId: =System.ConversationId
|
||||
headers:
|
||||
Foundry-Features: Toolboxes=V1Preview
|
||||
output:
|
||||
autoSend: true
|
||||
result: Local.ToolboxTools
|
||||
|
||||
# Invoke a specific tool exposed through the toolbox and add the result to the conversation.
|
||||
- kind: InvokeMcpTool
|
||||
id: search_docs_with_toolbox
|
||||
serverUrl: =Env.FOUNDRY_TOOLBOX_MCP_SERVER_URL
|
||||
serverLabel: foundry_toolbox
|
||||
toolName: =Env.FOUNDRY_TOOLBOX_DOCS_SERVER_LABEL & "___microsoft_docs_search"
|
||||
conversationId: =System.ConversationId
|
||||
headers:
|
||||
Foundry-Features: Toolboxes=V1Preview
|
||||
arguments:
|
||||
query: =Local.SearchQuery
|
||||
output:
|
||||
autoSend: true
|
||||
result: Local.SearchResult
|
||||
|
||||
# Invoke the web_search built-in tool through the same toolbox proxy. The toolbox surfaces
|
||||
# built-in Foundry tools (like web_search) alongside MCP tools through one MCP-compatible
|
||||
# endpoint. Note that web_search expects argument 'search_query' (not 'query').
|
||||
- kind: InvokeMcpTool
|
||||
id: search_web_with_toolbox
|
||||
serverUrl: =Env.FOUNDRY_TOOLBOX_MCP_SERVER_URL
|
||||
serverLabel: foundry_toolbox
|
||||
toolName: =Env.FOUNDRY_TOOLBOX_WEB_SEARCH_TOOL_NAME
|
||||
conversationId: =System.ConversationId
|
||||
headers:
|
||||
Foundry-Features: Toolboxes=V1Preview
|
||||
arguments:
|
||||
search_query: =Local.SearchQuery
|
||||
output:
|
||||
autoSend: true
|
||||
result: Local.WebSearchResult
|
||||
|
||||
# Use the agent to summarize what happened and answer from the toolbox result.
|
||||
- kind: InvokeAzureAgent
|
||||
id: summarize_toolbox_result
|
||||
agent:
|
||||
name: FoundryToolboxMcpAgent
|
||||
conversationId: =System.ConversationId
|
||||
input:
|
||||
messages: =UserMessage("Combine the Microsoft Learn docs results and the Foundry web search results in the conversation to answer the query " & Local.SearchQuery)
|
||||
output:
|
||||
autoSend: true
|
||||
messages: Local.Summary
|
||||
@@ -1,218 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates using InvokeMcpTool to call MCP tools through a Foundry toolbox.
|
||||
// It creates a sample toolbox that exposes Microsoft Learn MCP tools, lists the toolbox tools
|
||||
// through the reserved tools/list operation, then calls microsoft_docs_search from the workflow.
|
||||
|
||||
using System.ClientModel;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Http.Headers;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Azure.Core;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Mcp;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using OpenAI.Responses;
|
||||
using Shared.Foundry;
|
||||
using Shared.Workflows;
|
||||
|
||||
#pragma warning disable OPENAI001 // Experimental API
|
||||
#pragma warning disable AAIP001 // AgentToolboxes is experimental
|
||||
|
||||
namespace Demo.Workflows.Declarative.InvokeFoundryToolboxMcp;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates a workflow that uses InvokeMcpTool to call MCP tools exposed through a Foundry toolbox.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This sample provisions a toolbox with Microsoft Learn MCP tools, uses the reserved
|
||||
/// <c>tools/list</c> tool name to list the toolbox tools, calls one specific toolbox tool,
|
||||
/// and has a Foundry agent summarize the results.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
{
|
||||
private const string ToolboxNameSetting = "FOUNDRY_TOOLBOX_NAME";
|
||||
private const string ToolboxApiVersionSetting = "FOUNDRY_AGENT_TOOLSET_API_VERSION";
|
||||
private const string ToolboxMcpServerUrlSetting = "FOUNDRY_TOOLBOX_MCP_SERVER_URL";
|
||||
private const string DocsServerLabelSetting = "FOUNDRY_TOOLBOX_DOCS_SERVER_LABEL";
|
||||
private const string WebSearchToolNameSetting = "FOUNDRY_TOOLBOX_WEB_SEARCH_TOOL_NAME";
|
||||
private const string DefaultToolboxName = "declarative_foundry_toolbox_mcp";
|
||||
private const string DefaultToolboxApiVersion = "v1";
|
||||
private const string DefaultDocsServerLabel = "microsoft_docs";
|
||||
private const string DefaultWebSearchToolName = "web_search";
|
||||
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// Initialize configuration
|
||||
IConfiguration configuration = Application.InitializeConfig();
|
||||
Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
|
||||
string toolboxName = configuration[ToolboxNameSetting] ?? DefaultToolboxName;
|
||||
string toolboxApiVersion = configuration[ToolboxApiVersionSetting] ?? DefaultToolboxApiVersion;
|
||||
string docsServerLabel = configuration[DocsServerLabelSetting] ?? DefaultDocsServerLabel;
|
||||
string webSearchToolName = configuration[WebSearchToolNameSetting] ?? DefaultWebSearchToolName;
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
DefaultAzureCredential credential = new();
|
||||
|
||||
// Ensure sample toolbox and agent exist in Foundry
|
||||
string toolboxEndpoint = await CreateSampleToolboxAsync(toolboxName, docsServerLabel, foundryEndpoint, credential);
|
||||
string toolboxMcpServerUrl = BuildToolboxMcpServerUrl(toolboxEndpoint, toolboxName, toolboxApiVersion);
|
||||
IConfiguration workflowConfiguration = new ConfigurationBuilder()
|
||||
.AddConfiguration(configuration)
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
[ToolboxMcpServerUrlSetting] = toolboxMcpServerUrl,
|
||||
[DocsServerLabelSetting] = docsServerLabel,
|
||||
[WebSearchToolNameSetting] = webSearchToolName,
|
||||
})
|
||||
.Build();
|
||||
|
||||
await CreateAgentAsync(foundryEndpoint, configuration, credential);
|
||||
|
||||
// Get input from command line or console
|
||||
string workflowInput = Application.GetInput(args);
|
||||
|
||||
// Create the MCP tool handler for invoking the Foundry toolbox MCP proxy.
|
||||
ConcurrentBag<HttpClient> createdHttpClients = [];
|
||||
DefaultMcpToolHandler mcpToolHandler = new(
|
||||
httpClientProvider: async (serverUrl, _) =>
|
||||
{
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
|
||||
if (!string.Equals(serverUrl, toolboxMcpServerUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
FoundryToolboxBearerTokenHandler handler = new(credential)
|
||||
{
|
||||
InnerHandler = new HttpClientHandler()
|
||||
};
|
||||
HttpClient httpClient = new(handler);
|
||||
createdHttpClients.Add(httpClient);
|
||||
return httpClient;
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
// Create the workflow factory with MCP tool provider
|
||||
WorkflowFactory workflowFactory = new("InvokeFoundryToolboxMcp.yaml", foundryEndpoint)
|
||||
{
|
||||
Configuration = workflowConfiguration,
|
||||
McpToolHandler = mcpToolHandler
|
||||
};
|
||||
|
||||
// Execute the workflow
|
||||
WorkflowRunner runner = new() { UseJsonCheckpoints = true };
|
||||
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up connections and dispose created HttpClients
|
||||
await mcpToolHandler.DisposeAsync();
|
||||
|
||||
foreach (HttpClient httpClient in createdHttpClients)
|
||||
{
|
||||
httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration, TokenCredential credential)
|
||||
{
|
||||
AIProjectClient aiProjectClient = new(foundryEndpoint, credential);
|
||||
|
||||
await aiProjectClient.CreateAgentAsync(
|
||||
agentName: "FoundryToolboxMcpAgent",
|
||||
agentDefinition: DefineToolboxAgent(configuration),
|
||||
agentDescription: "Summarizes Foundry toolbox MCP tool results");
|
||||
}
|
||||
|
||||
private static DeclarativeAgentDefinition DefineToolboxAgent(IConfiguration configuration)
|
||||
{
|
||||
return new DeclarativeAgentDefinition(configuration.GetValue(Application.Settings.FoundryModel))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You are a helpful assistant that explains results produced by tools exposed through a Foundry toolbox.
|
||||
The conversation history contains output from BOTH a Microsoft Learn documentation search (MCP) and a Foundry web search.
|
||||
Synthesize an answer that draws on both sources, calls out where they agree or differ, and notes which toolbox tool produced each fact when it is relevant.
|
||||
Be concise.
|
||||
"""
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<string> CreateSampleToolboxAsync(string name, string serverLabel, Uri foundryEndpoint, TokenCredential credential)
|
||||
{
|
||||
AgentAdministrationClientOptions options = new();
|
||||
options.AddPolicy(new FoundryFeaturesPolicy("Toolboxes=V1Preview"), PipelinePosition.PerCall);
|
||||
AgentAdministrationClient adminClient = new(foundryEndpoint, credential, options);
|
||||
AgentToolboxes toolboxClient = adminClient.GetAgentToolboxes();
|
||||
|
||||
try
|
||||
{
|
||||
await toolboxClient.DeleteToolboxAsync(name);
|
||||
Console.WriteLine($"Deleted existing toolbox '{name}'");
|
||||
}
|
||||
catch (ClientResultException ex) when (ex.Status == 404)
|
||||
{
|
||||
// Toolbox does not exist.
|
||||
}
|
||||
|
||||
ProjectsAgentTool webTool = ProjectsAgentTool.AsProjectTool(ResponseTool.CreateWebSearchTool());
|
||||
|
||||
ProjectsAgentTool mcpTool = ProjectsAgentTool.AsProjectTool(ResponseTool.CreateMcpTool(
|
||||
serverLabel: serverLabel,
|
||||
serverUri: new Uri("https://learn.microsoft.com/api/mcp"),
|
||||
toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)));
|
||||
|
||||
ToolboxVersion created = (await toolboxClient.CreateToolboxVersionAsync(
|
||||
name: name,
|
||||
tools: [webTool, mcpTool],
|
||||
description: "Sample toolbox combining Foundry web search with the Microsoft Learn MCP tools for the declarative InvokeFoundryToolboxMcp sample.")).Value;
|
||||
|
||||
Console.WriteLine($"Created toolbox '{created.Name}' v{created.Version} ({created.Tools.Count} tool(s))");
|
||||
|
||||
return $"{foundryEndpoint.ToString().TrimEnd('/')}/toolboxes";
|
||||
}
|
||||
|
||||
private static string BuildToolboxMcpServerUrl(string toolboxEndpoint, string toolboxName, string apiVersion) =>
|
||||
$"{toolboxEndpoint.TrimEnd('/')}/{toolboxName}/mcp?api-version={Uri.EscapeDataString(apiVersion)}";
|
||||
|
||||
private sealed class FoundryToolboxBearerTokenHandler(TokenCredential credential) : DelegatingHandler
|
||||
{
|
||||
private static readonly TokenRequestContext s_tokenContext =
|
||||
new(["https://ai.azure.com/.default"]);
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
AccessToken token = await credential.GetTokenAsync(s_tokenContext, cancellationToken);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
|
||||
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FoundryFeaturesPolicy(string feature) : PipelinePolicy
|
||||
{
|
||||
private const string FeatureHeader = "Foundry-Features";
|
||||
|
||||
public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
|
||||
{
|
||||
message.Request.Headers.Add(FeatureHeader, feature);
|
||||
ProcessNext(message, pipeline, currentIndex);
|
||||
}
|
||||
|
||||
public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
|
||||
{
|
||||
message.Request.Headers.Add(FeatureHeader, feature);
|
||||
return ProcessNextAsync(message, pipeline, currentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
@@ -27,14 +24,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.Mcp;
|
||||
/// </remarks>
|
||||
public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Reserved <c>toolName</c> value that maps an <see cref="IMcpToolHandler.InvokeToolAsync"/> request
|
||||
/// to the MCP protocol <c>tools/list</c> discovery operation.
|
||||
/// </summary>
|
||||
public const string ListToolsToolName = "tools/list";
|
||||
|
||||
private static readonly JsonWriterOptions s_toolListJsonWriterOptions = new() { Indented = true };
|
||||
|
||||
private readonly Func<string, CancellationToken, Task<HttpClient?>>? _httpClientProvider;
|
||||
private readonly Dictionary<string, McpClient> _clients = [];
|
||||
private readonly Dictionary<string, HttpClient> _ownedHttpClients = [];
|
||||
@@ -64,17 +53,8 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// TODO: Handle connectionName and server label appropriately when Hosted scenario supports them. For now, ignore
|
||||
if (IsListToolsToolName(toolName))
|
||||
{
|
||||
ThrowIfListToolsArgumentsSpecified(arguments);
|
||||
McpClient listToolsClient = await this.GetOrCreateClientAsync(serverUrl, serverLabel, headers, cancellationToken).ConfigureAwait(false);
|
||||
IList<McpClientTool> tools = await listToolsClient.ListToolsAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return CreateListToolsResultContent(tools.Select(tool => tool.ProtocolTool));
|
||||
}
|
||||
|
||||
McpClient client = await this.GetOrCreateClientAsync(serverUrl, serverLabel, headers, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
McpServerToolResultContent resultContent = new(Guid.NewGuid().ToString());
|
||||
McpClient client = await this.GetOrCreateClientAsync(serverUrl, serverLabel, headers, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Convert IDictionary to IReadOnlyDictionary for CallToolAsync
|
||||
IReadOnlyDictionary<string, object?>? readOnlyArguments = arguments is null
|
||||
@@ -92,23 +72,6 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
|
||||
return resultContent;
|
||||
}
|
||||
|
||||
internal static bool IsListToolsToolName(string toolName) =>
|
||||
string.Equals(toolName, ListToolsToolName, StringComparison.Ordinal);
|
||||
|
||||
internal static McpServerToolResultContent CreateListToolsResultContent(IEnumerable<Tool> tools)
|
||||
{
|
||||
Throw.IfNull(tools);
|
||||
|
||||
McpServerToolResultContent resultContent = new(Guid.NewGuid().ToString())
|
||||
{
|
||||
Outputs = []
|
||||
};
|
||||
|
||||
resultContent.Outputs.Add(new TextContent(SerializeToolsList(tools)));
|
||||
|
||||
return resultContent;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
@@ -220,16 +183,6 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
|
||||
return hashCode.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static void ThrowIfListToolsArgumentsSpecified(IDictionary<string, object?>? arguments)
|
||||
{
|
||||
if (arguments is { Count: > 0 })
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The reserved MCP '{ListToolsToolName}' operation does not accept tool arguments.",
|
||||
nameof(arguments));
|
||||
}
|
||||
}
|
||||
|
||||
private static void PopulateResultContent(McpServerToolResultContent resultContent, CallToolResult result)
|
||||
{
|
||||
// Ensure Outputs list is initialized
|
||||
@@ -277,17 +230,6 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
|
||||
TextContentBlock text => new TextContent(text.Text),
|
||||
ImageContentBlock image => CreateDataContent(image.Data, image.MimeType ?? "image/*"),
|
||||
AudioContentBlock audio => CreateDataContent(audio.Data, audio.MimeType ?? "audio/*"),
|
||||
EmbeddedResourceBlock embedded => ConvertEmbeddedResource(embedded),
|
||||
_ => new TextContent(block.ToString() ?? string.Empty),
|
||||
};
|
||||
}
|
||||
|
||||
private static AIContent ConvertEmbeddedResource(EmbeddedResourceBlock block)
|
||||
{
|
||||
return block.Resource switch
|
||||
{
|
||||
TextResourceContents text => new TextContent(text.Text),
|
||||
BlobResourceContents blob => CreateDataContent(blob.Blob, blob.MimeType ?? "application/octet-stream"),
|
||||
_ => new TextContent(block.ToString() ?? string.Empty),
|
||||
};
|
||||
}
|
||||
@@ -313,39 +255,4 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
|
||||
|
||||
return new DataContent($"data:{mediaType};base64,{base64}", mediaType);
|
||||
}
|
||||
|
||||
private static string SerializeToolsList(IEnumerable<Tool> tools)
|
||||
{
|
||||
using MemoryStream stream = new();
|
||||
using (Utf8JsonWriter writer = new(stream, s_toolListJsonWriterOptions))
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteStartArray("tools");
|
||||
|
||||
foreach (Tool tool in tools)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("name", tool.Name);
|
||||
writer.WriteString("description", tool.Description);
|
||||
writer.WritePropertyName("inputSchema");
|
||||
tool.InputSchema.WriteTo(writer);
|
||||
writer.WritePropertyName("outputSchema");
|
||||
if (tool.OutputSchema is JsonElement outputSchema)
|
||||
{
|
||||
outputSchema.WriteTo(writer);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(stream.GetBuffer(), 0, (int)stream.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,6 @@ public class HandoffWorkflowBuilderCore<TBuilder> where TBuilder : HandoffWorkfl
|
||||
private bool _emitAgentResponseUpdateEvents;
|
||||
private HandoffToolCallFilteringBehavior _toolCallFilteringBehavior = HandoffToolCallFilteringBehavior.HandoffOnly;
|
||||
private bool _returnToPrevious;
|
||||
private string? _name;
|
||||
private string? _description;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HandoffsWorkflowBuilder"/> class with no handoff relationships.
|
||||
@@ -99,20 +97,6 @@ public class HandoffWorkflowBuilderCore<TBuilder> where TBuilder : HandoffWorkfl
|
||||
return (TBuilder)this;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="WorkflowBuilder.WithName(string)"/>
|
||||
public TBuilder WithName(string name)
|
||||
{
|
||||
this._name = name;
|
||||
return (TBuilder)this;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="WorkflowBuilder.WithDescription(string)"/>
|
||||
public TBuilder WithDescription(string description)
|
||||
{
|
||||
this._description = description;
|
||||
return (TBuilder)this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a value indicating whether agent streaming update events should be emitted during execution.
|
||||
/// If <see langword="null"/>, the value will be taken from the <see cref="TurnToken"/>
|
||||
@@ -346,16 +330,7 @@ public class HandoffWorkflowBuilderCore<TBuilder> where TBuilder : HandoffWorkfl
|
||||
builder.AddEdge(start, executors[this._initialAgent.Id]);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(this._name))
|
||||
{
|
||||
builder.WithName(this._name);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(this._description))
|
||||
{
|
||||
builder.WithDescription(this._description);
|
||||
}
|
||||
|
||||
// Build the workflow.
|
||||
return builder.WithOutputFrom(end).Build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
@@ -141,15 +140,7 @@ public class MagenticWorkflowBuilder(AIAgent managerAgent)
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="WorkflowBuilder.Build"/>
|
||||
public Workflow Build()
|
||||
{
|
||||
if (this._team.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("At least one participant must be added via AddParticipants() before building the workflow.");
|
||||
}
|
||||
|
||||
return this.ReduceToWorkflowBuilder().Build();
|
||||
}
|
||||
public Workflow Build() => this.ReduceToWorkflowBuilder().Build();
|
||||
|
||||
private TaskLimits Limits => new(
|
||||
MaxRoundCount: this._maxRounds,
|
||||
|
||||
+9
-24
@@ -101,7 +101,6 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List<AIAgent> team, Ta
|
||||
return base.ConfigureProtocol(protocolBuilder)
|
||||
.SendsMessage<ChatMessage>()
|
||||
.SendsMessage<ResetChatSignal>()
|
||||
.YieldsOutput<List<ChatMessage>>()
|
||||
.ConfigureRoutes(ConfigureRoutes);
|
||||
|
||||
void ConfigureRoutes(RouteBuilder routeBuilder) => routeBuilder.AddPortHandler<MagenticPlanReviewRequest, MagenticPlanReviewResponse>(
|
||||
@@ -110,7 +109,7 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List<AIAgent> team, Ta
|
||||
out this._planReviewPort);
|
||||
}
|
||||
|
||||
private ValueTask SubmitPlanReviewRequestAsync(MagenticTaskContext taskContext, IWorkflowContext workflowContext, bool replanAfterStall = false)
|
||||
private ValueTask SubmitPlanReviewRequestAsync(MagenticTaskContext taskContext, IWorkflowContext workflowContext)
|
||||
{
|
||||
MagenticProgressLedger? progressLedger = taskContext.ProgressLedger;
|
||||
if (progressLedger?.IsStarted is not true)
|
||||
@@ -118,7 +117,7 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List<AIAgent> team, Ta
|
||||
progressLedger = null;
|
||||
}
|
||||
|
||||
MagenticPlanReviewRequest request = new(taskContext.TaskLedger!.CurrentPlan, progressLedger, replanAfterStall);
|
||||
MagenticPlanReviewRequest request = new(taskContext.TaskLedger!.CurrentPlan, progressLedger, taskContext.IsStalled);
|
||||
|
||||
return this._planReviewPort!.PostRequestAsync(request);
|
||||
}
|
||||
@@ -147,7 +146,7 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List<AIAgent> team, Ta
|
||||
|
||||
if (this._taskContext.IsTerminated)
|
||||
{
|
||||
throw new InvalidOperationException("This Magentic orchestration has already terminated. To process new messages, create a new workflow instance.");
|
||||
throw new InvalidOperationException("Magentic Orchestration has already been terminated and cannot process new messages. Please start a new session.");
|
||||
}
|
||||
|
||||
if (response.IsApproved)
|
||||
@@ -162,7 +161,7 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List<AIAgent> team, Ta
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask UpdatePlanAndDelegateAsync(MagenticTaskContext taskContext, IWorkflowContext context, CancellationToken cancellationToken, bool replanAfterStall = false)
|
||||
private async ValueTask UpdatePlanAndDelegateAsync(MagenticTaskContext taskContext, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
bool isReplan = taskContext.TaskLedger != null;
|
||||
|
||||
@@ -178,7 +177,7 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List<AIAgent> team, Ta
|
||||
|
||||
if (requirePlanSignoff)
|
||||
{
|
||||
await this.SubmitPlanReviewRequestAsync(taskContext, context, replanAfterStall).ConfigureAwait(false);
|
||||
await this.SubmitPlanReviewRequestAsync(taskContext, context).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -188,22 +187,9 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List<AIAgent> team, Ta
|
||||
|
||||
protected override async ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._taskContext?.IsTerminated == true)
|
||||
{
|
||||
throw new InvalidOperationException("This Magentic orchestration has already terminated. To process new messages, create a new workflow instance.");
|
||||
}
|
||||
|
||||
if (this._taskContext == null)
|
||||
{
|
||||
// First Turn: Initialize the task context and create the initial plan
|
||||
this._taskContext = new(messages, team, limits, emitEvents, []);
|
||||
await this.UpdatePlanAndDelegateAsync(this._taskContext, context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Subsequent turns: agent returned control, go directly to coordination (progress ledger only, no replan)
|
||||
await this.RunCoordinationRoundAsync(this._taskContext, context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
// First Turn: Initialize the task context and send the initial messages to the planner agent
|
||||
this._taskContext ??= new(messages, team, limits, emitEvents, []);
|
||||
await this.UpdatePlanAndDelegateAsync(this._taskContext, context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private ChatMessage? _fullTaskLedgerMessage;
|
||||
@@ -302,11 +288,10 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List<AIAgent> team, Ta
|
||||
|
||||
private async ValueTask ResetAndReplanAsync(MagenticTaskContext taskContext, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
bool wasStalled = taskContext.IsStalled;
|
||||
taskContext.Reset();
|
||||
await context.SendMessageAsync(new ResetChatSignal(), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await this.UpdatePlanAndDelegateAsync(taskContext, context, cancellationToken, replanAfterStall: wasStalled).ConfigureAwait(false);
|
||||
await this.UpdatePlanAndDelegateAsync(taskContext, context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask PrepareFinalAnswerAsync(MagenticTaskContext taskContext, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ internal class MagenticTaskContext(List<ChatMessage> taskDefinition, List<AIAgen
|
||||
|
||||
public bool IsTerminated { get; internal set; }
|
||||
|
||||
public bool IsStalled => this.TaskCounters.StallCount > this.TaskLimits.MaxStallCount;
|
||||
public bool IsStalled => this.TaskCounters.StallCount >= this.TaskLimits.MaxStallCount;
|
||||
|
||||
public (bool HitRoundLimit, bool HitResetLimit) CheckLimits()
|
||||
{
|
||||
|
||||
@@ -36,13 +36,9 @@ public sealed class SwitchBuilder
|
||||
Throw.IfNull(executors);
|
||||
|
||||
HashSet<int> indicies = [];
|
||||
int executorIndex = 0;
|
||||
|
||||
foreach (ExecutorBinding executor in executors)
|
||||
{
|
||||
// Explicit name: null element inside the collection argument.
|
||||
Throw.IfNull(executor, $"{nameof(executors)}[{executorIndex++}]");
|
||||
|
||||
if (!this._executorIndicies.TryGetValue(executor.Id, out int index))
|
||||
{
|
||||
index = this._executors.Count;
|
||||
@@ -68,13 +64,8 @@ public sealed class SwitchBuilder
|
||||
{
|
||||
Throw.IfNull(executors);
|
||||
|
||||
int executorIndex = 0;
|
||||
|
||||
foreach (ExecutorBinding executor in executors)
|
||||
{
|
||||
// Explicit name: null element inside the collection argument.
|
||||
Throw.IfNull(executor, $"{nameof(executors)}[{executorIndex++}]");
|
||||
|
||||
if (!this._executorIndicies.TryGetValue(executor.Id, out int index))
|
||||
{
|
||||
index = this._executors.Count;
|
||||
|
||||
@@ -25,11 +25,7 @@ public static class WorkflowBuilderExtensions
|
||||
/// <param name="target">The target executor to which messages will be forwarded.</param>
|
||||
/// <returns>The updated <see cref="WorkflowBuilder"/> instance.</returns>
|
||||
public static WorkflowBuilder ForwardMessage<TMessage>(this WorkflowBuilder builder, ExecutorBinding source, ExecutorBinding target)
|
||||
{
|
||||
Throw.IfNull(target, nameof(target));
|
||||
|
||||
return builder.ForwardMessage<TMessage>(source, [target], condition: null);
|
||||
}
|
||||
=> builder.ForwardMessage<TMessage>(source, [target], condition: null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds edges to the workflow that forward messages of the specified type from the source executor to
|
||||
@@ -56,8 +52,6 @@ public static class WorkflowBuilderExtensions
|
||||
/// <returns>The updated <see cref="WorkflowBuilder"/> instance.</returns>
|
||||
public static WorkflowBuilder ForwardMessage<TMessage>(this WorkflowBuilder builder, ExecutorBinding source, IEnumerable<ExecutorBinding> targets, Func<TMessage, bool>? condition = null)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(source);
|
||||
Throw.IfNull(targets);
|
||||
|
||||
Func<object?, bool> predicate = WorkflowBuilder.CreateConditionFunc<TMessage>(IsAllowedTypeAndMatchingCondition)!;
|
||||
@@ -68,16 +62,14 @@ public static class WorkflowBuilderExtensions
|
||||
if (targets is ICollection<ExecutorBinding> { Count: 1 })
|
||||
#endif
|
||||
{
|
||||
return builder.AddEdge(source, Throw.IfNull(targets.First(), nameof(targets)), predicate);
|
||||
return builder.AddEdge(source, targets.First(), predicate);
|
||||
}
|
||||
|
||||
return builder.AddSwitch(source, (switch_) => switch_.AddCase(predicate, targets.Select(ValidateTarget)));
|
||||
return builder.AddSwitch(source, (switch_) => switch_.AddCase(predicate, targets));
|
||||
|
||||
// The reason we can check for "not null" here is that CreateConditionFunc<T> will do the correct unwrapping
|
||||
// logic for PortableValues.
|
||||
bool IsAllowedTypeAndMatchingCondition(TMessage? message) => message != null && (condition == null || condition(message));
|
||||
|
||||
ExecutorBinding ValidateTarget(ExecutorBinding target) => Throw.IfNull(target, nameof(targets));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -89,11 +81,7 @@ public static class WorkflowBuilderExtensions
|
||||
/// <param name="target">The target executor to which messages, except those of type <typeparamref name="TMessage"/>, will be forwarded.</param>
|
||||
/// <returns>The updated <see cref="WorkflowBuilder"/> instance with the added edges.</returns>
|
||||
public static WorkflowBuilder ForwardExcept<TMessage>(this WorkflowBuilder builder, ExecutorBinding source, ExecutorBinding target)
|
||||
{
|
||||
Throw.IfNull(target, nameof(target));
|
||||
|
||||
return builder.ForwardExcept<TMessage>(source, [target]);
|
||||
}
|
||||
=> builder.ForwardExcept<TMessage>(source, [target]);
|
||||
|
||||
/// <summary>
|
||||
/// Adds edges from the specified source to the provided executors, excluding messages of a specified type.
|
||||
@@ -105,8 +93,6 @@ public static class WorkflowBuilderExtensions
|
||||
/// <returns>The updated <see cref="WorkflowBuilder"/> instance with the added edges.</returns>
|
||||
public static WorkflowBuilder ForwardExcept<TMessage>(this WorkflowBuilder builder, ExecutorBinding source, IEnumerable<ExecutorBinding> targets)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(source);
|
||||
Throw.IfNull(targets);
|
||||
|
||||
Func<object?, bool> predicate = WorkflowBuilder.CreateConditionFunc<TMessage>((Func<object?, bool>)IsAllowedType)!;
|
||||
@@ -117,16 +103,14 @@ public static class WorkflowBuilderExtensions
|
||||
if (targets is ICollection<ExecutorBinding> { Count: 1 })
|
||||
#endif
|
||||
{
|
||||
return builder.AddEdge(source, Throw.IfNull(targets.First(), nameof(targets)), predicate);
|
||||
return builder.AddEdge(source, targets.First(), predicate);
|
||||
}
|
||||
|
||||
return builder.AddSwitch(source, (switch_) => switch_.AddCase(predicate, targets.Select(ValidateTarget)));
|
||||
return builder.AddSwitch(source, (switch_) => switch_.AddCase(predicate, targets));
|
||||
|
||||
// The reason we can check for "null" here is that CreateConditionFunc<T> will do the correct unwrapping
|
||||
// logic for PortableValues.
|
||||
static bool IsAllowedType(object? message) => message is null;
|
||||
|
||||
ExecutorBinding ValidateTarget(ExecutorBinding target) => Throw.IfNull(target, nameof(targets));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -145,7 +129,6 @@ public static class WorkflowBuilderExtensions
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(source);
|
||||
Throw.IfNull(executors);
|
||||
|
||||
HashSet<string> seenExecutors = [source.Id];
|
||||
|
||||
|
||||
-24
@@ -103,30 +103,6 @@ public class HostApplicationBuilderWorkflowExtensionsTests
|
||||
Assert.Contains(workflowDescriptors, d => (string)d.ServiceKey! == "workflow3");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a handoff workflow can be named from the DI workflow key.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddWorkflow_HandoffWorkflowWithName_ResolvesWorkflow()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
const string WorkflowName = "handoffWorkflow";
|
||||
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(a => a.Name).Returns("handoffAgent");
|
||||
|
||||
#pragma warning disable MAAIW001 // This test covers hosting handoff workflows.
|
||||
builder.AddWorkflow(WorkflowName, (sp, key) =>
|
||||
AgentWorkflowBuilder.CreateHandoffBuilderWith(mockAgent.Object)
|
||||
.WithName(key)
|
||||
.Build());
|
||||
#pragma warning restore MAAIW001
|
||||
|
||||
var workflow = builder.Build().Services.GetRequiredKeyedService<Workflow>(WorkflowName);
|
||||
|
||||
Assert.Equal(WorkflowName, workflow.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddWorkflow handles empty strings for name.
|
||||
/// </summary>
|
||||
|
||||
-157
@@ -4,7 +4,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
@@ -321,92 +320,6 @@ public sealed class DefaultMcpToolHandlerTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reserved Tools/List Tests
|
||||
|
||||
[Fact]
|
||||
public void IsListToolsToolName_WithReservedName_ShouldReturnTrue()
|
||||
{
|
||||
// Act
|
||||
bool result = DefaultMcpToolHandler.IsListToolsToolName(DefaultMcpToolHandler.ListToolsToolName);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsListToolsToolName_WithRegularToolName_ShouldReturnFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = DefaultMcpToolHandler.IsListToolsToolName("search");
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeToolAsync_WithListToolsArguments_ShouldThrowArgumentExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
DefaultMcpToolHandler handler = new();
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
Func<Task> act = async () => await handler.InvokeToolAsync(
|
||||
serverUrl: "http://localhost:12345/mcp",
|
||||
serverLabel: "test",
|
||||
toolName: DefaultMcpToolHandler.ListToolsToolName,
|
||||
arguments: new Dictionary<string, object?> { ["ignored"] = true },
|
||||
headers: null,
|
||||
connectionName: null);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentException>()
|
||||
.WithMessage("*does not accept tool arguments*");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await handler.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateListToolsResultContent_WithTools_ShouldSerializeToolMetadataAsync()
|
||||
{
|
||||
// Arrange
|
||||
JsonElement inputSchema = JsonSerializer.Deserialize<JsonElement>(
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [ "query" ]
|
||||
}
|
||||
""");
|
||||
Tool tool = new()
|
||||
{
|
||||
Name = "search",
|
||||
Description = "Searches documentation.",
|
||||
InputSchema = inputSchema
|
||||
};
|
||||
|
||||
// Act
|
||||
McpServerToolResultContent result = DefaultMcpToolHandler.CreateListToolsResultContent([tool]);
|
||||
|
||||
// Assert
|
||||
TextContent text = result.Outputs.Should().ContainSingle().Subject.Should().BeOfType<TextContent>().Subject;
|
||||
using JsonDocument document = JsonDocument.Parse(text.Text);
|
||||
JsonElement listedTool = document.RootElement.GetProperty("tools")[0];
|
||||
listedTool.GetProperty("name").GetString().Should().Be("search");
|
||||
listedTool.GetProperty("description").GetString().Should().Be("Searches documentation.");
|
||||
listedTool.GetProperty("inputSchema").GetProperty("properties").GetProperty("query").GetProperty("type").GetString().Should().Be("string");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interface Implementation Tests
|
||||
|
||||
[Fact]
|
||||
@@ -575,75 +488,5 @@ public sealed class DefaultMcpToolHandlerTests
|
||||
dataContent.MediaType.Should().Be("audio/*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertContentBlock_EmbeddedResourceBlock_WithTextResource_ShouldReturnTextContent()
|
||||
{
|
||||
// Arrange
|
||||
EmbeddedResourceBlock block = new()
|
||||
{
|
||||
Resource = new TextResourceContents
|
||||
{
|
||||
Text = "embedded text payload",
|
||||
Uri = "resource://example",
|
||||
MimeType = "text/plain",
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
AIContent result = DefaultMcpToolHandler.ConvertContentBlock(block);
|
||||
|
||||
// Assert
|
||||
result.Should().BeOfType<TextContent>()
|
||||
.Which.Text.Should().Be("embedded text payload");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertContentBlock_EmbeddedResourceBlock_WithBlobResource_ShouldReturnDataContent()
|
||||
{
|
||||
// Arrange
|
||||
byte[] base64Bytes = Encoding.UTF8.GetBytes("UklGRiQA");
|
||||
EmbeddedResourceBlock block = new()
|
||||
{
|
||||
Resource = new BlobResourceContents
|
||||
{
|
||||
Blob = new ReadOnlyMemory<byte>(base64Bytes),
|
||||
Uri = "resource://example.bin",
|
||||
MimeType = "application/zip",
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
AIContent result = DefaultMcpToolHandler.ConvertContentBlock(block);
|
||||
|
||||
// Assert
|
||||
DataContent dataContent = result.Should().BeOfType<DataContent>().Subject;
|
||||
dataContent.MediaType.Should().Be("application/zip");
|
||||
dataContent.Uri.Should().Be("data:application/zip;base64,UklGRiQA");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertContentBlock_EmbeddedResourceBlock_WithBlobResource_NullMimeType_DefaultsToOctetStream()
|
||||
{
|
||||
// Arrange
|
||||
byte[] base64Bytes = Encoding.UTF8.GetBytes("UklGRiQA");
|
||||
EmbeddedResourceBlock block = new()
|
||||
{
|
||||
Resource = new BlobResourceContents
|
||||
{
|
||||
Blob = new ReadOnlyMemory<byte>(base64Bytes),
|
||||
Uri = "resource://example.bin",
|
||||
MimeType = null!,
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
AIContent result = DefaultMcpToolHandler.ConvertContentBlock(block);
|
||||
|
||||
// Assert
|
||||
DataContent dataContent = result.Should().BeOfType<DataContent>().Subject;
|
||||
dataContent.MediaType.Should().Be("application/octet-stream");
|
||||
dataContent.Uri.Should().Be("data:application/octet-stream;base64,UklGRiQA");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
-38
@@ -432,44 +432,6 @@ public sealed class InvokeMcpToolExecutorTest(ITestOutputHelper output) : Workfl
|
||||
VerifyInvocationEvent(events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeMcpToolExecuteWithReservedListToolsNameAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
const string ListToolsToolName = "tools/list";
|
||||
string? capturedToolName = null;
|
||||
InvokeMcpTool model = this.CreateModel(
|
||||
displayName: nameof(InvokeMcpToolExecuteWithReservedListToolsNameAsync),
|
||||
serverUrl: TestServerUrl,
|
||||
toolName: ListToolsToolName);
|
||||
Mock<IMcpToolHandler> mockProvider = new();
|
||||
mockProvider.Setup(provider => provider.InvokeToolAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<IDictionary<string, object?>?>(),
|
||||
It.IsAny<IDictionary<string, string>?>(),
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<string, string?, string, IDictionary<string, object?>?, IDictionary<string, string>?, string?, CancellationToken>(
|
||||
(_, _, toolName, _, _, _, _) => capturedToolName = toolName)
|
||||
.ReturnsAsync(new McpServerToolResultContent("list-tools-call-id")
|
||||
{
|
||||
Outputs = [new TextContent("{\"tools\":[]}")]
|
||||
});
|
||||
MockAgentProvider mockAgentProvider = new();
|
||||
InvokeMcpToolExecutor action = new(model, mockProvider.Object, mockAgentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events = await this.ExecuteAsync(action, isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
VerifyInvocationEvent(events);
|
||||
Assert.Equal(ListToolsToolName, capturedToolName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeMcpToolExecuteWithMultipleContentTypesAsync()
|
||||
{
|
||||
|
||||
@@ -86,23 +86,6 @@ public class HandoffOrchestrationTests
|
||||
target.Reason.Should().Be("instructions");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildHandoffs_WithNameAndDescription_SetsWorkflowMetadata()
|
||||
{
|
||||
const string WorkflowName = "handoff-workflow";
|
||||
const string WorkflowDescription = "A handoff workflow";
|
||||
|
||||
DoubleEchoAgent agent = new("agent");
|
||||
|
||||
var workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(agent)
|
||||
.WithName(WorkflowName)
|
||||
.WithDescription(WorkflowDescription)
|
||||
.Build();
|
||||
|
||||
Assert.Equal(WorkflowName, workflow.Name);
|
||||
Assert.Equal(WorkflowDescription, workflow.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handoffs_NoTransfers_ResponseServedByOriginalAgentAsync()
|
||||
{
|
||||
|
||||
+1
-22
@@ -34,13 +34,7 @@ public sealed class InputWaiterTests : IDisposable
|
||||
[Fact]
|
||||
public async Task InputWaiter_WaitForInputAsync_BlocksUntilSignaledAsync()
|
||||
{
|
||||
// Use the no-timeout overload so that the wait can only be released by SignalInput.
|
||||
// A finite timeout would make this test's logic racy: the component correctly
|
||||
// honors the timeout, but if the test thread is starved of CPU time (CI load,
|
||||
// GC pause) long enough for the timeout to fire, waitTask completes before
|
||||
// SignalInput is called and the "should not complete before signaled" assertion
|
||||
// flakes. Timeout behavior is covered separately below.
|
||||
Task waitTask = this._waiter.WaitForInputAsync(CancellationToken.None);
|
||||
Task waitTask = this._waiter.WaitForInputAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
Task completedBeforeSignal = await Task.WhenAny(waitTask, Task.Delay(100));
|
||||
completedBeforeSignal.Should().NotBeSameAs(
|
||||
@@ -106,21 +100,6 @@ public sealed class InputWaiterTests : IDisposable
|
||||
this._waiter.SignalInput();
|
||||
await this._waiter.WaitForInputAsync(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InputWaiter_WaitForInputAsync_CompletesWhenTimeoutExpiresAsync()
|
||||
{
|
||||
// Verify that a finite timeout releases the block even without a signal.
|
||||
// We only assert that it *does* complete (within a generous outer bound);
|
||||
// we intentionally do not assert that it stays blocked until the timeout,
|
||||
// because that would re-introduce the same wall-clock flakiness
|
||||
// described in BlocksUntilSignaledAsync (see comment on that test).
|
||||
Task waitTask = this._waiter.WaitForInputAsync(TimeSpan.FromMilliseconds(300));
|
||||
|
||||
Task completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(5)));
|
||||
completed.Should().BeSameAs(waitTask, "the wait task should complete once the timeout expires");
|
||||
await waitTask;
|
||||
}
|
||||
}
|
||||
|
||||
public class OutputFilterTests
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -133,31 +133,31 @@ public sealed class ObservabilityTests : IDisposable
|
||||
activityEvents.Should().Contain(e => e.Name == EventNames.WorkflowCompleted, "activity should have workflow completed event");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task CreatesWorkflowEndToEndActivities_WithCorrectName_DefaultAsync()
|
||||
{
|
||||
await this.TestWorkflowEndToEndActivitiesAsync("Default");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task CreatesWorkflowEndToEndActivities_WithCorrectName_OffThreadAsync()
|
||||
{
|
||||
await this.TestWorkflowEndToEndActivitiesAsync("OffThread");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task CreatesWorkflowEndToEndActivities_WithCorrectName_ConcurrentAsync()
|
||||
{
|
||||
await this.TestWorkflowEndToEndActivitiesAsync("Concurrent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task CreatesWorkflowEndToEndActivities_WithCorrectName_LockstepAsync()
|
||||
{
|
||||
await this.TestWorkflowEndToEndActivitiesAsync("Lockstep");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task CreatesWorkflowActivities_WithCorrectNameAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -182,7 +182,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
tags.Should().ContainKey(Tags.WorkflowDefinition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task TelemetryDisabledByDefault_CreatesNoActivitiesAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -200,7 +200,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
capturedActivities.Should().BeEmpty("No activities should be created when telemetry is disabled (default).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task WithOpenTelemetry_UsesProvidedActivitySourceAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -235,7 +235,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
"All activities should come from the user-provided ActivitySource.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task DisableWorkflowBuild_PreventsWorkflowBuildActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -255,7 +255,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
"WorkflowBuild activity should be disabled.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task DisableWorkflowRun_PreventsWorkflowRunActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -285,7 +285,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task DisableExecutorProcess_PreventsExecutorProcessActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -312,7 +312,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task DisableEdgeGroupProcess_PreventsEdgeGroupProcessActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -333,7 +333,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task DisableMessageSend_PreventsMessageSendActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -382,7 +382,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
return builder.WithOpenTelemetry(configure: opts => opts.DisableMessageSend = true).Build();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task EnableSensitiveData_LogsExecutorInputAndOutputAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -413,7 +413,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
tags[Tags.ExecutorOutput].Should().Contain("HELLO", "Output should contain the transformed value.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task EnableSensitiveData_Disabled_DoesNotLogInputOutputAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -442,7 +442,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
tags.Should().NotContainKey(Tags.ExecutorOutput, "Output should NOT be logged when EnableSensitiveData is false.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task EnableSensitiveData_LogsMessageSendContentAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -474,7 +474,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
tags.Should().ContainKey(Tags.MessageSourceId, "Source ID should be logged.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task EnableSensitiveData_Disabled_DoesNotLogMessageContentAsync()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
@@ -158,301 +157,4 @@ public partial class WorkflowBuilderSmokeTests
|
||||
workflow3.Name.Should().Be("Named Only");
|
||||
workflow3.Description.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForwardMessage_WithSingleTarget_CreatesDirectEdge()
|
||||
{
|
||||
// Arrange
|
||||
NoOpExecutor source = new("start");
|
||||
NoOpExecutor target = new("target");
|
||||
|
||||
// Act
|
||||
Workflow workflow = new WorkflowBuilder(source.Id)
|
||||
.ForwardMessage<string>(source, target)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Edge edge = GetSingleEdge(workflow, source.Id);
|
||||
edge.Kind.Should().Be(EdgeKind.Direct);
|
||||
edge.DirectEdgeData.Should().NotBeNull();
|
||||
edge.DirectEdgeData!.SourceId.Should().Be(source.Id);
|
||||
edge.DirectEdgeData!.SinkId.Should().Be(target.Id);
|
||||
edge.DirectEdgeData.Condition.Should().NotBeNull();
|
||||
edge.DirectEdgeData.Condition!("message").Should().BeTrue();
|
||||
edge.DirectEdgeData.Condition!(42).Should().BeFalse();
|
||||
edge.DirectEdgeData.Condition!(null).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForwardMessage_WithMultipleTargets_CreatesFanOutEdge()
|
||||
{
|
||||
// Arrange
|
||||
NoOpExecutor source = new("start");
|
||||
NoOpExecutor target1 = new("target1");
|
||||
NoOpExecutor target2 = new("target2");
|
||||
|
||||
// Act
|
||||
Workflow workflow = new WorkflowBuilder(source.Id)
|
||||
.ForwardMessage<string>(source, [target1, target2], message => message == "match")
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Edge edge = GetSingleEdge(workflow, source.Id);
|
||||
edge.Kind.Should().Be(EdgeKind.FanOut);
|
||||
edge.FanOutEdgeData.Should().NotBeNull();
|
||||
edge.FanOutEdgeData!.SourceId.Should().Be(source.Id);
|
||||
edge.FanOutEdgeData!.SinkIds.Should().Equal([target1.Id, target2.Id]);
|
||||
edge.FanOutEdgeData.EdgeAssigner.Should().NotBeNull();
|
||||
edge.FanOutEdgeData.EdgeAssigner!("match", 2).Should().Equal([0, 1]);
|
||||
edge.FanOutEdgeData.EdgeAssigner!("other", 2).Should().BeEmpty();
|
||||
edge.FanOutEdgeData.EdgeAssigner!(42, 2).Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForwardExcept_WithSingleTarget_CreatesDirectEdge()
|
||||
{
|
||||
// Arrange
|
||||
NoOpExecutor source = new("start");
|
||||
NoOpExecutor target = new("target");
|
||||
|
||||
// Act
|
||||
Workflow workflow = new WorkflowBuilder(source.Id)
|
||||
.ForwardExcept<string>(source, target)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Edge edge = GetSingleEdge(workflow, source.Id);
|
||||
edge.Kind.Should().Be(EdgeKind.Direct);
|
||||
edge.DirectEdgeData.Should().NotBeNull();
|
||||
edge.DirectEdgeData!.SourceId.Should().Be(source.Id);
|
||||
edge.DirectEdgeData!.SinkId.Should().Be(target.Id);
|
||||
edge.DirectEdgeData.Condition.Should().NotBeNull();
|
||||
edge.DirectEdgeData.Condition!("message").Should().BeFalse();
|
||||
edge.DirectEdgeData.Condition!(42).Should().BeTrue();
|
||||
edge.DirectEdgeData.Condition!(null).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForwardExcept_WithMultipleTargets_CreatesFanOutEdge()
|
||||
{
|
||||
// Arrange
|
||||
NoOpExecutor source = new("start");
|
||||
NoOpExecutor target1 = new("target1");
|
||||
NoOpExecutor target2 = new("target2");
|
||||
|
||||
// Act
|
||||
Workflow workflow = new WorkflowBuilder(source.Id)
|
||||
.ForwardExcept<string>(source, [target1, target2])
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Edge edge = GetSingleEdge(workflow, source.Id);
|
||||
edge.Kind.Should().Be(EdgeKind.FanOut);
|
||||
edge.FanOutEdgeData.Should().NotBeNull();
|
||||
edge.FanOutEdgeData!.SourceId.Should().Be(source.Id);
|
||||
edge.FanOutEdgeData!.SinkIds.Should().Equal([target1.Id, target2.Id]);
|
||||
edge.FanOutEdgeData.EdgeAssigner.Should().NotBeNull();
|
||||
edge.FanOutEdgeData.EdgeAssigner!(42, 2).Should().Equal([0, 1]);
|
||||
edge.FanOutEdgeData.EdgeAssigner!("message", 2).Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddChain_CreatesSequentialDirectEdges()
|
||||
{
|
||||
// Arrange
|
||||
NoOpExecutor source = new("start");
|
||||
NoOpExecutor middle = new("middle");
|
||||
NoOpExecutor end = new("end");
|
||||
|
||||
// Act
|
||||
Workflow workflow = new WorkflowBuilder(source.Id)
|
||||
.AddChain(source, [middle, end])
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Edge firstEdge = GetSingleEdge(workflow, source.Id);
|
||||
firstEdge.Kind.Should().Be(EdgeKind.Direct);
|
||||
firstEdge.DirectEdgeData!.SourceId.Should().Be(source.Id);
|
||||
firstEdge.DirectEdgeData.SinkId.Should().Be(middle.Id);
|
||||
|
||||
Edge secondEdge = GetSingleEdge(workflow, middle.Id);
|
||||
secondEdge.Kind.Should().Be(EdgeKind.Direct);
|
||||
secondEdge.DirectEdgeData!.SourceId.Should().Be(middle.Id);
|
||||
secondEdge.DirectEdgeData.SinkId.Should().Be(end.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddChain_WhenExecutorRepeats_Throws()
|
||||
{
|
||||
// Arrange
|
||||
NoOpExecutor source = new("start");
|
||||
NoOpExecutor middle = new("middle");
|
||||
|
||||
// Act
|
||||
Action act = () => new WorkflowBuilder(source.Id)
|
||||
.AddChain(source, [middle, source]);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithParameterName("executors");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExternalCall_CreatesRequestPortAndRoundTripEdges()
|
||||
{
|
||||
// Arrange
|
||||
const string PortId = "port1";
|
||||
NoOpExecutor source = new("start");
|
||||
|
||||
// Act
|
||||
Workflow workflow = new WorkflowBuilder(source.Id)
|
||||
.AddExternalCall<string, int>(source, PortId)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
workflow.Ports.Should().ContainKey(PortId);
|
||||
workflow.Ports[PortId].Request.Should().Be(typeof(string));
|
||||
workflow.Ports[PortId].Response.Should().Be(typeof(int));
|
||||
workflow.ExecutorBindings.Should().ContainKey(PortId);
|
||||
|
||||
Edge requestEdge = GetSingleEdge(workflow, source.Id);
|
||||
requestEdge.Kind.Should().Be(EdgeKind.Direct);
|
||||
requestEdge.DirectEdgeData!.SourceId.Should().Be(source.Id);
|
||||
requestEdge.DirectEdgeData.SinkId.Should().Be(PortId);
|
||||
|
||||
Edge responseEdge = GetSingleEdge(workflow, PortId);
|
||||
responseEdge.Kind.Should().Be(EdgeKind.Direct);
|
||||
responseEdge.DirectEdgeData!.SourceId.Should().Be(PortId);
|
||||
responseEdge.DirectEdgeData.SinkId.Should().Be(source.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSwitch_CreatesFanOutEdgeWithCasesAndDefault()
|
||||
{
|
||||
// Arrange
|
||||
NoOpExecutor source = new("start");
|
||||
NoOpExecutor stringTarget = new("string-target");
|
||||
NoOpExecutor intTarget = new("int-target");
|
||||
NoOpExecutor defaultTarget = new("default-target");
|
||||
|
||||
// Act
|
||||
Workflow workflow = new WorkflowBuilder(source.Id)
|
||||
.AddSwitch(source, switchBuilder => switchBuilder
|
||||
.AddCase<string>(message => message == "match", [stringTarget])
|
||||
.AddCase<int>(message => message > 0, [intTarget])
|
||||
.WithDefault([defaultTarget]))
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Edge edge = GetSingleEdge(workflow, source.Id);
|
||||
edge.Kind.Should().Be(EdgeKind.FanOut);
|
||||
edge.FanOutEdgeData.Should().NotBeNull();
|
||||
edge.FanOutEdgeData!.SourceId.Should().Be(source.Id);
|
||||
edge.FanOutEdgeData!.SinkIds.Should().Equal([stringTarget.Id, intTarget.Id, defaultTarget.Id]);
|
||||
edge.FanOutEdgeData.EdgeAssigner.Should().NotBeNull();
|
||||
edge.FanOutEdgeData.EdgeAssigner!("match", 3).Should().Equal([0]);
|
||||
edge.FanOutEdgeData.EdgeAssigner!(2, 3).Should().Equal([1]);
|
||||
edge.FanOutEdgeData.EdgeAssigner!("other", 3).Should().Equal([2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForwardMessage_InvalidArguments_Throw()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowBuilder builder = new("start");
|
||||
NoOpExecutor source = new("start");
|
||||
NoOpExecutor target = new("target");
|
||||
|
||||
// Act/Assert
|
||||
Assert.Throws<ArgumentNullException>(() => ((WorkflowBuilder)null!).ForwardMessage<string>(source, target));
|
||||
Assert.Throws<ArgumentNullException>("source", () => builder.ForwardMessage<string>(null!, target));
|
||||
Assert.Throws<ArgumentNullException>("target", () => builder.ForwardMessage<string>(source, (ExecutorBinding)null!));
|
||||
Assert.Throws<ArgumentNullException>("targets", () => builder.ForwardMessage<string>(source, (IEnumerable<ExecutorBinding>)null!));
|
||||
Assert.Throws<ArgumentNullException>("targets", () => builder.ForwardMessage<string>(source, [target, null!]));
|
||||
Assert.Throws<ArgumentException>("targets", () => builder.ForwardMessage<string>(source, []));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForwardExcept_InvalidArguments_Throw()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowBuilder builder = new("start");
|
||||
NoOpExecutor source = new("start");
|
||||
NoOpExecutor target = new("target");
|
||||
|
||||
// Act/Assert
|
||||
Assert.Throws<ArgumentNullException>(() => ((WorkflowBuilder)null!).ForwardExcept<string>(source, target));
|
||||
Assert.Throws<ArgumentNullException>("source", () => builder.ForwardExcept<string>(null!, target));
|
||||
Assert.Throws<ArgumentNullException>("target", () => builder.ForwardExcept<string>(source, (ExecutorBinding)null!));
|
||||
Assert.Throws<ArgumentNullException>("targets", () => builder.ForwardExcept<string>(source, (IEnumerable<ExecutorBinding>)null!));
|
||||
Assert.Throws<ArgumentNullException>("targets", () => builder.ForwardExcept<string>(source, [target, null!]));
|
||||
Assert.Throws<ArgumentException>("targets", () => builder.ForwardExcept<string>(source, []));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddChain_InvalidArguments_Throw()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowBuilder builder = new("start");
|
||||
NoOpExecutor source = new("start");
|
||||
NoOpExecutor target = new("target");
|
||||
NoOpExecutor otherTarget = new("other-target");
|
||||
|
||||
// Act/Assert
|
||||
Assert.Throws<ArgumentNullException>(() => ((WorkflowBuilder)null!).AddChain(source, [target]));
|
||||
Assert.Throws<ArgumentNullException>("source", () => builder.AddChain(null!, [target]));
|
||||
Assert.Throws<ArgumentNullException>("executors", () => builder.AddChain(source, null!));
|
||||
Assert.Throws<ArgumentNullException>("executors", () => builder.AddChain(source, [target, null!]));
|
||||
Assert.Throws<ArgumentException>("executors", () => builder.AddChain(source, [target, source]));
|
||||
Assert.Throws<ArgumentException>("executors", () => builder.AddChain(source, [target, otherTarget, target]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExternalCall_InvalidArguments_Throw()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowBuilder builder = new("start");
|
||||
NoOpExecutor source = new("start");
|
||||
|
||||
// Act/Assert
|
||||
Assert.Throws<ArgumentNullException>(() => ((WorkflowBuilder)null!).AddExternalCall<string, int>(source, "port"));
|
||||
Assert.Throws<ArgumentNullException>("source", () => builder.AddExternalCall<string, int>(null!, "port"));
|
||||
Assert.Throws<ArgumentNullException>("portId", () => builder.AddExternalCall<string, int>(source, null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSwitch_InvalidArguments_Throw()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowBuilder builder = new("start");
|
||||
NoOpExecutor source = new("start");
|
||||
|
||||
// Act/Assert
|
||||
Assert.Throws<ArgumentNullException>(() => ((WorkflowBuilder)null!).AddSwitch(source, _ => { }));
|
||||
Assert.Throws<ArgumentNullException>("source", () => builder.AddSwitch(null!, _ => { }));
|
||||
Assert.Throws<ArgumentNullException>("configureSwitch", () => builder.AddSwitch(source, null!));
|
||||
Assert.Throws<ArgumentException>("targets", () => builder.AddSwitch(source, _ => { }));
|
||||
Assert.Throws<ArgumentException>("targets", () => builder.AddSwitch(source, switchBuilder => switchBuilder.AddCase<string>(_ => true, [])));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SwitchBuilder_InvalidArguments_Throw()
|
||||
{
|
||||
// Arrange
|
||||
SwitchBuilder switchBuilder = new();
|
||||
NoOpExecutor target = new("target");
|
||||
|
||||
// Act/Assert
|
||||
Assert.Throws<ArgumentNullException>("predicate", () => switchBuilder.AddCase<string>(null!, [target]));
|
||||
Assert.Throws<ArgumentNullException>("executors", () => switchBuilder.AddCase<string>(_ => true, null!));
|
||||
Assert.Throws<ArgumentNullException>("executors[1]", () => switchBuilder.AddCase<string>(_ => true, [target, null!]));
|
||||
Assert.Throws<ArgumentNullException>("executors", () => switchBuilder.WithDefault(null!));
|
||||
Assert.Throws<ArgumentNullException>("executors[1]", () => switchBuilder.WithDefault([target, null!]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the only edge emitted by the specified workflow source.
|
||||
/// </summary>
|
||||
private static Edge GetSingleEdge(Workflow workflow, string sourceId)
|
||||
=> workflow.Edges[sourceId].Should().ContainSingle().Subject;
|
||||
}
|
||||
|
||||
+6
-6
@@ -67,7 +67,7 @@ public sealed class WorkflowRunActivityStopTests : IDisposable
|
||||
/// Bug: The Activity created by LockstepRunEventStream.TakeEventStreamAsync is never
|
||||
/// disposed because yield break in async iterators does not trigger using disposal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task WorkflowRunActivity_IsStopped_LockstepAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -111,7 +111,7 @@ public sealed class WorkflowRunActivityStopTests : IDisposable
|
||||
/// Verifies that the workflow_invoke Activity is stopped when using the OffThread (Default)
|
||||
/// execution environment (StreamingRunEventStream).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task WorkflowRunActivity_IsStopped_OffThreadAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -156,7 +156,7 @@ public sealed class WorkflowRunActivityStopTests : IDisposable
|
||||
/// (StreamingRun.WatchStreamAsync) with the OffThread execution environment.
|
||||
/// This matches the exact usage pattern described in the issue.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task WorkflowRunActivity_IsStopped_Streaming_OffThreadAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -203,7 +203,7 @@ public sealed class WorkflowRunActivityStopTests : IDisposable
|
||||
/// streaming invocation, even when using the same workflow in a multi-turn pattern,
|
||||
/// and that each session gets its own session activity.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task WorkflowRunActivity_IsStopped_Streaming_OffThread_MultiTurnAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -264,7 +264,7 @@ public sealed class WorkflowRunActivityStopTests : IDisposable
|
||||
/// Verifies that all started activities (not just workflow_invoke) are properly stopped.
|
||||
/// This ensures no spans are "leaked" without being exported.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task AllActivities_AreStopped_AfterWorkflowCompletionAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -305,7 +305,7 @@ public sealed class WorkflowRunActivityStopTests : IDisposable
|
||||
/// be parented under the workflow session span. The run activity should
|
||||
/// still nest correctly under the session.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Fact(Skip = "Flaky test - temporarily disabled.")]
|
||||
public async Task Lockstep_SessionActivity_DoesNotLeak_IntoCaller_ActivityCurrentAsync()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
+1
-24
@@ -7,27 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.4.0] - 2026-05-14
|
||||
|
||||
### Added
|
||||
- **agent-framework-core**: Forward MCP tool call metadata ([#5815](https://github.com/microsoft/agent-framework/pull/5815))
|
||||
- **agent-framework-core**: Support `list[str]` arguments for file-based skill scripts ([#5850](https://github.com/microsoft/agent-framework/pull/5850))
|
||||
- **agent-framework-core**: Strip server-issued response item IDs under storage ([#5690](https://github.com/microsoft/agent-framework/pull/5690))
|
||||
- **agent-framework-ag-ui**: Add tool result display channel ([#5762](https://github.com/microsoft/agent-framework/pull/5762))
|
||||
- **agent-framework-ag-ui**: Promote to release candidate stage ([#5844](https://github.com/microsoft/agent-framework/pull/5844))
|
||||
- **agent-framework-devui**: Improvements for DevUI ([#5840](https://github.com/microsoft/agent-framework/pull/5840))
|
||||
|
||||
### Changed
|
||||
- **agent-framework-core**: [BREAKING — experimental skills API] Align file skill folder discovery with agentskills.io spec ([#5807](https://github.com/microsoft/agent-framework/pull/5807))
|
||||
- **agent-framework-core**: [BREAKING — experimental skills API] Extract skill spec metadata into `SkillFrontmatter` ([#5775](https://github.com/microsoft/agent-framework/pull/5775))
|
||||
- **agent-framework-devui**: [BREAKING] Tighten default access controls and CORS posture ([#5740](https://github.com/microsoft/agent-framework/pull/5740))
|
||||
- **agent-framework-a2a**: [BREAKING] Migrate to a2a-sdk v1.0 ([#5752](https://github.com/microsoft/agent-framework/pull/5752))
|
||||
|
||||
### Fixed
|
||||
- **agent-framework-a2a**: Fix A2A v1.0 non-streaming response and sample runtime issues ([#5849](https://github.com/microsoft/agent-framework/pull/5849))
|
||||
- **agent-framework-foundry-hosting**: Reject path-traversal context IDs in checkpoint storage ([#5851](https://github.com/microsoft/agent-framework/pull/5851))
|
||||
- **agent-framework-core**: Prevent MCP message_handler deadlock on notification reload ([#4866](https://github.com/microsoft/agent-framework/pull/4866))
|
||||
|
||||
## [1.3.0] - 2026-05-07
|
||||
|
||||
### Added
|
||||
@@ -1071,9 +1050,7 @@ Release candidate for **agent-framework-core** and **agent-framework-azure-ai**
|
||||
|
||||
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.4.0...HEAD
|
||||
[1.4.0]: https://github.com/microsoft/agent-framework/compare/python-1.3.0...python-1.4.0
|
||||
[1.3.0]: https://github.com/microsoft/agent-framework/compare/python-1.2.2...python-1.3.0
|
||||
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.2.2...HEAD
|
||||
[1.2.2]: https://github.com/microsoft/agent-framework/compare/python-1.2.1...python-1.2.2
|
||||
[1.2.1]: https://github.com/microsoft/agent-framework/compare/python-1.2.0...python-1.2.1
|
||||
[1.2.0]: https://github.com/microsoft/agent-framework/compare/python-1.1.1...python-1.2.0
|
||||
|
||||
@@ -42,7 +42,7 @@ request_handler = DefaultRequestHandler(
|
||||
app = Starlette(
|
||||
routes=[
|
||||
*create_agent_card_routes(my_agent_card),
|
||||
*create_jsonrpc_routes(request_handler, "/"),
|
||||
*create_jsonrpc_routes(request_handler),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
@@ -78,7 +78,7 @@ class A2AExecutor(AgentExecutor):
|
||||
app = Starlette(
|
||||
routes=[
|
||||
*create_agent_card_routes(public_agent_card),
|
||||
*create_jsonrpc_routes(request_handler, "/"),
|
||||
*create_jsonrpc_routes(request_handler),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -365,10 +365,6 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent):
|
||||
|
||||
all_updates: list[AgentResponseUpdate] = []
|
||||
streamed_artifact_ids_by_task: dict[str, set[str]] = {}
|
||||
# In non-streaming mode, accumulate intermediate status content so it
|
||||
# can be surfaced when the terminal event arrives (mirroring v0.3.x
|
||||
# behavior where the full Task history was available at completion).
|
||||
pending_updates_by_task: dict[str, list[AgentResponseUpdate]] = {}
|
||||
async for item in a2a_stream:
|
||||
payload_type = item.WhichOneof("payload")
|
||||
if payload_type == "message":
|
||||
@@ -395,55 +391,27 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent):
|
||||
)
|
||||
if task.status.state in TERMINAL_TASK_STATES:
|
||||
streamed_artifact_ids_by_task.pop(task.id, None)
|
||||
# If the terminal Task has no content, flush accumulated updates
|
||||
if not updates or all(not u.contents for u in updates):
|
||||
pending = pending_updates_by_task.pop(task.id, [])
|
||||
for update in pending:
|
||||
all_updates.append(update)
|
||||
yield update
|
||||
else:
|
||||
pending_updates_by_task.pop(task.id, None)
|
||||
for update in updates:
|
||||
all_updates.append(update)
|
||||
yield update
|
||||
elif payload_type == "status_update":
|
||||
status_event = item.status_update
|
||||
updates = self._updates_from_task_update_event(status_event)
|
||||
is_terminal = status_event.status.state in TERMINAL_TASK_STATES
|
||||
if emit_intermediate:
|
||||
for update in updates:
|
||||
all_updates.append(update)
|
||||
yield update
|
||||
elif is_terminal:
|
||||
if updates:
|
||||
# Terminal event with content — discard accumulated intermediates
|
||||
pending_updates_by_task.pop(status_event.task_id, None)
|
||||
for update in updates:
|
||||
all_updates.append(update)
|
||||
yield update
|
||||
else:
|
||||
# Terminal event with NO content — flush accumulated updates
|
||||
pending = pending_updates_by_task.pop(status_event.task_id, [])
|
||||
for update in pending:
|
||||
all_updates.append(update)
|
||||
yield update
|
||||
else:
|
||||
# Non-streaming intermediate: accumulate for later
|
||||
if updates:
|
||||
pending_updates_by_task.setdefault(status_event.task_id, []).extend(updates)
|
||||
elif payload_type == "artifact_update":
|
||||
artifact_event = item.artifact_update
|
||||
updates = self._updates_from_task_update_event(artifact_event)
|
||||
# Always yield artifact updates — they carry actual response
|
||||
# content (files, data). Track IDs so that a subsequent
|
||||
# terminal Task doesn't duplicate the same artifacts.
|
||||
if updates:
|
||||
streamed_artifact_ids_by_task.setdefault(artifact_event.task_id, set()).add(
|
||||
artifact_event.artifact.artifact_id
|
||||
)
|
||||
for update in updates:
|
||||
all_updates.append(update)
|
||||
yield update
|
||||
if emit_intermediate:
|
||||
for update in updates:
|
||||
all_updates.append(update)
|
||||
yield update
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported StreamResponse payload: {payload_type}")
|
||||
|
||||
|
||||
@@ -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.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"a2a-sdk>=1.0.0,<2",
|
||||
]
|
||||
|
||||
|
||||
@@ -1570,102 +1570,4 @@ async def test_none_metadata_leaves_additional_properties_empty(
|
||||
assert not response.additional_properties
|
||||
|
||||
|
||||
async def test_non_streaming_terminal_status_update_surfaces_content(
|
||||
a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient
|
||||
) -> None:
|
||||
"""Non-streaming run() should surface content from terminal status_update events."""
|
||||
completed_msg = A2AMessage(
|
||||
message_id="msg-complete",
|
||||
role=A2ARole.ROLE_AGENT,
|
||||
parts=[Part(text="Done! Here is your answer.")],
|
||||
)
|
||||
status = TaskStatus(state=TaskState.TASK_STATE_COMPLETED, message=completed_msg)
|
||||
event = TaskStatusUpdateEvent(task_id="task-ts", context_id="ctx-ts", status=status)
|
||||
mock_a2a_client.responses.append(StreamResponse(status_update=event))
|
||||
|
||||
response = await a2a_agent.run("Hello")
|
||||
|
||||
assert len(response.messages) == 1
|
||||
assert response.messages[0].text == "Done! Here is your answer."
|
||||
|
||||
|
||||
async def test_non_streaming_accumulates_working_content_for_empty_terminal(
|
||||
a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient
|
||||
) -> None:
|
||||
"""Non-streaming run() accumulates WORKING content and flushes on empty terminal event."""
|
||||
# Intermediate WORKING event with content
|
||||
working_msg = A2AMessage(
|
||||
message_id="msg-working",
|
||||
role=A2ARole.ROLE_AGENT,
|
||||
parts=[Part(text="Here is your answer from working state.")],
|
||||
)
|
||||
working_status = TaskStatus(state=TaskState.TASK_STATE_WORKING, message=working_msg)
|
||||
working_event = TaskStatusUpdateEvent(task_id="task-acc", context_id="ctx-acc", status=working_status)
|
||||
mock_a2a_client.responses.append(StreamResponse(status_update=working_event))
|
||||
|
||||
# Terminal COMPLETED event with NO content
|
||||
completed_status = TaskStatus(state=TaskState.TASK_STATE_COMPLETED)
|
||||
completed_event = TaskStatusUpdateEvent(task_id="task-acc", context_id="ctx-acc", status=completed_status)
|
||||
mock_a2a_client.responses.append(StreamResponse(status_update=completed_event))
|
||||
|
||||
response = await a2a_agent.run("Hello")
|
||||
|
||||
# The accumulated WORKING content is flushed when terminal arrives empty
|
||||
assert len(response.messages) == 1
|
||||
assert response.messages[0].text == "Here is your answer from working state."
|
||||
|
||||
|
||||
async def test_non_streaming_intermediate_discarded_when_terminal_has_content(
|
||||
a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient
|
||||
) -> None:
|
||||
"""Non-streaming: if terminal event has content, intermediate content is discarded."""
|
||||
# Intermediate WORKING event
|
||||
working_msg = A2AMessage(
|
||||
message_id="msg-working",
|
||||
role=A2ARole.ROLE_AGENT,
|
||||
parts=[Part(text="Still thinking...")],
|
||||
)
|
||||
working_status = TaskStatus(state=TaskState.TASK_STATE_WORKING, message=working_msg)
|
||||
working_event = TaskStatusUpdateEvent(task_id="task-wi", context_id="ctx-wi", status=working_status)
|
||||
mock_a2a_client.responses.append(StreamResponse(status_update=working_event))
|
||||
|
||||
# Terminal COMPLETED event WITH content
|
||||
completed_msg = A2AMessage(
|
||||
message_id="msg-final",
|
||||
role=A2ARole.ROLE_AGENT,
|
||||
parts=[Part(text="Final answer")],
|
||||
)
|
||||
completed_status = TaskStatus(state=TaskState.TASK_STATE_COMPLETED, message=completed_msg)
|
||||
completed_event = TaskStatusUpdateEvent(task_id="task-wi", context_id="ctx-wi", status=completed_status)
|
||||
mock_a2a_client.responses.append(StreamResponse(status_update=completed_event))
|
||||
|
||||
response = await a2a_agent.run("Hello")
|
||||
|
||||
# Terminal content supersedes accumulated intermediates
|
||||
assert len(response.messages) == 1
|
||||
assert response.messages[0].text == "Final answer"
|
||||
|
||||
|
||||
async def test_non_streaming_artifact_update_surfaces_content(
|
||||
a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient
|
||||
) -> None:
|
||||
"""Non-streaming run() should surface content from artifact_update events."""
|
||||
artifact = Artifact(
|
||||
artifact_id="art-ns",
|
||||
parts=[Part(text="Artifact content")],
|
||||
)
|
||||
event = TaskArtifactUpdateEvent(task_id="task-anu", context_id="ctx-anu", artifact=artifact, append=False)
|
||||
mock_a2a_client.responses.append(StreamResponse(artifact_update=event))
|
||||
|
||||
# Terminal task with the same artifact ID — should be deduped
|
||||
mock_a2a_client.add_task_response("task-anu", [{"id": "art-ns", "content": "Artifact content"}])
|
||||
|
||||
response = await a2a_agent.run("Hello")
|
||||
|
||||
# Artifact update + terminal task with same artifact ID = content emitted once from
|
||||
# the artifact_update, then the duplicate from the task is filtered by streamed_artifact_ids
|
||||
assert len(response.messages) == 1
|
||||
assert response.messages[0].text == "Artifact content"
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -22,7 +22,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"ag-ui-protocol>=0.1.16,<0.2",
|
||||
"fastapi>=0.115.0,<0.133.1",
|
||||
"uvicorn[standard]>=0.30.0,<0.42.0"
|
||||
|
||||
@@ -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.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"anthropic>=0.80.0,<0.80.1",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Azure AI Search integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"azure-search-documents>=11.7.0b2,<11.7.0b3",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Azure Content Understanding integration for Microsoft Agent Frame
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com" }]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0a260514"
|
||||
version = "1.0.0a260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,8 +23,8 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-foundry>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"agent-framework-foundry>=1.3.0,<2",
|
||||
"azure-ai-contentunderstanding>=1.0.1,<1.1",
|
||||
"aiohttp>=3.9,<4",
|
||||
"filetype>=1.2,<2",
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Azure Cosmos DB history provider integration for Microsoft Agent
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"azure-cosmos>=4.3.0,<5",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Azure Functions integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -22,7 +22,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"agent-framework-durabletask",
|
||||
"azure-functions>=1.24.0,<2",
|
||||
"azure-functions-durable>=1.3.1,<2",
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Amazon Bedrock integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"boto3>=1.35.0,<2.0.0",
|
||||
"botocore>=1.35.0,<2.0.0",
|
||||
]
|
||||
|
||||
@@ -21,7 +21,6 @@ from chatkit.types import (
|
||||
HiddenContextItem,
|
||||
ImageAttachment,
|
||||
SDKHiddenContextItem,
|
||||
StructuredInputItem,
|
||||
TaskItem,
|
||||
ThreadItem,
|
||||
UserMessageItem,
|
||||
@@ -528,9 +527,6 @@ class ThreadItemConverter:
|
||||
case GeneratedImageItem():
|
||||
# TODO(evmattso): Implement generated image handling in a future PR
|
||||
return []
|
||||
case StructuredInputItem():
|
||||
# TODO(evmattso): Implement structured input handling in a future PR
|
||||
return []
|
||||
case _:
|
||||
assert_never(item)
|
||||
|
||||
|
||||
@@ -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.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -22,7 +22,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"openai-chatkit>=1.4.1,<2.0.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Claude Agent SDK integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"claude-agent-sdk>=0.1.36,<0.1.49",
|
||||
]
|
||||
|
||||
|
||||
@@ -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.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"microsoft-agents-copilotstudio-client>=0.3.1,<0.3.2",
|
||||
]
|
||||
|
||||
|
||||
@@ -261,7 +261,6 @@ class MCPTool:
|
||||
self.request_timeout = request_timeout
|
||||
self.client = client
|
||||
self._functions: list[FunctionTool] = []
|
||||
self._tool_call_meta_by_name: dict[str, dict[str, Any]] = {}
|
||||
self.is_connected: bool = False
|
||||
self._tools_loaded: bool = False
|
||||
self._prompts_loaded: bool = False
|
||||
@@ -1027,7 +1026,6 @@ class MCPTool:
|
||||
|
||||
# Track existing function names to prevent duplicates
|
||||
existing_names = {func.name for func in self._functions}
|
||||
self._tool_call_meta_by_name.clear()
|
||||
|
||||
params: types.PaginatedRequestParams | None = None
|
||||
while True:
|
||||
@@ -1037,9 +1035,6 @@ class MCPTool:
|
||||
tool_list = await self.session.list_tools(params=params) # type: ignore[union-attr]
|
||||
|
||||
for tool in tool_list.tools:
|
||||
if tool.meta is not None:
|
||||
self._tool_call_meta_by_name[tool.name] = dict(tool.meta)
|
||||
|
||||
normalized_name = _normalize_mcp_name(tool.name)
|
||||
local_name = _build_prefixed_mcp_name(normalized_name, self.tool_name_prefix)
|
||||
|
||||
@@ -1190,15 +1185,14 @@ class MCPTool:
|
||||
}
|
||||
}
|
||||
|
||||
# Some MCP proxies require their tools/list metadata to be echoed on tools/call.
|
||||
tool_meta = self._tool_call_meta_by_name.get(tool_name)
|
||||
meta = _inject_otel_into_mcp_meta(dict(tool_meta) if tool_meta is not None else None)
|
||||
# Inject OpenTelemetry trace context into MCP _meta for distributed tracing.
|
||||
otel_meta = _inject_otel_into_mcp_meta()
|
||||
|
||||
parser = self.parse_tool_results or self._parse_tool_result_from_mcp
|
||||
# Try the operation, reconnecting once if the connection is closed
|
||||
for attempt in range(2):
|
||||
try:
|
||||
result = await self.session.call_tool(tool_name, arguments=filtered_kwargs, meta=meta) # type: ignore
|
||||
result = await self.session.call_tool(tool_name, arguments=filtered_kwargs, meta=otel_meta) # type: ignore
|
||||
if result.isError:
|
||||
parsed = parser(result)
|
||||
text = (
|
||||
|
||||
@@ -289,15 +289,13 @@ class SkillScript(ABC):
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
async def run(self, skill: Skill, args: dict[str, Any] | list[str] | None = None, **kwargs: Any) -> Any:
|
||||
async def run(self, skill: Skill, args: dict[str, Any] | None = None, **kwargs: Any) -> Any:
|
||||
"""Run this script.
|
||||
|
||||
Args:
|
||||
skill: The skill that owns this script.
|
||||
args: Optional arguments for the script, provided by the
|
||||
agent/LLM. May be a ``dict`` (named keyword arguments
|
||||
for inline scripts) or a ``list[str]`` (positional CLI
|
||||
arguments for file-based scripts).
|
||||
args: Optional keyword arguments for the script, provided by the
|
||||
agent/LLM.
|
||||
**kwargs: Runtime keyword arguments forwarded only to script
|
||||
functions that accept ``**kwargs``.
|
||||
|
||||
@@ -363,31 +361,19 @@ class InlineSkillScript(SkillScript):
|
||||
self._parameters_schema_resolved = True
|
||||
return self._parameters_schema
|
||||
|
||||
async def run(self, skill: Skill, args: dict[str, Any] | list[str] | None = None, **kwargs: Any) -> Any:
|
||||
async def run(self, skill: Skill, args: dict[str, Any] | None = None, **kwargs: Any) -> Any:
|
||||
"""Run the script by invoking the callable in-process.
|
||||
|
||||
Args:
|
||||
skill: The skill that owns this script.
|
||||
args: Optional keyword arguments for the script, provided by the
|
||||
agent/LLM. Must be a ``dict`` or ``None``; passing a
|
||||
``list`` raises :class:`TypeError` because inline scripts
|
||||
bind arguments by keyword name.
|
||||
agent/LLM.
|
||||
**kwargs: Runtime keyword arguments forwarded only to script
|
||||
functions that accept ``**kwargs``.
|
||||
|
||||
Returns:
|
||||
The script execution result.
|
||||
|
||||
Raises:
|
||||
TypeError: If ``args`` is a ``list`` (array-style arguments
|
||||
are only supported for file-based scripts).
|
||||
"""
|
||||
if isinstance(args, list):
|
||||
raise TypeError(
|
||||
f"Inline script '{self.name}' requires keyword arguments (dict), "
|
||||
f"but received a list. Array-style arguments are only supported "
|
||||
f"for file-based scripts."
|
||||
)
|
||||
if self._accepts_kwargs: # noqa: SIM108
|
||||
result = self.function(**(args or {}), **kwargs)
|
||||
else:
|
||||
@@ -445,23 +431,13 @@ class FileSkillScript(SkillScript):
|
||||
self.full_path = full_path
|
||||
self._runner = runner
|
||||
|
||||
@property
|
||||
def parameters_schema(self) -> dict[str, Any] | None:
|
||||
"""JSON Schema advertising that file scripts accept a string array.
|
||||
|
||||
Returns a fixed schema ``{"type": "array", "items": {"type": "string"}}``
|
||||
so that the LLM knows to pass positional CLI arguments as a JSON array
|
||||
of strings.
|
||||
"""
|
||||
return {"type": "array", "items": {"type": "string"}}
|
||||
|
||||
async def run(self, skill: Skill, args: dict[str, Any] | list[str] | None = None, **kwargs: Any) -> Any:
|
||||
async def run(self, skill: Skill, args: dict[str, Any] | None = None, **kwargs: Any) -> Any:
|
||||
"""Run the script by delegating to the configured runner.
|
||||
|
||||
Args:
|
||||
skill: The skill that owns this script. Must be a
|
||||
:class:`FileSkill`.
|
||||
args: Optional arguments for the script.
|
||||
args: Optional keyword arguments for the script.
|
||||
**kwargs: Additional runtime keyword arguments (unused).
|
||||
|
||||
Returns:
|
||||
@@ -1372,7 +1348,6 @@ class FileSkill(Skill):
|
||||
self.path = path
|
||||
self._resources: list[SkillResource] = list(resources) if resources is not None else []
|
||||
self._scripts: list[SkillScript] = list(scripts) if scripts is not None else []
|
||||
self._cached_content: str | None = None
|
||||
|
||||
@property
|
||||
def frontmatter(self) -> SkillFrontmatter:
|
||||
@@ -1381,23 +1356,8 @@ class FileSkill(Skill):
|
||||
|
||||
@property
|
||||
def content(self) -> str:
|
||||
"""The skill content with appended scripts block.
|
||||
|
||||
When scripts are present, a ``<scripts>`` XML block is appended
|
||||
to the raw SKILL.md content so that the LLM can discover each
|
||||
script's ``<parameters_schema>``.
|
||||
|
||||
The result is cached after the first access. Adding scripts
|
||||
after the first access will not be reflected.
|
||||
"""
|
||||
if self._cached_content is not None:
|
||||
return self._cached_content
|
||||
if not self._scripts:
|
||||
self._cached_content = self._content
|
||||
else:
|
||||
script_lines = "\n".join(_create_script_element(s) for s in self._scripts)
|
||||
self._cached_content = f"{self._content}\n\n<scripts>\n{script_lines}\n</scripts>"
|
||||
return self._cached_content
|
||||
"""The skill content provided at construction time."""
|
||||
return self._content
|
||||
|
||||
@property
|
||||
def resources(self) -> list[SkillResource]:
|
||||
@@ -1432,9 +1392,7 @@ class SkillScriptRunner(Protocol):
|
||||
satisfies this protocol.
|
||||
"""
|
||||
|
||||
def __call__(
|
||||
self, skill: FileSkill, script: FileSkillScript, args: dict[str, Any] | list[str] | None = None
|
||||
) -> Any:
|
||||
def __call__(self, skill: FileSkill, script: FileSkillScript, args: dict[str, Any] | None = None) -> Any:
|
||||
"""Run a skill script.
|
||||
|
||||
The :class:`SkillsProvider` resolves skill and script names
|
||||
@@ -1444,7 +1402,7 @@ class SkillScriptRunner(Protocol):
|
||||
Args:
|
||||
skill: The file-based skill that owns the script.
|
||||
script: The file-based script to run.
|
||||
args: Optional arguments for the script.
|
||||
args: Optional keyword arguments for the script.
|
||||
|
||||
Returns:
|
||||
The result. May be any type; the framework
|
||||
@@ -2024,7 +1982,7 @@ class SkillsProvider(ContextProvider):
|
||||
if include_script_runner_tool:
|
||||
|
||||
async def _run_script(
|
||||
skill_name: str, script_name: str, args: dict[str, Any] | list[str] | None = None, **kwargs: Any
|
||||
skill_name: str, script_name: str, args: dict[str, Any] | None = None, **kwargs: Any
|
||||
) -> Any:
|
||||
return await self._run_skill_script(skills, skill_name, script_name, args, **kwargs)
|
||||
|
||||
@@ -2047,31 +2005,12 @@ class SkillsProvider(ContextProvider):
|
||||
),
|
||||
},
|
||||
"args": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": True,
|
||||
"description": (
|
||||
"Named arguments as key-value pairs "
|
||||
'(e.g. {"length": 24, "uppercase": true}).'
|
||||
),
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": (
|
||||
"Positional CLI arguments as a string array "
|
||||
'(e.g. ["input.docx", "--output", "result.idx"]).'
|
||||
),
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": True,
|
||||
"default": None,
|
||||
"description": (
|
||||
"Arguments to pass to the script. "
|
||||
"Use an array of strings for CLI-style positional arguments "
|
||||
'(e.g. ["input.docx", "--output", "result.idx"]), '
|
||||
"or an object for named parameters "
|
||||
"Arguments to pass to the script as key-value pairs. "
|
||||
"Use parameter names as keys without leading dashes "
|
||||
'(e.g. {"length": 24, "uppercase": true}). '
|
||||
"How these values are mapped to the underlying script "
|
||||
"is determined by the script implementation or configured runner."
|
||||
@@ -2121,7 +2060,7 @@ class SkillsProvider(ContextProvider):
|
||||
skills: Sequence[Skill],
|
||||
skill_name: str,
|
||||
script_name: str,
|
||||
args: dict[str, Any] | list[str] | None = None,
|
||||
args: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Run a named script from a skill.
|
||||
@@ -2133,8 +2072,9 @@ class SkillsProvider(ContextProvider):
|
||||
skills: The skills to look up the skill from.
|
||||
skill_name: The name of the owning skill.
|
||||
script_name: The script name to look up (case-insensitive).
|
||||
args: Optional arguments for the script, provided by the
|
||||
agent/LLM.
|
||||
args: Optional keyword arguments for the script, provided by the
|
||||
agent/LLM. These are mapped to the function's declared
|
||||
parameters.
|
||||
**kwargs: Runtime keyword arguments forwarded only to script
|
||||
functions that accept ``**kwargs`` (e.g. arguments passed via
|
||||
``agent.run(user_id="123")``).
|
||||
@@ -2314,7 +2254,7 @@ class FileSkillsSource(SkillsSource):
|
||||
|
||||
Args:
|
||||
skill_paths: One or more directory paths to search for file-based
|
||||
skills. Each path may point to an individual skill directory
|
||||
skills. Each path may point to an individual skill folder
|
||||
(containing ``SKILL.md``) or to a parent that contains skill
|
||||
subdirectories.
|
||||
|
||||
|
||||
@@ -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.4.0"
|
||||
version = "1.3.0"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4194,57 +4194,6 @@ async def test_mcp_tool_call_tool_otel_meta(use_span, expect_traceparent, span_e
|
||||
assert meta is None
|
||||
|
||||
|
||||
async def test_mcp_tool_call_tool_forwards_tool_list_meta():
|
||||
"""call_tool echoes per-tool metadata returned by tools/list."""
|
||||
from opentelemetry import trace
|
||||
|
||||
tool_meta = {
|
||||
"tool_configuration": {
|
||||
"name": "WorkIQSharePoint.readSmallBinaryFile",
|
||||
"type": "foundry_toolbox",
|
||||
}
|
||||
}
|
||||
|
||||
class TestServer(MCPTool):
|
||||
async def connect(self):
|
||||
self.session = Mock(spec=ClientSession)
|
||||
self.session.list_tools = AsyncMock(
|
||||
return_value=types.ListToolsResult(
|
||||
tools=[
|
||||
types.Tool(
|
||||
name="WorkIQSharePoint.readSmallBinaryFile",
|
||||
description="Read a binary file",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {"fileId": {"type": "string"}},
|
||||
"required": ["fileId"],
|
||||
},
|
||||
_meta=tool_meta,
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
self.session.call_tool = AsyncMock(
|
||||
return_value=types.CallToolResult(content=[types.TextContent(type="text", text="result")])
|
||||
)
|
||||
self.session.list_prompts = AsyncMock(
|
||||
return_value=types.ListPromptsResult(prompts=[])
|
||||
)
|
||||
|
||||
def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
|
||||
return None
|
||||
|
||||
server = TestServer(name="test_server")
|
||||
async with server:
|
||||
await server.load_tools()
|
||||
await server.load_prompts()
|
||||
|
||||
with trace.use_span(trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT)):
|
||||
await server.call_tool("WorkIQSharePoint.readSmallBinaryFile", fileId="file-1")
|
||||
|
||||
assert server.session.call_tool.call_args.kwargs["meta"] == tool_meta
|
||||
|
||||
|
||||
async def test_mcp_streamable_http_tool_hook_not_duplicated_on_repeated_get_mcp_client():
|
||||
"""Test that calling get_mcp_client multiple times does not accumulate duplicate hooks."""
|
||||
tool = MCPStreamableHTTPTool(
|
||||
|
||||
@@ -3518,6 +3518,7 @@ class TestSkillsProviderFactories:
|
||||
await _init_provider(provider)
|
||||
run_tool = next(t for t in _ctx(provider)[2] if hasattr(t, "name") and t.name == "run_skill_script")
|
||||
args_desc = run_tool.parameters()["properties"]["args"]["description"]
|
||||
assert "without leading dashes" in args_desc
|
||||
assert "script implementation or configured runner" in args_desc
|
||||
|
||||
async def test_require_script_approval_sets_approval_mode(self) -> None:
|
||||
@@ -4743,16 +4744,12 @@ class TestCreateScriptElement:
|
||||
def test_name_only(self) -> None:
|
||||
s = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/scripts/run.py")
|
||||
elem = _create_script_element(s)
|
||||
assert 'name="run.py"' in elem
|
||||
assert "<parameters_schema>" in elem
|
||||
assert '"type": "array"' in elem
|
||||
assert elem == ' <script name="run.py"/>'
|
||||
|
||||
def test_with_description(self) -> None:
|
||||
s = FileSkillScript(name="run.py", description="Execute script.", full_path=f"{_ABS}/test/scripts/run.py")
|
||||
elem = _create_script_element(s)
|
||||
assert 'name="run.py"' in elem
|
||||
assert 'description="Execute script."' in elem
|
||||
assert "<parameters_schema>" in elem
|
||||
assert elem == ' <script name="run.py" description="Execute script."/>'
|
||||
|
||||
def test_xml_escapes_name(self) -> None:
|
||||
s = FileSkillScript(name='script"special', full_path=f"{_ABS}/test/scripts/s.py")
|
||||
@@ -4779,12 +4776,10 @@ class TestCreateScriptElement:
|
||||
assert "query" in elem
|
||||
assert """ not in elem
|
||||
|
||||
def test_file_script_includes_array_parameters(self) -> None:
|
||||
def test_no_parameters_for_file_script(self) -> None:
|
||||
s = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/scripts/run.py")
|
||||
elem = _create_script_element(s)
|
||||
assert "<parameters_schema>" in elem
|
||||
assert '"type": "array"' in elem
|
||||
assert '"type": "string"' in elem
|
||||
assert "<parameters_schema>" not in elem
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -4805,7 +4800,7 @@ class TestSkillScriptParametersSchema:
|
||||
|
||||
def test_none_for_file_based_script(self) -> None:
|
||||
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/scripts/run.py")
|
||||
assert script.parameters_schema == {"type": "array", "items": {"type": "string"}}
|
||||
assert script.parameters_schema is None
|
||||
|
||||
def test_no_params_function_returns_none(self) -> None:
|
||||
def noop() -> None:
|
||||
@@ -5412,169 +5407,3 @@ class TestInlineSkillContentCaching:
|
||||
second = skill.content
|
||||
assert first is second # Same object (cached)
|
||||
assert "<name>test-skill</name>" in first
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: Array-style (list[str]) script arguments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestArrayStyleScriptArgs:
|
||||
"""Tests for list[str] arguments on skill scripts (port of .NET PR #5475)."""
|
||||
|
||||
async def test_inline_script_rejects_list_args(self) -> None:
|
||||
"""InlineSkillScript.run() raises TypeError when args is a list."""
|
||||
script = InlineSkillScript(name="greet", function=lambda name="world": f"hello {name}")
|
||||
skill = InlineSkill(frontmatter=SkillFrontmatter(name="s", description="d"), instructions="c")
|
||||
with pytest.raises(TypeError, match="requires keyword arguments"):
|
||||
await script.run(skill, args=["hello", "--name", "Alice"])
|
||||
|
||||
async def test_inline_script_error_message_mentions_script_name(self) -> None:
|
||||
"""The TypeError message includes the script name for debugging."""
|
||||
script = InlineSkillScript(name="my-script", function=lambda: None)
|
||||
skill = InlineSkill(frontmatter=SkillFrontmatter(name="s", description="d"), instructions="c")
|
||||
with pytest.raises(TypeError, match="my-script"):
|
||||
await script.run(skill, args=["arg1"])
|
||||
|
||||
async def test_file_script_passes_list_to_runner(self) -> None:
|
||||
"""FileSkillScript.run() passes list[str] args through to the runner."""
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def runner(skill: Any, script: Any, args: Any = None) -> str:
|
||||
captured["args"] = args
|
||||
return "ok"
|
||||
|
||||
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py", runner=runner)
|
||||
skill = FileSkill(
|
||||
frontmatter=SkillFrontmatter(name="my-skill", description="d"), content="c", path=f"{_ABS}/test"
|
||||
)
|
||||
result = await script.run(skill, args=["input.docx", "--output", "result.idx"])
|
||||
assert result == "ok"
|
||||
assert captured["args"] == ["input.docx", "--output", "result.idx"]
|
||||
|
||||
async def test_file_script_passes_dict_to_runner(self) -> None:
|
||||
"""FileSkillScript.run() still passes dict args through to the runner."""
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def runner(skill: Any, script: Any, args: Any = None) -> str:
|
||||
captured["args"] = args
|
||||
return "ok"
|
||||
|
||||
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py", runner=runner)
|
||||
skill = FileSkill(
|
||||
frontmatter=SkillFrontmatter(name="my-skill", description="d"), content="c", path=f"{_ABS}/test"
|
||||
)
|
||||
result = await script.run(skill, args={"key": "val"})
|
||||
assert result == "ok"
|
||||
assert captured["args"] == {"key": "val"}
|
||||
|
||||
async def test_file_script_passes_none_to_runner(self) -> None:
|
||||
"""FileSkillScript.run() passes None args through to the runner."""
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def runner(skill: Any, script: Any, args: Any = None) -> str:
|
||||
captured["args"] = args
|
||||
return "ok"
|
||||
|
||||
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py", runner=runner)
|
||||
skill = FileSkill(
|
||||
frontmatter=SkillFrontmatter(name="my-skill", description="d"), content="c", path=f"{_ABS}/test"
|
||||
)
|
||||
result = await script.run(skill)
|
||||
assert result == "ok"
|
||||
assert captured["args"] is None
|
||||
|
||||
def test_file_script_parameters_schema_returns_array(self) -> None:
|
||||
"""FileSkillScript.parameters_schema returns the string-array JSON schema."""
|
||||
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py")
|
||||
assert script.parameters_schema == {"type": "array", "items": {"type": "string"}}
|
||||
|
||||
async def test_runner_protocol_accepts_list_args(self) -> None:
|
||||
"""A runner accepting list[str] args satisfies the SkillScriptRunner protocol."""
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def my_runner(skill: Any, script: Any, args: Any = None) -> str:
|
||||
captured["args"] = args
|
||||
return "ok"
|
||||
|
||||
assert isinstance(my_runner, SkillScriptRunner)
|
||||
skill = FileSkill(
|
||||
frontmatter=SkillFrontmatter(name="s", description="d"), content="c", path=f"{_ABS}/test"
|
||||
)
|
||||
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py")
|
||||
result = my_runner(skill, script, args=["--flag", "value"])
|
||||
assert result == "ok"
|
||||
assert captured["args"] == ["--flag", "value"]
|
||||
|
||||
async def test_tool_schema_accepts_array_args(self) -> None:
|
||||
"""The run_skill_script tool schema accepts array-style args via oneOf."""
|
||||
skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
|
||||
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
|
||||
|
||||
provider = SkillsProvider([skill])
|
||||
await _init_provider(provider)
|
||||
run_tool = next(t for t in _ctx(provider)[2] if hasattr(t, "name") and t.name == "run_skill_script")
|
||||
args_schema = run_tool.parameters()["properties"]["args"]
|
||||
assert "oneOf" in args_schema
|
||||
types = [s.get("type") for s in args_schema["oneOf"]]
|
||||
assert "object" in types
|
||||
assert "array" in types
|
||||
assert "null" in types
|
||||
|
||||
async def test_run_skill_script_with_list_args_via_provider(self) -> None:
|
||||
"""End-to-end: list args flow through provider to file-based script runner."""
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def runner(skill: Any, script: Any, args: Any = None) -> str:
|
||||
captured["args"] = args
|
||||
return "list_result"
|
||||
|
||||
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py", runner=runner)
|
||||
skill = FileSkill(
|
||||
frontmatter=SkillFrontmatter(name="my-skill", description="test"),
|
||||
content="Body",
|
||||
path=f"{_ABS}/test",
|
||||
scripts=[script],
|
||||
)
|
||||
|
||||
provider = SkillsProvider([skill])
|
||||
await _init_provider(provider)
|
||||
run_tool = next(t for t in _ctx(provider)[2] if hasattr(t, "name") and t.name == "run_skill_script")
|
||||
result = await run_tool.func(skill_name="my-skill", script_name="run.py", args=["input.docx", "--verbose"])
|
||||
assert result == "list_result"
|
||||
assert captured["args"] == ["input.docx", "--verbose"]
|
||||
|
||||
async def test_run_skill_script_inline_with_list_args_returns_error(self) -> None:
|
||||
"""Inline script called with list args through provider returns error (TypeError caught)."""
|
||||
skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
|
||||
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: "ok"))
|
||||
|
||||
provider = SkillsProvider([skill])
|
||||
await _init_provider(provider)
|
||||
run_tool = next(t for t in _ctx(provider)[2] if hasattr(t, "name") and t.name == "run_skill_script")
|
||||
result = await run_tool.func(skill_name="my-skill", script_name="s1", args=["arg1"])
|
||||
assert "Error" in result
|
||||
assert "Failed to run" in result
|
||||
|
||||
def test_file_skill_content_includes_scripts_block(self) -> None:
|
||||
"""FileSkill.content appends a <scripts> block when scripts are present."""
|
||||
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py")
|
||||
skill = FileSkill(
|
||||
frontmatter=SkillFrontmatter(name="my-skill", description="test"),
|
||||
content="---\nname: my-skill\n---\nBody",
|
||||
path=f"{_ABS}/test",
|
||||
scripts=[script],
|
||||
)
|
||||
assert "<scripts>" in skill.content
|
||||
assert 'name="run.py"' in skill.content
|
||||
assert "<parameters_schema>" in skill.content
|
||||
assert '"type": "array"' in skill.content
|
||||
|
||||
def test_file_skill_content_no_scripts_no_block(self) -> None:
|
||||
"""FileSkill.content does not append a <scripts> block when no scripts."""
|
||||
skill = FileSkill(
|
||||
frontmatter=SkillFrontmatter(name="my-skill", description="test"),
|
||||
content="---\nname: my-skill\n---\nBody",
|
||||
path=f"{_ABS}/test",
|
||||
)
|
||||
assert "<scripts>" not in skill.content
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -22,7 +22,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"httpx>=0.27,<1",
|
||||
"powerfx>=0.0.32,<0.0.35; python_version < '3.14'",
|
||||
"pyyaml>=6.0,<7.0",
|
||||
|
||||
@@ -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.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://github.com/microsoft/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"openai>=1.99.0,<3",
|
||||
"opentelemetry-sdk>=1.39.0,<2",
|
||||
"fastapi>=0.115.0,<0.133.1",
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Durable Task integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -22,7 +22,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"durabletask>=1.3.0,<2",
|
||||
"durabletask-azuremanaged>=1.3.0,<2",
|
||||
"python-dateutil>=2.8.0,<3",
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Microsoft Foundry integrations for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.4.0"
|
||||
version = "1.3.0"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,8 +23,8 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-openai>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"agent-framework-openai>=1.3.0,<2",
|
||||
"azure-ai-inference>=1.0.0b9,<1.0.0b10",
|
||||
"azure-ai-projects>=2.1.0,<3.0",
|
||||
]
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Foundry Hosting integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0a260514"
|
||||
version = "1.0.0a260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"azure-ai-agentserver-core>=2.0.0b3,<3",
|
||||
"azure-ai-agentserver-responses>=1.0.0b5,<2",
|
||||
"azure-ai-agentserver-invocations>=1.0.0b3,<2",
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Foundry Local integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,8 +23,8 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-openai>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"agent-framework-openai>=1.3.0,<2",
|
||||
"foundry-local-sdk>=0.5.1,<0.5.2",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Google Gemini integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0a260514"
|
||||
version = "1.0.0a260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -24,7 +24,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2.0",
|
||||
"agent-framework-core>=1.3.0,<2.0",
|
||||
"google-genai>=1.65.0,<2.0.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"github-copilot-sdk>=1.0.0b2,<=1.0.0b2; python_version >= '3.11'",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Hyperlight CodeAct integrations for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -22,7 +22,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"hyperlight-sandbox>=0.4.0,<0.5",
|
||||
"hyperlight-sandbox-backend-wasm>=0.4.0,<0.5 ; ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'win32' and platform_machine == 'AMD64')) and python_version < '3.14'",
|
||||
"hyperlight-sandbox-python-guest>=0.4.0,<0.5",
|
||||
|
||||
@@ -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.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -22,7 +22,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.14",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -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.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"mem0ai>=1.0.0,<2",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Ollama integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"ollama>=0.5.3,<0.5.4",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "OpenAI integrations for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.4.0"
|
||||
version = "1.3.0"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"openai>=1.99.0,<3",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Orchestration patterns for Microsoft Agent Framework. Includes Se
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
||||
@@ -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.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://github.com/microsoft/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -24,7 +24,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"azure-core>=1.30.0,<2",
|
||||
"httpx>=0.27.0,<0.29",
|
||||
]
|
||||
|
||||
@@ -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.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.4.0,<2",
|
||||
"agent-framework-core>=1.3.0,<2",
|
||||
"redis>=6.4.0,<7.2.1",
|
||||
"redisvl>=0.11.0,<0.16",
|
||||
"numpy>=2.2.6,<3"
|
||||
|
||||
@@ -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.4.0"
|
||||
version = "1.3.0"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core[all]==1.4.0",
|
||||
"agent-framework-core[all]==1.3.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -10,7 +10,7 @@ import os
|
||||
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
|
||||
from textwrap import dedent
|
||||
|
||||
from agent_framework import Agent, ClassSkill, SkillFrontmatter, SkillsProvider
|
||||
from agent_framework import Agent, ClassSkill, SkillsProvider
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
@@ -49,12 +49,10 @@ class UnitConverterSkill(ClassSkill):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
frontmatter=SkillFrontmatter(
|
||||
name="unit-converter",
|
||||
description=(
|
||||
"Convert between common units using a multiplication factor. "
|
||||
"Use when asked to convert miles, kilometers, pounds, or kilograms."
|
||||
),
|
||||
name="unit-converter",
|
||||
description=(
|
||||
"Convert between common units using a multiplication factor. "
|
||||
"Use when asked to convert miles, kilometers, pounds, or kilograms."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -20,43 +20,30 @@ from typing import Any
|
||||
from agent_framework import FileSkill, FileSkillScript
|
||||
|
||||
|
||||
def subprocess_script_runner(
|
||||
skill: FileSkill, script: FileSkillScript, args: dict[str, Any] | list[str] | None = None
|
||||
) -> str:
|
||||
def subprocess_script_runner(skill: FileSkill, script: FileSkillScript, args: dict[str, Any] | None = None) -> str:
|
||||
"""Run a skill script as a local Python subprocess.
|
||||
Uses ``FileSkillScript.full_path`` as the script path, converts the
|
||||
``args`` to CLI arguments, and returns captured output.
|
||||
``args`` dict to CLI flags, and returns captured output.
|
||||
Args:
|
||||
skill: The file-based skill that owns the script.
|
||||
script: The file-based script to run.
|
||||
args: Optional arguments. A ``list[str]`` is forwarded as
|
||||
positional CLI arguments. Passing a ``dict`` or any other
|
||||
type raises :class:`TypeError` — file-based scripts expect
|
||||
positional arguments as a JSON array of strings.
|
||||
args: Optional arguments forwarded as CLI flags.
|
||||
Returns:
|
||||
The combined stdout/stderr output, or an error message.
|
||||
Raises:
|
||||
TypeError: If ``args`` is not a ``list[str]`` or ``None``, or if
|
||||
any list element is not a string.
|
||||
"""
|
||||
script_path = Path(script.full_path)
|
||||
if not script_path.is_file():
|
||||
return f"Error: Script file not found: {script_path}"
|
||||
cmd = [sys.executable, str(script_path)]
|
||||
if isinstance(args, list):
|
||||
for item in args:
|
||||
if not isinstance(item, str):
|
||||
raise TypeError(
|
||||
f"File-based skill scripts only accept string CLI arguments "
|
||||
f"but received a {type(item).__name__}. "
|
||||
f"All array elements must be strings."
|
||||
)
|
||||
cmd.extend(args)
|
||||
elif args is not None:
|
||||
raise TypeError(
|
||||
f"Expected a list of CLI arguments but received {type(args).__name__}. "
|
||||
f"File-based skill scripts expect positional arguments as a list of strings."
|
||||
)
|
||||
# Convert args dict to CLI flags
|
||||
if args:
|
||||
for key, value in args.items():
|
||||
if isinstance(value, bool):
|
||||
if value:
|
||||
cmd.append(f"--{key}")
|
||||
elif value is not None:
|
||||
cmd.append(f"--{key}")
|
||||
cmd.append(str(value))
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
|
||||
@@ -103,7 +103,7 @@ def main() -> None:
|
||||
app = Starlette(
|
||||
routes=[
|
||||
*create_agent_card_routes(agent_card),
|
||||
*create_jsonrpc_routes(request_handler, "/"),
|
||||
*create_jsonrpc_routes(request_handler),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -8,11 +8,16 @@ AgentCards for the invoice, policy, and logistics agent types.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from invoice_data import query_by_invoice_id, query_by_transaction_id, query_invoices
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent instructions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -10,12 +10,18 @@ published back through the a2a-sdk event queue.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from a2a.helpers import new_task_from_user_message
|
||||
from a2a.server.agent_execution.agent_executor import AgentExecutor
|
||||
from a2a.server.tasks import TaskUpdater
|
||||
from a2a.types import Part, TaskState
|
||||
from a2a.types import (
|
||||
Message,
|
||||
Part,
|
||||
Role,
|
||||
TaskState,
|
||||
TaskStatus,
|
||||
TaskStatusUpdateEvent,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.server.agent_execution.context import RequestContext
|
||||
@@ -41,17 +47,17 @@ class AgentFrameworkExecutor(AgentExecutor):
|
||||
if not user_text:
|
||||
user_text = "Hello"
|
||||
|
||||
# v1.0 requires a Task object in the queue before any TaskStatusUpdateEvent
|
||||
task = context.current_task
|
||||
if not task and context.message:
|
||||
task = new_task_from_user_message(context.message)
|
||||
await event_queue.enqueue_event(task)
|
||||
|
||||
task_id = task.id if task else context.task_id
|
||||
updater = TaskUpdater(event_queue, task_id, context.context_id)
|
||||
task_id = context.task_id or str(uuid.uuid4())
|
||||
context_id = context.context_id or str(uuid.uuid4())
|
||||
|
||||
# Signal that the agent is working
|
||||
await updater.start_work()
|
||||
await event_queue.enqueue_event(
|
||||
TaskStatusUpdateEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
status=TaskStatus(state=TaskState.TASK_STATE_WORKING),
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
response = await self.agent.run(user_text)
|
||||
@@ -65,19 +71,48 @@ class AgentFrameworkExecutor(AgentExecutor):
|
||||
if not response_parts:
|
||||
response_parts.append(Part(text=str(response)))
|
||||
|
||||
# Publish the agent's response and mark as completed
|
||||
await updater.complete(
|
||||
message=updater.new_agent_message(response_parts),
|
||||
# Publish the agent's response as a completed message
|
||||
await event_queue.enqueue_event(
|
||||
TaskStatusUpdateEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
status=TaskStatus(
|
||||
state=TaskState.TASK_STATE_COMPLETED,
|
||||
message=Message(
|
||||
message_id=str(uuid.uuid4()),
|
||||
role=Role.ROLE_AGENT,
|
||||
parts=response_parts,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
await updater.update_status(
|
||||
state=TaskState.TASK_STATE_FAILED,
|
||||
message=updater.new_agent_message([Part(text=f"Agent error: {e}")]),
|
||||
await event_queue.enqueue_event(
|
||||
TaskStatusUpdateEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
status=TaskStatus(
|
||||
state=TaskState.TASK_STATE_FAILED,
|
||||
message=Message(
|
||||
message_id=str(uuid.uuid4()),
|
||||
role=Role.ROLE_AGENT,
|
||||
parts=[Part(text=f"Agent error: {e}")],
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
|
||||
"""Handle cancellation by publishing a canceled status."""
|
||||
updater = TaskUpdater(event_queue, context.task_id, context.context_id)
|
||||
await updater.update_status(state=TaskState.TASK_STATE_CANCELED)
|
||||
task_id = context.task_id or str(uuid.uuid4())
|
||||
context_id = context.context_id or str(uuid.uuid4())
|
||||
|
||||
await event_queue.enqueue_event(
|
||||
TaskStatusUpdateEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
status=TaskStatus(state=TaskState.TASK_STATE_CANCELED),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -65,7 +65,7 @@ if __name__ == "__main__":
|
||||
server = Starlette(
|
||||
routes=[
|
||||
*create_agent_card_routes(public_agent_card),
|
||||
*create_jsonrpc_routes(request_handler, "/"),
|
||||
*create_jsonrpc_routes(request_handler),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Generated
+31
-31
@@ -108,7 +108,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework"
|
||||
version = "1.4.0"
|
||||
version = "1.3.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", extra = ["all"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -163,7 +163,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-a2a"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/a2a" }
|
||||
dependencies = [
|
||||
{ name = "a2a-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -206,7 +206,7 @@ provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-anthropic"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/anthropic" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -221,7 +221,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-azure-ai-search"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/azure-ai-search" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -236,7 +236,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-azure-contentunderstanding"
|
||||
version = "1.0.0a260514"
|
||||
version = "1.0.0a260507"
|
||||
source = { editable = "packages/azure-contentunderstanding" }
|
||||
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-azure-cosmos"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/azure-cosmos" }
|
||||
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-azurefunctions"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/azurefunctions" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -294,7 +294,7 @@ dev = []
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-bedrock"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/bedrock" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -311,7 +311,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-chatkit"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/chatkit" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -326,7 +326,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-claude"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/claude" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -341,7 +341,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-copilotstudio"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/copilotstudio" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -356,7 +356,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-core"
|
||||
version = "1.4.0"
|
||||
version = "1.3.0"
|
||||
source = { editable = "packages/core" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -430,7 +430,7 @@ provides-extras = ["all"]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-declarative"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/declarative" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -457,7 +457,7 @@ dev = [{ name = "types-pyyaml", specifier = "==6.0.12.20250915" }]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-devui"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/devui" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -495,7 +495,7 @@ provides-extras = ["dev", "all"]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-durabletask"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/durabletask" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -522,7 +522,7 @@ dev = [{ name = "types-python-dateutil", specifier = "==2.9.0.20260402" }]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-foundry"
|
||||
version = "1.4.0"
|
||||
version = "1.3.0"
|
||||
source = { editable = "packages/foundry" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -541,7 +541,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-foundry-hosting"
|
||||
version = "1.0.0a260514"
|
||||
version = "1.0.0a260507"
|
||||
source = { editable = "packages/foundry_hosting" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -560,7 +560,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-foundry-local"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/foundry_local" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -577,7 +577,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-gemini"
|
||||
version = "1.0.0a260514"
|
||||
version = "1.0.0a260507"
|
||||
source = { editable = "packages/gemini" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -592,7 +592,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-github-copilot"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/github_copilot" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -607,7 +607,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-hyperlight"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/hyperlight" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -626,7 +626,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-lab"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/lab" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -707,7 +707,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-mem0"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/mem0" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -722,7 +722,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-ollama"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/ollama" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -737,7 +737,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-openai"
|
||||
version = "1.4.0"
|
||||
version = "1.3.0"
|
||||
source = { editable = "packages/openai" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -752,7 +752,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-orchestrations"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/orchestrations" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -763,7 +763,7 @@ requires-dist = [{ name = "agent-framework-core", editable = "packages/core" }]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-purview"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/purview" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -780,7 +780,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-redis"
|
||||
version = "1.0.0b260514"
|
||||
version = "1.0.0b260507"
|
||||
source = { editable = "packages/redis" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -4361,7 +4361,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai-chatkit"
|
||||
version = "1.6.4"
|
||||
version = "1.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -4370,9 +4370,9 @@ dependencies = [
|
||||
{ name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "uvicorn", extra = ["standard"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/4b/acd3155d535398656c829fb0fed2f71a7146114b05b6ed55351921f74196/openai_chatkit-1.6.4.tar.gz", hash = "sha256:68c7f6091987bec97bc8a9ad2e1c815d3f43c5abe642b0e2d3d653364478fa66", size = 64756, upload-time = "2026-05-14T21:23:55.192Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/46/b15fd77f7df12a2cabd8475de6226ce04d1cec7b283b21e8f0f52edc63a7/openai_chatkit-1.6.3.tar.gz", hash = "sha256:f16e347f39c376a78dddb5ceaf5398a4bb700c0145bfa7cb899d65135972956e", size = 61822, upload-time = "2026-03-04T19:30:19.564Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/c3/b57f9a4991f3bfc4c1e4fad743822eab1929c246b60a87aded12d735e116/openai_chatkit-1.6.4-py3-none-any.whl", hash = "sha256:d20008ab5d2e837044d606171801623a21e5081150f2bcbeb8613a605186fb03", size = 44019, upload-time = "2026-05-14T21:23:53.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/5e/e06a4bec431083c282dea5729b0947b940900a4014216835182048078877/openai_chatkit-1.6.3-py3-none-any.whl", hash = "sha256:642ecdf810eda3619964f316e393f252741130a5500dc3a357d501f8657b3941", size = 42578, upload-time = "2026-03-04T19:30:18.314Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user