Compare commits

..
Author SHA1 Message Date
copilot-swe-agent[bot] a8a48563e3 Initial plan 2026-03-03 19:22:01 +00:00
109 changed files with 1554 additions and 6001 deletions
+1 -4
View File
@@ -29,7 +29,4 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
timeout: 3600
interval: 30
# "Cleanup artifacts", "Agent", "Prepare", and "Upload results" are check runs
# created by an org-level GitHub App (MSDO), not by any workflow in this repo.
# They are outside our control and their transient failures should not block merges.
ignored: CodeQL,CodeQL analysis (csharp),Cleanup artifacts,Agent,Prepare,Upload results
ignored: CodeQL,CodeQL analysis (csharp)
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -19,8 +19,8 @@
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Projects" Version="2.0.0-beta.1" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="2.0.0-beta.1" />
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.5" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.8" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
@@ -35,7 +35,7 @@
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.3" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="System.ClientModel" Version="1.9.0" />
<PackageVersion Include="System.ClientModel" Version="1.8.1" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
@@ -94,7 +94,7 @@
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
<!-- Agent SDKs -->
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.29" />
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.23" />
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
<!-- M365 Agents SDK -->
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
@@ -187,4 +187,4 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
</Project>
+4 -4
View File
@@ -2,11 +2,11 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<RCNumber>3</RCNumber>
<RCNumber>2</RCNumber>
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260304.1</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260304.1</PackageVersion>
<GitTag>1.0.0-rc3</GitTag>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260225.1</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260225.1</PackageVersion>
<GitTag>1.0.0-rc2</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -60,7 +60,7 @@ Console.WriteLine();
// Submit the red team run to the service
Console.WriteLine("Submitting red team run...");
RedTeam redTeamRun = await aiProjectClient.RedTeams.CreateAsync(redTeamConfig, options: null);
RedTeam redTeamRun = await aiProjectClient.RedTeams.CreateAsync(redTeamConfig);
Console.WriteLine($"Red team run created: {redTeamRun.Name}");
Console.WriteLine($"Status: {redTeamRun.Status}");
@@ -35,7 +35,7 @@ string userScope = $"user_{Environment.MachineName}";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
// Create the Memory Search tool configuration
MemorySearchPreviewTool memorySearchTool = new(memoryStoreName, userScope)
MemorySearchTool memorySearchTool = new(memoryStoreName, userScope)
{
// Optional: Configure how quickly new memories are indexed (in seconds)
UpdateDelay = 1,
@@ -88,9 +88,7 @@ internal sealed class Program
{
string workflowYaml = File.ReadAllText("MathChat.yaml");
#pragma warning disable AAIP001 // WorkflowAgentDefinition is experimental
WorkflowAgentDefinition workflowAgentDefinition = WorkflowAgentDefinition.FromYaml(workflowYaml);
#pragma warning restore AAIP001
return
await agentClient.CreateAgentAsync(
@@ -39,7 +39,7 @@ public static partial class AzureAIProjectChatClientExtensions
/// <exception cref="InvalidOperationException">The agent with the specified name was not found.</exception>
/// <remarks>
/// When instantiating a <see cref="ChatClientAgent"/> by using an <see cref="AgentReference"/>, minimal information will be available about the agent in the instance level, and any logic that relies
/// on <see cref="AIAgent.GetService{TService}(object?)"/> to retrieve information about the agent like <see cref="AgentVersion" /> will receive <see langword="null"/> as the result.
/// on <see cref="AIAgent.GetService(Type, object?)"/> to retrieve information about the agent like <see cref="AgentVersion" /> will receive <see langword="null"/> as the result.
/// </remarks>
public static ChatClientAgent AsAIAgent(
this AIProjectClient aiProjectClient,
@@ -355,27 +355,28 @@ public static partial class AzureAIProjectChatClientExtensions
private static readonly ModelReaderWriterOptions s_modelWriterOptionsWire = new("W");
/// <summary>
/// Asynchronously retrieves an agent record by name using the protocol method to inject user-agent headers.
/// Asynchronously retrieves an agent record by name using the Protocol method with user-agent header.
/// </summary>
private static async Task<AgentRecord> GetAgentRecordByNameAsync(AIProjectClient aiProjectClient, string agentName, CancellationToken cancellationToken)
{
ClientResult protocolResponse = await aiProjectClient.Agents.GetAgentAsync(agentName, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false);
var rawResponse = protocolResponse.GetRawResponse();
AgentRecord? result = ModelReaderWriter.Read<AgentRecord>(rawResponse.Content, s_modelWriterOptionsWire, AzureAIProjectsOpenAIContext.Default);
return result ?? throw new InvalidOperationException($"Agent with name '{agentName}' not found.");
return ClientResult.FromOptionalValue(result, rawResponse).Value!
?? throw new InvalidOperationException($"Agent with name '{agentName}' not found.");
}
/// <summary>
/// Asynchronously creates an agent version using the protocol method to inject user-agent headers.
/// Asynchronously creates an agent version using the Protocol method with user-agent header.
/// </summary>
private static async Task<AgentVersion> CreateAgentVersionWithProtocolAsync(AIProjectClient aiProjectClient, string agentName, AgentVersionCreationOptions creationOptions, CancellationToken cancellationToken)
{
BinaryData serializedOptions = ModelReaderWriter.Write(creationOptions, s_modelWriterOptionsWire, AzureAIProjectsContext.Default);
BinaryContent content = BinaryContent.Create(serializedOptions);
ClientResult protocolResponse = await aiProjectClient.Agents.CreateAgentVersionAsync(agentName, content, foundryFeatures: null, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false);
using BinaryContent protocolRequest = BinaryContent.Create(ModelReaderWriter.Write(creationOptions, ModelReaderWriterOptions.Json, AzureAIProjectsContext.Default));
ClientResult protocolResponse = await aiProjectClient.Agents.CreateAgentVersionAsync(agentName, protocolRequest, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false);
var rawResponse = protocolResponse.GetRawResponse();
AgentVersion? result = ModelReaderWriter.Read<AgentVersion>(rawResponse.Content, s_modelWriterOptionsWire, AzureAIProjectsOpenAIContext.Default);
return result ?? throw new InvalidOperationException($"Failed to create agent version for agent '{agentName}'.");
return ClientResult.FromValue(result, rawResponse).Value!;
}
private static async Task<ChatClientAgent> CreateAIAgentAsync(
@@ -747,15 +747,9 @@ public sealed partial class ChatClientAgent : AIAgent
{
// The agent has a ChatHistoryProvider configured, but the service returned a conversation id,
// meaning the service manages chat history server-side. Both cannot be used simultaneously.
if (this._agentOptions?.WarnOnChatHistoryProviderConflict is true
&& this._logger.IsEnabled(LogLevel.Warning))
if (this._agentOptions?.WarnOnChatHistoryProviderConflict is true)
{
var loggingAgentName = this.GetLoggingAgentName();
this._logger.LogAgentChatClientHistoryProviderConflict(
nameof(ChatClientAgentSession.ConversationId),
nameof(this.ChatHistoryProvider),
this.Id,
loggingAgentName);
this._logger.LogAgentChatClientHistoryProviderConflict(nameof(ChatClientAgentSession.ConversationId), nameof(this.ChatHistoryProvider), this.Id, this.GetLoggingAgentName());
}
if (this._agentOptions?.ThrowOnChatHistoryProviderConflict is true)
@@ -17,9 +17,8 @@ namespace Microsoft.Agents.AI;
/// </summary>
/// <remarks>
/// Searches directories recursively (up to <see cref="MaxSearchDepth"/> levels) for SKILL.md files.
/// Each file is validated for YAML frontmatter. Resource files are discovered by scanning the skill
/// directory for files with matching extensions. Invalid resources are skipped with logged warnings.
/// Resource paths are checked against path traversal and symlink escape attacks.
/// Each file is validated for YAML frontmatter and resource integrity. Invalid skills are excluded
/// with logged warnings. Resource paths are checked against path traversal and symlink escape attacks.
/// </remarks>
internal sealed partial class FileAgentSkillLoader
{
@@ -34,6 +33,14 @@ internal sealed partial class FileAgentSkillLoader
// Example: "---\nname: foo\n---\nBody" → Group 1: "name: foo\n"
private static readonly Regex s_frontmatterRegex = new(@"\A\uFEFF?^---\s*$(.+?)^---\s*$", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled, TimeSpan.FromSeconds(5));
// Matches markdown links to local resource files. Group 1 = relative file path.
// Supports optional ./ or ../ prefixes; excludes URLs (no ":" in the path character class).
// Intentionally conservative: only matches paths with word characters, hyphens, dots,
// and forward slashes. Paths with spaces or special characters are not supported.
// Examples: [doc](refs/FAQ.md) → "refs/FAQ.md", [s](./s.json) → "./s.json",
// [p](../shared/doc.txt) → "../shared/doc.txt"
private static readonly Regex s_resourceLinkRegex = new(@"\[.*?\]\((\.?\.?/?[\w][\w\-./]*\.\w+)\)", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
// Matches YAML "key: value" lines. Group 1 = key, Group 2 = quoted value, Group 3 = unquoted value.
// Accepts single or double quotes; the lazy quantifier trims trailing whitespace on unquoted values.
// Examples: "name: foo" → (name, _, foo), "name: 'foo bar'" → (name, foo bar, _),
@@ -45,22 +52,14 @@ internal sealed partial class FileAgentSkillLoader
private static readonly Regex s_validNameRegex = new(@"^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$", RegexOptions.Compiled);
private readonly ILogger _logger;
private readonly HashSet<string> _allowedResourceExtensions;
/// <summary>
/// Initializes a new instance of the <see cref="FileAgentSkillLoader"/> class.
/// </summary>
/// <param name="logger">The logger instance.</param>
/// <param name="allowedResourceExtensions">File extensions to recognize as skill resources. When <see langword="null"/>, defaults are used.</param>
internal FileAgentSkillLoader(ILogger logger, IEnumerable<string>? allowedResourceExtensions = null)
internal FileAgentSkillLoader(ILogger logger)
{
this._logger = logger;
ValidateExtensions(allowedResourceExtensions);
this._allowedResourceExtensions = new HashSet<string>(
allowedResourceExtensions ?? [".md", ".json", ".yaml", ".yml", ".csv", ".xml", ".txt"],
StringComparer.OrdinalIgnoreCase);
}
/// <summary>
@@ -184,9 +183,9 @@ internal sealed partial class FileAgentSkillLoader
}
}
private FileAgentSkill? ParseSkillFile(string skillDirectoryFullPath)
private FileAgentSkill? ParseSkillFile(string skillDirectoryPath)
{
string skillFilePath = Path.Combine(skillDirectoryFullPath, SkillFileName);
string skillFilePath = Path.Combine(skillDirectoryPath, SkillFileName);
string content = File.ReadAllText(skillFilePath, Encoding.UTF8);
@@ -195,12 +194,17 @@ internal sealed partial class FileAgentSkillLoader
return null;
}
List<string> resourceNames = this.DiscoverResourceFiles(skillDirectoryFullPath, frontmatter.Name);
List<string> resourceNames = ExtractResourcePaths(body);
if (!this.ValidateResources(skillDirectoryPath, resourceNames, frontmatter.Name))
{
return null;
}
return new FileAgentSkill(
frontmatter: frontmatter,
body: body,
sourcePath: skillDirectoryFullPath,
sourcePath: skillDirectoryPath,
resourceNames: resourceNames);
}
@@ -266,84 +270,34 @@ internal sealed partial class FileAgentSkillLoader
return true;
}
/// <summary>
/// Scans a skill directory for resource files matching the configured extensions.
/// </summary>
/// <remarks>
/// Recursively walks <paramref name="skillDirectoryFullPath"/> and collects files whose extension
/// matches <see cref="_allowedResourceExtensions"/>, excluding <c>SKILL.md</c> itself. Each candidate
/// is validated against path-traversal and symlink-escape checks; unsafe files are skipped with
/// a warning.
/// </remarks>
private List<string> DiscoverResourceFiles(string skillDirectoryFullPath, string skillName)
private bool ValidateResources(string skillDirectoryPath, List<string> resourceNames, string skillName)
{
string normalizedSkillDirectoryFullPath = skillDirectoryFullPath + Path.DirectorySeparatorChar;
string normalizedSkillPath = Path.GetFullPath(skillDirectoryPath) + Path.DirectorySeparatorChar;
var resources = new List<string>();
#if NET
var enumerationOptions = new EnumerationOptions
foreach (string resourceName in resourceNames)
{
RecurseSubdirectories = true,
IgnoreInaccessible = true,
AttributesToSkip = FileAttributes.ReparsePoint,
};
string fullPath = Path.GetFullPath(Path.Combine(skillDirectoryPath, resourceName));
foreach (string filePath in Directory.EnumerateFiles(skillDirectoryFullPath, "*", enumerationOptions))
#else
foreach (string filePath in Directory.EnumerateFiles(skillDirectoryFullPath, "*", SearchOption.AllDirectories))
#endif
{
string fileName = Path.GetFileName(filePath);
// Exclude SKILL.md itself
if (string.Equals(fileName, SkillFileName, StringComparison.OrdinalIgnoreCase))
if (!IsPathWithinDirectory(fullPath, normalizedSkillPath))
{
continue;
LogResourcePathTraversal(this._logger, skillName, resourceName);
return false;
}
// Filter by extension
string extension = Path.GetExtension(filePath);
if (string.IsNullOrEmpty(extension) || !this._allowedResourceExtensions.Contains(extension))
if (!File.Exists(fullPath))
{
if (this._logger.IsEnabled(LogLevel.Debug))
{
LogResourceSkippedExtension(this._logger, skillName, SanitizePathForLog(filePath), extension);
}
continue;
LogMissingResource(this._logger, skillName, resourceName);
return false;
}
// Normalize the enumerated path to guard against non-canonical forms
// (redundant separators, 8.3 short names, etc.) that would produce
// malformed relative resource names.
string resolvedFilePath = Path.GetFullPath(filePath);
// Path containment check
if (!IsPathWithinDirectory(resolvedFilePath, normalizedSkillDirectoryFullPath))
if (HasSymlinkInPath(fullPath, normalizedSkillPath))
{
if (this._logger.IsEnabled(LogLevel.Warning))
{
LogResourcePathTraversal(this._logger, skillName, SanitizePathForLog(filePath));
}
continue;
LogResourceSymlinkEscape(this._logger, skillName, resourceName);
return false;
}
// Symlink check
if (HasSymlinkInPath(resolvedFilePath, normalizedSkillDirectoryFullPath))
{
if (this._logger.IsEnabled(LogLevel.Warning))
{
LogResourceSymlinkEscape(this._logger, skillName, SanitizePathForLog(filePath));
}
continue;
}
// Compute relative path and normalize to forward slashes
string relativePath = resolvedFilePath.Substring(normalizedSkillDirectoryFullPath.Length);
resources.Add(NormalizeResourcePath(relativePath));
}
return resources;
return true;
}
/// <summary>
@@ -382,6 +336,22 @@ internal sealed partial class FileAgentSkillLoader
return false;
}
private static List<string> ExtractResourcePaths(string content)
{
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var paths = new List<string>();
foreach (Match m in s_resourceLinkRegex.Matches(content))
{
string path = NormalizeResourcePath(m.Groups[1].Value);
if (seen.Add(path))
{
paths.Add(path);
}
}
return paths;
}
/// <summary>
/// Normalizes a relative resource path by trimming a leading <c>./</c> prefix and replacing
/// backslashes with forward slashes so that <c>./refs/doc.md</c> and <c>refs/doc.md</c> are
@@ -402,43 +372,6 @@ internal sealed partial class FileAgentSkillLoader
return path;
}
/// <summary>
/// Replaces control characters in a file path with '?' to prevent log injection
/// via crafted filenames (e.g., filenames containing newlines on Linux).
/// </summary>
private static string SanitizePathForLog(string path)
{
char[]? chars = null;
for (int i = 0; i < path.Length; i++)
{
if (char.IsControl(path[i]))
{
chars ??= path.ToCharArray();
chars[i] = '?';
}
}
return chars is null ? path : new string(chars);
}
private static void ValidateExtensions(IEnumerable<string>? extensions)
{
if (extensions is null)
{
return;
}
foreach (string ext in extensions)
{
if (string.IsNullOrWhiteSpace(ext) || !ext.StartsWith(".", StringComparison.Ordinal))
{
#pragma warning disable CA2208 // Instantiate argument exceptions correctly
throw new ArgumentException($"Each extension must start with '.'. Invalid value: '{ext}'", nameof(FileAgentSkillsProviderOptions.AllowedResourceExtensions));
#pragma warning restore CA2208 // Instantiate argument exceptions correctly
}
}
}
[LoggerMessage(LogLevel.Information, "Discovered {Count} potential skills")]
private static partial void LogSkillsDiscovered(ILogger logger, int count);
@@ -457,18 +390,18 @@ internal sealed partial class FileAgentSkillLoader
[LoggerMessage(LogLevel.Error, "SKILL.md at '{SkillFilePath}' has an invalid '{FieldName}' value: {Reason}")]
private static partial void LogInvalidFieldValue(ILogger logger, string skillFilePath, string fieldName, string reason);
[LoggerMessage(LogLevel.Warning, "Skipping resource in skill '{SkillName}': '{ResourcePath}' references a path outside the skill directory")]
private static partial void LogResourcePathTraversal(ILogger logger, string skillName, string resourcePath);
[LoggerMessage(LogLevel.Warning, "Excluding skill '{SkillName}': referenced resource '{ResourceName}' does not exist")]
private static partial void LogMissingResource(ILogger logger, string skillName, string resourceName);
[LoggerMessage(LogLevel.Warning, "Excluding skill '{SkillName}': resource '{ResourceName}' references a path outside the skill directory")]
private static partial void LogResourcePathTraversal(ILogger logger, string skillName, string resourceName);
[LoggerMessage(LogLevel.Warning, "Duplicate skill name '{SkillName}': skill from '{NewPath}' skipped in favor of existing skill from '{ExistingPath}'")]
private static partial void LogDuplicateSkillName(ILogger logger, string skillName, string newPath, string existingPath);
[LoggerMessage(LogLevel.Warning, "Skipping resource in skill '{SkillName}': '{ResourcePath}' is a symlink that resolves outside the skill directory")]
private static partial void LogResourceSymlinkEscape(ILogger logger, string skillName, string resourcePath);
[LoggerMessage(LogLevel.Warning, "Excluding skill '{SkillName}': resource '{ResourceName}' is a symlink that resolves outside the skill directory")]
private static partial void LogResourceSymlinkEscape(ILogger logger, string skillName, string resourceName);
[LoggerMessage(LogLevel.Information, "Reading resource '{FileName}' from skill '{SkillName}'")]
private static partial void LogResourceReading(ILogger logger, string fileName, string skillName);
[LoggerMessage(LogLevel.Debug, "Skipping file '{FilePath}' in skill '{SkillName}': extension '{Extension}' is not in the allowed list")]
private static partial void LogResourceSkippedExtension(ILogger logger, string skillName, string filePath, string extension);
}
@@ -88,7 +88,7 @@ public sealed partial class FileAgentSkillsProvider : AIContextProvider
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<FileAgentSkillsProvider>();
this._loader = new FileAgentSkillLoader(this._logger, options?.AllowedResourceExtensions);
this._loader = new FileAgentSkillLoader(this._logger);
this._skills = this._loader.DiscoverAndLoadSkills(skillPaths);
this._skillsInstructionPrompt = BuildSkillsInstructionPrompt(options, this._skills);
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
@@ -18,15 +17,4 @@ public sealed class FileAgentSkillsProviderOptions
/// When <see langword="null"/>, a default template is used.
/// </summary>
public string? SkillsInstructionPrompt { get; set; }
/// <summary>
/// Gets or sets the file extensions recognized as discoverable skill resources.
/// Each value must start with a <c>'.'</c> character (for example, <c>.md</c>), and
/// extension comparisons are performed in a case-insensitive manner.
/// Files in the skill directory (and its subdirectories) whose extension matches
/// one of these values will be automatically discovered as resources.
/// When <see langword="null"/>, a default set of extensions is used
/// (<c>.md</c>, <c>.json</c>, <c>.yaml</c>, <c>.yml</c>, <c>.csv</c>, <c>.xml</c>, <c>.txt</c>).
/// </summary>
public IEnumerable<string>? AllowedResourceExtensions { get; set; }
}
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
@@ -132,15 +132,10 @@ public class AzureAIAgentsPersistentCreateTests
}
}
[Fact]
public Task CreateAgent_CreatesAgentWithCodeInterpreter_ChatClientAgentOptionsAsync()
=> this.CreateAgent_CreatesAgentWithCodeInterpreterAsync("CreateWithChatClientAgentOptionsAsync");
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
public Task CreateAgent_CreatesAgentWithCodeInterpreter_FoundryOptionsAsync()
=> this.CreateAgent_CreatesAgentWithCodeInterpreterAsync("CreateWithFoundryOptionsAsync");
private async Task CreateAgent_CreatesAgentWithCodeInterpreterAsync(string createMechanism)
[Theory]
[InlineData("CreateWithChatClientAgentOptionsAsync")]
[InlineData("CreateWithFoundryOptionsAsync")]
public async Task CreateAgent_CreatesAgentWithCodeInterpreterAsync(string createMechanism)
{
// Arrange.
const string AgentInstructions = """
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
@@ -467,7 +467,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithModelAndOptions_CreatesValidAgentAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", instructions: "Test instructions");
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", instructions: "Test instructions");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -475,7 +475,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
var agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -490,7 +490,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithModelAndOptions_WithClientFactory_AppliesFactoryCorrectlyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", instructions: "Test instructions");
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", instructions: "Test instructions");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -499,7 +499,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
TestChatClient? testChatClient = null;
// Act
var agent = await testClient.Client.CreateAIAgentAsync(
var agent = await client.CreateAIAgentAsync(
"test-model",
options,
clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient));
@@ -560,12 +560,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithDefinition_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
var agent = await client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -582,12 +582,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var definitionResponse = GeneratePromptDefinitionResponse(definition, null);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
var agent = await client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -602,12 +602,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
{
// Arrange
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definition);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definition);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
var agent = await client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -628,12 +628,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Create a response definition with the same tool
var definitionResponse = GeneratePromptDefinitionResponse(definition, definition.Tools.Select(t => t.AsAITool()).ToList());
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
var agent = await client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -667,12 +667,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
definitionResponse.Tools.Add(tool);
}
using var testClient = CreateTestAgentClientWithHandler(agentDefinitionResponse: definitionResponse);
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
var agent = await client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -803,10 +803,10 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var definitionResponse = GeneratePromptDefinitionResponse(new PromptAgentDefinition("test-model") { Instructions = "Test instructions" }, tools);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
// Act
var agent = await testClient.Client.CreateAIAgentAsync(
var agent = await client.CreateAIAgentAsync(
"test-agent",
"test-model",
"Test instructions",
@@ -831,14 +831,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithDefinitionTools_CreatesAgentAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" };
definition.Tools.Add(ResponseTool.CreateFunctionTool("async_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
var agent = await client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -885,7 +885,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var sharepointOptions = new SharePointGroundingToolOptions();
sharepointOptions.ProjectConnections.Add(new ToolProjectConnection("connection-id"));
var structuredOutputs = new StructuredOutputDefinition("name", "description", new Dictionary<string, BinaryData> { ["schema"] = BinaryData.FromString(AIJsonUtilities.CreateJsonSchema(new { id = "test" }.GetType()).ToString()) }, false);
var structuredOutputs = new StructuredOutputDefinition("name", "description", BinaryData.FromString(AIJsonUtilities.CreateJsonSchema(new { id = "test" }.GetType()).ToString()), false);
// Add tools to the definition
definition.Tools.Add(ResponseTool.CreateFunctionTool("create_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
@@ -902,12 +902,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Generate agent definition response with the tools
var definitionResponse = GeneratePromptDefinitionResponse(definition, definition.Tools.Select(t => t.AsAITool()).ToList());
using var testClient = CreateTestAgentClientWithHandler(agentDefinitionResponse: definitionResponse);
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
var agent = await client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -942,12 +942,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test" };
definitionResponse.Tools.Add(functionTool);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
var agent = await client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -961,7 +961,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithDeclarativeFunctionFromDefinition_AcceptsDeclarativeFunctionAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
// Create a declarative function (not invocable) using AIFunctionFactory.CreateDeclaration
@@ -974,7 +974,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
var agent = await client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -1001,12 +1001,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test" };
definitionResponse.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException());
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
var agent = await client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -1027,12 +1027,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" };
var definitionResponse = GeneratePromptDefinitionResponse(definition, null);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
var agent = await client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -1083,7 +1083,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
new PromptAgentDefinition("test-model") { Instructions = "Test" },
tools);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new ChatClientAgentOptions
{
@@ -1092,7 +1092,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
var agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -1278,14 +1278,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithClientFactory_ReceivesCorrectUnderlyingClientAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
IChatClient? receivedClient = null;
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync(
var agent = await client.CreateAIAgentAsync(
"test-agent",
options,
clientFactory: (innerClient) =>
@@ -1340,10 +1340,10 @@ public sealed class AzureAIProjectChatClientExtensionsTests
const string AgentName = "test-agent";
const string Model = "test-model";
const string Instructions = "Test instructions";
using var testClient = CreateTestAgentClientWithHandler(AgentName, Instructions);
AIProjectClient client = this.CreateTestAgentClient(AgentName, Instructions);
// Act
var agent = await testClient.Client.CreateAIAgentAsync(
var agent = await client.CreateAIAgentAsync(
AgentName,
Model,
Instructions,
@@ -1367,12 +1367,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var agentDefinitionResponse = GeneratePromptDefinitionResponse(definition, null);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: agentDefinitionResponse);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: agentDefinitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync(
var agent = await client.CreateAIAgentAsync(
"test-agent",
options,
clientFactory: (innerClient) => new TestChatClient(innerClient));
@@ -1390,8 +1390,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
#region User-Agent Header Tests
/// <summary>
/// Verifies that the MEAI user-agent header is added to CreateAIAgentAsync POST requests
/// via the protocol method's RequestOptions pipeline policy.
/// Verifies that the user-agent header is added to both synchronous and asynchronous requests made by agent creation methods.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_UserAgentHeaderAddedToRequestsAsync()
@@ -1399,12 +1398,9 @@ public sealed class AzureAIProjectChatClientExtensionsTests
using var httpHandler = new HttpHandlerAssert(request =>
{
Assert.Equal("POST", request.Method.Method);
Assert.Contains("MEAI", request.Headers.UserAgent.ToString());
// Verify MEAI user-agent header is present on CreateAgentVersion POST request
Assert.True(request.Headers.TryGetValues("User-Agent", out var userAgentValues));
Assert.Contains(userAgentValues, v => v.Contains("MEAI"));
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json") };
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
@@ -1944,7 +1940,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithTextResponseFormat_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -1956,7 +1952,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -1970,7 +1966,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithJsonResponseFormatWithoutSchema_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -1982,7 +1978,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -1996,7 +1992,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchema_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
JsonElement schemaElement = AIJsonUtilities.CreateJsonSchema(typeof(TestSchema));
var jsonFormat = ChatResponseFormat.ForJsonSchema(schemaElement, "test_schema", "A test schema");
var options = new ChatClientAgentOptions
@@ -2010,7 +2006,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2024,7 +2020,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchemaAndStrictMode_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
JsonElement schemaElement = AIJsonUtilities.CreateJsonSchema(typeof(TestSchema));
var jsonFormat = ChatResponseFormat.ForJsonSchema(schemaElement, "test_schema", "A test schema");
var additionalProps = new AdditionalPropertiesDictionary
@@ -2043,7 +2039,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2057,7 +2053,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchemaAndStrictModeFalse_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
JsonElement schemaElement = AIJsonUtilities.CreateJsonSchema(typeof(TestSchema));
var jsonFormat = ChatResponseFormat.ForJsonSchema(schemaElement, "test_schema", "A test schema");
var additionalProps = new AdditionalPropertiesDictionary
@@ -2076,7 +2072,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2094,7 +2090,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithRawRepresentationFactory_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -2106,7 +2102,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2120,7 +2116,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithRawRepresentationFactoryReturningNull_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -2132,7 +2128,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2146,7 +2142,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithRawRepresentationFactoryReturningNonCreateResponseOptions_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -2158,7 +2154,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2176,7 +2172,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithDescription_SetsDescriptionAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler(description: "Test description");
AIProjectClient client = this.CreateTestAgentClient(description: "Test description");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -2185,7 +2181,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2199,7 +2195,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithoutDescription_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -2207,7 +2203,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2692,7 +2688,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithHostedToolTypes_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
AIProjectClient client = this.CreateTestAgentClient();
var webSearchTool = new HostedWebSearchTool();
var options = new ChatClientAgentOptions
@@ -2706,7 +2702,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2859,54 +2855,6 @@ public sealed class AzureAIProjectChatClientExtensionsTests
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse);
}
/// <summary>
/// Creates a test AIProjectClient backed by an HTTP handler that returns canned responses.
/// Used for tests that exercise the protocol-method code path (CreateAgentVersion).
/// The returned client must be disposed to clean up the underlying HttpClient/handler.
/// </summary>
private static DisposableTestClient CreateTestAgentClientWithHandler(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
{
var responseJson = TestDataUtil.GetAgentVersionResponseJson(agentName, agentDefinitionResponse, instructions, description);
var httpHandler = new HttpHandlerAssert(_ =>
new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(responseJson, Encoding.UTF8, "application/json") });
#pragma warning disable CA5399
var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var client = new AIProjectClient(
new Uri("https://test.openai.azure.com/"),
new FakeAuthenticationTokenProvider(),
new() { Transport = new HttpClientPipelineTransport(httpClient) });
return new DisposableTestClient(client, httpClient, httpHandler);
}
/// <summary>
/// Wraps an AIProjectClient and its disposable dependencies for deterministic cleanup.
/// </summary>
private sealed class DisposableTestClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly HttpHandlerAssert _httpHandler;
public DisposableTestClient(AIProjectClient client, HttpClient httpClient, HttpHandlerAssert httpHandler)
{
this.Client = client;
this._httpClient = httpClient;
this._httpHandler = httpHandler;
}
public AIProjectClient Client { get; }
public void Dispose()
{
this._httpClient.Dispose();
this._httpHandler.Dispose();
}
}
/// <summary>
/// Creates a test AgentRecord for testing.
/// </summary>
@@ -3091,13 +3039,25 @@ public sealed class AzureAIProjectChatClientExtensionsTests
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
}
public override ClientResult<AgentVersion> CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default)
public override ClientResult CreateAgentVersion(string agentName, BinaryContent content, RequestOptions? options = null)
{
var responseJson = this.GetAgentVersionResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)));
}
public override ClientResult<AgentVersion> CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentVersionResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
}
public override Task<ClientResult<AgentVersion>> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default)
public override Task<ClientResult> CreateAgentVersionAsync(string agentName, BinaryContent content, RequestOptions? options = null)
{
var responseJson = this.GetAgentVersionResponseJson();
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))));
}
public override Task<ClientResult<AgentVersion>> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentVersionResponseJson();
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
@@ -22,7 +22,7 @@ public class AzureAIProjectChatClientTests
var requestTriggered = false;
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses"))
if (request.RequestUri!.PathAndQuery.Contains("openai/responses"))
{
requestTriggered = true;
@@ -71,7 +71,7 @@ public class AzureAIProjectChatClientTests
var requestTriggered = false;
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses"))
if (request.RequestUri!.PathAndQuery.Contains("openai/responses"))
{
requestTriggered = true;
@@ -120,7 +120,7 @@ public class AzureAIProjectChatClientTests
var requestTriggered = false;
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses"))
if (request.RequestUri!.PathAndQuery.Contains("openai/responses"))
{
requestTriggered = true;
@@ -169,7 +169,7 @@ public class AzureAIProjectChatClientTests
var requestTriggered = false;
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses"))
if (request.RequestUri!.PathAndQuery.Contains("openai/responses"))
{
requestTriggered = true;
@@ -109,7 +109,7 @@ public sealed class GitHubCopilotAgentTests
var hooks = new SessionHooks();
var infiniteSessions = new InfiniteSessionConfig();
var systemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Append, Content = "Be helpful" };
PermissionRequestHandler permissionHandler = (_, _) => Task.FromResult(new PermissionRequestResult());
PermissionHandler permissionHandler = (_, _) => Task.FromResult(new PermissionRequestResult());
UserInputHandler userInputHandler = (_, _) => Task.FromResult(new UserInputResponse { Answer = "input" });
var mcpServers = new Dictionary<string, object> { ["server1"] = new McpLocalServerConfig() };
@@ -160,7 +160,7 @@ public sealed class GitHubCopilotAgentTests
var hooks = new SessionHooks();
var infiniteSessions = new InfiniteSessionConfig();
var systemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Append, Content = "Be helpful" };
PermissionRequestHandler permissionHandler = (_, _) => Task.FromResult(new PermissionRequestResult());
PermissionHandler permissionHandler = (_, _) => Task.FromResult(new PermissionRequestResult());
UserInputHandler userInputHandler = (_, _) => Task.FromResult(new UserInputResponse { Answer = "input" });
var mcpServers = new Dictionary<string, object> { ["server1"] = new McpLocalServerConfig() };
@@ -169,17 +169,16 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
}
[Fact]
public void DiscoverAndLoadSkills_FilesWithMatchingExtensions_DiscoveredAsResources()
public void DiscoverAndLoadSkills_WithValidResourceLinks_ExtractsResourceNames()
{
// Arrange — create resource files in the skill directory
// Arrange
string skillDir = Path.Combine(this._testRoot, "resource-skill");
string refsDir = Path.Combine(skillDir, "refs");
Directory.CreateDirectory(refsDir);
File.WriteAllText(Path.Combine(refsDir, "FAQ.md"), "FAQ content");
File.WriteAllText(Path.Combine(refsDir, "data.json"), "{}");
File.WriteAllText(
Path.Combine(skillDir, "SKILL.md"),
"---\nname: resource-skill\ndescription: Has resources\n---\nSee docs for details.");
"---\nname: resource-skill\ndescription: Has resources\n---\nSee [FAQ](refs/FAQ.md) for details.");
// Act
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
@@ -187,176 +186,29 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
// Assert
Assert.Single(skills);
var skill = skills["resource-skill"];
Assert.Equal(2, skill.ResourceNames.Count);
Assert.Contains(skill.ResourceNames, r => r.Equals("refs/FAQ.md", StringComparison.OrdinalIgnoreCase));
Assert.Contains(skill.ResourceNames, r => r.Equals("refs/data.json", StringComparison.OrdinalIgnoreCase));
Assert.Single(skill.ResourceNames);
Assert.Equal("refs/FAQ.md", skill.ResourceNames[0]);
}
[Fact]
public void DiscoverAndLoadSkills_FilesWithNonMatchingExtensions_NotDiscovered()
public void DiscoverAndLoadSkills_PathTraversal_ExcludesSkill()
{
// Arrange — create a file with an extension not in the default list
string skillDir = Path.Combine(this._testRoot, "ext-skill");
// Arrange — resource links outside the skill directory
string skillDir = Path.Combine(this._testRoot, "traversal-skill");
Directory.CreateDirectory(skillDir);
File.WriteAllText(Path.Combine(skillDir, "image.png"), "fake image");
File.WriteAllText(Path.Combine(skillDir, "data.json"), "{}");
// Create a file outside the skill dir that the traversal would resolve to
File.WriteAllText(Path.Combine(this._testRoot, "secret.txt"), "secret");
File.WriteAllText(
Path.Combine(skillDir, "SKILL.md"),
"---\nname: ext-skill\ndescription: Extension test\n---\nBody.");
"---\nname: traversal-skill\ndescription: Traversal attempt\n---\nSee [doc](../secret.txt).");
// Act
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
// Assert
Assert.Single(skills);
var skill = skills["ext-skill"];
Assert.Single(skill.ResourceNames);
Assert.Equal("data.json", skill.ResourceNames[0]);
}
[Fact]
public void DiscoverAndLoadSkills_SkillMdFile_NotIncludedAsResource()
{
// Arrange — the SKILL.md file itself should not be in the resource list
string skillDir = Path.Combine(this._testRoot, "selfref-skill");
Directory.CreateDirectory(skillDir);
File.WriteAllText(Path.Combine(skillDir, "notes.md"), "notes");
File.WriteAllText(
Path.Combine(skillDir, "SKILL.md"),
"---\nname: selfref-skill\ndescription: Self ref test\n---\nBody.");
// Act
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
// Assert
Assert.Single(skills);
var skill = skills["selfref-skill"];
Assert.Single(skill.ResourceNames);
Assert.Equal("notes.md", skill.ResourceNames[0]);
}
[Fact]
public void DiscoverAndLoadSkills_NestedResourceFiles_Discovered()
{
// Arrange — resource files in nested subdirectories
string skillDir = Path.Combine(this._testRoot, "nested-res-skill");
string deepDir = Path.Combine(skillDir, "level1", "level2");
Directory.CreateDirectory(deepDir);
File.WriteAllText(Path.Combine(deepDir, "deep.md"), "deep content");
File.WriteAllText(
Path.Combine(skillDir, "SKILL.md"),
"---\nname: nested-res-skill\ndescription: Nested resources\n---\nBody.");
// Act
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
// Assert
Assert.Single(skills);
var skill = skills["nested-res-skill"];
Assert.Single(skill.ResourceNames);
Assert.Contains(skill.ResourceNames, r => r.Equals("level1/level2/deep.md", StringComparison.OrdinalIgnoreCase));
}
private static readonly string[] s_customExtensions = new[] { ".custom" };
private static readonly string[] s_validExtensions = new[] { ".md", ".json", ".custom" };
private static readonly string[] s_mixedValidInvalidExtensions = new[] { ".md", "json" };
[Fact]
public void DiscoverAndLoadSkills_CustomResourceExtensions_UsedForDiscovery()
{
// Arrange — use a loader with custom extensions
var customLoader = new FileAgentSkillLoader(NullLogger.Instance, s_customExtensions);
string skillDir = Path.Combine(this._testRoot, "custom-ext-skill");
Directory.CreateDirectory(skillDir);
File.WriteAllText(Path.Combine(skillDir, "data.custom"), "custom data");
File.WriteAllText(Path.Combine(skillDir, "data.json"), "{}");
File.WriteAllText(
Path.Combine(skillDir, "SKILL.md"),
"---\nname: custom-ext-skill\ndescription: Custom extensions\n---\nBody.");
// Act
var skills = customLoader.DiscoverAndLoadSkills(new[] { this._testRoot });
// Assert — only .custom files should be discovered, not .json
Assert.Single(skills);
var skill = skills["custom-ext-skill"];
Assert.Single(skill.ResourceNames);
Assert.Equal("data.custom", skill.ResourceNames[0]);
}
[Theory]
[InlineData("txt")]
[InlineData("")]
[InlineData(" ")]
public void Constructor_InvalidExtension_ThrowsArgumentException(string badExtension)
{
// Arrange & Act & Assert
Assert.Throws<ArgumentException>(() => new FileAgentSkillLoader(NullLogger.Instance, new[] { badExtension }));
}
[Fact]
public void Constructor_NullExtensions_UsesDefaults()
{
// Arrange & Act
var loader = new FileAgentSkillLoader(NullLogger.Instance, null);
string skillDir = this.CreateSkillDirectory("null-ext", "A skill", "Body.");
File.WriteAllText(Path.Combine(skillDir, "notes.md"), "notes");
// Assert — default extensions include .md
var skills = loader.DiscoverAndLoadSkills(new[] { this._testRoot });
Assert.Single(skills["null-ext"].ResourceNames);
}
[Fact]
public void Constructor_ValidExtensions_DoesNotThrow()
{
// Arrange & Act & Assert — should not throw
var loader = new FileAgentSkillLoader(NullLogger.Instance, s_validExtensions);
Assert.NotNull(loader);
}
[Fact]
public void Constructor_MixOfValidAndInvalidExtensions_ThrowsArgumentException()
{
// Arrange & Act & Assert — one bad extension in the list should cause failure
Assert.Throws<ArgumentException>(() => new FileAgentSkillLoader(NullLogger.Instance, s_mixedValidInvalidExtensions));
}
[Fact]
public void DiscoverAndLoadSkills_ResourceInSkillRoot_Discovered()
{
// Arrange — resource file directly in the skill directory (not in a subdirectory)
string skillDir = Path.Combine(this._testRoot, "root-resource-skill");
Directory.CreateDirectory(skillDir);
File.WriteAllText(Path.Combine(skillDir, "guide.md"), "guide content");
File.WriteAllText(Path.Combine(skillDir, "config.json"), "{}");
File.WriteAllText(
Path.Combine(skillDir, "SKILL.md"),
"---\nname: root-resource-skill\ndescription: Root resources\n---\nBody.");
// Act
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
// Assert — both root-level resource files should be discovered
Assert.Single(skills);
var skill = skills["root-resource-skill"];
Assert.Equal(2, skill.ResourceNames.Count);
Assert.Contains(skill.ResourceNames, r => r.Equals("guide.md", StringComparison.OrdinalIgnoreCase));
Assert.Contains(skill.ResourceNames, r => r.Equals("config.json", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void DiscoverAndLoadSkills_NoResourceFiles_ReturnsEmptyResourceNames()
{
// Arrange — skill with no resource files
_ = this.CreateSkillDirectory("no-resources", "A skill", "No resources here.");
// Act
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
// Assert
Assert.Single(skills);
Assert.Empty(skills["no-resources"].ResourceNames);
Assert.Empty(skills);
}
[Fact]
@@ -400,11 +252,8 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
[Fact]
public async Task ReadSkillResourceAsync_ValidResource_ReturnsContentAsync()
{
// Arrange — create a skill with a resource file discovered from the directory
string skillDir = this.CreateSkillDirectory("read-skill", "A skill", "See docs for details.");
string refsDir = Path.Combine(skillDir, "refs");
Directory.CreateDirectory(refsDir);
File.WriteAllText(Path.Combine(refsDir, "doc.md"), "Document content here.");
// Arrange
_ = this.CreateSkillDirectoryWithResource("read-skill", "A skill", "See [doc](refs/doc.md).", "refs/doc.md", "Document content here.");
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
var skill = skills["read-skill"];
@@ -432,10 +281,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
public async Task ReadSkillResourceAsync_PathTraversal_ThrowsInvalidOperationExceptionAsync()
{
// Arrange — skill with a legitimate resource, then try to read a traversal path at read time
string skillDir = this.CreateSkillDirectory("traverse-read", "A skill", "See docs.");
string refsDir = Path.Combine(skillDir, "refs");
Directory.CreateDirectory(refsDir);
File.WriteAllText(Path.Combine(refsDir, "doc.md"), "legit");
_ = this.CreateSkillDirectoryWithResource("traverse-read", "A skill", "See [doc](refs/doc.md).", "refs/doc.md", "legit");
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
var skill = skills["traverse-read"];
@@ -487,14 +333,75 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
Assert.Empty(skills);
}
[Fact]
public void DiscoverAndLoadSkills_DuplicateResourceLinks_DeduplicatesResources()
{
// Arrange — body references the same resource twice
string skillDir = Path.Combine(this._testRoot, "dedup-skill");
string refsDir = Path.Combine(skillDir, "refs");
Directory.CreateDirectory(refsDir);
File.WriteAllText(Path.Combine(refsDir, "doc.md"), "content");
File.WriteAllText(
Path.Combine(skillDir, "SKILL.md"),
"---\nname: dedup-skill\ndescription: Dedup test\n---\nSee [doc](refs/doc.md) and [again](refs/doc.md).");
// Act
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
// Assert
Assert.Single(skills);
Assert.Single(skills["dedup-skill"].ResourceNames);
}
[Fact]
public void DiscoverAndLoadSkills_DotSlashPrefix_NormalizesToBarePath()
{
// Arrange — body references a resource with ./ prefix
string skillDir = Path.Combine(this._testRoot, "dotslash-skill");
string refsDir = Path.Combine(skillDir, "refs");
Directory.CreateDirectory(refsDir);
File.WriteAllText(Path.Combine(refsDir, "doc.md"), "content");
File.WriteAllText(
Path.Combine(skillDir, "SKILL.md"),
"---\nname: dotslash-skill\ndescription: Dot-slash test\n---\nSee [doc](./refs/doc.md).");
// Act
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
// Assert
Assert.Single(skills);
var skill = skills["dotslash-skill"];
Assert.Single(skill.ResourceNames);
Assert.Equal("refs/doc.md", skill.ResourceNames[0]);
}
[Fact]
public void DiscoverAndLoadSkills_DotSlashAndBarePath_DeduplicatesResources()
{
// Arrange — body references the same resource with and without ./ prefix
string skillDir = Path.Combine(this._testRoot, "mixed-prefix-skill");
string refsDir = Path.Combine(skillDir, "refs");
Directory.CreateDirectory(refsDir);
File.WriteAllText(Path.Combine(refsDir, "doc.md"), "content");
File.WriteAllText(
Path.Combine(skillDir, "SKILL.md"),
"---\nname: mixed-prefix-skill\ndescription: Mixed prefix test\n---\nSee [a](./refs/doc.md) and [b](refs/doc.md).");
// Act
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
// Assert
Assert.Single(skills);
var skill = skills["mixed-prefix-skill"];
Assert.Single(skill.ResourceNames);
Assert.Equal("refs/doc.md", skill.ResourceNames[0]);
}
[Fact]
public async Task ReadSkillResourceAsync_DotSlashPrefix_MatchesNormalizedResourceAsync()
{
// Arrange — skill loaded with bare path, caller uses ./ prefix
string skillDir = this.CreateSkillDirectory("dotslash-read", "A skill", "See docs.");
string refsDir = Path.Combine(skillDir, "refs");
Directory.CreateDirectory(refsDir);
File.WriteAllText(Path.Combine(refsDir, "doc.md"), "Document content.");
_ = this.CreateSkillDirectoryWithResource("dotslash-read", "A skill", "See [doc](refs/doc.md).", "refs/doc.md", "Document content.");
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
var skill = skills["dotslash-read"];
@@ -509,10 +416,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
public async Task ReadSkillResourceAsync_BackslashSeparator_MatchesNormalizedResourceAsync()
{
// Arrange — skill loaded with forward-slash path, caller uses backslashes
string skillDir = this.CreateSkillDirectory("backslash-read", "A skill", "See docs.");
string refsDir = Path.Combine(skillDir, "refs");
Directory.CreateDirectory(refsDir);
File.WriteAllText(Path.Combine(refsDir, "doc.md"), "Backslash content.");
_ = this.CreateSkillDirectoryWithResource("backslash-read", "A skill", "See [doc](refs/doc.md).", "refs/doc.md", "Backslash content.");
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
var skill = skills["backslash-read"];
@@ -527,10 +431,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
public async Task ReadSkillResourceAsync_DotSlashWithBackslash_MatchesNormalizedResourceAsync()
{
// Arrange — skill loaded with forward-slash path, caller uses .\ prefix with backslashes
string skillDir = this.CreateSkillDirectory("mixed-sep-read", "A skill", "See docs.");
string refsDir = Path.Combine(skillDir, "refs");
Directory.CreateDirectory(refsDir);
File.WriteAllText(Path.Combine(refsDir, "doc.md"), "Mixed separator content.");
_ = this.CreateSkillDirectoryWithResource("mixed-sep-read", "A skill", "See [doc](refs/doc.md).", "refs/doc.md", "Mixed separator content.");
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
var skill = skills["mixed-sep-read"];
@@ -542,13 +443,14 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
}
#if NET
private static readonly string[] s_symlinkResource = ["refs/data.md"];
[Fact]
public void DiscoverAndLoadSkills_SymlinkInPath_SkipsSymlinkedResources()
public void DiscoverAndLoadSkills_SymlinkInPath_ExcludesSkill()
{
// Arrange — a "refs" subdirectory is a symlink pointing outside the skill directory
string skillDir = Path.Combine(this._testRoot, "symlink-escape-skill");
Directory.CreateDirectory(skillDir);
File.WriteAllText(Path.Combine(skillDir, "legit.md"), "legit content");
string outsideDir = Path.Combine(this._testRoot, "outside");
Directory.CreateDirectory(outsideDir);
@@ -567,20 +469,15 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
File.WriteAllText(
Path.Combine(skillDir, "SKILL.md"),
"---\nname: symlink-escape-skill\ndescription: Symlinked directory escape\n---\nBody.");
"---\nname: symlink-escape-skill\ndescription: Symlinked directory escape\n---\nSee [doc](refs/secret.md).");
// Act
var skills = this._loader.DiscoverAndLoadSkills(new[] { this._testRoot });
// Assert — skill should still load, but symlinked resources should be excluded
Assert.True(skills.ContainsKey("symlink-escape-skill"));
var skill = skills["symlink-escape-skill"];
Assert.Single(skill.ResourceNames);
Assert.Equal("legit.md", skill.ResourceNames[0]);
// Assert — skill should be excluded because refs/ is a symlink (reparse point)
Assert.False(skills.ContainsKey("symlink-escape-skill"));
}
private static readonly string[] s_symlinkResource = ["refs/data.md"];
[Fact]
public async Task ReadSkillResourceAsync_SymlinkInPath_ThrowsInvalidOperationExceptionAsync()
{
@@ -652,4 +549,13 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
File.WriteAllText(Path.Combine(skillDir, "SKILL.md"), rawContent);
return skillDir;
}
private string CreateSkillDirectoryWithResource(string name, string description, string body, string resourceRelativePath, string resourceContent)
{
string skillDir = this.CreateSkillDirectory(name, description, body);
string resourcePath = Path.Combine(skillDir, resourceRelativePath);
Directory.CreateDirectory(Path.GetDirectoryName(resourcePath)!);
File.WriteAllText(resourcePath, resourceContent);
return skillDir;
}
}
@@ -15,7 +15,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests;
public sealed class DeclarativeCodeGenTest(ITestOutputHelper output) : WorkflowTest(output)
{
[Theory]
[InlineData("CheckSystem.yaml", "CheckSystem.json", Skip = "Temporarily skipped")]
[InlineData("CheckSystem.yaml", "CheckSystem.json")]
[InlineData("SendActivity.yaml", "SendActivity.json")]
[InlineData("InvokeAgent.yaml", "InvokeAgent.json")]
[InlineData("InvokeAgent.yaml", "InvokeAgent.json", true)]
@@ -16,7 +16,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests;
public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : WorkflowTest(output)
{
[Theory]
[InlineData("CheckSystem.yaml", "CheckSystem.json", Skip = "Temporarily skipped")]
[InlineData("CheckSystem.yaml", "CheckSystem.json")]
[InlineData("ConversationMessages.yaml", "ConversationMessages.json")]
[InlineData("ConversationMessages.yaml", "ConversationMessages.json", true)]
[InlineData("InputArguments.yaml", "InputArguments.json")]
@@ -34,7 +34,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
public Task ValidateScenarioAsync(string workflowFileName, string testcaseFileName, bool externalConveration = false) =>
this.RunWorkflowAsync(GetWorkflowPath(workflowFileName, isSample: true), testcaseFileName, externalConveration);
[Theory(Skip = "Multi-turn tests hang in CI - needs investigation")]
[Theory]
[InlineData("ConfirmInput.yaml", "ConfirmInput.json", false)]
[InlineData("RequestExternalInput.yaml", "RequestExternalInput.json", false)]
public Task ValidateMultiTurnAsync(string workflowFileName, string testcaseFileName, bool isSample) =>
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Azure.Identity;
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
using Microsoft.Extensions.AI;
@@ -145,7 +145,7 @@ public sealed class ObservabilityTests : IDisposable
await this.TestWorkflowEndToEndActivitiesAsync("OffThread");
}
[Fact(Skip = "Flaky test - temporarily disabled")]
[Fact]
public async Task CreatesWorkflowEndToEndActivities_WithCorrectName_ConcurrentAsync()
{
await this.TestWorkflowEndToEndActivitiesAsync("Concurrent");
@@ -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(Skip = "Flaky test - temporarily disabled")]
[Fact]
public async Task WorkflowRunActivity_IsStopped_Streaming_OffThread_MultiTurnAsync()
{
// Arrange
@@ -1,23 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
namespace OpenAIAssistant.IntegrationTests;
public class OpenAIAssistantStructuredOutputRunTests() : StructuredOutputRunTests<OpenAIAssistantFixture>(() => new())
{
private const string SkipReason = "Fails intermittently on the build agent/CI";
[Fact(Skip = SkipReason)]
public override Task RunWithResponseFormatReturnsExpectedResultAsync() =>
base.RunWithResponseFormatReturnsExpectedResultAsync();
[Fact(Skip = SkipReason)]
public override Task RunWithGenericTypeReturnsExpectedResultAsync() =>
base.RunWithGenericTypeReturnsExpectedResultAsync();
[Fact(Skip = SkipReason)]
public override Task RunWithPrimitiveTypeReturnsExpectedResultAsync() =>
base.RunWithPrimitiveTypeReturnsExpectedResultAsync();
}
+1 -43
View File
@@ -7,47 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.0rc3] - 2026-03-04
### Added
- **agent-framework-core**: Add Shell tool ([#4339](https://github.com/microsoft/agent-framework/pull/4339))
- **agent-framework-core**: Add `file_ids` and `data_sources` support to `get_code_interpreter_tool()` ([#4201](https://github.com/microsoft/agent-framework/pull/4201))
- **agent-framework-core**: Map file citation annotations from `TextDeltaBlock` in Assistants API streaming ([#4316](https://github.com/microsoft/agent-framework/pull/4316), [#4320](https://github.com/microsoft/agent-framework/pull/4320))
- **agent-framework-claude**: Add OpenTelemetry instrumentation to `ClaudeAgent` ([#4278](https://github.com/microsoft/agent-framework/pull/4278), [#4326](https://github.com/microsoft/agent-framework/pull/4326))
- **agent-framework-azure-cosmos**: Add Azure Cosmos history provider package ([#4271](https://github.com/microsoft/agent-framework/pull/4271))
- **samples**: Add `auto_retry.py` sample for rate limit handling ([#4223](https://github.com/microsoft/agent-framework/pull/4223))
- **tests**: Add regression tests for Entry JoinExecutor workflow input initialization ([#4335](https://github.com/microsoft/agent-framework/pull/4335))
### Changed
- **samples**: Restructure and improve Python samples ([#4092](https://github.com/microsoft/agent-framework/pull/4092))
- **agent-framework-orchestrations**: [BREAKING] Tighten `HandoffBuilder` to require `Agent` instead of `SupportsAgentRun` ([#4301](https://github.com/microsoft/agent-framework/pull/4301), [#4302](https://github.com/microsoft/agent-framework/pull/4302))
- **samples**: Update workflow orchestration samples to use `AzureOpenAIResponsesClient` ([#4285](https://github.com/microsoft/agent-framework/pull/4285))
### Fixed
- **agent-framework-bedrock**: Fix embedding test stub missing `meta` attribute ([#4287](https://github.com/microsoft/agent-framework/pull/4287))
- **agent-framework-ag-ui**: Fix approval payloads being re-processed on subsequent conversation turns ([#4232](https://github.com/microsoft/agent-framework/pull/4232))
- **agent-framework-core**: Fix `response_format` resolution in streaming finalizer ([#4291](https://github.com/microsoft/agent-framework/pull/4291))
- **agent-framework-core**: Strip reserved kwargs in `AgentExecutor` to prevent duplicate-argument `TypeError` ([#4298](https://github.com/microsoft/agent-framework/pull/4298))
- **agent-framework-core**: Preserve workflow run kwargs when continuing with `run(responses=...)` ([#4296](https://github.com/microsoft/agent-framework/pull/4296))
- **agent-framework-core**: Fix `WorkflowAgent` not persisting response messages to session history ([#4319](https://github.com/microsoft/agent-framework/pull/4319))
- **agent-framework-core**: Fix single-tool input handling in `OpenAIResponsesClient._prepare_tools_for_openai` ([#4312](https://github.com/microsoft/agent-framework/pull/4312))
- **agent-framework-core**: Fix agent option merge to support dict-defined tools ([#4314](https://github.com/microsoft/agent-framework/pull/4314))
- **agent-framework-core**: Fix executor handler type resolution when using `from __future__ import annotations` ([#4317](https://github.com/microsoft/agent-framework/pull/4317))
- **agent-framework-core**: Fix walrus operator precedence for `model_id` kwarg in `AzureOpenAIResponsesClient` ([#4310](https://github.com/microsoft/agent-framework/pull/4310))
- **agent-framework-core**: Handle `thread.message.completed` event in Assistants API streaming ([#4333](https://github.com/microsoft/agent-framework/pull/4333))
- **agent-framework-core**: Fix MCP tools duplicated on second turn when runtime tools are present ([#4432](https://github.com/microsoft/agent-framework/pull/4432))
- **agent-framework-core**: Fix PowerFx eval crash on non-English system locales by setting `CurrentUICulture` to `en-US` ([#4408](https://github.com/microsoft/agent-framework/pull/4408))
- **agent-framework-orchestrations**: Fix `StandardMagenticManager` to propagate session to manager agent ([#4409](https://github.com/microsoft/agent-framework/pull/4409))
- **agent-framework-orchestrations**: Fix `IndexError` when reasoning models produce reasoning-only messages in Magentic-One workflow ([#4413](https://github.com/microsoft/agent-framework/pull/4413))
- **agent-framework-azure-ai**: Fix parsing `oauth_consent_request` events in Azure AI client ([#4197](https://github.com/microsoft/agent-framework/pull/4197))
- **agent-framework-anthropic**: Set `role="assistant"` on `message_start` streaming update ([#4329](https://github.com/microsoft/agent-framework/pull/4329))
- **samples**: Fix samples discovered by auto validation pipeline ([#4355](https://github.com/microsoft/agent-framework/pull/4355))
- **samples**: Use `AgentResponse.value` instead of `model_validate_json` in HITL sample ([#4405](https://github.com/microsoft/agent-framework/pull/4405))
- **agent-framework-devui**: Fix .NET conversation memory handling in DevUI integration ([#3484](https://github.com/microsoft/agent-framework/pull/3484), [#4294](https://github.com/microsoft/agent-framework/pull/4294))
## [1.0.0rc2] - 2026-02-25
### Added
@@ -741,8 +700,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.0.0rc3...HEAD
[1.0.0rc3]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc2...python-1.0.0rc3
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc2...HEAD
[1.0.0rc2]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc1...python-1.0.0rc2
[1.0.0rc1]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260212...python-1.0.0rc1
[1.0.0b260212]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260210...python-1.0.0b260212
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"a2a-sdk>=0.3.5",
]
@@ -372,15 +372,6 @@ def _emit_usage(content: Content) -> list[BaseEvent]:
return [CustomEvent(name="usage", value=usage_details)]
def _emit_oauth_consent(content: Content) -> list[BaseEvent]:
"""Emit an OAuth consent request as a custom event so frontends can render a consent link."""
return (
[CustomEvent(name="oauth_consent_request", value={"consent_link": content.consent_link})]
if content.consent_link
else []
)
def _emit_content(
content: Any,
flow: FlowState,
@@ -400,7 +391,5 @@ def _emit_content(
return _emit_approval_request(content, flow, predictive_handler, require_confirmation)
if content_type == "usage":
return _emit_usage(content)
if content_type == "oauth_consent_request":
return _emit_oauth_consent(content)
logger.debug("Skipping unsupported content type in AG-UI emitter: %s", content_type)
return []
+2 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "agent-framework-ag-ui"
version = "1.0.0b260304"
version = "1.0.0b260225"
description = "AG-UI protocol integration for Agent Framework"
readme = "README.md"
license-files = ["LICENSE"]
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"ag-ui-protocol>=0.1.9",
"fastapi>=0.115.0",
"uvicorn>=0.30.0"
@@ -4,7 +4,6 @@
import pytest
from ag_ui.core import (
CustomEvent,
TextMessageEndEvent,
TextMessageStartEvent,
ToolCallArgsEvent,
@@ -872,26 +871,3 @@ class TestTextMessageEventBalancing:
assert len(start_events) == 2
assert len(end_events) == 2
def test_emit_oauth_consent_request():
"""Test that oauth_consent_request content emits a CustomEvent."""
content = Content.from_oauth_consent_request(
consent_link="https://login.microsoftonline.com/consent",
)
flow = FlowState()
events = _emit_content(content, flow)
assert len(events) == 1
assert isinstance(events[0], CustomEvent)
assert events[0].name == "oauth_consent_request"
assert events[0].value == {"consent_link": "https://login.microsoftonline.com/consent"}
def test_emit_oauth_consent_request_no_link():
"""Test that oauth_consent_request without a consent_link emits no events."""
content = Content("oauth_consent_request")
flow = FlowState()
events = _emit_content(content, flow)
assert len(events) == 0
@@ -894,7 +894,6 @@ class AnthropicClient(
usage_details.append(Content.from_usage(usage_details=details))
return ChatResponseUpdate(
role="assistant",
response_id=event.message.id,
contents=[
*self._parse_contents_from_anthropic(event.message.content),
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"anthropic>=0.70.0,<1",
]
@@ -1044,128 +1044,6 @@ async def test_inner_get_response_ignores_options_stream_streaming(mock_anthropi
assert mock_anthropic_client.beta.messages.create.call_args.kwargs["stream"] is True
def test_process_stream_event_message_start_sets_assistant_role(mock_anthropic_client: MagicMock) -> None:
"""Test that message_start streaming event sets role='assistant'.
This is critical: without role='assistant', _process_update cannot detect
a role boundary between a prior tool message and the new assistant turn,
causing tool_use blocks to collapse into a user-role message and triggering
Anthropic's '`tool_use` blocks can only be in `assistant` messages' error.
"""
client = create_test_anthropic_client(mock_anthropic_client)
mock_event = MagicMock()
mock_event.type = "message_start"
mock_event.message.id = "msg_abc"
mock_event.message.role = "assistant"
mock_event.message.model = "claude-3-5-sonnet-20241022"
mock_event.message.content = []
mock_event.message.stop_reason = None
mock_event.message.usage = None
result = client._process_stream_event(mock_event)
assert result is not None
assert result.role == "assistant"
def test_process_stream_event_message_start_role_prevents_tool_use_collapse() -> None:
"""Regression test: tool_use blocks must not end up in a user-role message.
Simulates two consecutive streaming tool-call iterations:
Iteration 1: assistant emits tool_use → framework appends tool result (role=tool)
Iteration 2: assistant starts a new message_start → must create a NEW message
Without role='assistant' on the message_start update, _process_update sees
update.role=None (falsy) and appends to the last message (role='tool'),
producing {"role": "user", "content": [tool_result, tool_use]} which
Anthropic rejects with HTTP 400.
"""
from agent_framework import ChatResponse, ChatResponseUpdate, Content, Message
# Simulate what the streaming tool loop produces after iteration 1:
# an existing 'tool' message is the last in the response
existing_tool_message = Message(
role="tool",
contents=[Content.from_function_result(call_id="call_1", result="some result")],
)
response = ChatResponse(messages=[existing_tool_message])
# Now simulate the message_start update from iteration 2 — WITH role set
message_start_update = ChatResponseUpdate(
role="assistant",
response_id="msg_iter2",
)
# Simulate a content_block_start carrying a tool_use — no role on this one (correct)
tool_use_update = ChatResponseUpdate(
contents=[
Content.from_function_call(
call_id="call_2",
name="get_weather",
arguments={"location": "NYC"},
)
],
)
# Apply updates exactly as from_updates / _process_update would
from agent_framework._types import _process_update
_process_update(response, message_start_update)
_process_update(response, tool_use_update)
# Must have TWO messages: the original tool message + a new assistant message
assert len(response.messages) == 2, "tool_use from iteration 2 collapsed into the tool message from iteration 1"
assert response.messages[0].role == "tool"
assert response.messages[1].role == "assistant"
# The assistant message must contain the tool_use, not the tool result
assert response.messages[1].contents[0].type == "function_call"
assert response.messages[1].contents[0].call_id == "call_2"
def test_process_stream_event_message_start_without_role_reproduces_bug() -> None:
"""Documents the original bug: missing role causes tool_use to collapse into tool message.
This test demonstrates WHY the fix (adding role='assistant') was necessary.
It intentionally reproduces the broken behavior when role is absent.
"""
from agent_framework import ChatResponse, ChatResponseUpdate, Content, Message
from agent_framework._types import _process_update
existing_tool_message = Message(
role="tool",
contents=[Content.from_function_result(call_id="call_1", result="some result")],
)
response = ChatResponse(messages=[existing_tool_message])
# message_start WITHOUT role (the original broken state)
message_start_update = ChatResponseUpdate(
role=None,
response_id="msg_iter2",
)
tool_use_update = ChatResponseUpdate(
contents=[
Content.from_function_call(
call_id="call_2",
name="get_weather",
arguments={"location": "NYC"},
)
],
)
_process_update(response, message_start_update)
_process_update(response, tool_use_update)
# BUG: only 1 message — tool_use collapsed into the tool message
assert len(response.messages) == 1, "Expected bug: should still be 1 message without the fix"
# The single message has role='tool' but contains a function_call — invalid for Anthropic API
assert response.messages[0].role == "tool"
has_function_call = any(c.type == "function_call" for c in response.messages[0].contents)
assert has_function_call, "Expected bug: function_call leaked into tool message"
# Integration Tests
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"azure-search-documents==11.7.0b2",
]
@@ -87,11 +87,10 @@ from azure.ai.agents.models import (
ToolApproval,
ToolDefinition,
ToolOutput,
VectorStoreDataSource,
)
from pydantic import BaseModel
from ._shared import AzureAISettings, resolve_file_ids, to_azure_ai_agent_tools
from ._shared import AzureAISettings, to_azure_ai_agent_tools
if sys.version_info >= (3, 13):
from typing import TypeVar # type: ignore # pragma: no cover
@@ -220,21 +219,9 @@ class AzureAIAgentClient(
# region Hosted Tool Factory Methods
@staticmethod
def get_code_interpreter_tool(
*,
file_ids: list[str | Content] | None = None,
data_sources: list[VectorStoreDataSource] | None = None,
) -> CodeInterpreterTool:
def get_code_interpreter_tool() -> CodeInterpreterTool:
"""Create a code interpreter tool configuration for Azure AI Agents.
Keyword Args:
file_ids: List of uploaded file IDs or Content objects to make available to
the code interpreter. Accepts plain strings or Content.from_hosted_file()
instances. The underlying SDK raises ValueError if both file_ids and
data_sources are provided.
data_sources: List of vector store data sources for enterprise file search.
Mutually exclusive with file_ids.
Returns:
A CodeInterpreterTool instance ready to pass to ChatAgent.
@@ -243,21 +230,10 @@ class AzureAIAgentClient(
from agent_framework.azure import AzureAIAgentClient
# Basic code interpreter
tool = AzureAIAgentClient.get_code_interpreter_tool()
# With uploaded file IDs
tool = AzureAIAgentClient.get_code_interpreter_tool(file_ids=["file-abc123"])
# With Content objects
from agent_framework import Content
tool = AzureAIAgentClient.get_code_interpreter_tool(file_ids=[Content.from_hosted_file("file-abc123")])
agent = ChatAgent(client, tools=[tool])
"""
resolved = resolve_file_ids(file_ids)
return CodeInterpreterTool(file_ids=resolved, data_sources=data_sources)
return CodeInterpreterTool()
@staticmethod
def get_file_search_tool(
@@ -37,13 +37,12 @@ from agent_framework.openai._responses_client import RawOpenAIResponsesClient
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import (
ApproximateLocation,
CodeInterpreterContainerAuto,
CodeInterpreterTool,
FoundryFeaturesOptInKeys,
CodeInterpreterToolAuto,
ImageGenTool,
MCPTool,
PromptAgentDefinition,
PromptAgentDefinitionTextOptions,
PromptAgentDefinitionText,
RaiConfig,
Reasoning,
WebSearchPreviewTool,
@@ -51,7 +50,7 @@ from azure.ai.projects.models import (
from azure.ai.projects.models import FileSearchTool as ProjectsFileSearchTool
from azure.core.exceptions import ResourceNotFoundError
from ._shared import AzureAISettings, create_text_format_config, resolve_file_ids
from ._shared import AzureAISettings, create_text_format_config
if sys.version_info >= (3, 13):
from typing import TypeVar # type: ignore # pragma: no cover
@@ -79,9 +78,6 @@ class AzureAIProjectAgentOptions(OpenAIResponsesOptions, total=False):
reasoning: Reasoning # type: ignore[misc]
"""Configuration for enabling reasoning capabilities (requires azure.ai.projects.models.Reasoning)."""
foundry_features: FoundryFeaturesOptInKeys | str
"""Optional Foundry preview feature opt-in for agent version creation."""
AzureAIClientOptionsT = TypeVar(
"AzureAIClientOptionsT",
@@ -396,7 +392,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
# response_format is accessed from chat_options or additional_properties
# since the base class excludes it from run_options
if chat_options and (response_format := chat_options.get("response_format")):
args["text"] = PromptAgentDefinitionTextOptions(format=create_text_format_config(response_format))
args["text"] = PromptAgentDefinitionText(format=create_text_format_config(response_format))
# Combine instructions from messages and options
# instructions is accessed from chat_options since the base class excludes it from run_options
@@ -408,15 +404,11 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
if combined_instructions:
args["instructions"] = "".join(combined_instructions)
create_version_kwargs: dict[str, Any] = {
"agent_name": self.agent_name,
"definition": PromptAgentDefinition(**args),
"description": self.agent_description,
}
if foundry_features := run_options.get("foundry_features"):
create_version_kwargs["foundry_features"] = foundry_features
created_agent = await self.project_client.agents.create_version(**create_version_kwargs)
created_agent = await self.project_client.agents.create_version(
agent_name=self.agent_name,
definition=PromptAgentDefinition(**args),
description=self.agent_description,
)
self.agent_version = created_agent.version
self.warn_runtime_tools_and_structure_changed = True
@@ -508,7 +500,6 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
"temperature": ("temperature",),
"top_p": ("top_p",),
"reasoning": ("reasoning",),
"foundry_features": ("foundry_features",),
}
for run_keys in agent_level_option_to_run_keys.values():
@@ -535,9 +526,9 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
run_options["input"] = self._transform_input_for_azure_ai(cast(list[dict[str, Any]], run_options["input"]))
if not self._is_application_endpoint:
# Application-scoped response APIs do not support "agent_reference" property.
# Application-scoped response APIs do not support "agent" property.
agent_reference = await self._get_agent_reference_or_create(run_options, instructions, options)
run_options["extra_body"] = {"agent_reference": agent_reference}
run_options["extra_body"] = {"agent": agent_reference}
# Remove only keys that map to this client's declared options TypedDict.
self._remove_agent_level_run_options(run_options, options)
@@ -597,68 +588,6 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
"""Get the current conversation ID from chat options or kwargs."""
return options.get("conversation_id") or kwargs.get("conversation_id") or self.conversation_id
@override
def _parse_response_from_openai(
self,
response: Any,
options: dict[str, Any],
) -> ChatResponse:
"""Parse an Azure AI Responses API response, handling Azure-specific output item types."""
result = super()._parse_response_from_openai(response, options)
if result.messages:
for item in response.output:
if item.type == "oauth_consent_request":
consent_link = item.consent_link
if consent_link and not consent_link.startswith("https://"):
logger.warning("Skipping oauth_consent_request with non-HTTPS consent_link: %s", item)
consent_link = ""
if consent_link:
result.messages[0].contents.append(
Content.from_oauth_consent_request(
consent_link=consent_link,
raw_representation=item,
)
)
else:
logger.warning("Received oauth_consent_request output without consent_link: %s", item)
return result
@override
def _parse_chunk_from_openai(
self,
event: Any,
options: dict[str, Any],
function_call_ids: dict[int, tuple[str, str]],
) -> ChatResponseUpdate:
"""Parse an Azure AI streaming event, handling Azure-specific event types."""
# Intercept output_item.added events for Azure-specific item types
if event.type == "response.output_item.added" and event.item.type == "oauth_consent_request":
event_item = event.item
consent_link = event_item.consent_link
if consent_link and not consent_link.startswith("https://"):
logger.warning("Skipping oauth_consent_request with non-HTTPS consent_link: %s", event_item)
consent_link = ""
contents: list[Content] = []
if consent_link:
contents.append(
Content.from_oauth_consent_request(
consent_link=consent_link,
raw_representation=event_item,
)
)
else:
logger.warning("Received oauth_consent_request output without consent_link: %s", event_item)
return ChatResponseUpdate(
contents=contents,
role="assistant",
model_id=self.model_id,
raw_representation=event,
)
return super()._parse_chunk_from_openai(event, options, function_call_ids)
def _prepare_messages_for_azure_ai(self, messages: Sequence[Message]) -> tuple[list[Message], str | None]:
"""Prepare input from messages and convert system/developer messages to instructions."""
result: list[Message] = []
@@ -901,16 +830,14 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
@staticmethod
def get_code_interpreter_tool( # type: ignore[override]
*,
file_ids: list[str | Content] | None = None,
file_ids: list[str] | None = None,
container: Literal["auto"] | dict[str, Any] = "auto",
**kwargs: Any,
) -> CodeInterpreterTool:
"""Create a code interpreter tool configuration for Azure AI Projects.
Keyword Args:
file_ids: Optional list of file IDs or Content objects to make available to
the code interpreter. Accepts plain strings or Content.from_hosted_file()
instances.
file_ids: Optional list of file IDs to make available to the code interpreter.
container: Container configuration. Use "auto" for automatic container management.
Note: Custom container settings from this parameter are not used by Azure AI Projects;
use file_ids instead.
@@ -930,8 +857,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
# Extract file_ids from container if provided as dict and file_ids not explicitly set
if file_ids is None and isinstance(container, dict):
file_ids = container.get("file_ids")
resolved = resolve_file_ids(file_ids)
tool_container = CodeInterpreterContainerAuto(file_ids=resolved)
tool_container = CodeInterpreterToolAuto(file_ids=file_ids if file_ids else None)
return CodeInterpreterTool(container=tool_container, **kwargs)
@staticmethod
@@ -18,6 +18,7 @@ from agent_framework._sessions import AgentSession, BaseContextProvider, Session
from agent_framework._settings import load_settings
from agent_framework.azure._entra_id_authentication import AzureCredentialTypes
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import ItemParam, ResponsesAssistantMessageItemParam, ResponsesUserMessageItemParam
from ._shared import AzureAISettings
@@ -148,7 +149,7 @@ class FoundryMemoryProvider(BaseContextProvider):
# On first run, retrieve static memories (user profile memories)
if not state.get("initialized"):
try:
static_search_result = await self.project_client.beta.memory_stores.search_memories(
static_search_result = await self.project_client.memory_stores.search_memories(
name=self.memory_store_name,
scope=self.scope or context.session_id, # type: ignore[arg-type]
)
@@ -168,15 +169,15 @@ class FoundryMemoryProvider(BaseContextProvider):
if not has_input:
return
# Convert input messages to memory search item format
# Convert input messages to ItemParam format for search
items = [
{"type": "text", "text": msg.text}
ItemParam({"type": "text", "text": msg.text})
for msg in context.input_messages
if msg and msg.text and msg.text.strip()
]
try:
search_result = await self.project_client.beta.memory_stores.search_memories(
search_result = await self.project_client.memory_stores.search_memories(
name=self.memory_store_name,
scope=self.scope or context.session_id, # type: ignore[arg-type]
items=items,
@@ -223,24 +224,24 @@ class FoundryMemoryProvider(BaseContextProvider):
if context.response and context.response.messages:
messages_to_store.extend(context.response.messages)
# Filter and convert messages to memory update item format
items: list[dict[str, str]] = []
# Filter and convert messages to ItemParam format
items: list[ResponsesUserMessageItemParam | ResponsesAssistantMessageItemParam] = []
for message in messages_to_store:
if message.role in {"user", "assistant", "system"} and message.text and message.text.strip():
if message.role == "user":
items.append({"role": "user", "type": "message", "content": message.text})
items.append(ResponsesUserMessageItemParam(content=message.text))
elif message.role == "assistant":
items.append({"role": "assistant", "type": "message", "content": message.text})
items.append(ResponsesAssistantMessageItemParam(content=message.text))
if not items:
return
try:
# Fire and forget - don't wait for the update to complete
update_poller = await self.project_client.beta.memory_stores.begin_update_memories(
update_poller = await self.project_client.memory_stores.begin_update_memories(
name=self.memory_store_name,
scope=self.scope or context.session_id, # type: ignore[arg-type]
items=items,
items=items, # type: ignore[arg-type]
previous_update_id=state.get("previous_update_id"),
update_delay=self.update_delay,
)
@@ -4,7 +4,7 @@ from __future__ import annotations
import logging
import sys
from collections.abc import Callable, Mapping, MutableMapping, Sequence
from collections.abc import Callable, MutableMapping, Sequence
from typing import Any, Generic
from agent_framework import (
@@ -21,9 +21,10 @@ from agent_framework._tools import ToolTypes
from agent_framework.azure._entra_id_authentication import AzureCredentialTypes
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import (
AgentReference,
AgentVersionDetails,
PromptAgentDefinition,
PromptAgentDefinitionTextOptions,
PromptAgentDefinitionText,
)
from azure.ai.projects.models import (
FunctionTool as AzureFunctionTool,
@@ -199,14 +200,13 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
response_format = opts.get("response_format")
rai_config = opts.get("rai_config")
reasoning = opts.get("reasoning")
foundry_features = opts.get("foundry_features")
args: dict[str, Any] = {"model": resolved_model}
if instructions:
args["instructions"] = instructions
if response_format and isinstance(response_format, (type, dict)):
args["text"] = PromptAgentDefinitionTextOptions(
args["text"] = PromptAgentDefinitionText(
format=create_text_format_config(response_format) # type: ignore[arg-type]
)
if rai_config:
@@ -241,15 +241,11 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
if all_tools_for_azure:
args["tools"] = to_azure_ai_tools(all_tools_for_azure)
create_version_kwargs: dict[str, Any] = {
"agent_name": name,
"definition": PromptAgentDefinition(**args),
"description": description,
}
if foundry_features:
create_version_kwargs["foundry_features"] = foundry_features
created_agent = await self._project_client.agents.create_version(**create_version_kwargs)
created_agent = await self._project_client.agents.create_version(
agent_name=name,
definition=PromptAgentDefinition(**args),
description=description,
)
return self._to_chat_agent_from_details(
created_agent,
@@ -263,7 +259,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
self,
*,
name: str | None = None,
reference: Mapping[str, str | None] | None = None,
reference: AgentReference | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
@@ -276,7 +272,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
Args:
name: The name of the agent to retrieve (fetches latest version).
reference: Mapping containing the agent's ``name`` and optionally a specific ``version``.
reference: Reference containing the agent's name and optionally a specific version.
tools: Tools to make available to the agent. Required if the agent has function tools.
default_options: A TypedDict containing default chat options for the agent.
These options are applied to every run unless overridden.
@@ -291,15 +287,12 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
"""
existing_agent: AgentVersionDetails
reference_name = str(reference.get("name")) if reference and reference.get("name") else None
reference_version = str(reference.get("version")) if reference and reference.get("version") else None
if reference_name and reference_version:
if reference and reference.version:
# Fetch specific version
existing_agent = await self._project_client.agents.get_version(
agent_name=reference_name, agent_version=reference_version
agent_name=reference.name, agent_version=reference.version
)
elif agent_name := (reference_name if reference_name else name):
elif agent_name := (reference.name if reference else name):
# Fetch latest version
details = await self._project_client.agents.get(agent_name=agent_name)
existing_agent = details.versions.latest
@@ -8,7 +8,6 @@ from collections.abc import Mapping, MutableMapping, Sequence
from typing import Any, cast
from agent_framework import (
Content,
FunctionTool,
)
from agent_framework.exceptions import IntegrationInvalidRequestException
@@ -19,9 +18,9 @@ from azure.ai.agents.models import (
from azure.ai.projects.models import (
CodeInterpreterTool,
MCPTool,
TextResponseFormatConfigurationResponseFormatJsonObject,
TextResponseFormatConfigurationResponseFormatText,
TextResponseFormatJsonSchema,
ResponseTextFormatConfigurationJsonObject,
ResponseTextFormatConfigurationJsonSchema,
ResponseTextFormatConfigurationText,
Tool,
WebSearchPreviewTool,
)
@@ -110,47 +109,6 @@ def _extract_project_connection_id(additional_properties: dict[str, Any] | None)
return None
def resolve_file_ids(file_ids: Sequence[str | Content] | None) -> list[str] | None:
"""Resolve a list of file ID values that may include Content objects.
Accepts plain strings and Content objects with type "hosted_file", extracting
the file_id from each. This enables users to pass Content.from_hosted_file()
alongside plain file ID strings.
Args:
file_ids: Sequence of file ID strings or Content objects, or None.
Returns:
A list of resolved file ID strings, or None if input is None or empty.
Raises:
ValueError: If a Content object has an unsupported type (not "hosted_file").
"""
if not file_ids:
return None
resolved: list[str] = []
for item in file_ids:
if isinstance(item, str):
if not item:
raise ValueError("file_ids must not contain empty strings.")
resolved.append(item)
elif isinstance(item, Content):
if item.type != "hosted_file":
raise ValueError(
f"Unsupported Content type '{item.type}' for code interpreter file_ids. "
"Only Content.from_hosted_file() is supported."
)
if item.file_id is None:
raise ValueError(
"Content.from_hosted_file() item is missing a file_id. "
"Ensure the Content object has a valid file_id before using it in file_ids."
)
resolved.append(item.file_id)
return resolved if resolved else None
def to_azure_ai_agent_tools(
tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None,
run_options: dict[str, Any] | None = None,
@@ -463,9 +421,9 @@ def _prepare_mcp_tool_dict_for_azure_ai(tool_dict: dict[str, Any]) -> MCPTool:
def create_text_format_config(
response_format: type[BaseModel] | Mapping[str, Any],
) -> (
TextResponseFormatJsonSchema
| TextResponseFormatConfigurationResponseFormatJsonObject
| TextResponseFormatConfigurationResponseFormatText
ResponseTextFormatConfigurationJsonSchema
| ResponseTextFormatConfigurationJsonObject
| ResponseTextFormatConfigurationText
):
"""Convert response_format into Azure text format configuration."""
if isinstance(response_format, type) and issubclass(response_format, BaseModel):
@@ -473,7 +431,7 @@ def create_text_format_config(
# Ensure additionalProperties is explicitly false to satisfy Azure validation
if isinstance(schema, dict):
schema.setdefault("additionalProperties", False)
return TextResponseFormatJsonSchema(
return ResponseTextFormatConfigurationJsonSchema(
name=response_format.__name__,
schema=schema,
strict=True,
@@ -494,11 +452,11 @@ def create_text_format_config(
config_kwargs["strict"] = format_config["strict"]
if "description" in format_config:
config_kwargs["description"] = format_config["description"]
return TextResponseFormatJsonSchema(**config_kwargs)
return ResponseTextFormatConfigurationJsonSchema(**config_kwargs)
if format_type == "json_object":
return TextResponseFormatConfigurationResponseFormatJsonObject()
return ResponseTextFormatConfigurationJsonObject()
if format_type == "text":
return TextResponseFormatConfigurationResponseFormatText()
return ResponseTextFormatConfigurationText()
raise IntegrationInvalidRequestException("response_format must be a Pydantic model or mapping.")
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Azure AI Foundry integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0rc3"
version = "1.0.0rc2"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"azure-ai-agents == 1.2.0b5",
"azure-ai-inference>=1.0.0b9",
"aiohttp",
@@ -855,110 +855,6 @@ async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_file_search_with_
assert run_options["tool_resources"] == {"file_search": {"vector_store_ids": ["vs-123"]}}
async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_code_interpreter_with_file_ids(
mock_agents_client: MagicMock,
) -> None:
"""Test _prepare_tools_for_azure_ai with CodeInterpreterTool with file_ids from get_code_interpreter_tool()."""
client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
code_interpreter_tool = client.get_code_interpreter_tool(file_ids=["file-123", "file-456"])
run_options: dict[str, Any] = {}
result = await client._prepare_tools_for_azure_ai([code_interpreter_tool], run_options) # type: ignore
assert len(result) == 1
assert result[0] == {"type": "code_interpreter"}
assert "tool_resources" in run_options
assert "code_interpreter" in run_options["tool_resources"]
assert sorted(run_options["tool_resources"]["code_interpreter"]["file_ids"]) == ["file-123", "file-456"]
async def test_azure_ai_chat_client_get_code_interpreter_tool_basic() -> None:
"""Test get_code_interpreter_tool returns CodeInterpreterTool without files."""
from azure.ai.agents.models import CodeInterpreterTool
tool = AzureAIAgentClient.get_code_interpreter_tool()
assert isinstance(tool, CodeInterpreterTool)
assert len(tool.file_ids) == 0
async def test_azure_ai_chat_client_get_code_interpreter_tool_with_file_ids() -> None:
"""Test get_code_interpreter_tool forwards file_ids to the SDK."""
from azure.ai.agents.models import CodeInterpreterTool
tool = AzureAIAgentClient.get_code_interpreter_tool(file_ids=["file-abc", "file-def"])
assert isinstance(tool, CodeInterpreterTool)
assert "file-abc" in tool.file_ids
assert "file-def" in tool.file_ids
async def test_azure_ai_chat_client_get_code_interpreter_tool_with_data_sources() -> None:
"""Test get_code_interpreter_tool forwards data_sources to the SDK."""
from azure.ai.agents.models import CodeInterpreterTool, VectorStoreDataSource
ds = VectorStoreDataSource(asset_identifier="test-asset-id", asset_type="id_asset")
tool = AzureAIAgentClient.get_code_interpreter_tool(data_sources=[ds])
assert isinstance(tool, CodeInterpreterTool)
assert "test-asset-id" in tool.data_sources
async def test_azure_ai_chat_client_get_code_interpreter_tool_mutually_exclusive() -> None:
"""Test get_code_interpreter_tool raises ValueError when both file_ids and data_sources are provided."""
from azure.ai.agents.models import VectorStoreDataSource
ds = VectorStoreDataSource(asset_identifier="test-asset-id", asset_type="id_asset")
with pytest.raises(ValueError, match="mutually exclusive"):
AzureAIAgentClient.get_code_interpreter_tool(file_ids=["file-abc"], data_sources=[ds])
async def test_azure_ai_chat_client_get_code_interpreter_tool_with_content() -> None:
"""Test get_code_interpreter_tool accepts Content.from_hosted_file in file_ids."""
from agent_framework import Content
from azure.ai.agents.models import CodeInterpreterTool
content = Content.from_hosted_file("file-content-123")
tool = AzureAIAgentClient.get_code_interpreter_tool(file_ids=[content])
assert isinstance(tool, CodeInterpreterTool)
assert "file-content-123" in tool.file_ids
async def test_azure_ai_chat_client_get_code_interpreter_tool_with_mixed_file_ids() -> None:
"""Test get_code_interpreter_tool accepts a mix of strings and Content objects."""
from agent_framework import Content
from azure.ai.agents.models import CodeInterpreterTool
content = Content.from_hosted_file("file-from-content")
tool = AzureAIAgentClient.get_code_interpreter_tool(file_ids=["file-plain", content])
assert isinstance(tool, CodeInterpreterTool)
assert "file-plain" in tool.file_ids
assert "file-from-content" in tool.file_ids
async def test_azure_ai_chat_client_get_code_interpreter_tool_content_unsupported_type() -> None:
"""Test get_code_interpreter_tool raises ValueError for unsupported Content types."""
from agent_framework import Content
content = Content.from_hosted_vector_store("vs-123")
with pytest.raises(ValueError, match="Unsupported Content type"):
AzureAIAgentClient.get_code_interpreter_tool(file_ids=[content])
async def test_azure_ai_chat_client_get_code_interpreter_tool_content_missing_file_id() -> None:
"""Test get_code_interpreter_tool raises ValueError when Content.file_id is None."""
from agent_framework import Content
content = Content(type="hosted_file")
with pytest.raises(ValueError, match="missing a file_id"):
AzureAIAgentClient.get_code_interpreter_tool(file_ids=[content])
async def test_azure_ai_chat_client_get_code_interpreter_tool_empty_string_file_id() -> None:
"""Test get_code_interpreter_tool raises ValueError for empty string file_ids."""
with pytest.raises(ValueError, match="must not contain empty strings"):
AzureAIAgentClient.get_code_interpreter_tool(file_ids=[""])
async def test_azure_ai_chat_client_create_agent_stream_submit_tool_approvals(
mock_agents_client: MagicMock,
) -> None:
@@ -28,12 +28,12 @@ from agent_framework.openai._responses_client import RawOpenAIResponsesClient
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import (
ApproximateLocation,
CodeInterpreterContainerAuto,
CodeInterpreterTool,
CodeInterpreterToolAuto,
FileSearchTool,
ImageGenTool,
MCPTool,
TextResponseFormatJsonSchema,
ResponseTextFormatConfigurationJsonSchema,
WebSearchPreviewTool,
)
from azure.core.exceptions import ResourceNotFoundError
@@ -427,7 +427,7 @@ async def test_prepare_options_basic(mock_project_client: MagicMock) -> None:
run_options = await client._prepare_options(messages, {})
assert "extra_body" in run_options
assert run_options["extra_body"]["agent_reference"]["name"] == "test-agent"
assert run_options["extra_body"]["agent"]["name"] == "test-agent"
@pytest.mark.parametrize(
@@ -465,7 +465,7 @@ async def test_prepare_options_with_application_endpoint(
if expects_agent:
assert "extra_body" in run_options
assert run_options["extra_body"]["agent_reference"]["name"] == "test-agent"
assert run_options["extra_body"]["agent"]["name"] == "test-agent"
else:
assert "extra_body" not in run_options
@@ -507,7 +507,7 @@ async def test_prepare_options_with_application_project_client(
if expects_agent:
assert "extra_body" in run_options
assert run_options["extra_body"]["agent_reference"]["name"] == "test-agent"
assert run_options["extra_body"]["agent"]["name"] == "test-agent"
else:
assert "extra_body" not in run_options
@@ -979,10 +979,10 @@ async def test_agent_creation_with_response_format(
assert hasattr(created_definition, "text")
assert created_definition.text is not None
# Check that the format is a TextResponseFormatJsonSchema
# Check that the format is a ResponseTextFormatConfigurationJsonSchema
assert hasattr(created_definition.text, "format")
format_config = created_definition.text.format
assert isinstance(format_config, TextResponseFormatJsonSchema)
assert isinstance(format_config, ResponseTextFormatConfigurationJsonSchema)
# Check the schema name matches the model class name
assert format_config.name == "ResponseFormatModel"
@@ -1040,7 +1040,7 @@ async def test_agent_creation_with_mapping_response_format(
assert hasattr(created_definition, "text")
assert created_definition.text is not None
format_config = created_definition.text.format
assert isinstance(format_config, TextResponseFormatJsonSchema)
assert isinstance(format_config, ResponseTextFormatConfigurationJsonSchema)
assert format_config.name == runtime_schema["title"]
assert format_config.schema == runtime_schema
assert format_config.strict is True
@@ -1110,7 +1110,7 @@ async def test_prepare_options_excludes_response_format(
assert "text_format" not in run_options
# But extra_body should contain agent reference
assert "extra_body" in run_options
assert run_options["extra_body"]["agent_reference"]["name"] == "test-agent"
assert run_options["extra_body"]["agent"]["name"] == "test-agent"
async def test_prepare_options_keeps_values_for_unsupported_option_keys(
@@ -1254,7 +1254,7 @@ def test_from_azure_ai_tools_mcp() -> None:
def test_from_azure_ai_tools_code_interpreter() -> None:
"""Test from_azure_ai_tools with Code Interpreter tool."""
ci_tool = CodeInterpreterTool(container=CodeInterpreterContainerAuto(file_ids=["file-1"]))
ci_tool = CodeInterpreterTool(container=CodeInterpreterToolAuto(file_ids=["file-1"]))
parsed_tools = from_azure_ai_tools([ci_tool])
assert len(parsed_tools) == 1
assert parsed_tools[0]["type"] == "code_interpreter"
@@ -1685,35 +1685,6 @@ def test_get_code_interpreter_tool_with_file_ids() -> None:
assert tool["container"]["file_ids"] == ["file-123", "file-456"]
def test_get_code_interpreter_tool_with_content() -> None:
"""Test get_code_interpreter_tool accepts Content.from_hosted_file in file_ids."""
from agent_framework import Content
content = Content.from_hosted_file("file-content-123")
tool = AzureAIClient.get_code_interpreter_tool(file_ids=[content])
assert isinstance(tool, CodeInterpreterTool)
assert tool["container"]["file_ids"] == ["file-content-123"]
def test_get_code_interpreter_tool_with_mixed_file_ids() -> None:
"""Test get_code_interpreter_tool accepts a mix of strings and Content objects."""
from agent_framework import Content
content = Content.from_hosted_file("file-from-content")
tool = AzureAIClient.get_code_interpreter_tool(file_ids=["file-plain", content])
assert isinstance(tool, CodeInterpreterTool)
assert sorted(tool["container"]["file_ids"]) == ["file-from-content", "file-plain"]
def test_get_code_interpreter_tool_content_unsupported_type() -> None:
"""Test get_code_interpreter_tool raises ValueError for unsupported Content types."""
from agent_framework import Content
content = Content.from_hosted_vector_store("vs-123")
with pytest.raises(ValueError, match="Unsupported Content type"):
AzureAIClient.get_code_interpreter_tool(file_ids=[content])
def test_get_file_search_tool_basic() -> None:
"""Test get_file_search_tool returns FileSearchTool."""
tool = AzureAIClient.get_file_search_tool(vector_store_ids=["vs-123"])
@@ -2174,103 +2145,4 @@ def test_build_url_citation_content_with_dict(mock_project_client: MagicMock) ->
assert "get_url" not in ann.get("additional_properties", {})
# region OAuth Consent
def test_parse_chunk_with_oauth_consent_request(mock_project_client: MagicMock) -> None:
"""Test that a streaming oauth_consent_request output item is parsed into oauth_consent_request content.
This reproduces the bug from issue #3950 where the event was logged as "Unparsed event"
and silently discarded, causing the agent run to complete with zero content.
"""
client = AzureAIClient(project_client=mock_project_client, agent_name="test")
chat_options: dict[str, Any] = {}
function_call_ids: dict[int, tuple[str, str]] = {}
mock_item = MagicMock()
mock_item.type = "oauth_consent_request"
mock_item.consent_link = "https://login.microsoftonline.com/common/oauth2/authorize?client_id=abc123"
mock_event = MagicMock()
mock_event.type = "response.output_item.added"
mock_event.item = mock_item
mock_event.output_index = 0
update = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids)
assert len(update.contents) == 1
consent_content = update.contents[0]
assert consent_content.type == "oauth_consent_request"
assert consent_content.consent_link == "https://login.microsoftonline.com/common/oauth2/authorize?client_id=abc123"
assert consent_content.user_input_request is True
def test_parse_response_with_oauth_consent_output_item(mock_project_client: MagicMock) -> None:
"""Test that a non-streaming oauth_consent_request output item is parsed correctly."""
client = AzureAIClient(project_client=mock_project_client, agent_name="test")
mock_item = MagicMock()
mock_item.type = "oauth_consent_request"
mock_item.consent_link = "https://login.microsoftonline.com/consent?code=abc"
mock_response = MagicMock()
mock_response.output = [mock_item]
mock_response.output_parsed = None
mock_response.metadata = {}
mock_response.id = "resp-oauth-1"
mock_response.model = "test-model"
mock_response.created_at = 1000000000
mock_response.usage = None
mock_response.status = "completed"
response = client._parse_response_from_openai(mock_response, {})
assert len(response.messages) > 0
consent_contents = [c for c in response.messages[0].contents if c.type == "oauth_consent_request"]
assert len(consent_contents) == 1
assert consent_contents[0].consent_link == "https://login.microsoftonline.com/consent?code=abc"
def test_parse_chunk_oauth_consent_no_link(mock_project_client: MagicMock) -> None:
"""Test that a streaming oauth_consent_request with no consent_link produces empty contents."""
client = AzureAIClient(project_client=mock_project_client, agent_name="test")
mock_item = MagicMock()
mock_item.type = "oauth_consent_request"
mock_item.consent_link = ""
mock_event = MagicMock()
mock_event.type = "response.output_item.added"
mock_event.item = mock_item
mock_event.output_index = 0
update = client._parse_chunk_from_openai(mock_event, {}, {})
assert not any(c.type == "oauth_consent_request" for c in update.contents)
def test_parse_response_oauth_consent_no_link(mock_project_client: MagicMock) -> None:
"""Test that a non-streaming oauth_consent_request with no consent_link appends no content."""
client = AzureAIClient(project_client=mock_project_client, agent_name="test")
mock_item = MagicMock()
mock_item.type = "oauth_consent_request"
mock_item.consent_link = None
mock_response = MagicMock()
mock_response.output = [mock_item]
mock_response.output_parsed = None
mock_response.metadata = {}
mock_response.id = "resp-oauth-2"
mock_response.model = "test-model"
mock_response.created_at = 1000000000
mock_response.usage = None
mock_response.status = "completed"
response = client._parse_response_from_openai(mock_response, {})
consent_contents = [c for c in response.messages[0].contents if c.type == "oauth_consent_request"]
assert len(consent_contents) == 0
# endregion
@@ -17,10 +17,9 @@ from agent_framework_azure_ai._foundry_memory_provider import FoundryMemoryProvi
def mock_project_client() -> AsyncMock:
"""Create a mock AIProjectClient."""
mock_client = AsyncMock()
mock_client.beta = AsyncMock()
mock_client.beta.memory_stores = AsyncMock()
mock_client.beta.memory_stores.search_memories = AsyncMock()
mock_client.beta.memory_stores.begin_update_memories = AsyncMock()
mock_client.memory_stores = AsyncMock()
mock_client.memory_stores.search_memories = AsyncMock()
mock_client.memory_stores.begin_update_memories = AsyncMock()
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock()
return mock_client
@@ -147,7 +146,7 @@ class TestBeforeRun:
mem2.memory_item.content = "User is based in Seattle"
mock_search_result = Mock()
mock_search_result.memories = [mem1, mem2]
mock_project_client.beta.memory_stores.search_memories.return_value = mock_search_result
mock_project_client.memory_stores.search_memories.return_value = mock_search_result
provider = FoundryMemoryProvider(
project_client=mock_project_client,
@@ -162,7 +161,7 @@ class TestBeforeRun:
)
# Should call search_memories twice: once for static, once for contextual
assert mock_project_client.beta.memory_stores.search_memories.call_count == 2
assert mock_project_client.memory_stores.search_memories.call_count == 2
# Static memories should be cached
assert len(session.state[provider.source_id]["static_memories"]) == 2
assert session.state[provider.source_id]["initialized"] is True
@@ -182,7 +181,7 @@ class TestBeforeRun:
contextual_result.memories = [contextual_mem]
contextual_result.search_id = "search-123"
mock_project_client.beta.memory_stores.search_memories.side_effect = [static_result, contextual_result]
mock_project_client.memory_stores.search_memories.side_effect = [static_result, contextual_result]
provider = FoundryMemoryProvider(
project_client=mock_project_client,
@@ -209,7 +208,7 @@ class TestBeforeRun:
"""Empty input messages → only static search performed, no contextual search."""
static_result = Mock()
static_result.memories = []
mock_project_client.beta.memory_stores.search_memories.return_value = static_result
mock_project_client.memory_stores.search_memories.return_value = static_result
provider = FoundryMemoryProvider(
project_client=mock_project_client,
@@ -224,14 +223,14 @@ class TestBeforeRun:
)
# Should only call search_memories once for static memories
assert mock_project_client.beta.memory_stores.search_memories.call_count == 1
assert mock_project_client.memory_stores.search_memories.call_count == 1
assert provider.source_id not in ctx.context_messages
async def test_empty_search_results_no_messages(self, mock_project_client: AsyncMock) -> None:
"""Empty search results → no messages added."""
mock_search_result = Mock()
mock_search_result.memories = []
mock_project_client.beta.memory_stores.search_memories.return_value = mock_search_result
mock_project_client.memory_stores.search_memories.return_value = mock_search_result
provider = FoundryMemoryProvider(
project_client=mock_project_client,
@@ -256,7 +255,7 @@ class TestBeforeRun:
contextual_result = Mock()
contextual_result.memories = []
mock_project_client.beta.memory_stores.search_memories.side_effect = [static_result, contextual_result]
mock_project_client.memory_stores.search_memories.side_effect = [static_result, contextual_result]
provider = FoundryMemoryProvider(
project_client=mock_project_client,
@@ -270,24 +269,24 @@ class TestBeforeRun:
await provider.before_run( # type: ignore[arg-type]
agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {})
)
assert mock_project_client.beta.memory_stores.search_memories.call_count == 2
assert mock_project_client.memory_stores.search_memories.call_count == 2
# Reset mock for second call
mock_project_client.beta.memory_stores.search_memories.reset_mock()
mock_project_client.memory_stores.search_memories.reset_mock()
contextual_result2 = Mock()
contextual_result2.memories = []
mock_project_client.beta.memory_stores.search_memories.return_value = contextual_result2
mock_project_client.memory_stores.search_memories.return_value = contextual_result2
# Second call - should only search contextual, not static
ctx2 = SessionContext(input_messages=[Message(role="user", text="World")], session_id="s1")
await provider.before_run( # type: ignore[arg-type]
agent=None, session=session, context=ctx2, state=session.state.setdefault(provider.source_id, {})
)
assert mock_project_client.beta.memory_stores.search_memories.call_count == 1
assert mock_project_client.memory_stores.search_memories.call_count == 1
async def test_handles_search_exception_gracefully(self, mock_project_client: AsyncMock) -> None:
"""Search exception is logged but doesn't fail the operation."""
mock_project_client.beta.memory_stores.search_memories.side_effect = Exception("API error")
mock_project_client.memory_stores.search_memories.side_effect = Exception("API error")
provider = FoundryMemoryProvider(
project_client=mock_project_client,
@@ -316,7 +315,7 @@ class TestAfterRun:
"""Stores input+response messages via begin_update_memories."""
mock_poller = Mock()
mock_poller.update_id = "update-456"
mock_project_client.beta.memory_stores.begin_update_memories.return_value = mock_poller
mock_project_client.memory_stores.begin_update_memories.return_value = mock_poller
provider = FoundryMemoryProvider(
project_client=mock_project_client,
@@ -331,8 +330,8 @@ class TestAfterRun:
agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {})
)
mock_project_client.beta.memory_stores.begin_update_memories.assert_awaited_once()
call_kwargs = mock_project_client.beta.memory_stores.begin_update_memories.call_args.kwargs
mock_project_client.memory_stores.begin_update_memories.assert_awaited_once()
call_kwargs = mock_project_client.memory_stores.begin_update_memories.call_args.kwargs
assert call_kwargs["name"] == "test_store"
assert call_kwargs["scope"] == "user_123"
assert len(call_kwargs["items"]) == 2
@@ -343,7 +342,7 @@ class TestAfterRun:
async def test_only_stores_user_assistant_system(self, mock_project_client: AsyncMock) -> None:
"""Only stores user/assistant/system messages with text."""
mock_poller = Mock()
mock_project_client.beta.memory_stores.begin_update_memories.return_value = mock_poller
mock_project_client.memory_stores.begin_update_memories.return_value = mock_poller
provider = FoundryMemoryProvider(
project_client=mock_project_client,
@@ -364,7 +363,7 @@ class TestAfterRun:
agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {})
)
call_kwargs = mock_project_client.beta.memory_stores.begin_update_memories.call_args.kwargs
call_kwargs = mock_project_client.memory_stores.begin_update_memories.call_args.kwargs
items = call_kwargs["items"]
assert len(items) == 2
assert items[0]["content"] == "hello"
@@ -391,12 +390,12 @@ class TestAfterRun:
agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {})
)
mock_project_client.beta.memory_stores.begin_update_memories.assert_not_awaited()
mock_project_client.memory_stores.begin_update_memories.assert_not_awaited()
async def test_uses_configured_update_delay(self, mock_project_client: AsyncMock) -> None:
"""Uses the configured update_delay parameter."""
mock_poller = Mock()
mock_project_client.beta.memory_stores.begin_update_memories.return_value = mock_poller
mock_project_client.memory_stores.begin_update_memories.return_value = mock_poller
provider = FoundryMemoryProvider(
project_client=mock_project_client,
@@ -412,7 +411,7 @@ class TestAfterRun:
agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {})
)
call_kwargs = mock_project_client.beta.memory_stores.begin_update_memories.call_args.kwargs
call_kwargs = mock_project_client.memory_stores.begin_update_memories.call_args.kwargs
assert call_kwargs["update_delay"] == 60
async def test_uses_previous_update_id_for_incremental_updates(self, mock_project_client: AsyncMock) -> None:
@@ -422,7 +421,7 @@ class TestAfterRun:
mock_poller2 = Mock()
mock_poller2.update_id = "update-2"
mock_project_client.beta.memory_stores.begin_update_memories.side_effect = [mock_poller1, mock_poller2]
mock_project_client.memory_stores.begin_update_memories.side_effect = [mock_poller1, mock_poller2]
provider = FoundryMemoryProvider(
project_client=mock_project_client,
@@ -447,13 +446,13 @@ class TestAfterRun:
agent=None, session=session, context=ctx2, state=session.state.setdefault(provider.source_id, {})
)
call_kwargs = mock_project_client.beta.memory_stores.begin_update_memories.call_args.kwargs
call_kwargs = mock_project_client.memory_stores.begin_update_memories.call_args.kwargs
assert call_kwargs["previous_update_id"] == "update-1"
assert session.state[provider.source_id]["previous_update_id"] == "update-2"
async def test_handles_update_exception_gracefully(self, mock_project_client: AsyncMock) -> None:
"""Update exception is logged but doesn't fail the operation."""
mock_project_client.beta.memory_stores.begin_update_memories.side_effect = Exception("API error")
mock_project_client.memory_stores.begin_update_memories.side_effect = Exception("API error")
provider = FoundryMemoryProvider(
project_client=mock_project_client,
@@ -8,6 +8,7 @@ from agent_framework import Agent, FunctionTool
from agent_framework._mcp import MCPTool
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import (
AgentReference,
AgentVersionDetails,
PromptAgentDefinition,
)
@@ -344,7 +345,7 @@ async def test_provider_get_agent_with_reference(mock_project_client: MagicMock)
mock_project_client.agents = AsyncMock()
mock_project_client.agents.get_version.return_value = mock_agent_version
agent_reference = {"name": "test-agent", "version": "1.0"}
agent_reference = AgentReference(name="test-agent", version="1.0")
agent = await provider.get_agent(reference=agent_reference)
assert isinstance(agent, Agent)
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260219"
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.0.0rc3",
"agent-framework-core>=1.0.0rc1",
"azure-cosmos>=4.9.0",
]
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"agent-framework-durabletask",
"azure-functions",
"azure-functions-durable",
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"boto3>=1.35.0,<2.0.0",
"botocore>=1.35.0,<2.0.0",
]
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"openai-chatkit>=1.4.0,<2.0.0",
]
@@ -2,7 +2,7 @@
import importlib.metadata
from ._agent import ClaudeAgent, ClaudeAgentOptions, ClaudeAgentSettings, RawClaudeAgent
from ._agent import ClaudeAgent, ClaudeAgentOptions, ClaudeAgentSettings
try:
__version__ = importlib.metadata.version(__name__)
@@ -13,6 +13,5 @@ __all__ = [
"ClaudeAgent",
"ClaudeAgentOptions",
"ClaudeAgentSettings",
"RawClaudeAgent",
"__version__",
]
@@ -27,7 +27,6 @@ from agent_framework import (
normalize_tools,
)
from agent_framework.exceptions import AgentException
from agent_framework.observability import AgentTelemetryLayer
from claude_agent_sdk import (
AssistantMessage,
ClaudeSDKClient,
@@ -58,10 +57,7 @@ if TYPE_CHECKING:
PermissionMode,
SandboxSettings,
SdkBeta,
SdkPluginConfig,
SettingSource,
)
from claude_agent_sdk.types import ThinkingConfig
logger = logging.getLogger("agent_framework.claude")
@@ -121,6 +117,9 @@ class ClaudeAgentOptions(TypedDict, total=False):
fallback_model: str
"""Fallback model if primary fails."""
max_thinking_tokens: int
"""Maximum tokens for thinking blocks."""
allowed_tools: list[str]
"""Allowlist of tools. If set, Claude can ONLY use tools in this list."""
@@ -163,18 +162,6 @@ class ClaudeAgentOptions(TypedDict, total=False):
betas: list[SdkBeta]
"""Beta features to enable."""
plugins: list[SdkPluginConfig]
"""Plugin configurations for custom commands and capabilities."""
setting_sources: list[SettingSource]
"""Which Claude settings files to load ("user", "project", "local")."""
thinking: ThinkingConfig
"""Extended thinking configuration (adaptive, enabled, or disabled)."""
effort: Literal["low", "medium", "high", "max"]
"""Effort level for thinking depth."""
OptionsT = TypeVar(
"OptionsT",
@@ -184,11 +171,8 @@ OptionsT = TypeVar(
)
class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
"""Claude Agent using Claude Code CLI without telemetry layers.
This is the core Claude agent implementation without OpenTelemetry instrumentation.
For most use cases, prefer :class:`ClaudeAgent` which includes telemetry support.
class ClaudeAgent(BaseAgent, Generic[OptionsT]):
"""Claude Agent using Claude Code CLI.
Wraps the Claude Agent SDK to provide agentic capabilities including
tool use, session management, and streaming responses.
@@ -204,13 +188,45 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
.. code-block:: python
from agent_framework.anthropic import RawClaudeAgent
from agent_framework_claude import ClaudeAgent
async with RawClaudeAgent(
async with ClaudeAgent(
instructions="You are a helpful assistant.",
) as agent:
response = await agent.run("Hello!")
print(response.text)
With streaming:
.. code-block:: python
async with ClaudeAgent() as agent:
async for update in agent.run("Write a poem"):
print(update.text, end="", flush=True)
With session management:
.. code-block:: python
async with ClaudeAgent() as agent:
session = agent.create_session()
await agent.run("Remember my name is Alice", session=session)
response = await agent.run("What's my name?", session=session)
# Claude will remember "Alice" from the same session
With Agent Framework tools:
.. code-block:: python
from agent_framework import tool
@tool
def greet(name: str) -> str:
\"\"\"Greet someone by name.\"\"\"
return f"Hello, {name}!"
async with ClaudeAgent(tools=[greet]) as agent:
response = await agent.run("Greet Alice")
"""
AGENT_PROVIDER_NAME: ClassVar[str] = "anthropic.claude"
@@ -225,16 +241,12 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
description: str | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
middleware: Sequence[AgentMiddlewareTypes] | None = None,
tools: ToolTypes
| Callable[..., Any]
| str
| Sequence[ToolTypes | Callable[..., Any] | str]
| None = None,
tools: ToolTypes | Callable[..., Any] | str | Sequence[ToolTypes | Callable[..., Any] | str] | None = None,
default_options: OptionsT | MutableMapping[str, Any] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize a RawClaudeAgent instance.
"""Initialize a ClaudeAgent instance.
Args:
instructions: System prompt for the agent.
@@ -305,11 +317,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
def _normalize_tools(
self,
tools: ToolTypes
| Callable[..., Any]
| str
| Sequence[ToolTypes | Callable[..., Any] | str]
| None,
tools: ToolTypes | Callable[..., Any] | str | Sequence[ToolTypes | Callable[..., Any] | str] | None,
) -> None:
"""Separate built-in tools (strings) from custom tools.
@@ -335,7 +343,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
normalized = normalize_tools(tool)
self._custom_tools.extend(normalized)
async def __aenter__(self) -> RawClaudeAgent[OptionsT]:
async def __aenter__(self) -> ClaudeAgent[OptionsT]:
"""Start the agent when entering async context."""
await self.start()
return self
@@ -378,9 +386,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
session_id: The session ID to use, or None for a new session.
"""
needs_new_client = (
not self._started
or self._client is None
or (session_id and session_id != self._current_session_id)
not self._started or self._client is None or (session_id and session_id != self._current_session_id)
)
if needs_new_client:
@@ -403,9 +409,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
self._client = None
raise AgentException(f"Failed to start Claude SDK client: {ex}") from ex
def _prepare_client_options(
self, resume_session_id: str | None = None
) -> SDKOptions:
def _prepare_client_options(self, resume_session_id: str | None = None) -> SDKOptions:
"""Prepare SDK options for client initialization.
Args:
@@ -445,9 +449,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
# Prepare custom tools (FunctionTool instances)
custom_tools_server, custom_tool_names = (
self._prepare_tools(self._custom_tools)
if self._custom_tools
else (None, [])
self._prepare_tools(self._custom_tools) if self._custom_tools else (None, [])
)
# MCP servers - merge user-provided servers with custom tools server
@@ -494,13 +496,9 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
if not sdk_tools:
return None, []
return create_sdk_mcp_server(
name=TOOLS_MCP_SERVER_NAME, tools=sdk_tools
), tool_names
return create_sdk_mcp_server(name=TOOLS_MCP_SERVER_NAME, tools=sdk_tools), tool_names
def _function_tool_to_sdk_mcp_tool(
self, func_tool: FunctionTool
) -> SdkMcpTool[Any]:
def _function_tool_to_sdk_mcp_tool(self, func_tool: FunctionTool) -> SdkMcpTool[Any]:
"""Convert a FunctionTool to an SDK MCP tool.
Args:
@@ -523,9 +521,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
return {"content": [{"type": "text", "text": f"Error: {e}"}]}
# Get JSON schema from pydantic model
schema: dict[str, Any] = (
func_tool.input_model.model_json_schema() if func_tool.input_model else {}
)
schema: dict[str, Any] = func_tool.input_model.model_json_schema() if func_tool.input_model else {}
input_schema: dict[str, Any] = {
"type": "object",
"properties": schema.get("properties", {}),
@@ -572,44 +568,6 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
return ""
return "\n".join([msg.text or "" for msg in messages])
@property
def default_options(self) -> dict[str, Any]:
"""Expose options with ``instructions`` key.
Maps ``system_prompt`` to ``instructions`` for compatibility with
:class:`AgentTelemetryLayer`, which reads the system prompt from
the ``instructions`` key.
"""
opts = dict(self._default_options)
system_prompt = opts.pop("system_prompt", None)
if system_prompt is not None:
opts["instructions"] = system_prompt
return opts
def _finalize_response(
self, updates: Sequence[AgentResponseUpdate]
) -> AgentResponse[Any]:
"""Build AgentResponse and propagate structured_output as value.
Args:
updates: The collected stream updates.
Returns:
An AgentResponse with structured_output set as value if present.
"""
structured_output = getattr(self, "_structured_output", None)
return AgentResponse.from_updates(updates, value=structured_output)
@overload
def run(
self,
messages: AgentRunInputs | None = None,
*,
stream: Literal[False] = ...,
session: AgentSession | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(
self,
@@ -617,8 +575,20 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
*,
stream: Literal[True],
session: AgentSession | None = None,
options: OptionsT | MutableMapping[str, Any] | None = None,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
) -> AsyncIterable[AgentResponseUpdate]: ...
@overload
async def run(
self,
messages: AgentRunInputs | None = None,
*,
stream: Literal[False] = ...,
session: AgentSession | None = None,
options: OptionsT | MutableMapping[str, Any] | None = None,
**kwargs: Any,
) -> AgentResponse[Any]: ...
def run(
self,
@@ -626,11 +596,9 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
*,
stream: bool = False,
session: AgentSession | None = None,
options: OptionsT | MutableMapping[str, Any] | None = None,
**kwargs: Any,
) -> (
Awaitable[AgentResponse[Any]]
| ResponseStream[AgentResponseUpdate, AgentResponse[Any]]
):
) -> AsyncIterable[AgentResponseUpdate] | Awaitable[AgentResponse[Any]]:
"""Run the agent with the given messages.
Args:
@@ -641,23 +609,33 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
returns an awaitable AgentResponse.
session: The conversation session. If session has service_session_id set,
the agent will resume that session.
kwargs: Additional keyword arguments including 'options' for runtime options
(model, permission_mode can be changed per-request).
options: Runtime options (model, permission_mode can be changed per-request).
kwargs: Additional keyword arguments.
Returns:
When stream=True: An ResponseStream for streaming updates.
When stream=False: An Awaitable[AgentResponse] with the complete response.
"""
options = kwargs.pop("options", None)
response = ResponseStream(
self._get_stream(messages, session=session, options=options, **kwargs),
finalizer=self._finalize_response,
)
if stream:
return response
return response.get_final_response()
def _finalize_response(self, updates: Sequence[AgentResponseUpdate]) -> AgentResponse[Any]:
"""Build AgentResponse and propagate structured_output as value.
Args:
updates: The collected stream updates.
Returns:
An AgentResponse with structured_output set as value if present.
"""
structured_output = getattr(self, "_structured_output", None)
return AgentResponse.from_updates(updates, value=structured_output)
async def _get_stream(
self,
messages: AgentRunInputs | None = None,
@@ -696,11 +674,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
if text:
yield AgentResponseUpdate(
role="assistant",
contents=[
Content.from_text(
text=text, raw_representation=message
)
],
contents=[Content.from_text(text=text, raw_representation=message)],
raw_representation=message,
)
elif delta_type == "thinking_delta":
@@ -708,11 +682,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
if thinking:
yield AgentResponseUpdate(
role="assistant",
contents=[
Content.from_text_reasoning(
text=thinking, raw_representation=message
)
],
contents=[Content.from_text_reasoning(text=thinking, raw_representation=message)],
raw_representation=message,
)
elif isinstance(message, AssistantMessage):
@@ -729,9 +699,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
"server_error": "Claude API server error",
"unknown": "Unknown error from Claude API",
}
error_msg = error_messages.get(
message.error, f"Claude API error: {message.error}"
)
error_msg = error_messages.get(message.error, f"Claude API error: {message.error}")
# Extract any error details from content blocks
if message.content:
for block in message.content:
@@ -753,25 +721,3 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
# Store structured output for the finalizer
self._structured_output = structured_output
class ClaudeAgent(AgentTelemetryLayer, RawClaudeAgent[OptionsT], Generic[OptionsT]):
"""Claude Agent with OpenTelemetry instrumentation.
This is the recommended agent class for most use cases. It includes
OpenTelemetry-based telemetry for observability. For a minimal
implementation without telemetry, use :class:`RawClaudeAgent`.
Examples:
Basic usage with context manager:
.. code-block:: python
from agent_framework.anthropic import ClaudeAgent
async with ClaudeAgent(
instructions="You are a helpful assistant.",
) as agent:
response = await agent.run("Hello!")
print(response.text)
"""
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"claude-agent-sdk>=0.1.25",
]
@@ -945,191 +945,3 @@ class TestClaudeAgentStructuredOutput:
with pytest.raises(AgentException) as exc_info:
await agent.run("Hello")
assert "Something went wrong" in str(exc_info.value)
# region Test ClaudeAgent Telemetry
class TestClaudeAgentTelemetry:
"""Tests for ClaudeAgent OpenTelemetry instrumentation."""
@staticmethod
async def _create_async_generator(items: list[Any]) -> Any:
"""Helper to create async generator from list."""
for item in items:
yield item
def _create_mock_client(self, messages: list[Any]) -> MagicMock:
"""Create a mock ClaudeSDKClient that yields given messages."""
mock_client = MagicMock()
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client.query = AsyncMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
mock_client.receive_response = MagicMock(return_value=self._create_async_generator(messages))
return mock_client
def _create_standard_messages(self) -> list[Any]:
"""Create a standard set of mock messages for testing."""
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
from claude_agent_sdk.types import StreamEvent
return [
StreamEvent(
event={
"type": "content_block_delta",
"delta": {"type": "text_delta", "text": "Hello!"},
},
uuid="event-1",
session_id="session-123",
),
AssistantMessage(
content=[TextBlock(text="Hello!")],
model="claude-sonnet",
),
ResultMessage(
subtype="success",
duration_ms=100,
duration_api_ms=50,
is_error=False,
num_turns=1,
session_id="session-123",
),
]
async def test_run_emits_span_when_instrumentation_enabled(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that run() creates an OpenTelemetry span when instrumentation is enabled."""
from agent_framework.observability import OBSERVABILITY_SETTINGS
messages = self._create_standard_messages()
mock_client = self._create_mock_client(messages)
monkeypatch.setattr(OBSERVABILITY_SETTINGS, "enable_instrumentation", True)
with (
patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client),
patch("agent_framework.observability._get_span") as mock_get_span,
):
mock_span = MagicMock()
mock_get_span.return_value.__enter__ = MagicMock(return_value=mock_span)
mock_get_span.return_value.__exit__ = MagicMock(return_value=False)
agent = ClaudeAgent(name="test-agent")
response = await agent.run("Hello")
assert response.text == "Hello!"
mock_get_span.assert_called_once()
call_kwargs = mock_get_span.call_args[1]
assert call_kwargs["attributes"]["gen_ai.agent.name"] == "test-agent"
assert call_kwargs["attributes"]["gen_ai.operation.name"] == "invoke_agent"
async def test_run_skips_telemetry_when_instrumentation_disabled(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that run() skips telemetry when instrumentation is disabled."""
from agent_framework.observability import OBSERVABILITY_SETTINGS
messages = self._create_standard_messages()
mock_client = self._create_mock_client(messages)
monkeypatch.setattr(OBSERVABILITY_SETTINGS, "enable_instrumentation", False)
with (
patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client),
patch("agent_framework.observability._get_span") as mock_get_span,
):
agent = ClaudeAgent(name="test-agent")
response = await agent.run("Hello")
assert response.text == "Hello!"
mock_get_span.assert_not_called()
async def test_run_stream_emits_span_when_instrumentation_enabled(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that run(stream=True) creates a span when instrumentation is enabled."""
from agent_framework.observability import OBSERVABILITY_SETTINGS
messages = self._create_standard_messages()
mock_client = self._create_mock_client(messages)
monkeypatch.setattr(OBSERVABILITY_SETTINGS, "enable_instrumentation", True)
with (
patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client),
patch("agent_framework.observability.get_tracer") as mock_get_tracer,
):
mock_span = MagicMock()
mock_tracer = MagicMock()
mock_tracer.start_span.return_value = mock_span
mock_get_tracer.return_value = mock_tracer
agent = ClaudeAgent(name="stream-agent")
updates: list[AgentResponseUpdate] = []
async for update in agent.run("Hello", stream=True):
updates.append(update)
assert len(updates) == 1
mock_tracer.start_span.assert_called_once()
span_name = mock_tracer.start_span.call_args[0][0]
assert "stream-agent" in span_name
assert "invoke_agent" in span_name
async def test_run_captures_exception_in_span(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that exceptions during run() are captured in the telemetry span."""
from agent_framework.exceptions import AgentException
from agent_framework.observability import OBSERVABILITY_SETTINGS
from claude_agent_sdk import ResultMessage
error_messages = [
ResultMessage(
subtype="error",
duration_ms=100,
duration_api_ms=50,
is_error=True,
num_turns=0,
session_id="error-session",
result="Model not found",
),
]
mock_client = self._create_mock_client(error_messages)
monkeypatch.setattr(OBSERVABILITY_SETTINGS, "enable_instrumentation", True)
with (
patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client),
patch("agent_framework.observability._get_span") as mock_get_span,
patch("agent_framework.observability.capture_exception") as mock_capture_exc,
):
mock_span = MagicMock()
mock_get_span.return_value.__enter__ = MagicMock(return_value=mock_span)
mock_get_span.return_value.__exit__ = MagicMock(return_value=False)
agent = ClaudeAgent(name="error-agent")
with pytest.raises(AgentException):
await agent.run("Hello")
mock_capture_exc.assert_called_once()
exc_kwargs = mock_capture_exc.call_args[1]
assert exc_kwargs["span"] is mock_span
assert isinstance(exc_kwargs["exception"], AgentException)
async def test_telemetry_uses_correct_provider_name(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that telemetry uses AGENT_PROVIDER_NAME as provider."""
from agent_framework.observability import OBSERVABILITY_SETTINGS
messages = self._create_standard_messages()
mock_client = self._create_mock_client(messages)
monkeypatch.setattr(OBSERVABILITY_SETTINGS, "enable_instrumentation", True)
with (
patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client),
patch("agent_framework.observability._get_span") as mock_get_span,
):
mock_span = MagicMock()
mock_get_span.return_value.__enter__ = MagicMock(return_value=mock_span)
mock_get_span.return_value.__exit__ = MagicMock(return_value=False)
agent = ClaudeAgent(name="test-agent")
await agent.run("Hello")
call_kwargs = mock_get_span.call_args[1]
assert call_kwargs["attributes"]["gen_ai.provider.name"] == "anthropic.claude"
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"microsoft-agents-copilotstudio-client>=0.3.1",
]
@@ -59,7 +59,7 @@ from ._sessions import (
register_state_type,
)
from ._settings import SecretString, load_settings
from ._skills import Skill, SkillResource, SkillsProvider
from ._skills import FileAgentSkillsProvider
from ._telemetry import (
AGENT_FRAMEWORK_USER_AGENT,
APP_INFO,
@@ -205,9 +205,6 @@ __all__ = [
"AgentResponseUpdate",
"AgentRunInputs",
"AgentSession",
"Skill",
"SkillResource",
"SkillsProvider",
"Annotation",
"BaseAgent",
"BaseChatClient",
@@ -237,6 +234,7 @@ __all__ = [
"Executor",
"FanInEdgeGroup",
"FanOutEdgeGroup",
"FileAgentSkillsProvider",
"FileCheckpointStorage",
"FinalT",
"FinishReason",
@@ -1051,11 +1051,10 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
else:
final_tools.append(tool) # type: ignore
existing_names = {name for t in final_tools if (name := _get_tool_name(t)) is not None}
for mcp_server in self.mcp_tools:
if not mcp_server.is_connected:
await self._async_exit_stack.enter_async_context(mcp_server)
final_tools.extend(f for f in mcp_server.functions if f.name not in existing_names)
final_tools.extend(mcp_server.functions)
# Merge runtime kwargs into additional_function_arguments so they're available
# in function middleware context and tool invocation.
File diff suppressed because it is too large Load Diff
@@ -345,7 +345,6 @@ ContentType = Literal[
"shell_command_output",
"function_approval_request",
"function_approval_response",
"oauth_consent_request",
]
@@ -499,8 +498,6 @@ class Content:
function_call: Content | None = None,
user_input_request: bool | None = None,
approved: bool | None = None,
# OAuth consent fields
consent_link: str | None = None,
# Common fields
annotations: Sequence[Annotation] | None = None,
additional_properties: MutableMapping[str, Any] | None = None,
@@ -549,7 +546,6 @@ class Content:
self.function_call = function_call
self.user_input_request = user_input_request
self.approved = approved
self.consent_link = consent_link
@classmethod
def from_text(
@@ -1126,37 +1122,6 @@ class Content:
raw_representation=raw_representation,
)
@classmethod
def from_oauth_consent_request(
cls: type[ContentT],
consent_link: str,
*,
annotations: Sequence[Annotation] | None = None,
additional_properties: MutableMapping[str, Any] | None = None,
raw_representation: Any = None,
) -> ContentT:
"""Create OAuth consent request content.
Args:
consent_link: The URL the user must visit to complete OAuth consent.
Keyword Args:
annotations: Optional annotations.
additional_properties: Optional additional properties.
raw_representation: Optional raw representation from the provider.
Returns:
A new Content instance with type ``oauth_consent_request``.
"""
return cls(
"oauth_consent_request",
consent_link=consent_link,
user_input_request=True,
annotations=annotations,
additional_properties=additional_properties,
raw_representation=raw_representation,
)
def to_function_approval_response(
self,
approved: bool,
@@ -1211,7 +1176,6 @@ class Content:
"user_input_request",
"approved",
"id",
"consent_link",
"additional_properties",
)
@@ -11,7 +11,6 @@ Supported classes:
- AnthropicChatOptions
- ClaudeAgent
- ClaudeAgentOptions
- RawClaudeAgent
"""
import importlib
@@ -22,7 +21,6 @@ _IMPORTS: dict[str, tuple[str, str]] = {
"AnthropicChatOptions": ("agent_framework_anthropic", "agent-framework-anthropic"),
"ClaudeAgent": ("agent_framework_claude", "agent-framework-claude"),
"ClaudeAgentOptions": ("agent_framework_claude", "agent-framework-claude"),
"RawClaudeAgent": ("agent_framework_claude", "agent-framework-claude"),
}
+3 -3
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0rc3"
version = "1.0.0rc2"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -34,7 +34,8 @@ dependencies = [
# connectors and functions
"openai>=1.99.0",
"azure-identity>=1,<2",
"azure-ai-projects == 2.0.0b4",
# Pinned to 2.0.0b3 - breaking changes in 2.0.0b4, unpin once upgrades complete
"azure-ai-projects == 2.0.0b3",
"mcp[ws]>=1.24.0,<2",
"packaging>=24.1",
]
@@ -104,7 +105,6 @@ extend = "../../pyproject.toml"
[tool.pyright]
extends = "../../pyproject.toml"
include = ["tests/workflow"]
[tool.mypy]
plugins = ['pydantic.mypy']
@@ -755,49 +755,6 @@ async def test_chat_agent_with_local_mcp_tools(client: SupportsChatGetResponse)
pass
async def test_mcp_tools_not_duplicated_when_passed_as_runtime_tools(chat_client_base: Any) -> None:
"""Test that MCP tool functions from self.mcp_tools are not duplicated when already present in runtime tools."""
captured_options: list[dict[str, Any]] = []
original_inner = chat_client_base._inner_get_response
async def capturing_inner(
*, messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any
) -> ChatResponse:
captured_options.append(dict(options))
return await original_inner(messages=messages, options=options, **kwargs)
chat_client_base._inner_get_response = capturing_inner
# Create FunctionTool instances that simulate expanded MCP functions
mcp_func_a = FunctionTool(func=lambda: "a", name="tool_a", description="Tool A")
mcp_func_b = FunctionTool(func=lambda: "b", name="tool_b", description="Tool B")
# Create a mock MCP tool that is already connected (simulates turn 2)
mock_mcp_tool = MagicMock(spec=MCPTool)
mock_mcp_tool.is_connected = True
mock_mcp_tool.functions = [mcp_func_a, mcp_func_b]
mock_mcp_tool.__aenter__ = AsyncMock(return_value=mock_mcp_tool)
mock_mcp_tool.__aexit__ = AsyncMock(return_value=None)
# Agent has the MCP tool in its constructor (stored in self.mcp_tools)
agent = Agent(client=chat_client_base, name="TestAgent", tools=[mock_mcp_tool])
# Simulate AG-UI turn 2: pass already-expanded MCP functions + a client tool as runtime tools
client_tool = FunctionTool(func=lambda: "client", name="client_tool", description="Client tool")
runtime_tools = [mcp_func_a, mcp_func_b, client_tool]
await agent.run("hello", tools=runtime_tools)
# Verify the chat client received each tool exactly once
assert len(captured_options) >= 1
tool_names = [t.name for t in captured_options[0]["tools"]]
assert tool_names.count("tool_a") == 1, f"tool_a duplicated: {tool_names}"
assert tool_names.count("tool_b") == 1, f"tool_b duplicated: {tool_names}"
assert "client_tool" in tool_names
assert len(tool_names) == 3
async def test_agent_tool_receives_session_in_kwargs(chat_client_base: Any) -> None:
"""Verify tool execution receives 'session' inside **kwargs when function is called by client."""
File diff suppressed because it is too large Load Diff
@@ -3424,30 +3424,3 @@ class TestResponseStreamEdgeCases:
# endregion
# region OAuth Consent Content
def test_oauth_consent_request_creation():
"""Test Content.from_oauth_consent_request creates the correct content."""
content = Content.from_oauth_consent_request(
consent_link="https://login.microsoftonline.com/common/oauth2/authorize?client_id=abc",
)
assert content.type == "oauth_consent_request"
assert content.consent_link == "https://login.microsoftonline.com/common/oauth2/authorize?client_id=abc"
assert content.user_input_request is True
def test_oauth_consent_request_serialization_roundtrip():
"""Test that oauth_consent_request content serializes and includes consent_link."""
content = Content.from_oauth_consent_request(
consent_link="https://login.microsoftonline.com/consent",
)
d = content.to_dict()
assert d["type"] == "oauth_consent_request"
assert d["consent_link"] == "https://login.microsoftonline.com/consent"
assert d["user_input_request"] is True
# endregion
@@ -2,20 +2,19 @@
import logging
from collections.abc import AsyncIterable, Awaitable
from typing import TYPE_CHECKING, Any, Literal, overload
from typing import TYPE_CHECKING, Any
import pytest
from agent_framework import (
AgentExecutor,
AgentResponse,
AgentResponseUpdate,
AgentRunInputs,
AgentSession,
BaseAgent,
Content,
Message,
ResponseStream,
WorkflowEvent,
WorkflowRunState,
)
from agent_framework._workflows._agent_executor import AgentExecutorResponse
@@ -33,56 +32,26 @@ class _CountingAgent(BaseAgent):
super().__init__(**kwargs)
self.call_count = 0
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[False] = ...,
session: AgentSession | None = ...,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[True],
session: AgentSession | None = ...,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
messages: str | Message | list[str] | list[Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> (
Awaitable[AgentResponse[Any]]
| ResponseStream[AgentResponseUpdate, AgentResponse[Any]]
):
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
self.call_count += 1
if stream:
async def _stream() -> AsyncIterable[AgentResponseUpdate]:
yield AgentResponseUpdate(
contents=[
Content.from_text(
text=f"Response #{self.call_count}: {self.name}"
)
]
contents=[Content.from_text(text=f"Response #{self.call_count}: {self.name}")]
)
return ResponseStream(_stream(), finalizer=AgentResponse.from_updates)
async def _run() -> AgentResponse:
return AgentResponse(
messages=[
Message("assistant", [f"Response #{self.call_count}: {self.name}"])
]
)
return AgentResponse(messages=[Message("assistant", [f"Response #{self.call_count}: {self.name}"])])
return _run()
@@ -94,36 +63,13 @@ class _StreamingHookAgent(BaseAgent):
super().__init__(**kwargs)
self.result_hook_called = False
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[False] = ...,
session: AgentSession | None = ...,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[True],
session: AgentSession | None = ...,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
messages: str | Message | list[str] | list[Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> (
Awaitable[AgentResponse[Any]]
| ResponseStream[AgentResponseUpdate, AgentResponse[Any]]
):
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
if stream:
async def _stream() -> AsyncIterable[AgentResponseUpdate]:
@@ -132,15 +78,13 @@ class _StreamingHookAgent(BaseAgent):
role="assistant",
)
async def _mark_result_hook_called(
response: AgentResponse,
) -> AgentResponse:
async def _mark_result_hook_called(response: AgentResponse) -> AgentResponse:
self.result_hook_called = True
return response
return ResponseStream(
_stream(), finalizer=AgentResponse.from_updates
).with_result_hook(_mark_result_hook_called)
return ResponseStream(_stream(), finalizer=AgentResponse.from_updates).with_result_hook(
_mark_result_hook_called
)
async def _run() -> AgentResponse:
return AgentResponse(messages=[Message("assistant", ["hook test"])])
@@ -148,9 +92,7 @@ class _StreamingHookAgent(BaseAgent):
return _run()
async def test_agent_executor_streaming_finalizes_stream_and_runs_result_hooks() -> (
None
):
async def test_agent_executor_streaming_finalizes_stream_and_runs_result_hooks() -> None:
"""AgentExecutor should call get_final_response() so stream result hooks execute."""
agent = _StreamingHookAgent(id="hook_agent", name="HookAgent")
executor = AgentExecutor(agent, id="hook_exec")
@@ -217,9 +159,7 @@ async def test_agent_executor_checkpoint_stores_and_restores_state() -> None:
executor_state = executor_states[executor.id] # type: ignore[index]
assert "cache" in executor_state, "Checkpoint should store executor cache state"
assert "agent_session" in executor_state, (
"Checkpoint should store executor session state"
)
assert "agent_session" in executor_state, "Checkpoint should store executor session state"
# Verify session state structure
session_state = executor_state["agent_session"] # type: ignore[index]
@@ -240,15 +180,11 @@ async def test_agent_executor_checkpoint_stores_and_restores_state() -> None:
assert restored_agent.call_count == 0
# Build new workflow with the restored executor
wf_resume = SequentialBuilder(
participants=[restored_executor], checkpoint_storage=storage
).build()
wf_resume = SequentialBuilder(participants=[restored_executor], checkpoint_storage=storage).build()
# Resume from checkpoint
resumed_output: AgentExecutorResponse | None = None
async for ev in wf_resume.run(
checkpoint_id=restore_checkpoint.checkpoint_id, stream=True
):
async for ev in wf_resume.run(checkpoint_id=restore_checkpoint.checkpoint_id, stream=True):
if ev.type == "output":
resumed_output = ev.data # type: ignore[assignment]
if ev.type == "status" and ev.state in (
@@ -342,7 +278,7 @@ async def test_agent_executor_run_streaming_with_stream_kwarg_does_not_raise() -
workflow = SequentialBuilder(participants=[executor]).build()
# stream=True at workflow level triggers streaming mode (returns async iterable)
events: list[WorkflowEvent] = []
events = []
async for event in workflow.run("hello", stream=True):
events.append(event)
assert len(events) > 0
@@ -352,13 +288,10 @@ async def test_agent_executor_run_streaming_with_stream_kwarg_does_not_raise() -
@pytest.mark.parametrize("reserved_kwarg", ["session", "stream", "messages"])
async def test_prepare_agent_run_args_strips_reserved_kwargs(reserved_kwarg: str, caplog: "LogCaptureFixture") -> None:
"""_prepare_agent_run_args must remove reserved kwargs and log a warning."""
raw: dict[str, Any] = {
reserved_kwarg: "should-be-stripped",
"custom_key": "keep-me",
}
raw = {reserved_kwarg: "should-be-stripped", "custom_key": "keep-me"}
with caplog.at_level(logging.WARNING):
run_kwargs, options = AgentExecutor._prepare_agent_run_args(raw) # pyright: ignore[reportPrivateUsage]
run_kwargs, options = AgentExecutor._prepare_agent_run_args(raw)
assert reserved_kwarg not in run_kwargs
assert "custom_key" in run_kwargs
@@ -369,8 +302,8 @@ async def test_prepare_agent_run_args_strips_reserved_kwargs(reserved_kwarg: str
async def test_prepare_agent_run_args_preserves_non_reserved_kwargs() -> None:
"""Non-reserved workflow kwargs should pass through unchanged."""
raw: dict[str, Any] = {"custom_param": "value", "another": 42}
run_kwargs, _options = AgentExecutor._prepare_agent_run_args(raw) # pyright: ignore[reportPrivateUsage]
raw = {"custom_param": "value", "another": 42}
run_kwargs, options = AgentExecutor._prepare_agent_run_args(raw)
assert run_kwargs["custom_param"] == "value"
assert run_kwargs["another"] == 42
@@ -379,10 +312,10 @@ async def test_prepare_agent_run_args_strips_all_reserved_kwargs_at_once(
caplog: "LogCaptureFixture",
) -> None:
"""All reserved kwargs should be stripped when supplied together, each emitting a warning."""
raw: dict[str, Any] = {"session": "x", "stream": True, "messages": [], "custom": 1}
raw = {"session": "x", "stream": True, "messages": [], "custom": 1}
with caplog.at_level(logging.WARNING):
run_kwargs, options = AgentExecutor._prepare_agent_run_args(raw) # pyright: ignore[reportPrivateUsage]
run_kwargs, options = AgentExecutor._prepare_agent_run_args(raw)
assert "session" not in run_kwargs
assert "stream" not in run_kwargs
@@ -391,11 +324,7 @@ async def test_prepare_agent_run_args_strips_all_reserved_kwargs_at_once(
assert options is not None
assert options["additional_function_arguments"]["custom"] == 1
warned_keys = {
r.message.split("'")[1]
for r in caplog.records
if "reserved" in r.message.lower()
}
warned_keys = {r.message.split("'")[1] for r in caplog.records if "reserved" in r.message.lower()}
assert warned_keys == {"session", "stream", "messages"}
@@ -3,7 +3,7 @@
"""Tests for AgentExecutor handling of tool calls and results in streaming mode."""
from collections.abc import AsyncIterable, Awaitable, Mapping, Sequence
from typing import Any, Literal, overload
from typing import Any
from typing_extensions import Never
@@ -13,7 +13,6 @@ from agent_framework import (
AgentExecutorResponse,
AgentResponse,
AgentResponseUpdate,
AgentRunInputs,
AgentSession,
BaseAgent,
ChatResponse,
@@ -38,38 +37,18 @@ class _ToolCallingAgent(BaseAgent):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[False] = ...,
session: AgentSession | None = ...,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[True],
session: AgentSession | None = ...,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
if stream:
return ResponseStream(self._run_stream_impl(), finalizer=AgentResponse.from_updates)
async def _run() -> AgentResponse[Any]:
async def _run() -> AgentResponse:
return AgentResponse(messages=[Message("assistant", ["done"])])
return _run()
@@ -132,7 +111,6 @@ async def test_agent_executor_emits_tool_calls_in_streaming_mode() -> None:
# First event: text update
assert events[0].data is not None
assert events[0].data.contents[0].type == "text"
assert events[0].data.contents[0].text is not None
assert "Let me search" in events[0].data.contents[0].text
# Second event: function call
@@ -151,7 +129,6 @@ async def test_agent_executor_emits_tool_calls_in_streaming_mode() -> None:
# Fourth event: final text
assert events[3].data is not None
assert events[3].data.contents[0].type == "text"
assert events[3].data.contents[0].text is not None
assert "sunny" in events[3].data.contents[0].text
@@ -1,9 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import Awaitable
from typing import Any, Literal, overload
from collections.abc import AsyncIterable
from typing import Any
from agent_framework import AgentResponse, AgentResponseUpdate, AgentRunInputs, AgentSession, ResponseStream
from agent_framework import AgentResponse, AgentResponseUpdate, AgentSession, Message
from agent_framework._workflows._agent_utils import resolve_agent_id
@@ -11,23 +11,40 @@ class MockAgent:
"""Mock agent for testing agent utilities."""
def __init__(self, agent_id: str, name: str | None = None) -> None:
self.id: str = agent_id
self.name: str | None = name
self.description: str | None = None
self._id = agent_id
self._name = name
@overload
def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(self, messages: AgentRunInputs | None = None, *, stream: bool = False, session: AgentSession | None = None, **kwargs: Any) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
@property
def id(self) -> str:
return self._id
@property
def name(self) -> str | None:
return self._name
@property
def display_name(self) -> str:
"""Returns the display name of the agent."""
...
@property
def description(self) -> str | None:
"""Returns the description of the agent."""
...
def run(
self,
messages: str | Message | list[str] | list[Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> AgentResponse | AsyncIterable[AgentResponseUpdate]: ...
def create_session(self, **kwargs: Any) -> AgentSession:
"""Creates a new conversation session for the agent."""
...
def get_session(self, *, service_session_id: str, **kwargs: Any) -> AgentSession:
return AgentSession()
def test_resolve_agent_id_with_name() -> None:
"""Test that resolve_agent_id returns name when agent has a name."""
@@ -5,7 +5,6 @@ import tempfile
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
import pytest
@@ -25,7 +24,7 @@ class _TestToolApprovalRequest:
"""Request data for tool approval in tests."""
tool_name: str
arguments: dict[str, Any]
arguments: dict
timestamp: datetime
@@ -42,7 +41,7 @@ class _TestApprovalRequest:
"""Approval request data for tests."""
action: str
params: tuple[Any, ...]
params: tuple
@dataclass
@@ -79,8 +78,8 @@ def test_workflow_checkpoint_custom_values():
workflow_name="test-workflow-456",
graph_signature_hash="test-hash-456",
timestamp=custom_timestamp,
messages={"executor1": [{"data": "test"}]}, # type: ignore[arg-type] # raw dict for serialization test
pending_request_info_events={"req123": {"data": "test"}}, # type: ignore[arg-type] # raw dict for serialization test
messages={"executor1": [{"data": "test"}]},
pending_request_info_events={"req123": {"data": "test"}},
state={"key": "value"},
iteration_count=5,
metadata={"test": True},
@@ -104,7 +103,7 @@ def test_workflow_checkpoint_to_dict():
checkpoint_id="test-id",
workflow_name="test-workflow",
graph_signature_hash="test-hash",
messages={"executor1": [{"data": "test"}]}, # type: ignore[arg-type] # raw dict for serialization test
messages={"executor1": [{"data": "test"}]},
state={"key": "value"},
iteration_count=5,
)
@@ -162,8 +161,8 @@ async def test_memory_checkpoint_storage_save_and_load():
checkpoint = WorkflowCheckpoint(
workflow_name="test-workflow",
graph_signature_hash="test-hash",
messages={"executor1": [{"data": "hello"}]}, # type: ignore[arg-type] # raw dict for serialization test
pending_request_info_events={"req123": {"data": "test"}}, # type: ignore[arg-type] # raw dict for serialization test
messages={"executor1": [{"data": "hello"}]},
pending_request_info_events={"req123": {"data": "test"}},
)
# Save checkpoint
@@ -777,9 +776,9 @@ async def test_file_checkpoint_storage_save_and_load():
checkpoint = WorkflowCheckpoint(
workflow_name="test-workflow",
graph_signature_hash="test-hash",
messages={"executor1": [{"data": "hello", "source_id": "test", "target_id": None}]}, # type: ignore[arg-type] # raw dict for serialization test
messages={"executor1": [{"data": "hello", "source_id": "test", "target_id": None}]},
state={"key": "value"},
pending_request_info_events={"req123": {"data": "test"}}, # type: ignore[arg-type] # raw dict for serialization test
pending_request_info_events={"req123": {"data": "test"}},
)
# Save checkpoint
@@ -905,9 +904,9 @@ async def test_file_checkpoint_storage_json_serialization():
checkpoint = WorkflowCheckpoint(
workflow_name="test-workflow",
graph_signature_hash="test-hash",
messages={"executor1": [{"data": {"nested": {"value": 42}}, "source_id": "test", "target_id": None}]}, # type: ignore[arg-type] # raw dict for serialization test
messages={"executor1": [{"data": {"nested": {"value": 42}}, "source_id": "test", "target_id": None}]},
state={"list": [1, 2, 3], "dict": {"a": "b", "c": {"d": "e"}}, "bool": True, "null": None},
pending_request_info_events={"req123": {"data": "test"}}, # type: ignore[arg-type] # raw dict for serialization test
pending_request_info_events={"req123": {"data": "test"}},
)
# Save and load
@@ -3,11 +3,11 @@
import json
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, cast
from typing import Any
from agent_framework._workflows._checkpoint_encoding import (
_PICKLE_MARKER, # pyright: ignore[reportPrivateUsage]
_TYPE_MARKER, # pyright: ignore[reportPrivateUsage]
_PICKLE_MARKER,
_TYPE_MARKER,
encode_checkpoint_value,
)
@@ -185,9 +185,8 @@ def test_encode_list_of_dataclasses() -> None:
result = encode_checkpoint_value(data)
assert isinstance(result, list)
result_list = cast(list[Any], result)
assert len(result_list) == 2
for item in result_list:
assert len(result) == 2
for item in result:
assert _PICKLE_MARKER in item
@@ -4,8 +4,6 @@ from dataclasses import dataclass
from typing import Any
from unittest.mock import patch
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
import pytest
from agent_framework import (
@@ -277,7 +275,6 @@ async def test_single_edge_group_send_message_with_condition_pass() -> None:
success = await edge_runner.send_message(message, state, ctx)
assert success is True
assert target.call_count == 1
assert target.last_message is not None
assert target.last_message.data == "test"
@@ -304,7 +301,7 @@ async def test_single_edge_group_send_message_with_condition_fail() -> None:
assert target.call_count == 0
async def test_single_edge_group_tracing_success(span_exporter: InMemorySpanExporter) -> None:
async def test_single_edge_group_tracing_success(span_exporter) -> None:
"""Test that single edge group processing creates proper success spans."""
source = MockExecutor(id="source_executor")
target = MockExecutor(id="target_executor")
@@ -355,7 +352,7 @@ async def test_single_edge_group_tracing_success(span_exporter: InMemorySpanExpo
assert link.context.span_id == int("00f067aa0ba902b7", 16)
async def test_single_edge_group_tracing_condition_failure(span_exporter: InMemorySpanExporter) -> None:
async def test_single_edge_group_tracing_condition_failure(span_exporter) -> None:
"""Test that single edge group processing creates proper spans for condition failures."""
source = MockExecutor(id="source_executor")
target = MockExecutor(id="target_executor")
@@ -389,7 +386,7 @@ async def test_single_edge_group_tracing_condition_failure(span_exporter: InMemo
assert span.attributes.get("edge_group.delivery_status") == EdgeGroupDeliveryStatus.DROPPED_CONDITION_FALSE.value
async def test_single_edge_group_tracing_type_mismatch(span_exporter: InMemorySpanExporter) -> None:
async def test_single_edge_group_tracing_type_mismatch(span_exporter) -> None:
"""Test that single edge group processing creates proper spans for type mismatches."""
source = MockExecutor(id="source_executor")
target = MockExecutor(id="target_executor")
@@ -424,7 +421,7 @@ async def test_single_edge_group_tracing_type_mismatch(span_exporter: InMemorySp
assert span.attributes.get("edge_group.delivery_status") == EdgeGroupDeliveryStatus.DROPPED_TYPE_MISMATCH.value
async def test_single_edge_group_tracing_target_mismatch(span_exporter: InMemorySpanExporter) -> None:
async def test_single_edge_group_tracing_target_mismatch(span_exporter) -> None:
"""Test that single edge group processing creates proper spans for target mismatches."""
source = MockExecutor(id="source_executor")
target = MockExecutor(id="target_executor")
@@ -778,7 +775,7 @@ async def test_source_edge_group_with_selection_func_send_message_with_target_in
assert success is False
async def test_fan_out_edge_group_tracing_success(span_exporter: InMemorySpanExporter) -> None:
async def test_fan_out_edge_group_tracing_success(span_exporter) -> None:
"""Test that fan-out edge group processing creates proper success spans."""
source = MockExecutor(id="source_executor")
target1 = MockExecutor(id="target_executor_1")
@@ -830,7 +827,7 @@ async def test_fan_out_edge_group_tracing_success(span_exporter: InMemorySpanExp
assert link.context.span_id == int("00f067aa0ba902b7", 16)
async def test_fan_out_edge_group_tracing_with_target(span_exporter: InMemorySpanExporter) -> None:
async def test_fan_out_edge_group_tracing_with_target(span_exporter) -> None:
"""Test that fan-out edge group processing creates proper spans for targeted messages."""
source = MockExecutor(id="source_executor")
target1 = MockExecutor(id="target_executor_1")
@@ -997,7 +994,7 @@ async def test_target_edge_group_send_message_with_invalid_data() -> None:
assert success is False
async def test_fan_in_edge_group_tracing_buffered(span_exporter: InMemorySpanExporter) -> None:
async def test_fan_in_edge_group_tracing_buffered(span_exporter) -> None:
"""Test that fan-in edge group processing creates proper spans for buffered messages."""
source1 = MockExecutor(id="source_executor_1")
source2 = MockExecutor(id="source_executor_2")
@@ -1089,7 +1086,7 @@ async def test_fan_in_edge_group_tracing_buffered(span_exporter: InMemorySpanExp
assert link.context.span_id == int("00f067aa0ba902b8", 16)
async def test_fan_in_edge_group_tracing_type_mismatch(span_exporter: InMemorySpanExporter) -> None:
async def test_fan_in_edge_group_tracing_type_mismatch(span_exporter) -> None:
"""Test that fan-in edge group processing creates proper spans for type mismatches."""
source1 = MockExecutor(id="source_executor_1")
source2 = MockExecutor(id="source_executor_2")
@@ -3,6 +3,8 @@
from dataclasses import dataclass
import pytest
from typing_extensions import Never
from agent_framework import (
Executor,
Message,
@@ -14,7 +16,6 @@ from agent_framework import (
handler,
response_handler,
)
from typing_extensions import Never
# Module-level types for string forward reference tests
@@ -58,7 +59,7 @@ def test_executor_handler_without_annotations():
class MockExecutorWithOneHandlerWithoutAnnotations(Executor): # type: ignore
"""A mock executor with one handler that does not implement any annotations."""
@handler # pyright: ignore[reportUnknownArgumentType]
@handler
async def handle(self, message, ctx) -> None: # type: ignore
"""A mock handler that does not implement any annotations."""
pass
@@ -155,11 +156,7 @@ async def test_executor_invoked_event_contains_input_data():
workflow = WorkflowBuilder(start_executor=upper).add_edge(upper, collector).build()
events = await workflow.run("hello world")
invoked_events = [
e
for e in events
if isinstance(e, WorkflowEvent) and e.type == "executor_invoked"
]
invoked_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_invoked"]
assert len(invoked_events) == 2
@@ -193,16 +190,10 @@ async def test_executor_completed_event_contains_sent_messages():
sender = MultiSenderExecutor(id="sender")
collector = CollectorExecutor(id="collector")
workflow = (
WorkflowBuilder(start_executor=sender).add_edge(sender, collector).build()
)
workflow = WorkflowBuilder(start_executor=sender).add_edge(sender, collector).build()
events = await workflow.run("hello")
completed_events = [
e
for e in events
if isinstance(e, WorkflowEvent) and e.type == "executor_completed"
]
completed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_completed"]
# Sender should have completed with the sent messages
sender_completed = next(e for e in completed_events if e.executor_id == "sender")
@@ -210,9 +201,7 @@ async def test_executor_completed_event_contains_sent_messages():
assert sender_completed.data == ["hello-first", "hello-second"]
# Collector should have completed with no sent messages (None)
collector_completed_events = [
e for e in completed_events if e.executor_id == "collector"
]
collector_completed_events = [e for e in completed_events if e.executor_id == "collector"]
# Collector is called twice (once per message from sender)
assert len(collector_completed_events) == 2
for collector_completed in collector_completed_events:
@@ -231,11 +220,7 @@ async def test_executor_completed_event_includes_yielded_outputs():
workflow = WorkflowBuilder(start_executor=executor).build()
events = await workflow.run("test")
completed_events = [
e
for e in events
if isinstance(e, WorkflowEvent) and e.type == "executor_completed"
]
completed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_completed"]
assert len(completed_events) == 1
assert completed_events[0].executor_id == "yielder"
@@ -263,9 +248,7 @@ async def test_executor_events_with_complex_message_types():
class ProcessorExecutor(Executor):
@handler
async def handle(
self, request: Request, ctx: WorkflowContext[Response]
) -> None:
async def handle(self, request: Request, ctx: WorkflowContext[Response]) -> None:
response = Response(results=[request.query.upper()] * request.limit)
await ctx.send_message(response)
@@ -277,23 +260,13 @@ async def test_executor_events_with_complex_message_types():
processor = ProcessorExecutor(id="processor")
collector = CollectorExecutor(id="collector")
workflow = (
WorkflowBuilder(start_executor=processor).add_edge(processor, collector).build()
)
workflow = WorkflowBuilder(start_executor=processor).add_edge(processor, collector).build()
input_request = Request(query="hello", limit=3)
events = await workflow.run(input_request)
invoked_events = [
e
for e in events
if isinstance(e, WorkflowEvent) and e.type == "executor_invoked"
]
completed_events = [
e
for e in events
if isinstance(e, WorkflowEvent) and e.type == "executor_completed"
]
invoked_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_invoked"]
completed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_completed"]
# Check processor invoked event has the Request object
processor_invoked = next(e for e in invoked_events if e.executor_id == "processor")
@@ -302,9 +275,7 @@ async def test_executor_events_with_complex_message_types():
assert processor_invoked.data.limit == 3
# Check processor completed event has the Response object
processor_completed = next(
e for e in completed_events if e.executor_id == "processor"
)
processor_completed = next(e for e in completed_events if e.executor_id == "processor")
assert processor_completed.data is not None
assert len(processor_completed.data) == 1
assert isinstance(processor_completed.data[0], Response)
@@ -390,9 +361,7 @@ def test_executor_workflow_output_types_property():
# Test executor with union workflow output types
class UnionWorkflowOutputExecutor(Executor):
@handler
async def handle(
self, text: str, ctx: WorkflowContext[int, str | bool]
) -> None:
async def handle(self, text: str, ctx: WorkflowContext[int, str | bool]) -> None:
pass
executor = UnionWorkflowOutputExecutor(id="union_workflow_output")
@@ -403,15 +372,11 @@ def test_executor_workflow_output_types_property():
# Test executor with multiple handlers having different workflow output types
class MultiHandlerWorkflowExecutor(Executor):
@handler
async def handle_string(
self, text: str, ctx: WorkflowContext[int, str]
) -> None:
async def handle_string(self, text: str, ctx: WorkflowContext[int, str]) -> None:
pass
@handler
async def handle_number(
self, num: int, ctx: WorkflowContext[bool, float]
) -> None:
async def handle_number(self, num: int, ctx: WorkflowContext[bool, float]) -> None:
pass
executor = MultiHandlerWorkflowExecutor(id="multi_workflow")
@@ -465,9 +430,7 @@ def test_executor_output_types_includes_response_handlers():
pass
@response_handler
async def handle_response(
self, original_request: str, response: bool, ctx: WorkflowContext[float]
) -> None:
async def handle_response(self, original_request: str, response: bool, ctx: WorkflowContext[float]) -> None:
pass
executor = RequestResponseExecutor(id="request_response")
@@ -489,10 +452,7 @@ def test_executor_workflow_output_types_includes_response_handlers():
@response_handler
async def handle_response(
self,
original_request: str,
response: bool,
ctx: WorkflowContext[float, bool],
self, original_request: str, response: bool, ctx: WorkflowContext[float, bool]
) -> None:
pass
@@ -549,10 +509,7 @@ def test_executor_response_handler_union_output_types():
@response_handler
async def handle_response(
self,
original_request: str,
response: bool,
ctx: WorkflowContext[int | str | float, bool | int],
self, original_request: str, response: bool, ctx: WorkflowContext[int | str | float, bool | int]
) -> None:
pass
@@ -574,9 +531,7 @@ async def test_executor_invoked_event_data_not_mutated_by_handler():
"""Test that executor_invoked event (type='executor_invoked').data captures original input, not mutated input."""
@executor(id="Mutator")
async def mutator(
messages: list[Message], ctx: WorkflowContext[list[Message]]
) -> None:
async def mutator(messages: list[Message], ctx: WorkflowContext[list[Message]]) -> None:
# The handler mutates the input list by appending new messages
original_len = len(messages)
messages.append(Message(role="assistant", text="Added by executor"))
@@ -591,11 +546,7 @@ async def test_executor_invoked_event_data_not_mutated_by_handler():
events = await workflow.run(input_messages)
# Find the invoked event for the Mutator executor
invoked_events = [
e
for e in events
if isinstance(e, WorkflowEvent) and e.type == "executor_invoked"
]
invoked_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_invoked"]
assert len(invoked_events) == 1
mutator_invoked = invoked_events[0]
@@ -626,8 +577,8 @@ class TestHandlerExplicitTypes:
exec_instance = ExplicitInputExecutor(id="explicit_input")
# Handler should be registered for str (explicit), not Any (introspected)
assert str in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
assert len(exec_instance._handlers) == 1 # pyright: ignore[reportPrivateUsage]
assert str in exec_instance._handlers
assert len(exec_instance._handlers) == 1
# Can handle str messages
assert exec_instance.can_handle(WorkflowMessage(data="hello", source_id="mock"))
@@ -645,8 +596,8 @@ class TestHandlerExplicitTypes:
exec_instance = ExplicitOutputExecutor(id="explicit_output")
# Handler spec should have int as output type (explicit)
handler_func = exec_instance._handlers[str] # pyright: ignore[reportPrivateUsage]
assert handler_func._handler_spec["output_types"] == [int] # pyright: ignore[reportFunctionMemberAccess]
handler_func = exec_instance._handlers[str]
assert handler_func._handler_spec["output_types"] == [int]
# Executor output_types property should reflect explicit type
assert int in exec_instance.output_types
@@ -664,20 +615,16 @@ class TestHandlerExplicitTypes:
exec_instance = ExplicitBothExecutor(id="explicit_both")
# Handler should be registered for dict (explicit input type)
assert dict in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
assert len(exec_instance._handlers) == 1 # pyright: ignore[reportPrivateUsage]
assert dict in exec_instance._handlers
assert len(exec_instance._handlers) == 1
# Output type should be list (explicit)
handler_func = exec_instance._handlers[dict] # pyright: ignore[reportPrivateUsage]
assert handler_func._handler_spec["output_types"] == [list] # pyright: ignore[reportFunctionMemberAccess]
handler_func = exec_instance._handlers[dict]
assert handler_func._handler_spec["output_types"] == [list]
# Verify can_handle
assert exec_instance.can_handle(
WorkflowMessage(data={"key": "value"}, source_id="mock")
)
assert not exec_instance.can_handle(
WorkflowMessage(data="string", source_id="mock")
)
assert exec_instance.can_handle(WorkflowMessage(data={"key": "value"}, source_id="mock"))
assert not exec_instance.can_handle(WorkflowMessage(data="string", source_id="mock"))
def test_handler_with_explicit_union_input_type(self):
"""Test that explicit union input_type is handled correctly."""
@@ -692,15 +639,13 @@ class TestHandlerExplicitTypes:
# Handler should be registered for the union type
# The union type itself is stored as the key
assert len(exec_instance._handlers) == 1 # pyright: ignore[reportPrivateUsage]
assert len(exec_instance._handlers) == 1
# Can handle both str and int messages
assert exec_instance.can_handle(WorkflowMessage(data="hello", source_id="mock"))
assert exec_instance.can_handle(WorkflowMessage(data=42, source_id="mock"))
# Cannot handle float
assert not exec_instance.can_handle(
WorkflowMessage(data=3.14, source_id="mock")
)
assert not exec_instance.can_handle(WorkflowMessage(data=3.14, source_id="mock"))
def test_handler_with_explicit_union_output_type(self):
"""Test that explicit union output is normalized to a list."""
@@ -729,8 +674,8 @@ class TestHandlerExplicitTypes:
exec_instance = PrecedenceExecutor(id="precedence")
# Should use explicit input type (bytes), not introspected (str)
assert bytes in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
assert str not in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
assert bytes in exec_instance._handlers
assert str not in exec_instance._handlers
# Should use explicit output type (float), not introspected (int)
assert float in exec_instance.output_types
@@ -747,7 +692,7 @@ class TestHandlerExplicitTypes:
exec_instance = IntrospectedExecutor(id="introspected")
# Should use introspected types
assert str in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
assert str in exec_instance._handlers
assert int in exec_instance.output_types
def test_handler_explicit_mode_requires_input(self):
@@ -760,13 +705,13 @@ class TestHandlerExplicitTypes:
pass
exec_input = OnlyInputExecutor(id="only_input")
assert bytes in exec_input._handlers # pyright: ignore[reportPrivateUsage] # Explicit
assert bytes in exec_input._handlers # Explicit
assert exec_input.output_types == [] # No output types (not introspected)
# Only explicit output without input should raise error
with pytest.raises(ValueError, match="must specify 'input' type"):
class OnlyOutputExecutor(Executor): # pyright: ignore[reportUnusedClass]
class OnlyOutputExecutor(Executor):
@handler(output=float)
async def handle(self, message: str, ctx: WorkflowContext[int]) -> None:
pass
@@ -774,11 +719,9 @@ class TestHandlerExplicitTypes:
# Only explicit workflow_output without input should raise error
with pytest.raises(ValueError, match="must specify 'input' type"):
class OnlyWorkflowOutputExecutor(Executor): # pyright: ignore[reportUnusedClass]
class OnlyWorkflowOutputExecutor(Executor):
@handler(workflow_output=bool)
async def handle(
self, message: str, ctx: WorkflowContext[int, str]
) -> None:
async def handle(self, message: str, ctx: WorkflowContext[int, str]) -> None:
pass
def test_handler_explicit_input_type_allows_no_message_annotation(self):
@@ -791,7 +734,8 @@ class TestHandlerExplicitTypes:
exec_instance = NoAnnotationExecutor(id="no_annotation")
assert str in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
# Should work with explicit input_type
assert str in exec_instance._handlers
assert exec_instance.can_handle(WorkflowMessage(data="hello", source_id="mock"))
def test_handler_multiple_handlers_mixed_explicit_and_introspected(self):
@@ -803,17 +747,15 @@ class TestHandlerExplicitTypes:
pass
@handler
async def handle_introspected(
self, message: float, ctx: WorkflowContext[bool]
) -> None:
async def handle_introspected(self, message: float, ctx: WorkflowContext[bool]) -> None:
pass
exec_instance = MixedExecutor(id="mixed")
# Should have both handlers
assert len(exec_instance._handlers) == 2 # pyright: ignore[reportPrivateUsage]
assert str in exec_instance._handlers # pyright: ignore[reportPrivateUsage] # Explicit
assert float in exec_instance._handlers # pyright: ignore[reportPrivateUsage] # Introspected
assert len(exec_instance._handlers) == 2
assert str in exec_instance._handlers # Explicit
assert float in exec_instance._handlers # Introspected
# Should have both output types
assert int in exec_instance.output_types # Explicit
@@ -830,10 +772,8 @@ class TestHandlerExplicitTypes:
exec_instance = StringRefExecutor(id="string_ref")
# Should resolve the string to the actual type
assert ForwardRefMessage in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
assert exec_instance.can_handle(
WorkflowMessage(data=ForwardRefMessage("hello"), source_id="mock")
)
assert ForwardRefMessage in exec_instance._handlers
assert exec_instance.can_handle(WorkflowMessage(data=ForwardRefMessage("hello"), source_id="mock"))
def test_handler_with_string_forward_reference_union(self):
"""Test that string forward references work with union types."""
@@ -846,12 +786,8 @@ class TestHandlerExplicitTypes:
exec_instance = StringUnionExecutor(id="string_union")
# Should handle both types
assert exec_instance.can_handle(
WorkflowMessage(data=ForwardRefTypeA("hello"), source_id="mock")
)
assert exec_instance.can_handle(
WorkflowMessage(data=ForwardRefTypeB(42), source_id="mock")
)
assert exec_instance.can_handle(WorkflowMessage(data=ForwardRefTypeA("hello"), source_id="mock"))
assert exec_instance.can_handle(WorkflowMessage(data=ForwardRefTypeB(42), source_id="mock"))
def test_handler_with_string_forward_reference_output_type(self):
"""Test that string forward references work for output_type."""
@@ -877,8 +813,8 @@ class TestHandlerExplicitTypes:
exec_instance = ExplicitWorkflowOutputExecutor(id="explicit_workflow_output")
# Handler spec should have bool as workflow_output_type (explicit)
handler_func = exec_instance._handlers[str] # pyright: ignore[reportPrivateUsage]
assert handler_func._handler_spec["workflow_output_types"] == [bool] # pyright: ignore[reportFunctionMemberAccess]
handler_func = exec_instance._handlers[str]
assert handler_func._handler_spec["workflow_output_types"] == [bool]
# Executor workflow_output_types property should reflect explicit type
assert bool in exec_instance.workflow_output_types
@@ -890,14 +826,13 @@ class TestHandlerExplicitTypes:
class PrecedenceExecutor(Executor):
@handler(input=int, output=float, workflow_output=str)
async def handle(
self, message: int, ctx: WorkflowContext[int, bool]
) -> None:
async def handle(self, message: int, ctx: WorkflowContext[int, bool]) -> None:
pass
exec_instance = PrecedenceExecutor(id="precedence")
assert int in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
# All types should come from explicit params
assert int in exec_instance._handlers
assert float in exec_instance.output_types
assert str in exec_instance.workflow_output_types
# Introspected types should NOT be present
@@ -914,7 +849,8 @@ class TestHandlerExplicitTypes:
exec_instance = AllExplicitExecutor(id="all_explicit")
assert str in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
# Check input type
assert str in exec_instance._handlers
assert exec_instance.can_handle(WorkflowMessage(data="hello", source_id="mock"))
# Check output_type
@@ -958,9 +894,7 @@ class TestHandlerExplicitTypes:
async def handle(self, message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def]
pass
exec_instance = StringUnionWorkflowOutputExecutor(
id="string_union_workflow_output"
)
exec_instance = StringUnionWorkflowOutputExecutor(id="string_union_workflow_output")
# Should resolve both types from string union
assert ForwardRefTypeA in exec_instance.workflow_output_types
@@ -971,14 +905,10 @@ class TestHandlerExplicitTypes:
class IntrospectedWorkflowOutputExecutor(Executor):
@handler
async def handle(
self, message: str, ctx: WorkflowContext[int, bool]
) -> None:
async def handle(self, message: str, ctx: WorkflowContext[int, bool]) -> None:
pass
exec_instance = IntrospectedWorkflowOutputExecutor(
id="introspected_workflow_output"
)
exec_instance = IntrospectedWorkflowOutputExecutor(id="introspected_workflow_output")
# Should use introspected types from WorkflowContext[int, bool]
assert int in exec_instance.output_types
@@ -34,8 +34,8 @@ class TestExecutorFutureAnnotations:
pass
exec_instance = MyExecutor(id="test")
assert str in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
spec = exec_instance._handler_specs[0] # pyright: ignore[reportPrivateUsage]
assert str in exec_instance._handlers
spec = exec_instance._handler_specs[0]
assert spec["message_type"] is str
assert spec["output_types"] == [MyTypeA]
assert spec["workflow_output_types"] == [MyTypeB]
@@ -49,8 +49,8 @@ class TestExecutorFutureAnnotations:
pass
exec_instance = MyExecutor(id="test")
assert int in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
spec = exec_instance._handler_specs[0] # pyright: ignore[reportPrivateUsage]
assert int in exec_instance._handlers
spec = exec_instance._handler_specs[0]
assert spec["message_type"] is int
assert spec["output_types"] == [MyTypeA]
@@ -63,7 +63,7 @@ class TestExecutorFutureAnnotations:
pass
exec_instance = MyExecutor(id="test")
spec = exec_instance._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = exec_instance._handler_specs[0]
assert spec["message_type"] == dict[str, Any]
assert spec["output_types"] == [list[str]]
@@ -76,8 +76,8 @@ class TestExecutorFutureAnnotations:
pass
exec_instance = MyExecutor(id="test")
assert str in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
spec = exec_instance._handler_specs[0] # pyright: ignore[reportPrivateUsage]
assert str in exec_instance._handlers
spec = exec_instance._handler_specs[0]
assert spec["output_types"] == []
assert spec["workflow_output_types"] == []
@@ -86,12 +86,12 @@ class TestExecutorFutureAnnotations:
class MyExecutor(Executor):
@handler(input=str, output=MyTypeA)
async def example(self, input, ctx) -> None: # type: ignore[no-untyped-def]
async def example(self, input, ctx) -> None:
pass
exec_instance = MyExecutor(id="test")
assert str in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
spec = exec_instance._handler_specs[0] # pyright: ignore[reportPrivateUsage]
assert str in exec_instance._handlers
spec = exec_instance._handler_specs[0]
assert spec["message_type"] is str
assert spec["output_types"] == [MyTypeA]
@@ -104,8 +104,8 @@ class TestExecutorFutureAnnotations:
pass
exec_instance = MyExecutor(id="test")
assert str in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
spec = exec_instance._handler_specs[0] # pyright: ignore[reportPrivateUsage]
assert str in exec_instance._handlers
spec = exec_instance._handler_specs[0]
assert spec["output_types"] == [MyTypeA, MyTypeB]
assert spec["workflow_output_types"] == [MyTypeC]
@@ -118,7 +118,7 @@ class TestExecutorFutureAnnotations:
"""
with pytest.raises(ValueError):
class Bad(Executor): # pyright: ignore[reportUnusedClass]
@handler # pyright: ignore[reportUnknownArgumentType]
async def example(self, input: NonExistentType, ctx: WorkflowContext[MyTypeA, MyTypeB]) -> None: # noqa: F821 # type: ignore[name-defined]
class Bad(Executor):
@handler
async def example(self, input: NonExistentType, ctx: WorkflowContext[MyTypeA, MyTypeB]) -> None: # noqa: F821
pass
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncIterable, Awaitable
from typing import Any, Literal, overload
from collections.abc import AsyncIterable, Awaitable, Sequence
from typing import Any
import pytest
from pydantic import PrivateAttr
@@ -13,7 +13,6 @@ from agent_framework import (
AgentExecutorResponse,
AgentResponse,
AgentResponseUpdate,
AgentRunInputs,
AgentSession,
BaseAgent,
Content,
@@ -35,32 +34,14 @@ class _SimpleAgent(BaseAgent):
super().__init__(**kwargs)
self._reply_text = reply_text
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[False] = ...,
session: AgentSession | None = ...,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[True],
session: AgentSession | None = ...,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
if stream:
async def _stream() -> AsyncIterable[AgentResponseUpdate]:
@@ -100,32 +81,14 @@ class _ToolHistoryAgent(BaseAgent):
Message(role="assistant", contents=[Content.from_text(text=self._summary_text)]),
]
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[False] = ...,
session: AgentSession | None = ...,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[True],
session: AgentSession | None = ...,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
if stream:
async def _stream() -> AsyncIterable[AgentResponseUpdate]:
@@ -202,32 +165,14 @@ class _CaptureAgent(BaseAgent):
super().__init__(**kwargs)
self._reply_text = reply_text
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[False] = ...,
session: AgentSession | None = ...,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[True],
session: AgentSession | None = ...,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
# Normalize and record messages for verification
norm: list[Message] = []
if messages:
@@ -315,7 +260,7 @@ class _RoundTripCoordinator(Executor):
async def handle_response(
self,
response: AgentExecutorResponse,
ctx: WorkflowContext[AgentExecutorRequest, dict[str, Any]],
ctx: WorkflowContext[Never, dict[str, Any]],
) -> None:
self._seen += 1
if self._seen == 1:
@@ -369,32 +314,14 @@ class _SessionIdCapturingAgent(BaseAgent):
_captured_service_session_id: str | None = PrivateAttr(default="NOT_CAPTURED")
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[False] = ...,
session: AgentSession | None = ...,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[True],
session: AgentSession | None = ...,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
self._captured_service_session_id = session.service_session_id if session else None
async def _run() -> AgentResponse:
@@ -415,7 +342,7 @@ class _FullHistoryReplayCoordinator(Executor):
async def handle(
self,
response: AgentExecutorResponse,
ctx: WorkflowContext[AgentExecutorRequest, Any],
ctx: WorkflowContext[Never, Any],
) -> None:
full_conv = list(response.full_conversation or response.agent_response.messages)
full_conv.append(Message(role="user", text="follow-up"))
@@ -48,12 +48,12 @@ class TestFunctionExecutor:
func_exec = FunctionExecutor(process_string)
# Check that handler was registered
assert len(func_exec._handlers) == 1 # pyright: ignore[reportPrivateUsage]
assert str in func_exec._handlers # pyright: ignore[reportPrivateUsage]
assert len(func_exec._handlers) == 1
assert str in func_exec._handlers
# Check handler spec was created
assert len(func_exec._handler_specs) == 1 # pyright: ignore[reportPrivateUsage]
spec = func_exec._handler_specs[0] # pyright: ignore[reportPrivateUsage]
assert len(func_exec._handler_specs) == 1
spec = func_exec._handler_specs[0]
assert spec["name"] == "process_string"
assert spec["message_type"] is str
assert spec["output_types"] == [str]
@@ -67,10 +67,10 @@ class TestFunctionExecutor:
assert isinstance(process_int, FunctionExecutor)
assert process_int.id == "test_executor"
assert int in process_int._handlers # pyright: ignore[reportPrivateUsage]
assert int in process_int._handlers
# Check spec
spec = process_int._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = process_int._handler_specs[0]
assert spec["message_type"] is int
assert spec["output_types"] == [int]
@@ -78,7 +78,7 @@ class TestFunctionExecutor:
"""Test @executor decorator uses function name as default ID."""
@executor
async def my_function(data: dict[str, Any], ctx: WorkflowContext[Any]) -> None:
async def my_function(data: dict, ctx: WorkflowContext[Any]) -> None:
await ctx.send_message(data)
assert my_function.id == "my_function"
@@ -92,7 +92,7 @@ class TestFunctionExecutor:
assert isinstance(no_parens_function, FunctionExecutor)
assert no_parens_function.id == "no_parens_function"
assert str in no_parens_function._handlers # pyright: ignore[reportPrivateUsage]
assert str in no_parens_function._handlers
# Also test with single parameter function
@executor
@@ -101,7 +101,7 @@ class TestFunctionExecutor:
assert isinstance(simple_no_parens, FunctionExecutor)
assert simple_no_parens.id == "simple_no_parens"
assert int in simple_no_parens._handlers # pyright: ignore[reportPrivateUsage]
assert int in simple_no_parens._handlers
def test_union_output_types(self):
"""Test that union output types are properly inferred for both messages and workflow outputs."""
@@ -113,7 +113,7 @@ class TestFunctionExecutor:
else:
await ctx.send_message(text.upper())
spec = multi_output._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = multi_output._handler_specs[0]
assert set(spec["output_types"]) == {str, int}
assert spec["workflow_output_types"] == [] # No workflow outputs defined
@@ -127,7 +127,7 @@ class TestFunctionExecutor:
else:
await ctx.yield_output(data.upper())
workflow_spec = multi_workflow_output._handler_specs[0] # pyright: ignore[reportPrivateUsage]
workflow_spec = multi_workflow_output._handler_specs[0]
assert workflow_spec["output_types"] == [] # None means no message outputs
assert set(workflow_spec["workflow_output_types"]) == {str, int, bool}
@@ -139,7 +139,7 @@ class TestFunctionExecutor:
# This executor doesn't send any messages
pass
spec = no_output._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = no_output._handler_specs[0]
assert spec["output_types"] == []
assert spec["workflow_output_types"] == [] # No workflow outputs defined
@@ -150,7 +150,7 @@ class TestFunctionExecutor:
async def any_output(data: str, ctx: WorkflowContext[Any]) -> None:
await ctx.send_message("result")
spec = any_output._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = any_output._handler_specs[0]
assert spec["output_types"] == [Any]
assert spec["workflow_output_types"] == [] # No workflow outputs defined
@@ -160,7 +160,7 @@ class TestFunctionExecutor:
await ctx.send_message("message")
await ctx.yield_output("workflow_output")
both_spec = any_both_output._handler_specs[0] # pyright: ignore[reportPrivateUsage]
both_spec = any_both_output._handler_specs[0]
assert both_spec["output_types"] == [Any]
assert both_spec["workflow_output_types"] == [Any]
@@ -228,11 +228,11 @@ class TestFunctionExecutor:
await ctx.yield_output(result)
# Verify type inference for both executors
upper_spec = to_upper._handler_specs[0] # pyright: ignore[reportPrivateUsage]
upper_spec = to_upper._handler_specs[0]
assert upper_spec["output_types"] == [str]
assert upper_spec["workflow_output_types"] == [] # No workflow outputs
reverse_spec = reverse_text._handler_specs[0] # pyright: ignore[reportPrivateUsage]
reverse_spec = reverse_text._handler_specs[0]
assert reverse_spec["output_types"] == [Any] # First parameter is Any
assert reverse_spec["workflow_output_types"] == [str] # Second parameter is str
@@ -270,7 +270,7 @@ class TestFunctionExecutor:
await ctx.send_message(message)
with pytest.raises(ValueError, match="Handler for type .* already registered"):
func_exec._register_instance_handler( # pyright: ignore[reportPrivateUsage]
func_exec._register_instance_handler(
name="second",
func=second_handler,
message_type=str,
@@ -287,7 +287,7 @@ class TestFunctionExecutor:
result = {item: len(item) for item in items}
await ctx.send_message(result)
spec = process_list._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = process_list._handler_specs[0]
assert spec["message_type"] == list[str]
assert spec["output_types"] == [dict[str, int]]
@@ -300,10 +300,10 @@ class TestFunctionExecutor:
assert isinstance(process_simple, FunctionExecutor)
assert process_simple.id == "simple_processor"
assert str in process_simple._handlers # pyright: ignore[reportPrivateUsage]
assert str in process_simple._handlers
# Check spec - single parameter functions have no output types since they can't send messages
spec = process_simple._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = process_simple._handler_specs[0]
assert spec["message_type"] is str
assert spec["output_types"] == []
assert spec["ctx_annotation"] is None
@@ -316,7 +316,7 @@ class TestFunctionExecutor:
return data * 2
func_exec = FunctionExecutor(valid_single)
assert int in func_exec._handlers # pyright: ignore[reportPrivateUsage]
assert int in func_exec._handlers
# Single parameter with missing type annotation should still fail
async def no_annotation(data): # type: ignore
@@ -349,7 +349,7 @@ class TestFunctionExecutor:
# For testing purposes, we can check that the handler is registered correctly
assert double_value.can_handle(WorkflowMessage(data=5, source_id="mock"))
assert int in double_value._handlers # pyright: ignore[reportPrivateUsage]
assert int in double_value._handlers
def test_sync_function_basic(self):
"""Test basic synchronous function support."""
@@ -360,10 +360,10 @@ class TestFunctionExecutor:
assert isinstance(process_sync, FunctionExecutor)
assert process_sync.id == "sync_processor"
assert str in process_sync._handlers # pyright: ignore[reportPrivateUsage]
assert str in process_sync._handlers
# Check spec - sync single parameter functions have no output types
spec = process_sync._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = process_sync._handler_specs[0]
assert spec["message_type"] is str
assert spec["output_types"] == []
assert spec["ctx_annotation"] is None
@@ -378,10 +378,10 @@ class TestFunctionExecutor:
assert isinstance(sync_with_ctx, FunctionExecutor)
assert sync_with_ctx.id == "sync_with_ctx"
assert int in sync_with_ctx._handlers # pyright: ignore[reportPrivateUsage]
assert int in sync_with_ctx._handlers
# Check spec - sync functions with context can infer output types
spec = sync_with_ctx._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = sync_with_ctx._handler_specs[0]
assert spec["message_type"] is int
assert spec["output_types"] == [int]
@@ -404,18 +404,18 @@ class TestFunctionExecutor:
return data.upper()
func_exec = FunctionExecutor(valid_sync)
assert str in func_exec._handlers # pyright: ignore[reportPrivateUsage]
assert str in func_exec._handlers
# Valid sync function with two parameters
def valid_sync_with_ctx(data: int, ctx: WorkflowContext[str]):
return str(data)
func_exec2 = FunctionExecutor(valid_sync_with_ctx)
assert int in func_exec2._handlers # pyright: ignore[reportPrivateUsage]
assert int in func_exec2._handlers
# Sync function with missing type annotation should still fail
def no_annotation(data): # type: ignore # pyright: ignore[reportUnknownVariableType]
return data # pyright: ignore[reportUnknownVariableType]
def no_annotation(data): # type: ignore
return data
with pytest.raises(ValueError, match="type annotation for the message"):
FunctionExecutor(no_annotation) # type: ignore
@@ -457,11 +457,11 @@ class TestFunctionExecutor:
await ctx.yield_output(result)
# Verify type inference for sync and async functions
sync_spec = to_upper_sync._handler_specs[0] # pyright: ignore[reportPrivateUsage]
sync_spec = to_upper_sync._handler_specs[0]
assert sync_spec["output_types"] == [str]
assert sync_spec["workflow_output_types"] == [] # No workflow outputs
async_spec = reverse_async._handler_specs[0] # pyright: ignore[reportPrivateUsage]
async_spec = reverse_async._handler_specs[0]
assert async_spec["output_types"] == [Any] # First parameter is Any
assert async_spec["workflow_output_types"] == [str] # Second parameter is str
@@ -471,8 +471,8 @@ class TestFunctionExecutor:
# For integration testing, we mainly verify that the handlers are properly registered
# and the functions are wrapped correctly
assert str in to_upper_sync._handlers # pyright: ignore[reportPrivateUsage]
assert str in reverse_async._handlers # pyright: ignore[reportPrivateUsage]
assert str in to_upper_sync._handlers
assert str in reverse_async._handlers
async def test_sync_function_thread_execution(self):
"""Test that sync functions run in thread pool and don't block the event loop."""
@@ -491,13 +491,13 @@ class TestFunctionExecutor:
return data.upper()
# Verify the function is wrapped and registered
assert str in blocking_function._handlers # pyright: ignore[reportPrivateUsage]
assert str in blocking_function._handlers
# For a more complete test, we'd need to create a full workflow context,
# but for now we can verify that the function was properly wrapped
# and that sync functions store the correct metadata
assert not blocking_function._is_async # pyright: ignore[reportPrivateUsage]
assert not blocking_function._has_context # pyright: ignore[reportPrivateUsage]
assert not blocking_function._is_async
assert not blocking_function._has_context
# The actual thread execution test would require a full workflow setup,
# but the important thing is that asyncio.to_thread is used in the wrapper
@@ -506,7 +506,7 @@ class TestFunctionExecutor:
"""Test that @executor decorator properly rejects @staticmethod with clear error."""
with pytest.raises(ValueError) as exc_info:
class Example: # pyright: ignore[reportUnusedClass]
class Example:
@executor
@staticmethod
async def bad_handler(data: str) -> str:
@@ -519,7 +519,7 @@ class TestFunctionExecutor:
"""Test that @executor decorator properly rejects @classmethod with clear error."""
with pytest.raises(ValueError) as exc_info:
class Example: # pyright: ignore[reportUnusedClass]
class Example:
@executor
@classmethod
async def bad_handler(cls, data: str) -> str:
@@ -570,8 +570,8 @@ class TestExecutorExplicitTypes:
pass
# Handler should be registered for str (explicit)
assert str in process._handlers # pyright: ignore[reportPrivateUsage]
assert len(process._handlers) == 1 # pyright: ignore[reportPrivateUsage]
assert str in process._handlers
assert len(process._handlers) == 1
# Can handle str messages
assert process.can_handle(WorkflowMessage(data="hello", source_id="mock"))
@@ -586,7 +586,7 @@ class TestExecutorExplicitTypes:
pass
# Handler spec should have int as output type (explicit), not str (introspected)
spec = process._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = process._handler_specs[0]
assert spec["output_types"] == [int]
# Executor output_types property should reflect explicit type
@@ -601,11 +601,11 @@ class TestExecutorExplicitTypes:
pass
# Handler should be registered for dict (explicit input type)
assert dict in process._handlers # pyright: ignore[reportPrivateUsage]
assert len(process._handlers) == 1 # pyright: ignore[reportPrivateUsage]
assert dict in process._handlers
assert len(process._handlers) == 1
# Output type should be list (explicit)
spec = process._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = process._handler_specs[0]
assert spec["output_types"] == [list]
# Verify can_handle
@@ -620,7 +620,7 @@ class TestExecutorExplicitTypes:
pass
# Handler should be registered for the union type
assert len(process._handlers) == 1 # pyright: ignore[reportPrivateUsage]
assert len(process._handlers) == 1
# Can handle both str and int messages
assert process.can_handle(WorkflowMessage(data="hello", source_id="mock"))
@@ -648,8 +648,8 @@ class TestExecutorExplicitTypes:
pass
# Should use explicit input type (bytes), not introspected (str)
assert bytes in process._handlers # pyright: ignore[reportPrivateUsage]
assert str not in process._handlers # pyright: ignore[reportPrivateUsage]
assert bytes in process._handlers
assert str not in process._handlers
# Should use explicit output type (float), not introspected (int)
assert float in process.output_types
@@ -663,7 +663,7 @@ class TestExecutorExplicitTypes:
pass
# Should use introspected types
assert str in process._handlers # pyright: ignore[reportPrivateUsage]
assert str in process._handlers
assert int in process.output_types
def test_executor_partial_explicit_types(self):
@@ -674,7 +674,7 @@ class TestExecutorExplicitTypes:
async def process_input(message: str, ctx: WorkflowContext[int]) -> None:
pass
assert bytes in process_input._handlers # Explicit # pyright: ignore[reportPrivateUsage]
assert bytes in process_input._handlers # Explicit
assert int in process_input.output_types # Introspected
# Only explicit output_type, introspect input_type
@@ -682,7 +682,7 @@ class TestExecutorExplicitTypes:
async def process_output(message: str, ctx: WorkflowContext[int]) -> None:
pass
assert str in process_output._handlers # Introspected # pyright: ignore[reportPrivateUsage]
assert str in process_output._handlers # Introspected
assert float in process_output.output_types # Explicit
assert int not in process_output.output_types # Not introspected when explicit provided
@@ -694,7 +694,7 @@ class TestExecutorExplicitTypes:
pass
# Should work with explicit input_type
assert str in process._handlers # pyright: ignore[reportPrivateUsage]
assert str in process._handlers
assert process.can_handle(WorkflowMessage(data="hello", source_id="mock"))
def test_executor_explicit_types_with_id(self):
@@ -705,7 +705,7 @@ class TestExecutorExplicitTypes:
pass
assert process.id == "custom_id"
assert bytes in process._handlers # pyright: ignore[reportPrivateUsage]
assert bytes in process._handlers
assert int in process.output_types
def test_executor_explicit_types_with_single_param_function(self):
@@ -713,10 +713,10 @@ class TestExecutorExplicitTypes:
@executor(input=str)
async def process(message): # type: ignore[no-untyped-def]
return message.upper() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
return message.upper()
# Should work with explicit input_type
assert str in process._handlers # pyright: ignore[reportPrivateUsage]
assert str in process._handlers
assert process.can_handle(WorkflowMessage(data="hello", source_id="mock"))
assert not process.can_handle(WorkflowMessage(data=42, source_id="mock"))
@@ -727,7 +727,7 @@ class TestExecutorExplicitTypes:
def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def]
pass
assert int in process._handlers # pyright: ignore[reportPrivateUsage]
assert int in process._handlers
assert str in process.output_types
def test_function_executor_constructor_with_explicit_types(self):
@@ -736,10 +736,10 @@ class TestExecutorExplicitTypes:
async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def]
pass
func_exec = FunctionExecutor(process, id="test", input=dict, output=list) # pyright: ignore[reportUnknownArgumentType]
func_exec = FunctionExecutor(process, id="test", input=dict, output=list)
assert dict in func_exec._handlers # pyright: ignore[reportPrivateUsage]
spec = func_exec._handler_specs[0] # pyright: ignore[reportPrivateUsage]
assert dict in func_exec._handlers
spec = func_exec._handler_specs[0]
assert spec["message_type"] is dict
assert spec["output_types"] == [list]
@@ -766,7 +766,7 @@ class TestExecutorExplicitTypes:
pass
# Should resolve the string to the actual type
assert FuncExecForwardRefMessage in process._handlers # pyright: ignore[reportPrivateUsage]
assert FuncExecForwardRefMessage in process._handlers
assert process.can_handle(WorkflowMessage(data=FuncExecForwardRefMessage("hello"), source_id="mock"))
def test_executor_with_string_forward_reference_union(self):
@@ -798,7 +798,7 @@ class TestExecutorExplicitTypes:
pass
# Handler spec should have bool as workflow_output_type (explicit)
spec = process._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = process._handler_specs[0]
assert spec["workflow_output_types"] == [bool]
# Executor workflow_output_types property should reflect explicit type
@@ -826,7 +826,7 @@ class TestExecutorExplicitTypes:
pass
# Check input type
assert str in process._handlers # pyright: ignore[reportPrivateUsage]
assert str in process._handlers
assert process.can_handle(WorkflowMessage(data="hello", source_id="mock"))
# Check output_type
@@ -892,6 +892,6 @@ class TestExecutorExplicitTypes:
workflow_output=bool,
)
assert str in exec_instance._handlers # pyright: ignore[reportPrivateUsage]
assert str in exec_instance._handlers
assert int in exec_instance.output_types
assert bool in exec_instance.workflow_output_types
@@ -19,10 +19,10 @@ class TestFunctionExecutorFutureAnnotations:
assert isinstance(process_future, FunctionExecutor)
assert process_future.id == "future_test"
assert int in process_future._handlers # pyright: ignore[reportPrivateUsage]
assert int in process_future._handlers
# Check spec
spec = process_future._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = process_future._handler_specs[0]
assert spec["message_type"] is int
assert spec["output_types"] == [int]
@@ -34,6 +34,6 @@ class TestFunctionExecutorFutureAnnotations:
await ctx.send_message(["done"])
assert isinstance(process_complex, FunctionExecutor)
spec = process_complex._handler_specs[0] # pyright: ignore[reportPrivateUsage]
spec = process_complex._handler_specs[0]
assert spec["message_type"] == dict[str, Any]
assert spec["output_types"] == [list[str]]
@@ -794,7 +794,7 @@ class TestResponseHandlerExplicitTypes:
"""Test response_handler with explicit request and response types."""
@response_handler(request=str, response=int)
async def test_handler(self: Any, original_request: Any, response: Any, ctx: WorkflowContext) -> None:
async def test_handler(self, original_request, response, ctx) -> None:
pass
spec = test_handler._response_handler_spec # type: ignore[reportAttributeAccessIssue]
@@ -806,7 +806,7 @@ class TestResponseHandlerExplicitTypes:
"""Test response_handler with explicit output and workflow_output types."""
@response_handler(request=str, response=int, output=bool, workflow_output=float)
async def test_handler(self: Any, original_request: Any, response: Any, ctx: WorkflowContext) -> None:
async def test_handler(self, original_request, response, ctx) -> None:
pass
spec = test_handler._response_handler_spec # type: ignore[reportAttributeAccessIssue]
@@ -818,8 +818,8 @@ class TestResponseHandlerExplicitTypes:
def test_response_handler_with_union_types(self):
"""Test response_handler with union types."""
@response_handler(request=str | int, response=bool | float) # pyright: ignore[reportArgumentType]
async def test_handler(self: Any, original_request: Any, response: Any, ctx: WorkflowContext) -> None:
@response_handler(request=str | int, response=bool | float)
async def test_handler(self, original_request, response, ctx) -> None:
pass
spec = test_handler._response_handler_spec # type: ignore[reportAttributeAccessIssue]
@@ -830,7 +830,7 @@ class TestResponseHandlerExplicitTypes:
"""Test response_handler with string forward references."""
@response_handler(request="str", response="int")
async def test_handler(self: Any, original_request: Any, response: Any, ctx: WorkflowContext) -> None:
async def test_handler(self, original_request, response, ctx) -> None:
pass
spec = test_handler._response_handler_spec # type: ignore[reportAttributeAccessIssue]
@@ -842,7 +842,7 @@ class TestResponseHandlerExplicitTypes:
with pytest.raises(ValueError, match="must specify 'request' type"):
@response_handler(response=int)
async def test_handler(self: Any, original_request: Any, response: Any, ctx: WorkflowContext) -> None: # pyright: ignore[reportUnusedFunction]
async def test_handler(self, original_request, response, ctx) -> None:
pass
def test_response_handler_explicit_missing_response_raises_error(self):
@@ -850,7 +850,7 @@ class TestResponseHandlerExplicitTypes:
with pytest.raises(ValueError, match="must specify 'response' type"):
@response_handler(request=str)
async def test_handler(self: Any, original_request: Any, response: Any, ctx: WorkflowContext) -> None: # pyright: ignore[reportUnusedFunction]
async def test_handler(self, original_request, response, ctx) -> None:
pass
def test_response_handler_explicit_only_output_raises_error(self):
@@ -858,7 +858,7 @@ class TestResponseHandlerExplicitTypes:
with pytest.raises(ValueError, match="must specify 'request' type"):
@response_handler(output=bool)
async def test_handler(self: Any, original_request: Any, response: Any, ctx: WorkflowContext) -> None: # pyright: ignore[reportUnusedFunction]
async def test_handler(self, original_request, response, ctx) -> None:
pass
def test_executor_with_explicit_response_handlers(self):
@@ -873,7 +873,7 @@ class TestResponseHandlerExplicitTypes:
pass
@response_handler(request=str, response=int, output=bool)
async def handle_explicit(self, original_request: Any, response: Any, ctx: WorkflowContext) -> None:
async def handle_explicit(self, original_request, response, ctx) -> None:
pass
executor = TestExecutor()
@@ -907,7 +907,7 @@ class TestResponseHandlerExplicitTypes:
pass
@response_handler(request=str, response=int)
async def handle_response(self, original_request: Any, response: Any, ctx: WorkflowContext) -> None:
async def handle_response(self, original_request, response, ctx) -> None:
self.handled_request = original_request
self.handled_response = response
@@ -942,7 +942,7 @@ class TestResponseHandlerExplicitTypes:
# Explicit type handler
@response_handler(request=dict, response=bool)
async def handle_explicit(self, original_request: Any, response: Any, ctx: WorkflowContext) -> None:
async def handle_explicit(self, original_request, response, ctx) -> None:
pass
executor = TestExecutor()
@@ -2,7 +2,6 @@
import asyncio
from dataclasses import dataclass
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
@@ -114,7 +113,7 @@ async def test_runner_run_until_convergence():
assert result is not None and result == 10
# iteration count shouldn't be reset after convergence
assert runner._iteration == 10 # pyright: ignore[reportPrivateUsage]
assert runner._iteration == 10 # type: ignore
async def test_runner_run_until_convergence_not_completed():
@@ -174,7 +173,7 @@ async def test_runner_run_iteration_preserves_message_order_per_edge_runner() ->
for index in range(5):
await ctx.send_message(WorkflowMessage(data=MockMessage(data=index), source_id="source"))
await runner._run_iteration() # pyright: ignore[reportPrivateUsage]
await runner._run_iteration()
assert edge_runner.received == [0, 1, 2, 3, 4]
@@ -214,7 +213,7 @@ async def test_runner_run_iteration_delivers_different_edge_runners_concurrently
await ctx.send_message(WorkflowMessage(data=MockMessage(data=1), source_id="source"))
iteration_task = asyncio.create_task(runner._run_iteration()) # pyright: ignore[reportPrivateUsage]
iteration_task = asyncio.create_task(runner._run_iteration())
await blocking_edge_runner.started.wait()
await asyncio.wait_for(probe_edge_runner.probe_completed.wait(), timeout=2.0)
@@ -281,7 +280,7 @@ async def test_fanout_edge_runner_delivers_to_multiple_targets_concurrently() ->
# Queue a message from source (will be delivered to both targets via FanOut)
await ctx.send_message(WorkflowMessage(data=MockMessage(data=1), source_id=source.id))
iteration_task = asyncio.create_task(runner._run_iteration()) # pyright: ignore[reportPrivateUsage]
iteration_task = asyncio.create_task(runner._run_iteration())
# Wait for the blocking executor to start
await blocking_target.started.wait()
@@ -478,11 +477,11 @@ async def test_runner_reset_iteration_count():
ctx = InProcRunnerContext()
runner = Runner([], {executor_a.id: executor_a}, state, ctx, "test_name", graph_signature_hash="test_hash")
runner._iteration = 10 # pyright: ignore[reportPrivateUsage]
runner._iteration = 10
runner.reset_iteration_count()
assert runner._iteration == 0 # pyright: ignore[reportPrivateUsage]
assert runner._iteration == 0
class CheckpointingContext(InProcRunnerContext):
@@ -502,19 +501,18 @@ class CheckpointingContext(InProcRunnerContext):
graph_signature_hash: str,
state: State,
previous_checkpoint_id: str | None,
iteration_count: int,
metadata: dict[str, Any] | None = None,
iteration: int,
) -> str:
checkpoint = WorkflowCheckpoint(
workflow_name=workflow_name,
graph_signature_hash=graph_signature_hash,
state=state.export_state(),
state=state.export(),
previous_checkpoint_id=previous_checkpoint_id,
iteration_count=iteration_count,
iteration_count=iteration,
)
return await self._storage.save(checkpoint)
async def load_checkpoint(self, checkpoint_id: str) -> WorkflowCheckpoint | None: # pyright: ignore[reportIncompatibleMethodOverride]
async def load_checkpoint(self, checkpoint_id: str) -> WorkflowCheckpoint | None:
try:
return await self._storage.load(checkpoint_id)
except WorkflowCheckpointException:
@@ -539,8 +537,7 @@ class FailingCheckpointContext(InProcRunnerContext):
graph_signature_hash: str,
state: State,
previous_checkpoint_id: str | None,
iteration_count: int,
metadata: dict[str, Any] | None = None,
iteration: int,
) -> str:
raise RuntimeError("Simulated checkpoint failure")
@@ -612,8 +609,8 @@ async def test_runner_restore_from_checkpoint_with_external_storage():
# Restore using external storage
await runner.restore_from_checkpoint(checkpoint_id, checkpoint_storage=storage)
assert runner._resumed_from_checkpoint is True # pyright: ignore[reportPrivateUsage]
assert runner._iteration == 5 # pyright: ignore[reportPrivateUsage]
assert runner._resumed_from_checkpoint is True
assert runner._iteration == 5
assert state.get("test_key") == "test_value"
@@ -687,7 +684,7 @@ async def test_runner_restore_executor_states_invalid_states_type():
runner = Runner([], {executor_a.id: executor_a}, state, ctx, "test_name", graph_signature_hash="test_hash")
with pytest.raises(WorkflowCheckpointException, match="not a dictionary"):
await runner._restore_executor_states() # pyright: ignore[reportPrivateUsage]
await runner._restore_executor_states()
async def test_runner_restore_executor_states_invalid_executor_id_type():
@@ -701,7 +698,7 @@ async def test_runner_restore_executor_states_invalid_executor_id_type():
runner = Runner([], {executor_a.id: executor_a}, state, ctx, "test_name", graph_signature_hash="test_hash")
with pytest.raises(WorkflowCheckpointException, match="not a string"):
await runner._restore_executor_states() # pyright: ignore[reportPrivateUsage]
await runner._restore_executor_states()
async def test_runner_restore_executor_states_invalid_state_type():
@@ -715,7 +712,7 @@ async def test_runner_restore_executor_states_invalid_state_type():
runner = Runner([], {executor_a.id: executor_a}, state, ctx, "test_name", graph_signature_hash="test_hash")
with pytest.raises(WorkflowCheckpointException, match="not a dict"):
await runner._restore_executor_states() # pyright: ignore[reportPrivateUsage]
await runner._restore_executor_states()
async def test_runner_restore_executor_states_invalid_state_keys():
@@ -729,7 +726,7 @@ async def test_runner_restore_executor_states_invalid_state_keys():
runner = Runner([], {executor_a.id: executor_a}, state, ctx, "test_name", graph_signature_hash="test_hash")
with pytest.raises(WorkflowCheckpointException, match="not a dict"):
await runner._restore_executor_states() # pyright: ignore[reportPrivateUsage]
await runner._restore_executor_states()
async def test_runner_restore_executor_states_missing_executor():
@@ -742,7 +739,7 @@ async def test_runner_restore_executor_states_missing_executor():
runner = Runner([], {}, state, ctx, "test_name", graph_signature_hash="test_hash")
with pytest.raises(WorkflowCheckpointException, match="not found during state restoration"):
await runner._restore_executor_states() # pyright: ignore[reportPrivateUsage]
await runner._restore_executor_states()
async def test_runner_set_executor_state_invalid_existing_states():
@@ -755,7 +752,7 @@ async def test_runner_set_executor_state_invalid_existing_states():
runner = Runner([], {executor_a.id: executor_a}, state, ctx, "test_name", graph_signature_hash="test_hash")
with pytest.raises(WorkflowCheckpointException, match="not a dictionary"):
await runner._set_executor_state("executor_a", {"key": "value"}) # pyright: ignore[reportPrivateUsage]
await runner._set_executor_state("executor_a", {"key": "value"})
async def test_runner_with_pre_loop_events():
@@ -782,7 +779,7 @@ class EventEmittingExecutor(Executor):
"""An executor that emits events during execution."""
@handler
async def handle(self, message: MockMessage, ctx: WorkflowContext[MockMessage, str]) -> None:
async def handle(self, message: MockMessage, ctx: WorkflowContext[MockMessage, int]) -> None:
# Emit event during processing
await ctx.yield_output(f"processed-{message.data}")
if message.data < 3:
@@ -834,7 +831,7 @@ async def test_runner_restore_executor_states_no_states():
runner = Runner([], {executor_a.id: executor_a}, state, ctx, "test_name", graph_signature_hash="test_hash")
# Should complete without error when no executor states exist
await runner._restore_executor_states() # pyright: ignore[reportPrivateUsage]
await runner._restore_executor_states()
async def test_runner_checkpoint_with_resumed_flag():
@@ -856,7 +853,7 @@ async def test_runner_checkpoint_with_resumed_flag():
state = State()
runner = Runner(edges, executors, state, ctx, "test_name", graph_signature_hash="test_hash")
runner._mark_resumed(5) # pyright: ignore[reportPrivateUsage]
runner._mark_resumed(5)
# Add a message to trigger the checkpoint creation path
await ctx.send_message(WorkflowMessage(data=MockMessage(data=8), source_id="START"))
@@ -873,7 +870,7 @@ async def test_runner_checkpoint_with_resumed_flag():
pass
# After completing, resumed flag should be reset
assert runner._resumed_from_checkpoint is False # pyright: ignore[reportPrivateUsage]
assert runner._resumed_from_checkpoint is False
class ExecutorThatFailsWithEvents(Executor):
@@ -886,7 +883,7 @@ class ExecutorThatFailsWithEvents(Executor):
self._iteration_count = 0
@handler
async def handle(self, message: MockMessage, ctx: WorkflowContext[MockMessage, str]) -> None:
async def handle(self, message: MockMessage, ctx: WorkflowContext[MockMessage, int]) -> None:
self._iteration_count += 1
# First emit an output event to the workflow context
await ctx.yield_output(f"output-before-failure-{message.data}")
@@ -954,7 +951,7 @@ class SlowEventEmittingExecutor(Executor):
self.current_iteration = 0
@handler
async def handle(self, message: MockMessage, ctx: WorkflowContext[MockMessage, str]) -> None:
async def handle(self, message: MockMessage, ctx: WorkflowContext[MockMessage, int]) -> None:
self.current_iteration += 1
# Emit output event
await ctx.yield_output(f"iteration-{self.current_iteration}")
@@ -61,9 +61,9 @@ class TestSuperstepCaching:
state.set("key", "value")
# Value is in pending
assert "key" in state._pending # pyright: ignore[reportPrivateUsage]
assert "key" in state._pending
# Value is NOT in committed
assert "key" not in state._committed # pyright: ignore[reportPrivateUsage]
assert "key" not in state._committed
# But get() still returns it
assert state.get("key") == "value"
@@ -72,14 +72,14 @@ class TestSuperstepCaching:
state.set("key", "value")
# Before commit: in pending, not committed
assert "key" in state._pending # pyright: ignore[reportPrivateUsage]
assert "key" not in state._committed # pyright: ignore[reportPrivateUsage]
assert "key" in state._pending
assert "key" not in state._committed
state.commit()
# After commit: in committed, pending cleared
assert "key" not in state._pending # pyright: ignore[reportPrivateUsage]
assert "key" in state._committed # pyright: ignore[reportPrivateUsage]
assert "key" not in state._pending
assert "key" in state._committed
assert state.get("key") == "value"
def test_discard_clears_pending_without_committing(self) -> None:
@@ -108,7 +108,7 @@ class TestSuperstepCaching:
# get() returns pending value, not committed
assert state.get("key") == "pending_value"
# But committed still has old value
assert state._committed["key"] == "committed_value" # pyright: ignore[reportPrivateUsage]
assert state._committed["key"] == "committed_value"
def test_multiple_sets_before_commit(self) -> None:
state = State()
@@ -130,13 +130,13 @@ class TestDeleteWithSuperstepCaching:
state = State()
state.set("key", "value")
# Key only in pending, not committed
assert "key" in state._pending # pyright: ignore[reportPrivateUsage]
assert "key" not in state._committed # pyright: ignore[reportPrivateUsage]
assert "key" in state._pending
assert "key" not in state._committed
state.delete("key")
# Should be removed from pending
assert "key" not in state._pending # pyright: ignore[reportPrivateUsage]
assert "key" not in state._pending
assert state.get("key") is None
assert state.has("key") is False
@@ -148,14 +148,14 @@ class TestDeleteWithSuperstepCaching:
state.delete("key")
# Key should be marked for deletion in pending (sentinel)
assert "key" in state._pending # pyright: ignore[reportPrivateUsage]
assert "key" in state._pending
# get() should return default (not the sentinel!)
assert state.get("key") is None
assert state.get("key", "default") == "default"
# has() should return False
assert state.has("key") is False
# But committed still has it until commit()
assert "key" in state._committed # pyright: ignore[reportPrivateUsage]
assert "key" in state._committed
def test_delete_committed_key_removed_on_commit(self) -> None:
state = State()
@@ -166,8 +166,8 @@ class TestDeleteWithSuperstepCaching:
state.commit()
# Now it should be gone from committed too
assert "key" not in state._committed # pyright: ignore[reportPrivateUsage]
assert "key" not in state._pending # pyright: ignore[reportPrivateUsage]
assert "key" not in state._committed
assert "key" not in state._pending
def test_delete_key_in_both_pending_and_committed(self) -> None:
"""Test delete when key exists in both pending (modified) and committed."""
@@ -177,8 +177,8 @@ class TestDeleteWithSuperstepCaching:
# Modify the key (now in both pending and committed)
state.set("key", "modified")
assert state._pending["key"] == "modified" # pyright: ignore[reportPrivateUsage]
assert state._committed["key"] == "original" # pyright: ignore[reportPrivateUsage]
assert state._pending["key"] == "modified"
assert state._committed["key"] == "original"
# Delete should mark for deletion from committed
state.delete("key")
@@ -189,8 +189,8 @@ class TestDeleteWithSuperstepCaching:
# After commit, key should be fully removed
state.commit()
assert "key" not in state._committed # pyright: ignore[reportPrivateUsage]
assert "key" not in state._pending # pyright: ignore[reportPrivateUsage]
assert "key" not in state._committed
assert "key" not in state._pending
def test_discard_after_delete_restores_committed_value(self) -> None:
state = State()
@@ -238,12 +238,12 @@ class TestFailureScenarios:
state.set("key3", "value3")
# Before commit - nothing in committed
assert len(state._committed) == 0 # pyright: ignore[reportPrivateUsage]
assert len(state._committed) == 0
state.commit()
# After commit - all three values committed together
assert state._committed == {"key1": "value1", "key2": "value2", "key3": "value3"} # pyright: ignore[reportPrivateUsage]
assert state._committed == {"key1": "value1", "key2": "value2", "key3": "value3"}
def test_repeated_supersteps_are_isolated(self) -> None:
"""Test that each superstep's changes are isolated until committed."""
@@ -300,4 +300,4 @@ class TestExportImport:
# Pending is still there
assert state.get("pending_key") == "pending_value"
assert "pending_key" in state._pending # pyright: ignore[reportPrivateUsage]
assert "pending_key" in state._pending
@@ -36,32 +36,32 @@ def test_normalize_type_to_list_none() -> None:
def test_normalize_type_to_list_union_pipe_syntax() -> None:
"""Test normalize_type_to_list with union types using | syntax."""
result = normalize_type_to_list(str | int) # pyright: ignore[reportArgumentType]
result = normalize_type_to_list(str | int)
assert set(result) == {str, int}
result = normalize_type_to_list(str | int | bool) # pyright: ignore[reportArgumentType]
result = normalize_type_to_list(str | int | bool)
assert set(result) == {str, int, bool}
def test_normalize_type_to_list_union_typing_syntax() -> None:
"""Test normalize_type_to_list with Union[] from typing module."""
result = normalize_type_to_list(Union[str, int]) # pyright: ignore[reportArgumentType]
result = normalize_type_to_list(Union[str, int])
assert set(result) == {str, int}
result = normalize_type_to_list(Union[str, int, bool]) # pyright: ignore[reportArgumentType]
result = normalize_type_to_list(Union[str, int, bool])
assert set(result) == {str, int, bool}
def test_normalize_type_to_list_optional() -> None:
"""Test normalize_type_to_list with Optional types (Union[T, None])."""
# Optional[str] is Union[str, None]
result = normalize_type_to_list(Optional[str]) # pyright: ignore[reportArgumentType]
result = normalize_type_to_list(Optional[str])
assert str in result
assert type(None) in result
assert len(result) == 2
# str | None is equivalent
result = normalize_type_to_list(str | None) # pyright: ignore[reportArgumentType]
result = normalize_type_to_list(str | None)
assert str in result
assert type(None) in result
assert len(result) == 2
@@ -77,7 +77,7 @@ def test_normalize_type_to_list_custom_types() -> None:
result = normalize_type_to_list(CustomMessage)
assert result == [CustomMessage]
result = normalize_type_to_list(CustomMessage | str) # pyright: ignore[reportArgumentType]
result = normalize_type_to_list(CustomMessage | str)
assert set(result) == {CustomMessage, str}
@@ -96,7 +96,7 @@ def test_resolve_type_annotation_actual_types() -> None:
"""Test resolve_type_annotation passes through actual types unchanged."""
assert resolve_type_annotation(str) is str
assert resolve_type_annotation(int) is int
assert resolve_type_annotation(str | int) == str | int # pyright: ignore[reportArgumentType]
assert resolve_type_annotation(str | int) == str | int
def test_resolve_type_annotation_string_builtin() -> None:
@@ -484,8 +484,8 @@ def test_handler_ctx_missing_annotation_raises() -> None:
# Validation now happens at handler registration time, not workflow build time
with pytest.raises(ValueError) as exc:
class BadExecutor(Executor): # pyright: ignore[reportUnusedClass]
@handler # pyright: ignore[reportUnknownArgumentType]
class BadExecutor(Executor):
@handler
async def handle(self, message: str, ctx) -> None: # type: ignore[no-untyped-def]
pass
@@ -496,8 +496,8 @@ def test_handler_ctx_invalid_t_out_entries_raises() -> None:
# Validation now happens at handler registration time, not workflow build time
with pytest.raises(ValueError) as exc:
class BadExecutor(Executor): # pyright: ignore[reportUnusedClass]
@handler # pyright: ignore[reportUnknownArgumentType]
class BadExecutor(Executor):
@handler
async def handle(self, message: str, ctx: WorkflowContext[123]) -> None: # type: ignore[valid-type]
pass
@@ -555,7 +555,7 @@ def test_output_validation_with_valid_output_executors():
)
assert workflow is not None
assert workflow._output_executors == ["executor2"] # pyright: ignore[reportPrivateUsage]
assert workflow._output_executors == ["executor2"]
def test_output_validation_with_multiple_valid_output_executors():
@@ -572,7 +572,7 @@ def test_output_validation_with_multiple_valid_output_executors():
)
assert workflow is not None
assert set(workflow._output_executors) == {"executor1", "executor3"} # pyright: ignore[reportPrivateUsage]
assert set(workflow._output_executors) == {"executor1", "executor3"}
def test_output_validation_fails_for_nonexistent_executor():
@@ -2,9 +2,6 @@
"""Tests for the workflow visualization module."""
from pathlib import Path
from typing import Any
import pytest
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, WorkflowExecutor, WorkflowViz, handler
@@ -28,7 +25,7 @@ class ListStrTargetExecutor(Executor):
@pytest.fixture
def basic_sub_workflow() -> dict[str, Any]:
def basic_sub_workflow():
"""Fixture that creates a basic sub-workflow setup for testing."""
# Create a sub-workflow
sub_exec1 = MockExecutor(id="sub_exec1")
@@ -101,7 +98,7 @@ def test_workflow_viz_export_dot():
assert '"executor1" -> "executor2"' in content
def test_workflow_viz_export_dot_with_filename(tmp_path: Path):
def test_workflow_viz_export_dot_with_filename(tmp_path):
"""Test exporting workflow as DOT format with specified filename."""
executor1 = MockExecutor(id="executor1")
executor2 = MockExecutor(id="executor2")
@@ -206,7 +203,7 @@ def test_workflow_viz_graphviz_binary_not_found():
mock_source_class.return_value = mock_source
# Import the ExecutableNotFound exception for the test
from graphviz.backend.execute import ExecutableNotFound # type: ignore[import-not-found]
from graphviz.backend.execute import ExecutableNotFound
mock_source.render.side_effect = ExecutableNotFound("failed to execute PosixPath('dot')")
@@ -332,7 +329,7 @@ def test_workflow_viz_mermaid_fan_in_edge_group():
assert "s2 --> t" not in mermaid
def test_workflow_viz_sub_workflow_digraph(basic_sub_workflow: dict[str, Any]):
def test_workflow_viz_sub_workflow_digraph(basic_sub_workflow):
"""Test that WorkflowViz can visualize sub-workflows in DOT format."""
main_workflow = basic_sub_workflow["main_workflow"]
@@ -356,7 +353,7 @@ def test_workflow_viz_sub_workflow_digraph(basic_sub_workflow: dict[str, Any]):
assert '"workflow_executor_1/sub_exec1" -> "workflow_executor_1/sub_exec2"' in dot_content
def test_workflow_viz_sub_workflow_mermaid(basic_sub_workflow: dict[str, Any]):
def test_workflow_viz_sub_workflow_mermaid(basic_sub_workflow):
"""Test that WorkflowViz can visualize sub-workflows in Mermaid format."""
main_workflow = basic_sub_workflow["main_workflow"]
@@ -4,7 +4,7 @@ import asyncio
import tempfile
from collections.abc import AsyncIterable, Awaitable, Sequence
from dataclasses import dataclass, field
from typing import Any, Literal, cast, overload
from typing import Any, cast
from uuid import uuid4
import pytest
@@ -13,7 +13,6 @@ from agent_framework import (
AgentExecutor,
AgentResponse,
AgentResponseUpdate,
AgentRunInputs,
AgentSession,
BaseAgent,
Content,
@@ -475,7 +474,7 @@ class StateTrackingExecutor(Executor):
) -> None:
"""Handle the message and track it in workflow state."""
# Get existing messages from workflow state
existing_messages: list[str] = ctx.get_state("processed_messages") or []
existing_messages = ctx.get_state("processed_messages") or []
# Record this message
message_record = f"{message.run_id}:{message.data}"
@@ -834,26 +833,6 @@ class _StreamingTestAgent(BaseAgent):
super().__init__(**kwargs)
self._reply_text = reply_text
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[False] = ...,
session: AgentSession | None = ...,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[True],
session: AgentSession | None = ...,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
@@ -904,10 +883,8 @@ async def test_agent_streaming_vs_non_streaming() -> None:
stream_events.append(event)
# Filter for agent events
agent_response: list[AgentResponse[Any]] = [
cast(AgentResponse[Any], e.data) # pyright: ignore[reportUnknownMemberType]
for e in stream_events
if e.type == "output" and isinstance(e.data, AgentResponse)
agent_response = [
cast(AgentResponse, e.data) for e in stream_events if e.type == "output" and isinstance(e.data, AgentResponse)
]
agent_response_updates = [
e.data for e in stream_events if e.type == "output" and isinstance(e.data, AgentResponseUpdate)
@@ -2,7 +2,7 @@
import uuid
from collections.abc import Awaitable, Sequence
from typing import Any, Literal, overload
from typing import Any
import pytest
from typing_extensions import Never
@@ -713,14 +713,6 @@ class TestWorkflowAgent:
def create_session(self, **kwargs: Any) -> AgentSession:
return AgentSession()
def get_session(self, *, service_session_id: str, **kwargs: Any) -> AgentSession:
return AgentSession()
@overload
def run(self, messages: str | Content | Message | Sequence[str | Content | Message] | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(self, messages: str | Content | Message | Sequence[str | Content | Message] | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
@@ -809,14 +801,6 @@ class TestWorkflowAgent:
def create_session(self, **kwargs: Any) -> AgentSession:
return AgentSession()
def get_session(self, *, service_session_id: str, **kwargs: Any) -> AgentSession:
return AgentSession()
@overload
def run(self, messages: str | Content | Message | Sequence[str | Content | Message] | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(self, messages: str | Content | Message | Sequence[str | Content | Message] | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
@@ -1223,7 +1207,7 @@ class TestWorkflowAgentMergeUpdates:
]
# Compare using role.value for Role enum
actual_sequence_normalized = [(t, r.value if hasattr(r, "value") else r) for t, r in content_sequence] # type: ignore[union-attr]
actual_sequence_normalized = [(t, r.value if hasattr(r, "value") else r) for t, r in content_sequence]
assert actual_sequence_normalized == expected_sequence, (
f"FunctionResultContent should come immediately after FunctionCallContent. "
@@ -1,8 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncIterator, Awaitable
from dataclasses import dataclass
from typing import Any, Literal, overload
from typing import Any
import pytest
@@ -10,12 +9,10 @@ from agent_framework import (
AgentExecutor,
AgentResponse,
AgentResponseUpdate,
AgentRunInputs,
AgentSession,
BaseAgent,
Executor,
Message,
ResponseStream,
WorkflowBuilder,
WorkflowContext,
WorkflowValidationError,
@@ -24,49 +21,22 @@ from agent_framework import (
class DummyAgent(BaseAgent):
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[False] = ...,
session: AgentSession | None = ...,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(
self,
messages: AgentRunInputs | None = ...,
*,
stream: Literal[True],
session: AgentSession | None = ...,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
def run(self, messages=None, *, stream: bool = False, session: AgentSession | None = None, **kwargs): # type: ignore[override]
if stream:
return ResponseStream[AgentResponseUpdate, AgentResponse[Any]](self._run_stream_impl())
return self._run_stream_impl()
return self._run_impl(messages)
async def _run_impl(self, messages: AgentRunInputs | None = None) -> AgentResponse:
async def _run_impl(self, messages=None) -> AgentResponse:
norm: list[Message] = []
if messages:
for m in messages: # type: ignore[union-attr]
for m in messages: # type: ignore[iteration-over-optional]
if isinstance(m, Message):
norm.append(m)
elif isinstance(m, str):
norm.append(Message(role="user", text=m))
return AgentResponse(messages=norm)
async def _run_stream_impl(self) -> AsyncIterator[AgentResponseUpdate]:
async def _run_stream_impl(self): # type: ignore[override]
# Minimal async generator
yield AgentResponseUpdate()
@@ -232,7 +202,7 @@ def test_with_output_from_returns_builder():
builder = WorkflowBuilder(output_executors=[executor_a], start_executor=executor_a)
# Verify builder was created with output_executors
assert builder._output_executors == [executor_a] # pyright: ignore[reportPrivateUsage]
assert builder._output_executors == [executor_a]
def test_with_output_from_with_executor_instances():
@@ -84,7 +84,7 @@ async def test_executor_emits_normal_event() -> None:
class _TestEvent(WorkflowEvent):
def __init__(self, data: Any = None) -> None:
super().__init__("test_event", data=data) # type: ignore[arg-type]
super().__init__("test_event", data=data)
async def test_workflow_context_type_annotations_no_parameter() -> None:
@@ -244,8 +244,8 @@ async def test_workflow_context_missing_annotation_error() -> None:
# Test class-based executor with missing ctx annotation
with pytest.raises(ValueError, match="must have a WorkflowContext"):
class _BadExecutor(Executor): # pyright: ignore[reportUnusedClass]
@handler # pyright: ignore[reportUnknownArgumentType]
class _BadExecutor(Executor):
@handler
async def bad_handler(self, text: str, ctx) -> None: # type: ignore[no-untyped-def]
pass
@@ -264,8 +264,8 @@ async def test_workflow_context_invalid_type_parameter_error() -> None:
# Test class-based executor with invalid type parameter
with pytest.raises(ValueError, match="invalid type entry"):
class _BadExecutor(Executor): # pyright: ignore[reportUnusedClass]
@handler # pyright: ignore[reportUnknownArgumentType]
class _BadExecutor(Executor):
@handler
async def bad_handler(self, text: str, ctx: WorkflowContext[456]) -> None: # type: ignore[valid-type]
pass
@@ -1,14 +1,13 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncIterable, Awaitable
from typing import Annotated, Any, Literal, overload
from collections.abc import AsyncIterable, Awaitable, Sequence
from typing import Annotated, Any
import pytest
from agent_framework import (
AgentResponse,
AgentResponseUpdate,
AgentRunInputs,
AgentSession,
BaseAgent,
Content,
@@ -51,19 +50,14 @@ class _KwargsCapturingAgent(BaseAgent):
super().__init__(name=name, description="Test agent for kwargs capture")
self.captured_kwargs = []
@overload
def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
self.captured_kwargs.append(dict(kwargs))
if stream:
@@ -89,20 +83,15 @@ class _OptionsAwareAgent(BaseAgent):
self.captured_options = []
self.captured_kwargs = []
@overload
def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
options: dict[str, Any] | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
self.captured_options.append(dict(options) if options is not None else None)
self.captured_kwargs.append(dict(kwargs))
if stream:
@@ -200,15 +189,15 @@ async def test_sequential_run_options_does_not_conflict_with_agent_options() ->
break
assert len(agent.captured_options) >= 1
captured_options: dict[str, Any] | None = agent.captured_options[0]
captured_options = agent.captured_options[0]
assert captured_options is not None
assert captured_options.get("store") is False
additional_args: Any = captured_options.get("additional_function_arguments")
additional_args = captured_options.get("additional_function_arguments")
assert isinstance(additional_args, dict)
assert additional_args.get("source") == "workflow-options" # pyright: ignore[reportUnknownMemberType]
assert additional_args.get("custom_data") == custom_data # pyright: ignore[reportUnknownMemberType]
assert additional_args.get("user_token") == user_token # pyright: ignore[reportUnknownMemberType]
assert additional_args.get("source") == "workflow-options"
assert additional_args.get("custom_data") == custom_data
assert additional_args.get("user_token") == user_token
# "options" should be passed once via the dedicated options parameter,
# not duplicated in **kwargs.
@@ -236,13 +225,13 @@ async def test_sequential_run_additional_function_arguments_flattened() -> None:
break
assert len(agent.captured_options) >= 1
captured_options: dict[str, Any] | None = agent.captured_options[0]
captured_options = agent.captured_options[0]
assert captured_options is not None
additional_args: Any = captured_options.get("additional_function_arguments")
additional_args = captured_options.get("additional_function_arguments")
assert isinstance(additional_args, dict)
assert additional_args.get("custom_data") == custom_data # pyright: ignore[reportUnknownMemberType]
assert additional_args.get("user_token") == user_token # pyright: ignore[reportUnknownMemberType]
assert additional_args.get("custom_data") == custom_data
assert additional_args.get("user_token") == user_token
assert "additional_function_arguments" not in additional_args
assert len(agent.captured_kwargs) >= 1
@@ -266,14 +255,14 @@ async def test_sequential_run_additional_function_arguments_merges_with_options(
break
assert len(agent.captured_options) >= 1
captured_options: dict[str, Any] | None = agent.captured_options[0]
captured_options = agent.captured_options[0]
assert captured_options is not None
additional_args: Any = captured_options.get("additional_function_arguments")
additional_args = captured_options.get("additional_function_arguments")
assert isinstance(additional_args, dict)
assert additional_args.get("source") == "workflow-options" # pyright: ignore[reportUnknownMemberType]
assert additional_args.get("custom_data") == {"session_id": "abc123"} # pyright: ignore[reportUnknownMemberType]
assert additional_args.get("user_token") == {"user_name": "alice"} # pyright: ignore[reportUnknownMemberType]
assert additional_args.get("source") == "workflow-options"
assert additional_args.get("custom_data") == {"session_id": "abc123"}
assert additional_args.get("user_token") == {"user_name": "alice"}
assert "additional_function_arguments" not in additional_args
@@ -474,19 +463,14 @@ async def test_kwargs_preserved_on_response_continuation() -> None:
self.captured_kwargs = []
self._asked = False
@overload
def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
self.captured_kwargs.append(dict(kwargs))
if not self._asked:
self._asked = True
@@ -537,19 +521,14 @@ async def test_kwargs_overridden_on_response_continuation() -> None:
self.captured_kwargs = []
self._asked = False
@overload
def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
self.captured_kwargs.append(dict(kwargs))
if not self._asked:
self._asked = True
@@ -604,19 +583,14 @@ async def test_kwargs_empty_value_passed_on_continuation() -> None:
self.captured_kwargs = []
self._asked = False
@overload
def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ...
@overload
def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
def run(
self,
messages: AgentRunInputs | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
self.captured_kwargs.append(dict(kwargs))
if not self._asked:
self._asked = True
@@ -716,8 +690,8 @@ async def test_handoff_kwargs_flow_to_agents() -> None:
workflow = (
HandoffBuilder(termination_condition=lambda conv: len(conv) >= 4)
.participants([agent1, agent2]) # type: ignore[list-item]
.with_start_agent(agent1) # type: ignore[arg-type]
.participants([agent1, agent2])
.with_start_agent(agent1)
.with_autonomous_mode()
.build()
)
@@ -109,7 +109,7 @@ async def test_span_creation_and_attributes(span_exporter: InMemorySpanExporter)
{
"id": "test-workflow-123",
"max_iterations": 100,
"model_dump_json": lambda self: '{"id": "test-workflow-123", "type": "mock"}', # pyright: ignore[reportUnknownLambdaType]
"model_dump_json": lambda self: '{"id": "test-workflow-123", "type": "mock"}',
},
)(),
)
@@ -122,7 +122,7 @@ async def test_span_creation_and_attributes(span_exporter: InMemorySpanExporter)
},
) as workflow_span:
workflow_span.add_event(OtelAttr.WORKFLOW_STARTED)
sending_attributes: dict[str, str | int] = {
sending_attributes = {
OtelAttr.MESSAGE_TYPE: "ResponseMessage",
OtelAttr.MESSAGE_DESTINATION_EXECUTOR_ID: "target-789",
}
@@ -231,7 +231,7 @@ async def test_trace_context_handling(span_exporter: InMemorySpanExporter) -> No
@pytest.mark.parametrize("enable_instrumentation", [False], indirect=True)
async def test_trace_context_disabled_when_tracing_disabled(
enable_instrumentation: bool, span_exporter: InMemorySpanExporter
enable_instrumentation, span_exporter: InMemorySpanExporter
) -> None:
"""Test that no trace context is added when tracing is disabled."""
# Tracing should be disabled by default
@@ -313,7 +313,7 @@ async def test_end_to_end_workflow_tracing(span_exporter: InMemorySpanExporter)
span_exporter.clear()
# Run workflow (this should create run spans)
events: list[Any] = []
events = []
async for event in workflow.run("test input", stream=True):
events.append(event)
@@ -1,7 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
import pytest
from typing_extensions import Never
@@ -38,16 +36,16 @@ async def test_executor_failed_and_workflow_failed_events_streaming():
events.append(ev)
# executor_failed event (type='executor_failed') should be emitted before workflow failed event
executor_failed_events: list[WorkflowEvent[Any]] = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_failed"]
executor_failed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_failed"]
assert executor_failed_events, "executor_failed event should be emitted when start executor fails"
assert executor_failed_events[0].executor_id == "f"
assert executor_failed_events[0].origin is WorkflowEventSource.FRAMEWORK
# Workflow-level failure and FAILED status should be surfaced
failed_events: list[WorkflowEvent[Any]] = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "failed"]
failed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "failed"]
assert failed_events
assert all(e.origin is WorkflowEventSource.FRAMEWORK for e in failed_events)
status: list[WorkflowEvent[Any]] = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "status"]
status = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "status"]
assert status and status[-1].state == WorkflowRunState.FAILED
assert all(e.origin is WorkflowEventSource.FRAMEWORK for e in status)
@@ -96,13 +94,13 @@ async def test_executor_failed_event_from_second_executor_in_chain():
events.append(ev)
# executor_failed event should be emitted for the failing executor
executor_failed_events: list[WorkflowEvent[Any]] = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_failed"]
executor_failed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_failed"]
assert executor_failed_events, "executor_failed event should be emitted when second executor fails"
assert executor_failed_events[0].executor_id == "failing"
assert executor_failed_events[0].origin is WorkflowEventSource.FRAMEWORK
# Workflow-level failure should also be surfaced
failed_events: list[WorkflowEvent[Any]] = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "failed"]
failed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "failed"]
assert failed_events
assert all(e.origin is WorkflowEventSource.FRAMEWORK for e in failed_events)
@@ -388,15 +388,11 @@ class DeclarativeWorkflowState:
from System.Globalization import CultureInfo
original_culture = CultureInfo.CurrentCulture
original_ui_culture = CultureInfo.CurrentUICulture
en_us_culture = CultureInfo("en-US")
CultureInfo.CurrentCulture = en_us_culture
CultureInfo.CurrentUICulture = en_us_culture
CultureInfo.CurrentCulture = CultureInfo("en-US")
try:
return engine.eval(formula, symbols=symbols)
finally:
CultureInfo.CurrentCulture = original_culture
CultureInfo.CurrentUICulture = original_ui_culture
except ValueError as e:
error_msg = str(e)
# Handle undefined variable errors gracefully by returning None
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"powerfx>=0.0.31; python_version < '3.14'",
"pyyaml>=6.0,<7.0",
]
@@ -493,31 +493,6 @@ class TestPowerFxUndefinedVariables:
result = state.eval("=Local.Something.Nested.Deep")
assert result is None
async def test_undefined_variable_returns_none_with_non_english_ui_culture(self, mock_state):
"""Test that undefined variables return None even when CurrentUICulture is non-English.
Regression test for #4321: on non-English systems, CurrentUICulture causes
PowerFx to emit localized error messages that don't match the English
string guards ("isn't recognized", "Name isn't valid"), crashing the workflow.
The fix sets CurrentUICulture to en-US alongside CurrentCulture before eval.
"""
from System.Globalization import CultureInfo
state = DeclarativeWorkflowState(mock_state)
state.initialize()
# Simulate a non-English UI culture (e.g. Italian)
original_ui_culture = CultureInfo.CurrentUICulture
CultureInfo.CurrentUICulture = CultureInfo("it-IT")
try:
# Should return None, not raise ValueError with Italian error text
result = state.eval("=Local.StatusConversationId")
assert result is None
# Verify the production code restored CurrentUICulture after eval
assert str(CultureInfo.CurrentUICulture) == str(CultureInfo("it-IT"))
finally:
CultureInfo.CurrentUICulture = original_ui_culture
class TestStringInterpolation:
"""Test string interpolation patterns."""
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
"python-dotenv>=1.0.0",
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"durabletask>=1.3.0",
"durabletask-azuremanaged>=1.3.0",
"python-dateutil>=2.8.0",
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"foundry-local-sdk>=0.5.1,<1",
]
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"github-copilot-sdk>=0.1.0",
]
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
]
[project.optional-dependencies]
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"mem0ai>=1.0.0",
]
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"ollama >= 0.5.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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
]
[tool.uv]
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"azure-core>=1.30.0",
"httpx>=0.27.0",
]
+2 -2
View File
@@ -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.0b260304"
version = "1.0.0b260225"
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.0.0rc3",
"agent-framework-core>=1.0.0rc2",
"redis>=6.4.0",
"redisvl>=0.8.2",
"numpy>=2.2.6"

Some files were not shown because too many files have changed in this diff Show More