Compare commits

..
Author SHA1 Message Date
westey f109a5fc9a Fix formatting 2025-12-04 09:18:48 +00:00
westey c5f003498b Fix formatting issue 2025-12-04 07:17:51 +00:00
westeyandGitHub 6c8ef728fa Merge branch 'main' into contextual-function-provider-port 2025-12-03 12:18:48 +00:00
CopilotGitHubwestey-mcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
7965285808 .NET: Add serialization support for ContextualFunctionProvider (#2601)
* Initial plan

* Add serialization support for ContextualFunctionProvider

- Added constructor overload that accepts JsonElement serializedState
- Implemented Serialize method to persist recent messages
- Added ContextualFunctionProviderState class
- Registered state class with AgentJsonUtilities JsonContext
- Added comprehensive unit tests for serialization/deserialization

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Fix constructor parameter ordering and make default more explicit

- Reordered constructor parameters to maintain consistency (options before jsonSerializerOptions)
- Made default JsonElement explicit with default(JsonElement) in constructor chaining
- All tests continue to pass

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Improve XML documentation comments for clarity

- Updated jsonSerializerOptions parameter comments to clarify actual usage
- Made it clear that AgentJsonUtilities.DefaultOptions is used
- All tests continue to pass

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Change NumberOfRecentMessagesInContext to 2 to demonstrate truncation

- Updated test to use limit of 2 messages instead of 3
- Now test shows truncation in action (3 messages added, only 2 kept)
- Updated assertion to expect 2 messages in serialized state
- All tests pass

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Apply suggestion from @westey-m

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>
2025-12-03 12:18:19 +00:00
CopilotGitHubwestey-mcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
9008bb8cb7 Skip adding messages to context when invocation fails with exception (#2600)
* Initial plan

* Add exception check in InvokedAsync to skip adding messages when invocation fails

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Refactor InvokedAsync to use early return pattern

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>
2025-12-03 11:56:29 +00:00
938961d2cc Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-27 18:16:21 +00:00
westey d9ce6ee292 Format files. 2025-11-27 17:33:37 +00:00
westey 2a3e41dbdd Port ContextualFunctionProvider from SK 2025-11-27 17:14:47 +00:00
193 changed files with 7371 additions and 10337 deletions
@@ -1636,4 +1636,4 @@ The property mapping guide from a `AutoFunctionInvocationContext` to a `Function
| Result | Use `return` from the delegate |
| Terminate | Terminate |
| CancellationToken | provided via argument to middleware delegate |
| Arguments | Arguments |
| Arguments | Arguments |
+1 -2
View File
@@ -21,7 +21,7 @@
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.3" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.4" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.8" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.7" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.7.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
@@ -46,7 +46,6 @@
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.0" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.13.1" />
@@ -14,6 +14,11 @@
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
@@ -15,6 +15,11 @@
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
@@ -2,11 +2,11 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);IDE0059;NU1510</NoWarn>
<NoWarn>$(NoWarn);IDE0059</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -14,10 +14,6 @@
<PackageReference Include="Mscc.GenerativeAI.Microsoft" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0' or '$(TargetFramework)' == 'net9.0'">
<PackageReference Include="System.Net.Security" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
@@ -32,7 +32,7 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Instructions = "You are good at telling jokes.",
Name = "Joker",
AIContextProviderFactory = (ctx) => new ChatHistoryMemoryProvider(
vectorStore,
@@ -30,7 +30,7 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details.",
AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined
// If each thread should have its own Mem0 scope, you can create a new id per thread here:
// ? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ThreadId = Guid.NewGuid().ToString() })
@@ -33,7 +33,7 @@ ChatClient chatClient = new AzureOpenAIClient(
// and its storage to that user id.
AIAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = "You are a friendly assistant. Always address the user by their name." },
Instructions = "You are a friendly assistant. Always address the user by their name.",
AIContextProviderFactory = ctx => new UserInfoMemory(chatClient.AsIChatClient(), ctx.SerializedState, ctx.JsonSerializerOptions)
});
@@ -62,7 +62,7 @@ AIAgent agent = azureOpenAIClient
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
@@ -71,7 +71,7 @@ AIAgent agent = azureOpenAIClient
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief." },
Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief.",
AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
@@ -29,7 +29,7 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
AIContextProviderFactory = ctx => new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
@@ -22,7 +22,7 @@ ChatClient chatClient = new AzureOpenAIClient(
.GetChatClient(deploymentName);
// Create the ChatClientAgent with the specified name and instructions.
ChatClientAgent agent = chatClient.CreateAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
ChatClientAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions(name: "HelpfulAssistant", instructions: "You are a helpful assistant."));
// Set PersonInfo as the type parameter of RunAsync method to specify the expected structured output from the agent and invoke the agent with some unstructured input.
AgentRunResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("Please provide information about John Smith, who is a 35-year-old software engineer.");
@@ -34,10 +34,12 @@ Console.WriteLine($"Age: {response.Result.Age}");
Console.WriteLine($"Occupation: {response.Result.Occupation}");
// Create the ChatClientAgent with the specified name, instructions, and expected structured output the agent should produce.
ChatClientAgent agentWithPersonInfo = chatClient.CreateAIAgent(new ChatClientAgentOptions()
ChatClientAgent agentWithPersonInfo = chatClient.CreateAIAgent(new ChatClientAgentOptions(name: "HelpfulAssistant", instructions: "You are a helpful assistant.")
{
Name = "HelpfulAssistant",
ChatOptions = new() { Instructions = "You are a helpful assistant.", ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<PersonInfo>() }
ChatOptions = new()
{
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<PersonInfo>()
}
});
// Invoke the agent with some unstructured input while streaming, to extract the structured information from.
@@ -28,7 +28,7 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Instructions = "You are good at telling jokes.",
Name = "Joker",
ChatMessageStoreFactory = ctx =>
{
@@ -18,7 +18,8 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Add agent options to the service collection.
builder.Services.AddSingleton(new ChatClientAgentOptions() { Name = "Joker", ChatOptions = new() { Instructions = "You are good at telling jokes." } });
builder.Services.AddSingleton(
new ChatClientAgentOptions(instructions: "You are good at telling jokes.", name: "Joker"));
// Add a chat client to the service collection.
builder.Services.AddKeyedChatClient("AzureOpenAI", (sp) => new AzureOpenAIClient(
@@ -16,6 +16,10 @@
<PackageReference Include="ModelContextProtocol" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
</ItemGroup>
@@ -21,7 +21,7 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Instructions = "You are good at telling jokes.",
Name = "Joker",
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions)
});
@@ -24,12 +24,10 @@ AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential(
// Create ChatClientAgent directly
ChatClientAgent agent = await aiProjectClient.CreateAIAgentAsync(
model: deploymentName,
new ChatClientAgentOptions()
new ChatClientAgentOptions(name: AssistantName, instructions: AssistantInstructions)
{
Name = AssistantName,
ChatOptions = new()
{
Instructions = AssistantInstructions,
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<PersonInfo>()
}
});
@@ -46,12 +44,10 @@ Console.WriteLine($"Occupation: {response.Result.Occupation}");
// Create the ChatClientAgent with the specified name, instructions, and expected structured output the agent should produce.
ChatClientAgent agentWithPersonInfo = aiProjectClient.CreateAIAgent(
model: deploymentName,
new ChatClientAgentOptions()
new ChatClientAgentOptions(name: AssistantName, instructions: AssistantInstructions)
{
Name = AssistantName,
ChatOptions = new()
{
Instructions = AssistantInstructions,
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<PersonInfo>()
}
});
@@ -17,6 +17,10 @@
<PackageReference Include="ModelContextProtocol" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
@@ -34,9 +34,9 @@ AIAgent agent = await persistentAgentsClient.CreateAIAgentAsync(
options: new()
{
Name = "MicrosoftLearnAgent",
Instructions = "You answer questions by searching the Microsoft Learn content only.",
ChatOptions = new()
{
Instructions = "You answer questions by searching the Microsoft Learn content only.",
Tools = [mcpTool]
},
});
@@ -67,9 +67,9 @@ AIAgent agentWithRequiredApproval = await persistentAgentsClient.CreateAIAgentAs
options: new()
{
Name = "MicrosoftLearnAgentWithApproval",
Instructions = "You answer questions by searching the Microsoft Learn content only.",
ChatOptions = new()
{
Instructions = "You answer questions by searching the Microsoft Learn content only.",
Tools = [mcpToolWithApproval]
},
});
@@ -118,11 +118,10 @@ internal sealed class SloganWriterExecutor : Executor
/// <param name="chatClient">The chat client to use for the AI agent.</param>
public SloganWriterExecutor(string id, IChatClient chatClient) : base(id)
{
ChatClientAgentOptions agentOptions = new()
ChatClientAgentOptions agentOptions = new(instructions: "You are a professional slogan writer. You will be given a task to create a slogan.")
{
ChatOptions = new()
{
Instructions = "You are a professional slogan writer. You will be given a task to create a slogan.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<SloganResult>()
}
};
@@ -194,11 +193,10 @@ internal sealed class FeedbackExecutor : Executor<SloganResult>
/// <param name="chatClient">The chat client to use for the AI agent.</param>
public FeedbackExecutor(string id, IChatClient chatClient) : base(id)
{
ChatClientAgentOptions agentOptions = new()
ChatClientAgentOptions agentOptions = new(instructions: "You are a professional editor. You will be given a slogan and the task it is meant to accomplish.")
{
ChatOptions = new()
{
Instructions = "You are a professional editor. You will be given a slogan and the task it is meant to accomplish.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<FeedbackResult>()
}
};
@@ -85,11 +85,10 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for spam detection</returns>
private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions()
new(chatClient, new ChatClientAgentOptions(instructions: "You are a spam detection assistant that identifies spam emails.")
{
ChatOptions = new()
{
Instructions = "You are a spam detection assistant that identifies spam emails.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<DetectionResult>()
}
});
@@ -99,11 +98,10 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for email assistance</returns>
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions()
new(chatClient, new ChatClientAgentOptions(instructions: "You are an email assistant that helps users draft responses to emails with professionalism.")
{
ChatOptions = new()
{
Instructions = "You are an email assistant that helps users draft responses to emails with professionalism.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<EmailResponse>()
}
});
@@ -100,11 +100,10 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for spam detection</returns>
private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions()
new(chatClient, new ChatClientAgentOptions(instructions: "You are a spam detection assistant that identifies spam emails. Be less confident in your assessments.")
{
ChatOptions = new()
{
Instructions = "You are a spam detection assistant that identifies spam emails. Be less confident in your assessments.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<DetectionResult>()
}
});
@@ -114,11 +113,10 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for email assistance</returns>
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions()
new(chatClient, new ChatClientAgentOptions(instructions: "You are an email assistant that helps users draft responses to emails with professionalism.")
{
ChatOptions = new()
{
Instructions = "You are an email assistant that helps users draft responses to emails with professionalism.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<EmailResponse>()
}
});
@@ -140,11 +140,10 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for email analysis</returns>
private static ChatClientAgent GetEmailAnalysisAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions()
new(chatClient, new ChatClientAgentOptions(instructions: "You are a spam detection assistant that identifies spam emails.")
{
ChatOptions = new()
{
Instructions = "You are a spam detection assistant that identifies spam emails.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<AnalysisResult>()
}
});
@@ -154,11 +153,10 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for email assistance</returns>
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions()
new(chatClient, new ChatClientAgentOptions(instructions: "You are an email assistant that helps users draft responses to emails with professionalism.")
{
ChatOptions = new()
{
Instructions = "You are an email assistant that helps users draft responses to emails with professionalism.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<EmailResponse>()
}
});
@@ -168,11 +166,10 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for email summarization</returns>
private static ChatClientAgent GetEmailSummaryAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions()
new(chatClient, new ChatClientAgentOptions(instructions: "You are an assistant that helps users summarize emails.")
{
ChatOptions = new()
{
Instructions = "You are an assistant that helps users summarize emails.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<EmailSummary>()
}
});
@@ -285,19 +285,19 @@ internal sealed class CriticExecutor : Executor<ChatMessage, CriticDecision>
this._agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
{
Name = "Critic",
Instructions = """
You are a constructive critic. Review the content and provide specific feedback.
Always try to provide actionable suggestions for improvement and strive to identify improvement points.
Only approve if the content is high quality, clear, and meets the original requirements and you see no improvement points.
Provide your decision as structured output with:
- approved: true if content is good, false if revisions needed
- feedback: specific improvements needed (empty if approved)
Be concise but specific in your feedback.
""",
ChatOptions = new()
{
Instructions = """
You are a constructive critic. Review the content and provide specific feedback.
Always try to provide actionable suggestions for improvement and strive to identify improvement points.
Only approve if the content is high quality, clear, and meets the original requirements and you see no improvement points.
Provide your decision as structured output with:
- approved: true if content is good, false if revisions needed
- feedback: specific improvements needed (empty if approved)
Be concise but specific in your feedback.
""",
ResponseFormat = ChatResponseFormat.ForJsonSchema<CriticDecision>()
}
});
@@ -33,9 +33,9 @@ public class WeatherForecastAgent : DelegatingAIAgent
new ChatClientAgentOptions()
{
Name = AgentName,
Instructions = AgentInstructions,
ChatOptions = new ChatOptions()
{
Instructions = AgentInstructions,
Tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))],
// We want the agent to return structured output in a known format
// so that we can easily create adaptive cards from the response.
@@ -14,6 +14,8 @@
<ItemGroup>
<PackageReference Include="A2A" />
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
</ItemGroup>
<PropertyGroup>
@@ -23,6 +23,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Net.Http.Json" />
<PackageReference Include="System.Threading.Channels" />
</ItemGroup>
@@ -124,7 +124,7 @@ public abstract class AIContextProvider
/// that will be used. Context providers can use this information to determine what additional context
/// should be provided for the invocation.
/// </remarks>
public sealed class InvokingContext
public class InvokingContext
{
/// <summary>
/// Initializes a new instance of the <see cref="InvokingContext"/> class with the specified request messages.
@@ -153,7 +153,7 @@ public abstract class AIContextProvider
/// request messages that were used and the response messages that were generated. It also indicates
/// whether the invocation succeeded or failed.
/// </remarks>
public sealed class InvokedContext
public class InvokedContext
{
/// <summary>
/// Initializes a new instance of the <see cref="InvokedContext"/> class with the specified request messages.
@@ -45,20 +45,14 @@ public static class AnthropicBetaServiceExtensions
{
var options = new ChatClientAgentOptions
{
Instructions = instructions,
Name = name,
Description = description,
};
if (!string.IsNullOrWhiteSpace(instructions))
{
options.ChatOptions ??= new();
options.ChatOptions.Instructions = instructions;
}
if (tools is { Count: > 0 })
{
options.ChatOptions ??= new();
options.ChatOptions.Tools = tools;
options.ChatOptions = new ChatOptions { Tools = tools };
}
var chatClient = betaService.AsIChatClient(model, defaultMaxTokens ?? DefaultMaxTokens);
@@ -45,20 +45,14 @@ public static class AnthropicClientExtensions
{
var options = new ChatClientAgentOptions
{
Instructions = instructions,
Name = name,
Description = description,
};
if (!string.IsNullOrWhiteSpace(instructions))
{
options.ChatOptions ??= new();
options.ChatOptions.Instructions = instructions;
}
if (tools is { Count: > 0 })
{
options.ChatOptions ??= new();
options.ChatOptions.Tools = tools;
options.ChatOptions = new ChatOptions { Tools = tools };
}
var chatClient = client.AsIChatClient(model, defaultMaxTokens ?? DefaultMaxTokens);
@@ -67,17 +67,12 @@ public static class PersistentAgentsClientExtensions
chatClient = clientFactory(chatClient);
}
if (!string.IsNullOrWhiteSpace(persistentAgentMetadata.Instructions) && chatOptions?.Instructions is null)
{
chatOptions ??= new ChatOptions();
chatOptions.Instructions = persistentAgentMetadata.Instructions;
}
return new ChatClientAgent(chatClient, options: new()
{
Id = persistentAgentMetadata.Id,
Name = persistentAgentMetadata.Name,
Description = persistentAgentMetadata.Description,
Instructions = persistentAgentMetadata.Instructions,
ChatOptions = chatOptions
}, services: services);
}
@@ -212,17 +207,12 @@ public static class PersistentAgentsClientExtensions
chatClient = clientFactory(chatClient);
}
if (!string.IsNullOrWhiteSpace(persistentAgentMetadata.Instructions) && options.ChatOptions?.Instructions is null)
{
options.ChatOptions ??= new ChatOptions();
options.ChatOptions.Instructions = persistentAgentMetadata.Instructions;
}
var agentOptions = new ChatClientAgentOptions()
{
Id = persistentAgentMetadata.Id,
Name = options.Name ?? persistentAgentMetadata.Name,
Description = options.Description ?? persistentAgentMetadata.Description,
Instructions = options.Instructions ?? persistentAgentMetadata.Instructions,
ChatOptions = options.ChatOptions,
AIContextProviderFactory = options.AIContextProviderFactory,
ChatMessageStoreFactory = options.ChatMessageStoreFactory,
@@ -463,7 +453,7 @@ public static class PersistentAgentsClientExtensions
model: model,
name: options.Name,
description: options.Description,
instructions: options.ChatOptions?.Instructions,
instructions: options.Instructions,
tools: toolDefinitionsAndResources.ToolDefinitions,
toolResources: toolDefinitionsAndResources.ToolResources,
temperature: null,
@@ -523,7 +513,7 @@ public static class PersistentAgentsClientExtensions
model: model,
name: options.Name,
description: options.Description,
instructions: options.ChatOptions?.Instructions,
instructions: options.Instructions,
tools: toolDefinitionsAndResources.ToolDefinitions,
toolResources: toolDefinitionsAndResources.ToolResources,
temperature: null,
@@ -393,7 +393,7 @@ public static partial class AzureAIProjectChatClientExtensions
PromptAgentDefinition agentDefinition = new(model)
{
Instructions = options.ChatOptions?.Instructions,
Instructions = options.Instructions,
Temperature = options.ChatOptions?.Temperature,
TopP = options.ChatOptions?.TopP,
TextOptions = new() { TextFormat = ToOpenAIResponseTextFormat(options.ChatOptions?.ResponseFormat, options.ChatOptions) }
@@ -459,7 +459,7 @@ public static partial class AzureAIProjectChatClientExtensions
PromptAgentDefinition agentDefinition = new(model)
{
Instructions = options.ChatOptions?.Instructions,
Instructions = options.Instructions,
Temperature = options.ChatOptions?.Temperature,
TopP = options.ChatOptions?.TopP,
TextOptions = new() { TextFormat = ToOpenAIResponseTextFormat(options.ChatOptions?.ResponseFormat, options.ChatOptions) }
@@ -822,9 +822,10 @@ public static partial class AzureAIProjectChatClientExtensions
if (agentDefinition is PromptAgentDefinition promptAgentDefinition)
{
agentOptions.ChatOptions ??= chatOptions?.Clone() ?? new();
agentOptions.ChatOptions.Instructions = promptAgentDefinition.Instructions;
agentOptions.Instructions = promptAgentDefinition.Instructions;
agentOptions.ChatOptions.Temperature = promptAgentDefinition.Temperature;
agentOptions.ChatOptions.TopP = promptAgentDefinition.TopP;
agentOptions.ChatOptions.Instructions = promptAgentDefinition.Instructions;
}
if (agentTools is { Count: > 0 })
@@ -38,6 +38,7 @@ public sealed class ChatClientPromptAgentFactory : PromptAgentFactory
{
Name = promptAgent.Name,
Description = promptAgent.Description,
Instructions = promptAgent.Instructions?.ToTemplateString(),
ChatOptions = promptAgent.GetChatOptions(this.Engine, this._functions),
};
@@ -35,7 +35,7 @@ public static class PromptAgentExtensions
return new ChatOptions()
{
Instructions = promptAgent.Instructions?.ToTemplateString(),
Instructions = promptAgent.ResponseInstructions?.ToTemplateString(),
Temperature = (float?)modelOptions?.Temperature?.Eval(engine),
MaxOutputTokens = (int?)modelOptions?.MaxOutputTokens?.Eval(engine),
TopP = (float?)modelOptions?.TopP?.Eval(engine),
@@ -14,6 +14,8 @@
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<ItemGroup>
@@ -18,6 +18,10 @@
<PackageReference Include="A2A" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
<ProjectReference Include="..\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
@@ -25,15 +25,16 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Microsoft.Agents.AI.AGUI\Shared\**\*.cs" LinkBase="Shared" />
<Compile Remove="ServerSentEventsResult.cs" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '10.0'))" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests" />
<InternalsVisibleTo Include="Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests" />
@@ -26,6 +26,8 @@
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<ItemGroup>
@@ -77,17 +77,12 @@ public static class OpenAIAssistantClientExtensions
chatClient = clientFactory(chatClient);
}
if (!string.IsNullOrWhiteSpace(assistantMetadata.Instructions) && chatOptions?.Instructions is null)
{
chatOptions ??= new ChatOptions();
chatOptions.Instructions = assistantMetadata.Instructions;
}
return new ChatClientAgent(chatClient, options: new()
{
Id = assistantMetadata.Id,
Name = assistantMetadata.Name,
Description = assistantMetadata.Description,
Instructions = assistantMetadata.Instructions,
ChatOptions = chatOptions
}, services: services);
}
@@ -220,17 +215,12 @@ public static class OpenAIAssistantClientExtensions
chatClient = clientFactory(chatClient);
}
if (string.IsNullOrWhiteSpace(options.ChatOptions?.Instructions) && !string.IsNullOrWhiteSpace(assistantMetadata.Instructions))
{
options.ChatOptions ??= new ChatOptions();
options.ChatOptions.Instructions = assistantMetadata.Instructions;
}
var mergedOptions = new ChatClientAgentOptions()
{
Id = assistantMetadata.Id,
Name = options.Name ?? assistantMetadata.Name,
Description = options.Description ?? assistantMetadata.Description,
Instructions = options.Instructions ?? assistantMetadata.Instructions,
ChatOptions = options.ChatOptions,
AIContextProviderFactory = options.AIContextProviderFactory,
ChatMessageStoreFactory = options.ChatMessageStoreFactory,
@@ -349,10 +339,10 @@ public static class OpenAIAssistantClientExtensions
{
Name = name,
Description = description,
ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
{
Tools = tools,
Instructions = instructions
}
},
clientFactory,
@@ -387,7 +377,7 @@ public static class OpenAIAssistantClientExtensions
{
Name = options.Name,
Description = options.Description,
Instructions = options.ChatOptions?.Instructions,
Instructions = options.Instructions,
};
// Convert AITools to ToolDefinitions and ToolResources
@@ -453,10 +443,10 @@ public static class OpenAIAssistantClientExtensions
{
Name = name,
Description = description,
ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
{
Tools = tools,
Instructions = instructions,
}
},
clientFactory,
@@ -494,7 +484,7 @@ public static class OpenAIAssistantClientExtensions
{
Name = options.Name,
Description = options.Description,
Instructions = options.ChatOptions?.Instructions,
Instructions = options.Instructions,
};
// Convert AITools to ToolDefinitions and ToolResources
@@ -47,9 +47,9 @@ public static class OpenAIChatClientExtensions
{
Name = name,
Description = description,
ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
{
Instructions = instructions,
Tools = tools,
}
},
@@ -50,9 +50,9 @@ public static class OpenAIResponseClientExtensions
{
Name = name,
Description = description,
ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
{
Instructions = instructions,
Tools = tools,
}
},
@@ -32,7 +32,7 @@ public class OpenAIChatClientAgent : DelegatingAIAgent
{
Name = name,
Description = description,
ChatOptions = new ChatOptions() { Instructions = instructions },
Instructions = instructions,
}, loggerFactory)
{
}
@@ -32,7 +32,7 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
{
Name = name,
Description = description,
ChatOptions = new ChatOptions() { Instructions = instructions },
Instructions = instructions,
}, loggerFactory)
{
}
@@ -69,6 +69,7 @@ internal static partial class AgentJsonUtilities
[JsonSerializable(typeof(ChatClientAgentThread.ThreadState))]
[JsonSerializable(typeof(TextSearchProvider.TextSearchProviderState))]
[JsonSerializable(typeof(ChatHistoryMemoryProvider.ChatHistoryMemoryProviderState))]
[JsonSerializable(typeof(Functions.ContextualFunctionProvider.ContextualFunctionProviderState))]
[ExcludeFromCodeCoverage]
internal sealed partial class JsonContext : JsonSerializerContext;
@@ -59,13 +59,13 @@ public sealed partial class ChatClientAgent : AIAgent
chatClient,
new ChatClientAgentOptions
{
ChatOptions = (tools is null && string.IsNullOrWhiteSpace(instructions)) ? null : new ChatOptions
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions
{
Tools = tools,
Instructions = instructions
},
Name = name,
Description = description
}
},
loggerFactory,
services)
@@ -141,7 +141,7 @@ public sealed partial class ChatClientAgent : AIAgent
/// These instructions are typically provided to the AI model as system messages to establish
/// the context and expected behavior for the agent's responses.
/// </remarks>
public string? Instructions => this._agentOptions?.ChatOptions?.Instructions;
public string? Instructions => this._agentOptions?.Instructions;
/// <summary>
/// Gets of the default <see cref="ChatOptions"/> used by the agent.
@@ -492,6 +492,7 @@ public sealed partial class ChatClientAgent : AIAgent
requestChatOptions.AllowMultipleToolCalls ??= this._agentOptions.ChatOptions.AllowMultipleToolCalls;
requestChatOptions.ConversationId ??= this._agentOptions.ChatOptions.ConversationId;
requestChatOptions.FrequencyPenalty ??= this._agentOptions.ChatOptions.FrequencyPenalty;
requestChatOptions.Instructions ??= this._agentOptions.ChatOptions.Instructions;
requestChatOptions.MaxOutputTokens ??= this._agentOptions.ChatOptions.MaxOutputTokens;
requestChatOptions.ModelId ??= this._agentOptions.ChatOptions.ModelId;
requestChatOptions.PresencePenalty ??= this._agentOptions.ChatOptions.PresencePenalty;
@@ -502,13 +503,6 @@ public sealed partial class ChatClientAgent : AIAgent
requestChatOptions.TopK ??= this._agentOptions.ChatOptions.TopK;
requestChatOptions.ToolMode ??= this._agentOptions.ChatOptions.ToolMode;
// Merge instructions by concatenating them if both are present.
requestChatOptions.Instructions = !string.IsNullOrWhiteSpace(requestChatOptions.Instructions) && !string.IsNullOrWhiteSpace(this.Instructions)
? $"{this.Instructions}\n{requestChatOptions.Instructions}"
: (!string.IsNullOrWhiteSpace(requestChatOptions.Instructions)
? requestChatOptions.Instructions
: this.Instructions);
// Merge only the additional properties from the agent if they are not already set in the request options.
if (requestChatOptions.AdditionalProperties is not null && this._agentOptions.ChatOptions.AdditionalProperties is not null)
{
@@ -691,6 +685,12 @@ public sealed partial class ChatClientAgent : AIAgent
""");
}
if (!string.IsNullOrWhiteSpace(this.Instructions))
{
chatOptions ??= new();
chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? this.Instructions : $"{this.Instructions}\n{chatOptions.Instructions}";
}
// Only create or update ChatOptions if we have an id on the thread and we don't have the same one already in ChatOptions.
if (!string.IsNullOrWhiteSpace(typedThread.ConversationId) && typedThread.ConversationId != chatOptions?.ConversationId)
{
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Extensions.AI;
@@ -14,8 +15,37 @@ namespace Microsoft.Agents.AI;
/// identifier, display name, operational instructions, and a descriptive summary. It can be used to store and transfer
/// agent-related metadata within a chat application.
/// </remarks>
public sealed class ChatClientAgentOptions
public class ChatClientAgentOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="ChatClientAgentOptions"/> class.
/// </summary>
public ChatClientAgentOptions()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChatClientAgentOptions"/> class with the specified parameters.
/// </summary>
/// <remarks>If <paramref name="tools"/> is provided, a new <see cref="ChatOptions"/> instance is created
/// with the specified instructions and tools.</remarks>
/// <param name="instructions">The instructions or guidelines for the chat client agent. Can be <see langword="null"/> if not specified.</param>
/// <param name="name">The name of the chat client agent. Can be <see langword="null"/> if not specified.</param>
/// <param name="description">The description of the chat client agent. Can be <see langword="null"/> if not specified.</param>
/// <param name="tools">A list of <see cref="AITool"/> instances available to the chat client agent. Can be <see langword="null"/> if no
/// tools are specified.</param>
public ChatClientAgentOptions(string? instructions, string? name = null, string? description = null, IList<AITool>? tools = null)
{
this.Name = name;
this.Instructions = instructions;
this.Description = description;
if (tools is not null)
{
(this.ChatOptions ??= new()).Tools = tools;
}
}
/// <summary>
/// Gets or sets the agent id.
/// </summary>
@@ -26,6 +56,11 @@ public sealed class ChatClientAgentOptions
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the agent instructions.
/// </summary>
public string? Instructions { get; set; }
/// <summary>
/// Gets or sets the agent description.
/// </summary>
@@ -71,6 +106,7 @@ public sealed class ChatClientAgentOptions
{
Id = this.Id,
Name = this.Name,
Instructions = this.Instructions,
Description = this.Description,
ChatOptions = this.ChatOptions?.Clone(),
ChatMessageStoreFactory = this.ChatMessageStoreFactory,
@@ -0,0 +1,230 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.VectorData;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Functions;
/// <summary>
/// Represents a contextual function provider that performs RAG (Retrieval-Augmented Generation) on the provided functions to identify
/// the most relevant functions for the current context. The provider vectorizes the provided function names and descriptions
/// and stores them in the specified vector store, allowing for a vector search to find the most relevant
/// functions for a given context and provide the functions to the AI model/agent.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>
/// The provider is designed to work with in-memory vector stores. Using other vector stores
/// will require the data synchronization and data lifetime management to be done by the caller.
/// </item>
/// <item>
/// The in-memory vector store is supposed to be created per provider and not shared between providers
/// unless each provider uses a different collection name. Not following this may lead to a situation
/// where one provider identifies a function belonging to another provider as relevant and, as a result,
/// an attempt to access it by the first provider will fail because the function is not registered with it.
/// </item>
/// <item>
/// The provider uses function name as a key for the records and as such the specified vector store
/// should support record keys of string type.
/// </item>
/// </list>
/// </remarks>
public sealed class ContextualFunctionProvider : AIContextProvider
{
private readonly FunctionStore _functionStore;
private readonly ConcurrentQueue<ChatMessage> _recentMessages = [];
private readonly ContextualFunctionProviderOptions _options;
private bool _areFunctionsVectorized;
/// <summary>
/// Initializes a new instance of the <see cref="ContextualFunctionProvider"/> class.
/// </summary>
/// <param name="vectorStore">An instance of a vector store.</param>
/// <param name="vectorDimensions">The number of dimensions to use for the memory embeddings.</param>
/// <param name="functions">The functions to vectorize and store for searching related functions.</param>
/// <param name="maxNumberOfFunctions">The maximum number of relevant functions to retrieve from the vector store.</param>
/// <param name="options">Further optional settings for configuring the provider.</param>
/// <param name="loggerFactory">The logger factory to use for logging. If not provided, no logging will be performed.</param>
public ContextualFunctionProvider(
VectorStore vectorStore,
int vectorDimensions,
IEnumerable<AIFunction> functions,
int maxNumberOfFunctions,
ContextualFunctionProviderOptions? options = null,
ILoggerFactory? loggerFactory = null)
: this(vectorStore, vectorDimensions, functions, maxNumberOfFunctions, default, options, null, loggerFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ContextualFunctionProvider"/> class.
/// </summary>
/// <param name="vectorStore">An instance of a vector store.</param>
/// <param name="vectorDimensions">The number of dimensions to use for the memory embeddings.</param>
/// <param name="functions">The functions to vectorize and store for searching related functions.</param>
/// <param name="maxNumberOfFunctions">The maximum number of relevant functions to retrieve from the vector store.</param>
/// <param name="serializedState">A <see cref="JsonElement"/> representing the serialized provider state.</param>
/// <param name="options">Further optional settings for configuring the provider.</param>
/// <param name="jsonSerializerOptions">Optional serializer options. If not provided, <see cref="AgentJsonUtilities.DefaultOptions"/> will be used.</param>
/// <param name="loggerFactory">The logger factory to use for logging. If not provided, no logging will be performed.</param>
public ContextualFunctionProvider(
VectorStore vectorStore,
int vectorDimensions,
IEnumerable<AIFunction> functions,
int maxNumberOfFunctions,
JsonElement serializedState,
ContextualFunctionProviderOptions? options = null,
JsonSerializerOptions? jsonSerializerOptions = null,
ILoggerFactory? loggerFactory = null)
{
Throw.IfNull(vectorStore);
Throw.IfLessThan(vectorDimensions, 1, "Vector dimensions must be greater than 0");
Throw.IfNull(functions);
Throw.IfLessThan(maxNumberOfFunctions, 1, "Max number of functions must be greater than 0");
this._options = options ?? new ContextualFunctionProviderOptions();
Throw.IfLessThan(this._options.NumberOfRecentMessagesInContext, 1, "Number of recent messages to include into context must be greater than 0");
this._functionStore = new FunctionStore(
vectorStore,
string.IsNullOrWhiteSpace(this._options.CollectionName) ? "functions" : this._options.CollectionName,
vectorDimensions,
functions,
maxNumberOfFunctions,
loggerFactory,
options: new()
{
EmbeddingValueProvider = this._options.EmbeddingValueProvider,
}
);
// Restore recent messages from serialized state if provided
if (serializedState.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined)
{
JsonSerializerOptions jso = jsonSerializerOptions ?? AgentJsonUtilities.DefaultOptions;
ContextualFunctionProviderState? state = serializedState.Deserialize(jso.GetTypeInfo(typeof(ContextualFunctionProviderState))) as ContextualFunctionProviderState;
if (state?.RecentMessages is { Count: > 0 })
{
// Restore recent messages respecting the limit (may truncate if limit changed afterwards).
foreach (ChatMessage message in state.RecentMessages.Take(this._options.NumberOfRecentMessagesInContext))
{
this._recentMessages.Enqueue(message);
}
}
}
}
/// <inheritdoc />
public override async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
Throw.IfNull(context);
// Vectorize the functions if they are not already vectorized
if (!this._areFunctionsVectorized)
{
await this._functionStore.SaveAsync(cancellationToken).ConfigureAwait(false);
this._areFunctionsVectorized = true;
}
// Build the search context
var searchContext = await this.BuildContextAsync(context.RequestMessages, cancellationToken).ConfigureAwait(false);
// Get the function relevant to the context
var functions = await this._functionStore
.SearchAsync(searchContext, cancellationToken: cancellationToken)
.ConfigureAwait(false);
return new AIContext { Tools = [.. functions] };
}
/// <inheritdoc/>
public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
Throw.IfNull(context);
// Don't add messages to the recent messages queue if the invocation failed
if (context.InvokeException is not null)
{
return default;
}
// Add the request and response messages to the recent messages queue
foreach (var message in context.RequestMessages)
{
this._recentMessages.Enqueue(message);
}
if (context.ResponseMessages is not null)
{
foreach (var message in context.ResponseMessages)
{
this._recentMessages.Enqueue(message);
}
}
// If there are more messages than the configured limit, remove the oldest ones
while (this._recentMessages.Count > this._options.NumberOfRecentMessagesInContext)
{
this._recentMessages.TryDequeue(out _);
}
return default;
}
/// <summary>
/// Serializes the current provider state to a <see cref="JsonElement"/> containing the recent messages.
/// </summary>
/// <param name="jsonSerializerOptions">Optional serializer options. This parameter is not used; <see cref="AgentJsonUtilities.DefaultOptions"/> is always used for serialization.</param>
/// <returns>A <see cref="JsonElement"/> with the recent messages, or default if there are no recent messages.</returns>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
ContextualFunctionProviderState state = new();
if (this._options.NumberOfRecentMessagesInContext > 0 && !this._recentMessages.IsEmpty)
{
state.RecentMessages = this._recentMessages.Take(this._options.NumberOfRecentMessagesInContext).ToList();
}
return JsonSerializer.SerializeToElement(state, AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ContextualFunctionProviderState)));
}
/// <summary>
/// Builds the context from chat messages.
/// </summary>
/// <param name="newMessages">The new messages.</param>
/// <param name="cancellationToken">The cancellation token to use for cancellation.</param>
private async Task<string> BuildContextAsync(IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken)
{
if (this._options.ContextEmbeddingValueProvider is not null)
{
// Ensure we only take the recent messages up to the configured limit
var recentMessages = this._recentMessages
.Skip(Math.Max(0, this._recentMessages.Count - this._options.NumberOfRecentMessagesInContext));
return await this._options.ContextEmbeddingValueProvider.Invoke(recentMessages, newMessages, cancellationToken).ConfigureAwait(false);
}
// Build context by concatenating the recent messages and the new messages
return string.Join(
Environment.NewLine,
this._recentMessages
.Skip(Math.Max(0, this._recentMessages.Count - this._options.NumberOfRecentMessagesInContext))
.Concat(newMessages)
.Where(m => !string.IsNullOrWhiteSpace(m?.Text))
.Select(m => m.Text));
}
internal sealed class ContextualFunctionProviderState
{
public List<ChatMessage>? RecentMessages { get; set; }
}
}
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Functions;
/// <summary>
/// Options for the <see cref="ContextualFunctionProvider"/>.
/// </summary>
public sealed class ContextualFunctionProviderOptions
{
/// <summary>
/// Gets or sets the collection name to use for storing and retrieving functions.
/// </summary>
/// <value>If not set, the default value "functions" will be used.</value>
public string? CollectionName { get; set; }
/// <summary>
/// Gets or sets the number of recent messages (messages from previous model/agent invocations) the provider uses to form a context.
/// The provider collects all messages from all model/agent invocations, up to this number,
/// and prepends them to the new messages of the current model/agent invocation to build a context.
/// While collecting new messages, the provider will remove the oldest messages
/// to keep the number of recent messages within the specified limit.
/// </summary>
/// <remarks>
/// Using the recent messages together with the new messages can be very useful
/// in cases where the model/agent is prompted to perform a task that requires details from
/// previous invocation(s). For example, if the agent is asked to provision an Azure resource in the first
/// invocation and deploy the resource in the second invocation, the second invocation will need
/// information about the provisioned resource in the first invocation to deploy it.
/// </remarks>
public int NumberOfRecentMessagesInContext { get; set; } = 2;
/// <summary>
/// Gets or sets a callback function that returns a value used to create a context embedding. The value is vectorized,
/// and the resulting vector is used to perform vector searches for functions relevant to the context.
/// If not provided, the default behavior is to concatenate the non-empty messages into a single string,
/// separated by a new line.
/// </summary>
/// <remarks>
/// The callback receives three parameters:
/// `recentMessages` - messages from the previous model/agent invocations.
/// `newMessages` - the new messages of the current model/agent invocation.
/// `cancellationToken` - a cancellation token that can be used to cancel the operation.
/// </remarks>
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>, CancellationToken, Task<string>>? ContextEmbeddingValueProvider { get; set; }
/// <summary>
/// Gets or sets a callback function that returns a value used to create a function embedding. The value is vectorized,
/// and the resulting vector is stored in the vector store for use in vector searches for functions relevant
/// to the context.
/// If not provided, the default behavior is to concatenate the function name and description into a single string.
/// </summary>
/// <remarks>
/// The callback receives two parameters:
/// `function` - the function to get embedding value for.
/// `cancellationToken` - a cancellation token that can be used to cancel the operation.
/// </remarks>
public Func<AIFunction, CancellationToken, Task<string>>? EmbeddingValueProvider { get; set; }
}
@@ -0,0 +1,185 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.VectorData;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Functions;
/// <summary>
/// Represents a vector store for <see cref="AIFunction"/> objects where the function name and description can be used for similarity searches.
/// </summary>
internal sealed class FunctionStore
{
private readonly VectorStore _vectorStore;
private readonly Dictionary<string, AIFunction> _functionByName;
private readonly string _collectionName;
private readonly int _maxNumberOfFunctions;
private readonly ILogger _logger;
private readonly FunctionStoreOptions _options;
private readonly VectorStoreCollection<object, Dictionary<string, object?>> _collection;
private bool _isCollectionExistenceAsserted;
/// <summary>
/// Initializes a new instance of the <see cref="FunctionStore"/> class.
/// </summary>
/// <param name="vectorStore">The vector store to use for storing functions.</param>
/// <param name="collectionName">The name of the collection to use for storing and retrieving functions.</param>
/// <param name="vectorDimensions">The number of dimensions to use for the memory embeddings.</param>
/// <param name="functions">The functions to vectorize and store for searching related functions.</param>
/// <param name="maxNumberOfFunctions">The maximum number of relevant functions to retrieve from the vector store.</param>
/// <param name="loggerFactory">The logger factory to use for logging. If not provided, no logging will be performed.</param>
/// <param name="options">The options to use for the function store.</param>
internal FunctionStore(
VectorStore vectorStore,
string collectionName,
int vectorDimensions,
IEnumerable<AIFunction> functions,
int maxNumberOfFunctions,
ILoggerFactory? loggerFactory = default,
FunctionStoreOptions? options = null)
{
Throw.IfNull(vectorStore);
Throw.IfNullOrWhitespace(collectionName);
Throw.IfLessThan(vectorDimensions, 1, "Vector dimensions must be greater than 0");
Throw.IfNull(functions);
Throw.IfLessThan(maxNumberOfFunctions, 1, "Max number of functions must be greater than 0");
this._vectorStore = vectorStore;
this._collectionName = collectionName;
this._functionByName = functions.ToDictionary(function => function.Name);
this._maxNumberOfFunctions = maxNumberOfFunctions;
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<FunctionStore>();
this._options = options ?? new FunctionStoreOptions();
// Create and assert the collection support record keys of string type
this._collection = this._vectorStore.GetDynamicCollection(collectionName, new VectorStoreCollectionDefinition()
{
Properties = [
new VectorStoreKeyProperty("Name", typeof(string)),
new VectorStoreVectorProperty("Embedding", typeof(string), dimensions: vectorDimensions)
]
});
}
/// <summary>
/// Saves the functions to the vector store.
/// </summary>
/// <param name="cancellationToken">The cancellation token to use for cancellation.</param>
public async Task SaveAsync(CancellationToken cancellationToken = default)
{
// Get function data to vectorize
var nameSourcePairs = await this.GetFunctionsVectorizationInfoAsync(cancellationToken).ConfigureAwait(false);
var functionRecords = new List<Dictionary<string, object?>>(nameSourcePairs.Count);
// Create vector store records
for (var i = 0; i < nameSourcePairs.Count; i++)
{
var (name, vectorizationSource) = nameSourcePairs[i];
functionRecords.Add(new Dictionary<string, object?>()
{
["Name"] = name,
["Embedding"] = vectorizationSource
});
}
// Create collection and upsert all vector store records
await this._collection.EnsureCollectionExistsAsync(cancellationToken).ConfigureAwait(false);
await this._collection.UpsertAsync(functionRecords, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Searches for functions based on the provided context.
/// </summary>
/// <param name="context">The context to search for functions.</param>
/// <param name="cancellationToken">The cancellation token to use for cancellation.</param>
public async Task<IEnumerable<AIFunction>> SearchAsync(string context, CancellationToken cancellationToken = default)
{
await this.AssertCollectionExistsAsync(cancellationToken).ConfigureAwait(false);
List<VectorSearchResult<Dictionary<string, object?>>> results = new();
await foreach (var result in this._collection
.SearchAsync(context, top: this._maxNumberOfFunctions, cancellationToken: cancellationToken).ConfigureAwait(false))
{
results.Add(result);
}
this._logger.LogFunctionsSearchResults(context, this._maxNumberOfFunctions, results);
return results.Select(result => this._functionByName[(string)result.Record["Name"]!]);
}
/// <summary>
/// Get the function vectorization information, which includes the function name and the source used for vectorization.
/// </summary>
/// <param name="cancellationToken">The cancellation token to use for cancellation.</param>
/// <returns>The function name and vectorization source pairs.</returns>
private async Task<List<FunctionVectorizationInfo>> GetFunctionsVectorizationInfoAsync(CancellationToken cancellationToken)
{
List<FunctionVectorizationInfo> nameSourcePairs = new(this._functionByName.Count);
var provider = this._options.EmbeddingValueProvider ?? ((function, _) =>
{
string descriptionPart = string.IsNullOrEmpty(function.Description) ? string.Empty : $", description: {function.Description}";
return Task.FromResult($"Function name: {function.Name}{descriptionPart}");
});
foreach (KeyValuePair<string, AIFunction> pair in this._functionByName)
{
var vectorizationSource = await provider.Invoke(pair.Value, cancellationToken).ConfigureAwait(false);
nameSourcePairs.Add(new FunctionVectorizationInfo(pair.Key, vectorizationSource));
}
this._logger.LogFunctionsVectorizationInfo(nameSourcePairs);
return nameSourcePairs;
}
/// <summary>
/// Asserts that the collection exists in the vector store.
/// </summary>
/// <param name="cancellationToken">The cancellation token to use for cancellation.</param>
private async Task AssertCollectionExistsAsync(CancellationToken cancellationToken)
{
if (!this._isCollectionExistenceAsserted)
{
if (!await this._collection.CollectionExistsAsync(cancellationToken).ConfigureAwait(false))
{
throw new InvalidOperationException($"Collection '{this._collectionName}' does not exist.");
}
this._isCollectionExistenceAsserted = true;
}
}
internal readonly struct FunctionVectorizationInfo
{
public string Name { get; }
public string VectorizationSource { get; }
public FunctionVectorizationInfo(string name, string vectorizationSource)
{
this.Name = name;
this.VectorizationSource = vectorizationSource;
}
public void Deconstruct(out string name, out string vectorizationSource)
{
name = this.Name;
vectorizationSource = this.VectorizationSource;
}
}
}
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.VectorData;
namespace Microsoft.Agents.AI.Functions;
[ExcludeFromCodeCoverage]
internal static class FunctionStoreLoggingExtensions
{
internal static void LogFunctionsVectorizationInfo(this ILogger logger, IList<FunctionStore.FunctionVectorizationInfo> vectorizationInfo)
{
logger.LogInformation("ContextualFunctionProvider: Number of function to vectorize: {Count}", vectorizationInfo.Count);
if (logger.IsEnabled(LogLevel.Trace))
{
logger.LogTrace("ContextualFunctionProvider: Functions vectorization info: {VectorizationInfo}",
string.Join(", ", vectorizationInfo.Select(info => $"\"Function: {info.Name}, VectorizationSource: {info.VectorizationSource}\"")));
}
}
internal static void LogFunctionsSearchResults(this ILogger logger, string context, int maxNumberOfFunctionsToReturn, IList<VectorSearchResult<Dictionary<string, object?>>> results)
{
logger.LogInformation("ContextualFunctionProvider: Search returned {Count} functions, with a maximum limit of {MaxCount}", results.Count, maxNumberOfFunctionsToReturn);
if (logger.IsEnabled(LogLevel.Trace))
{
logger.LogTrace("ContextualFunctionProvider: Functions search results for context {Context} with a maximum limit of {MaxCount}: {Results}",
$"\"{context}\"",
maxNumberOfFunctionsToReturn,
string.Join(", ", results.Select(result => $"\"Function: {result.Record["Name"]}, Score: {result.Score}\"")));
}
}
}
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Functions;
/// <summary>
/// Options for the <see cref="FunctionStore"/>
/// </summary>
internal sealed class FunctionStoreOptions
{
/// <summary>
/// A callback function that returns a value used to create a function embedding. The value is vectorized,
/// and the resulting vector is stored in the vector store for use in vector searches for functions relevant
/// to the context.
/// If not provided, the default behavior is to concatenate the function name and description into a single string.
/// </summary>
public Func<AIFunction, CancellationToken, Task<string>>? EmbeddingValueProvider { get; set; }
}
@@ -13,6 +13,8 @@
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>
@@ -84,7 +84,8 @@ public class AnthropicChatCompletionFixture : IChatClientAgentFixture
return Task.FromResult(new ChatClientAgent(chatClient, options: new()
{
Name = name,
ChatOptions = new() { Instructions = instructions, Tools = aiTools }
Instructions = instructions,
ChatOptions = new() { Tools = aiTools }
}));
}
@@ -37,20 +37,16 @@ public class AIProjectClientCreateTests
{
"CreateWithChatClientAgentOptionsAsync" => await this._client.CreateAIAgentAsync(
model: s_config.DeploymentName,
options: new ChatClientAgentOptions()
{
Name = AgentName,
Description = AgentDescription,
ChatOptions = new() { Instructions = AgentInstructions }
}),
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
name: AgentName,
description: AgentDescription)),
"CreateWithChatClientAgentOptionsSync" => this._client.CreateAIAgent(
model: s_config.DeploymentName,
options: new ChatClientAgentOptions()
{
Name = AgentName,
Description = AgentDescription,
ChatOptions = new() { Instructions = AgentInstructions }
}),
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
name: AgentName,
description: AgentDescription)),
"CreateWithFoundryOptionsAsync" => await this._client.CreateAIAgentAsync(
name: AgentName,
creationOptions: new AgentVersionCreationOptions(new PromptAgentDefinition(s_config.DeploymentName) { Instructions = AgentInstructions }) { Description = AgentDescription }),
@@ -243,18 +239,16 @@ public class AIProjectClientCreateTests
{
"CreateWithChatClientAgentOptionsAsync" => await this._client.CreateAIAgentAsync(
model: s_config.DeploymentName,
options: new ChatClientAgentOptions()
{
Name = AgentName,
ChatOptions = new() { Instructions = AgentInstructions, Tools = [weatherFunction] }
}),
options: new ChatClientAgentOptions(
name: AgentName,
instructions: AgentInstructions,
tools: [weatherFunction])),
"CreateWithChatClientAgentOptionsSync" => this._client.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions()
{
Name = AgentName,
ChatOptions = new() { Instructions = AgentInstructions, Tools = [weatherFunction] }
}),
options: new ChatClientAgentOptions(
name: AgentName,
instructions: AgentInstructions,
tools: [weatherFunction])),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
@@ -35,20 +34,16 @@ public class AzureAIAgentsPersistentCreateTests
{
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
options: new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = AgentInstructions },
Name = AgentName,
Description = AgentDescription
}),
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
name: AgentName,
description: AgentDescription)),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = AgentInstructions },
Name = AgentName,
Description = AgentDescription
}),
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
name: AgentName,
description: AgentDescription)),
"CreateWithFoundryOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
instructions: AgentInstructions,
@@ -109,32 +104,19 @@ public class AzureAIAgentsPersistentCreateTests
);
var vectorStoreMetadata = await this._persistentAgentsClient.VectorStores.CreateVectorStoreAsync([uploadedAgentFile.Id], name: "WordCodeLookup_VectorStore");
// Wait for vector store indexing to complete before using it
await this.WaitForVectorStoreReadyAsync(this._persistentAgentsClient, vectorStoreMetadata.Value.Id);
// Act.
var agent = createMechanism switch
{
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }]
}
}),
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }])),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }]
}
}),
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }])),
"CreateWithFoundryOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
instructions: AgentInstructions,
@@ -197,24 +179,15 @@ public class AzureAIAgentsPersistentCreateTests
// Hosted tool path (tools supplied via ChatClientAgentOptions)
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }]
}
}),
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }])),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }]
}
}),
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }])),
// Foundry (definitions + resources provided directly)
"CreateWithFoundryOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
instructions: AgentInstructions,
@@ -259,24 +232,14 @@ public class AzureAIAgentsPersistentCreateTests
{
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [weatherFunction]
}
}),
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [weatherFunction])),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [weatherFunction]
}
}),
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [weatherFunction])),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
@@ -296,42 +259,4 @@ public class AzureAIAgentsPersistentCreateTests
await this._persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
}
}
/// <summary>
/// Waits for a vector store to complete indexing by polling its status.
/// </summary>
/// <param name="client">The persistent agents client.</param>
/// <param name="vectorStoreId">The ID of the vector store.</param>
/// <param name="maxWaitSeconds">Maximum time to wait in seconds (default: 30).</param>
/// <returns>A task that completes when the vector store is ready or throws on timeout/failure.</returns>
private async Task WaitForVectorStoreReadyAsync(
PersistentAgentsClient client,
string vectorStoreId,
int maxWaitSeconds = 30)
{
Stopwatch sw = Stopwatch.StartNew();
while (sw.Elapsed.TotalSeconds < maxWaitSeconds)
{
PersistentAgentsVectorStore vectorStore = await client.VectorStores.GetVectorStoreAsync(vectorStoreId);
if (vectorStore.Status == VectorStoreStatus.Completed)
{
if (vectorStore.FileCounts.Failed > 0)
{
throw new InvalidOperationException("Vector store indexing failed for some files");
}
return;
}
if (vectorStore.Status == VectorStoreStatus.Expired)
{
throw new InvalidOperationException("Vector store has expired");
}
await Task.Delay(1000);
}
throw new TimeoutException($"Vector store did not complete indexing within {maxWaitSeconds}s");
}
}
@@ -1,5 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
</ItemGroup>
@@ -1,6 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="FluentAssertions" />
</ItemGroup>
@@ -13,6 +13,8 @@
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>
@@ -91,7 +91,7 @@ public sealed class AnthropicBetaServiceExtensionsTests
{
Name = "Test Agent",
Description = "Test description",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
};
// Act
@@ -158,7 +158,7 @@ public sealed class AnthropicClientExtensionsTests
{
Name = "Test Agent",
Description = "Test description",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
};
// Act
@@ -310,7 +310,7 @@ public sealed class PersistentAgentsClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
ChatOptions = new() { Instructions = "Override Instructions" }
Instructions = "Override Instructions"
};
// Act
@@ -337,7 +337,7 @@ public sealed class PersistentAgentsClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
ChatOptions = new() { Instructions = "Override Instructions" }
Instructions = "Override Instructions"
};
// Act
@@ -386,7 +386,7 @@ public sealed class PersistentAgentsClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
ChatOptions = new() { Instructions = "Override Instructions" }
Instructions = "Override Instructions"
};
// Act
@@ -413,7 +413,7 @@ public sealed class PersistentAgentsClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
ChatOptions = new() { Instructions = "Override Instructions" }
Instructions = "Override Instructions"
};
// Act
@@ -557,7 +557,7 @@ public sealed class PersistentAgentsClientExtensionsTests
{
Name = "Test Agent",
Description = "Test description",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
};
// Act
@@ -584,7 +584,7 @@ public sealed class PersistentAgentsClientExtensionsTests
{
Name = "Test Agent",
Description = "Test description",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
};
// Act
@@ -752,7 +752,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
};
// Act
@@ -775,7 +775,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
};
TestChatClient? testChatClient = null;
@@ -803,7 +803,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
};
// Act
@@ -826,7 +826,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
};
TestChatClient? testChatClient = null;
@@ -1575,8 +1575,8 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "test-agent",
Description = "Custom description",
ChatOptions = new ChatOptions { Instructions = "Custom instructions" }
Instructions = "Custom instructions",
Description = "Custom description"
};
// Act
@@ -1610,7 +1610,8 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test", Tools = tools }
Instructions = "Test",
ChatOptions = new ChatOptions { Tools = tools }
};
// Act
@@ -49,7 +49,8 @@ public class AzureAIProjectChatClientTests
new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new() { Instructions = "Test instructions", ConversationId = "conv_12345" }
Instructions = "Test instructions",
ChatOptions = new() { ConversationId = "conv_12345" }
});
// Act
@@ -98,7 +99,7 @@ public class AzureAIProjectChatClientTests
new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new() { Instructions = "Test instructions" },
Instructions = "Test instructions",
});
// Act
@@ -147,7 +148,8 @@ public class AzureAIProjectChatClientTests
new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new() { Instructions = "Test instructions", ConversationId = "conv_should_not_use_default" }
Instructions = "Test instructions",
ChatOptions = new() { ConversationId = "conv_should_not_use_default" }
});
// Act
@@ -196,7 +198,7 @@ public class AzureAIProjectChatClientTests
new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new() { Instructions = "Test instructions" },
Instructions = "Test instructions",
});
// Act
@@ -70,7 +70,7 @@ public sealed class ChatClientAgentFactoryTests
Assert.IsType<ChatClientAgent>(agent);
var chatClientAgent = agent as ChatClientAgent;
Assert.NotNull(chatClientAgent?.ChatOptions);
Assert.Equal("You are a helpful assistant.", chatClientAgent?.ChatOptions?.Instructions);
Assert.Equal("Provide detailed and accurate responses.", chatClientAgent?.ChatOptions?.Instructions);
Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.Temperature);
Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.FrequencyPenalty);
Assert.Equal(1024, chatClientAgent?.ChatOptions?.MaxOutputTokens);
@@ -20,6 +20,10 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
@@ -9,6 +9,8 @@
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>
@@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="ModelContextProtocol" />
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
@@ -92,7 +92,7 @@ public sealed class OpenAIAssistantClientExtensionsTests
{
Name = "Test Agent",
Description = "Test description",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
};
// Act
@@ -223,7 +223,7 @@ public sealed class OpenAIAssistantClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
ChatOptions = new() { Instructions = "Override Instructions" }
Instructions = "Override Instructions"
};
// Act
@@ -250,7 +250,7 @@ public sealed class OpenAIAssistantClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
ChatOptions = new() { Instructions = "Override Instructions" }
Instructions = "Override Instructions"
};
// Act
@@ -299,7 +299,7 @@ public sealed class OpenAIAssistantClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
ChatOptions = new() { Instructions = "Override Instructions" }
Instructions = "Override Instructions"
};
// Act
@@ -326,7 +326,7 @@ public sealed class OpenAIAssistantClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
ChatOptions = new() { Instructions = "Override Instructions" }
Instructions = "Override Instructions"
};
// Act
@@ -498,7 +498,7 @@ public sealed class OpenAIAssistantClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
};
// Act
@@ -130,7 +130,7 @@ public sealed class OpenAIChatClientExtensionsTests
{
Name = "Test Agent",
Description = "Test description",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
};
// Act
@@ -208,7 +208,7 @@ public sealed class OpenAIResponseClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
};
// Act
@@ -19,6 +19,7 @@ public class ChatClientAgentOptionsTests
// Assert
Assert.Null(options.Name);
Assert.Null(options.Instructions);
Assert.Null(options.Description);
Assert.Null(options.ChatOptions);
Assert.Null(options.ChatMessageStoreFactory);
@@ -26,44 +27,90 @@ public class ChatClientAgentOptionsTests
}
[Fact]
public void Constructor_WithNullValues_SetsPropertiesCorrectly()
public void ParameterizedConstructor_WithNullValues_SetsPropertiesCorrectly()
{
// Act
var options = new ChatClientAgentOptions() { Name = null, Description = null, ChatOptions = new() { Tools = null, Instructions = null } };
var options = new ChatClientAgentOptions(
instructions: null,
name: null,
description: null,
tools: null);
// Assert
Assert.Null(options.Name);
Assert.Null(options.Instructions);
Assert.Null(options.Description);
Assert.Null(options.ChatOptions);
Assert.Null(options.AIContextProviderFactory);
Assert.Null(options.ChatMessageStoreFactory);
Assert.NotNull(options.ChatOptions);
Assert.Null(options.ChatOptions.Instructions);
Assert.Null(options.ChatOptions.Tools);
}
[Fact]
public void Constructor_WithToolsOnly_SetsChatOptionsWithTools()
public void ParameterizedConstructor_WithInstructionsOnly_SetsChatOptionsWithInstructions()
{
// Arrange
const string Instructions = "Test instructions";
// Act
var options = new ChatClientAgentOptions(
instructions: Instructions,
name: null,
description: null,
tools: null);
// Assert
Assert.Null(options.Name);
Assert.Equal(Instructions, options.Instructions);
Assert.Null(options.Description);
Assert.Null(options.ChatOptions);
}
[Fact]
public void ParameterizedConstructor_WithToolsOnly_SetsChatOptionsWithTools()
{
// Arrange
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
// Act
var options = new ChatClientAgentOptions()
{
Name = null,
Description = null,
ChatOptions = new() { Tools = tools }
};
var options = new ChatClientAgentOptions(
instructions: null,
name: null,
description: null,
tools: tools);
// Assert
Assert.Null(options.Name);
Assert.Null(options.Instructions);
Assert.Null(options.Description);
Assert.NotNull(options.ChatOptions);
AssertSameTools(tools, options.ChatOptions.Tools);
Assert.Null(options.ChatOptions.Instructions);
Assert.Same(tools, options.ChatOptions.Tools);
}
[Fact]
public void Constructor_WithAllParameters_SetsAllPropertiesCorrectly()
public void ParameterizedConstructor_WithInstructionsAndTools_SetsChatOptionsWithBoth()
{
// Arrange
const string Instructions = "Test instructions";
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
// Act
var options = new ChatClientAgentOptions(
instructions: Instructions,
name: null,
description: null,
tools: tools);
// Assert
Assert.Null(options.Name);
Assert.Equal(Instructions, options.Instructions);
Assert.Null(options.Description);
Assert.NotNull(options.ChatOptions);
Assert.Null(options.ChatOptions.Instructions);
Assert.Same(tools, options.ChatOptions.Tools);
}
[Fact]
public void ParameterizedConstructor_WithAllParameters_SetsAllPropertiesCorrectly()
{
// Arrange
const string Instructions = "Test instructions";
@@ -72,37 +119,38 @@ public class ChatClientAgentOptionsTests
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
// Act
var options = new ChatClientAgentOptions()
{
Name = Name,
Description = Description,
ChatOptions = new() { Tools = tools, Instructions = Instructions }
};
var options = new ChatClientAgentOptions(
instructions: Instructions,
name: Name,
description: Description,
tools: tools);
// Assert
Assert.Equal(Name, options.Name);
Assert.Equal(Instructions, options.ChatOptions.Instructions);
Assert.Equal(Instructions, options.Instructions);
Assert.Equal(Description, options.Description);
Assert.NotNull(options.ChatOptions);
AssertSameTools(tools, options.ChatOptions.Tools);
Assert.Null(options.ChatOptions.Instructions);
Assert.Same(tools, options.ChatOptions.Tools);
}
[Fact]
public void Constructor_WithNameAndDescriptionOnly_DoesNotCreateChatOptions()
public void ParameterizedConstructor_WithNameAndDescriptionOnly_DoesNotCreateChatOptions()
{
// Arrange
const string Name = "Test name";
const string Description = "Test description";
// Act
var options = new ChatClientAgentOptions()
{
Name = Name,
Description = Description,
};
var options = new ChatClientAgentOptions(
instructions: null,
name: Name,
description: Description,
tools: null);
// Assert
Assert.Equal(Name, options.Name);
Assert.Null(options.Instructions);
Assert.Equal(Description, options.Description);
Assert.Null(options.ChatOptions);
}
@@ -111,6 +159,7 @@ public class ChatClientAgentOptionsTests
public void Clone_CreatesDeepCopyWithSameValues()
{
// Arrange
const string Instructions = "Test instructions";
const string Name = "Test name";
const string Description = "Test description";
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
@@ -122,11 +171,8 @@ public class ChatClientAgentOptionsTests
ChatClientAgentOptions.AIContextProviderFactoryContext ctx) =>
new Mock<AIContextProvider>().Object;
var original = new ChatClientAgentOptions()
var original = new ChatClientAgentOptions(Instructions, Name, Description, tools)
{
Name = Name,
Description = Description,
ChatOptions = new() { Tools = tools },
Id = "test-id",
ChatMessageStoreFactory = ChatMessageStoreFactory,
AIContextProviderFactory = AIContextProviderFactory
@@ -139,6 +185,7 @@ public class ChatClientAgentOptionsTests
Assert.NotSame(original, clone);
Assert.Equal(original.Id, clone.Id);
Assert.Equal(original.Name, clone.Name);
Assert.Equal(original.Instructions, clone.Instructions);
Assert.Equal(original.Description, clone.Description);
Assert.Same(original.ChatMessageStoreFactory, clone.ChatMessageStoreFactory);
Assert.Same(original.AIContextProviderFactory, clone.AIContextProviderFactory);
@@ -150,13 +197,14 @@ public class ChatClientAgentOptionsTests
}
[Fact]
public void Clone_WithoutProvidingChatOptions_ClonesCorrectly()
public void Clone_WithNullChatOptions_ClonesCorrectly()
{
// Arrange
var original = new ChatClientAgentOptions
{
Id = "test-id",
Name = "Test name",
Instructions = "Test instructions",
Description = "Test description"
};
@@ -167,19 +215,10 @@ public class ChatClientAgentOptionsTests
Assert.NotSame(original, clone);
Assert.Equal(original.Id, clone.Id);
Assert.Equal(original.Name, clone.Name);
Assert.Equal(original.Instructions, clone.Instructions);
Assert.Equal(original.Description, clone.Description);
Assert.Null(original.ChatOptions);
Assert.Null(clone.ChatOptions);
Assert.Null(clone.ChatMessageStoreFactory);
Assert.Null(clone.AIContextProviderFactory);
}
private static void AssertSameTools(IList<AITool>? expected, IList<AITool>? actual)
{
var index = 0;
foreach (var tool in expected ?? [])
{
Assert.Same(tool, actual?[index]);
index++;
}
}
}
@@ -31,7 +31,7 @@ public partial class ChatClientAgentTests
Id = "test-agent-id",
Name = "test name",
Description = "test description",
ChatOptions = new() { Instructions = "test instructions" },
Instructions = "test instructions",
});
// Assert
@@ -65,7 +65,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent =
new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "base instructions" },
Instructions = "test instructions"
});
// Act
@@ -99,7 +99,7 @@ public partial class ChatClientAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => agent.RunAsync((IReadOnlyCollection<ChatMessage>)null!));
@@ -120,7 +120,7 @@ public partial class ChatClientAgentTests
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 100),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
// Act
await agent.RunAsync([new(ChatRole.User, "test")], options: new ChatClientAgentRunOptions(chatOptions));
@@ -181,7 +181,7 @@ public partial class ChatClientAgentTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "base instructions" } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions" });
var runOptions = new AgentRunOptions();
// Act
@@ -212,7 +212,7 @@ public partial class ChatClientAgentTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse(responseMessages));
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" }, Name = authorName });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions", Name = authorName });
// Act
var result = await agent.RunAsync([new(ChatRole.User, "test")]);
@@ -239,7 +239,7 @@ public partial class ChatClientAgentTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
// Create a thread using the agent's GetNewThread method
var thread = agent.GetNewThread();
@@ -270,7 +270,7 @@ public partial class ChatClientAgentTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = null } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = null });
// Act
await agent.RunAsync([new(ChatRole.User, "test message")]);
@@ -300,7 +300,7 @@ public partial class ChatClientAgentTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
// Act
await agent.RunAsync([]);
@@ -326,7 +326,7 @@ public partial class ChatClientAgentTests
It.Is<ChatOptions>(opts => opts.ConversationId == "ConvId"),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgentThread thread = new() { ConversationId = "ConvId" };
@@ -346,7 +346,7 @@ public partial class ChatClientAgentTests
var chatOptions = new ChatOptions { ConversationId = "ConvId" };
Mock<IChatClient> mockService = new();
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgentThread thread = new() { ConversationId = "ThreadId" };
@@ -369,7 +369,7 @@ public partial class ChatClientAgentTests
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 100 && opts.ConversationId == "ConvId"),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgentThread thread = new() { ConversationId = "ConvId" };
@@ -394,7 +394,7 @@ public partial class ChatClientAgentTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgentThread thread = new() { ConversationId = "ConvId" };
@@ -415,7 +415,7 @@ public partial class ChatClientAgentTests
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgentThread thread = new();
// Act
@@ -442,7 +442,7 @@ public partial class ChatClientAgentTests
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
Instructions = "test instructions",
ChatMessageStoreFactory = mockFactory.Object
});
@@ -473,7 +473,7 @@ public partial class ChatClientAgentTests
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
Instructions = "test instructions",
});
// Act
@@ -508,7 +508,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
Instructions = "test instructions",
ChatMessageStoreFactory = mockFactory.Object
});
@@ -539,7 +539,7 @@ public partial class ChatClientAgentTests
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
Instructions = "test instructions",
ChatMessageStoreFactory = mockFactory.Object
});
@@ -592,7 +592,7 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
var thread = agent.GetNewThread() as ChatClientAgentThread;
@@ -654,7 +654,7 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(requestMessages));
@@ -700,7 +700,7 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AIContext());
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
await agent.RunAsync([new(ChatRole.User, "user message")]);
@@ -907,7 +907,7 @@ public partial class ChatClientAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
var metadata = new ChatClientAgentOptions { ChatOptions = new() { Instructions = "You are a helpful assistant" } };
var metadata = new ChatClientAgentOptions { Instructions = "You are a helpful assistant" };
ChatClientAgent agent = new(chatClient, metadata);
// Act & Assert
@@ -936,7 +936,7 @@ public partial class ChatClientAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
var metadata = new ChatClientAgentOptions { ChatOptions = new() { Instructions = null } };
var metadata = new ChatClientAgentOptions { Instructions = null };
ChatClientAgent agent = new(chatClient, metadata);
// Act & Assert
@@ -967,10 +967,10 @@ public partial class ChatClientAgentTests
}
/// <summary>
/// Verify that ChatOptions is created with instructions when instructions are provided and no tools are provided.
/// Verify that ChatOptions property returns null when no params are provided that require a ChatOptions instance.
/// </summary>
[Fact]
public void ChatOptionsCreatedWithInstructionsEvenWhenConstructorToolsNotProvided()
public void ChatOptionsReturnsNullWhenConstructorToolsNotProvided()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
@@ -980,8 +980,7 @@ public partial class ChatClientAgentTests
Assert.Equal("TestInstructions", agent.Instructions);
Assert.Equal("TestName", agent.Name);
Assert.Equal("TestDescription", agent.Description);
Assert.NotNull(agent.ChatOptions);
Assert.Equal("TestInstructions", agent.ChatOptions.Instructions);
Assert.Null(agent.ChatOptions);
}
#endregion
@@ -1072,7 +1071,7 @@ public partial class ChatClientAgentTests
public async Task ChatOptionsMergingUsesAgentOptionsWhenRequestHasNoneAsync()
{
// Arrange
var agentChatOptions = new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f, Instructions = "test instructions" };
var agentChatOptions = new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f };
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
@@ -1086,6 +1085,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
@@ -1114,7 +1114,7 @@ public partial class ChatClientAgentTests
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgent agent = new(mockService.Object, options: new("test instructions"));
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
@@ -1167,7 +1167,6 @@ public partial class ChatClientAgentTests
// Arrange
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
MaxOutputTokens = 100,
Temperature = 0.7f,
TopP = 0.9f,
@@ -1205,6 +1204,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
@@ -1263,7 +1263,6 @@ public partial class ChatClientAgentTests
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
Tools = [agentTool]
};
var requestChatOptions = new ChatOptions
@@ -1284,6 +1283,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
@@ -1312,7 +1312,6 @@ public partial class ChatClientAgentTests
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
Tools = [agentTool]
};
var requestChatOptions = new ChatOptions
@@ -1334,6 +1333,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
@@ -1360,7 +1360,6 @@ public partial class ChatClientAgentTests
// Arrange
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
RawRepresentationFactory = _ => agentSetting
};
var requestChatOptions = new ChatOptions
@@ -1381,6 +1380,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
@@ -1436,7 +1436,7 @@ public partial class ChatClientAgentTests
TopK = 50,
PresencePenalty = 0.1f,
FrequencyPenalty = 0.2f,
Instructions = "agent instructions\nrequest instructions",
Instructions = "test instructions\nrequest instructions",
ModelId = "agent-model",
Seed = 12345,
ConversationId = "agent-conversation",
@@ -1459,6 +1459,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
@@ -1508,7 +1509,7 @@ public partial class ChatClientAgentTests
{
Id = "test-agent-id",
Name = "TestAgent",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act
@@ -1531,7 +1532,7 @@ public partial class ChatClientAgentTests
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act
@@ -1555,7 +1556,7 @@ public partial class ChatClientAgentTests
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act
@@ -1581,7 +1582,7 @@ public partial class ChatClientAgentTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act
@@ -1605,7 +1606,7 @@ public partial class ChatClientAgentTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act
@@ -1631,7 +1632,7 @@ public partial class ChatClientAgentTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act
@@ -1660,7 +1661,7 @@ public partial class ChatClientAgentTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act
@@ -1693,7 +1694,7 @@ public partial class ChatClientAgentTests
{
Id = "test-agent-id",
Name = "TestAgent",
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act
@@ -1720,7 +1721,7 @@ public partial class ChatClientAgentTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act
@@ -1755,12 +1756,12 @@ public partial class ChatClientAgentTests
var chatClientAgent1 = new ChatClientAgent(mockChatClient1.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions 1" }
Instructions = "Test instructions 1"
});
var chatClientAgent2 = new ChatClientAgent(mockChatClient2.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions 2" }
Instructions = "Test instructions 2"
});
// Act
@@ -1795,7 +1796,7 @@ public partial class ChatClientAgentTests
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act
@@ -1819,7 +1820,7 @@ public partial class ChatClientAgentTests
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act
@@ -1844,7 +1845,7 @@ public partial class ChatClientAgentTests
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act - Request IChatClient with a service key (base.GetService will return null due to serviceKey)
@@ -1869,7 +1870,7 @@ public partial class ChatClientAgentTests
mockChatClient.Setup(c => c.GetService(typeof(string), "some-key")).Returns("test-result");
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" }
Instructions = "Test instructions"
});
// Act - Request string with a service key (base.GetService will return null due to serviceKey)
@@ -1910,7 +1911,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent =
new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" }
Instructions = "test instructions"
});
// Act
@@ -1957,7 +1958,7 @@ public partial class ChatClientAgentTests
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
Instructions = "test instructions",
ChatMessageStoreFactory = mockFactory.Object
});
@@ -1995,7 +1996,7 @@ public partial class ChatClientAgentTests
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
Instructions = "test instructions",
ChatMessageStoreFactory = mockFactory.Object
});
@@ -2048,7 +2049,7 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] }, AIContextProviderFactory = _ => mockProvider.Object });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
var thread = agent.GetNewThread() as ChatClientAgentThread;
@@ -2111,7 +2112,7 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] }, AIContextProviderFactory = _ => mockProvider.Object });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
@@ -20,7 +20,7 @@ public class ChatClientAgent_DeserializeThreadTests
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" },
Instructions = "Test instructions",
AIContextProviderFactory = _ =>
{
factoryCalled = true;
@@ -53,7 +53,7 @@ public class ChatClientAgent_DeserializeThreadTests
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" },
Instructions = "Test instructions",
ChatMessageStoreFactory = _ =>
{
factoryCalled = true;
@@ -19,7 +19,7 @@ public class ChatClientAgent_GetNewThreadTests
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" },
Instructions = "Test instructions",
AIContextProviderFactory = _ =>
{
factoryCalled = true;
@@ -46,7 +46,7 @@ public class ChatClientAgent_GetNewThreadTests
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" },
Instructions = "Test instructions",
ChatMessageStoreFactory = _ =>
{
factoryCalled = true;
@@ -90,7 +90,7 @@ public sealed class ChatClientBuilderExtensionsTests
{
Name = "AgentWithOptions",
Description = "Desc",
ChatOptions = new() { Instructions = "Instr" },
Instructions = "Instr",
UseProvidedChatClientAsIs = true
};
@@ -115,7 +115,7 @@ public sealed class ChatClientBuilderExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "ServiceAgent",
ChatOptions = new() { Instructions = "Service instructions" }
Instructions = "Service instructions"
};
// Act
@@ -148,7 +148,7 @@ public sealed class ChatClientBuilderExtensionsTests
ChatClientBuilder builder = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => builder.BuildAIAgent(options: new() { ChatOptions = new() { Instructions = "instructions" } }));
Assert.Throws<ArgumentNullException>(() => builder.BuildAIAgent(options: new() { Instructions = "instructions" }));
}
[Fact]
@@ -166,7 +166,7 @@ public sealed class ChatClientBuilderExtensionsTests
var agent = builder.BuildAIAgent(
new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Middleware test" },
Instructions = "Middleware test",
UseProvidedChatClientAsIs = true
}
);
@@ -57,7 +57,7 @@ public sealed class ChatClientExtensionsTests
{
Name = "AgentWithOptions",
Description = "Desc",
ChatOptions = new() { Instructions = "Instr" },
Instructions = "Instr",
UseProvidedChatClientAsIs = true
};
@@ -89,6 +89,6 @@ public sealed class ChatClientExtensionsTests
IChatClient chatClient = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => chatClient.CreateAIAgent(options: new() { ChatOptions = new() { Instructions = "instructions" } }));
Assert.Throws<ArgumentNullException>(() => chatClient.CreateAIAgent(options: new() { Instructions = "instructions" }));
}
}
@@ -0,0 +1,533 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Functions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Moq;
namespace Microsoft.Agents.AI.UnitTests.Functions;
/// <summary>
/// Contains unit tests for the <see cref="ContextualFunctionProvider"/> class.
/// </summary>
public sealed class ContextualFunctionProviderTests
{
private readonly Mock<VectorStore> _vectorStoreMock;
private readonly Mock<VectorStoreCollection<object, Dictionary<string, object?>>> _collectionMock;
public ContextualFunctionProviderTests()
{
this._vectorStoreMock = new Mock<VectorStore>(MockBehavior.Strict);
this._collectionMock = new Mock<VectorStoreCollection<object, Dictionary<string, object?>>>(MockBehavior.Strict);
this._vectorStoreMock
.Setup(vs => vs.GetDynamicCollection(It.IsAny<string>(), It.IsAny<VectorStoreCollectionDefinition>()))
.Returns(this._collectionMock.Object);
this._collectionMock
.Setup(c => c.CollectionExistsAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
this._collectionMock
.Setup(c => c.EnsureCollectionExistsAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
this._collectionMock
.Setup(c => c.UpsertAsync(It.IsAny<IEnumerable<Dictionary<string, object?>>>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
this._collectionMock
.Setup(c => c.SearchAsync(It.IsAny<string>(), It.IsAny<int>(), null, It.IsAny<CancellationToken>()))
.Returns(AsyncEnumerable.Empty<VectorSearchResult<Dictionary<string, object?>>>());
}
[Fact]
public void Constructor_ShouldThrow_OnInvalidArguments()
{
// Arrange
var vectorStore = new Mock<VectorStore>().Object;
var functions = new List<AIFunction> { CreateFunction("f1") };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ContextualFunctionProvider(null!, 1, functions, 3));
Assert.Throws<ArgumentOutOfRangeException>(() => new ContextualFunctionProvider(vectorStore, 0, functions, 3));
Assert.Throws<ArgumentNullException>(() => new ContextualFunctionProvider(vectorStore, 1, null!, 3));
}
[Fact]
public async Task Invoking_ShouldVectorizeFunctions_Once_Async()
{
// Arrange
var function = CreateFunction("f1", "desc");
var functions = new List<AIFunction> { function };
this._collectionMock
.Setup(c => c.UpsertAsync(It.IsAny<IEnumerable<Dictionary<string, object?>>>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
var provider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5);
var messages = new List<ChatMessage> { new() { Contents = [new TextContent("hello")] } };
var context = new AIContextProvider.InvokingContext(messages);
// Act
await provider.InvokingAsync(context);
await provider.InvokingAsync(context);
// Assert
this._collectionMock.Verify(
c => c.UpsertAsync(It.IsAny<IEnumerable<Dictionary<string, object?>>>(), It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task Invoking_ShouldReturnRelevantFunctions_Async()
{
// Arrange
var function = CreateFunction("f1", "desc");
var functions = new List<AIFunction> { function };
var searchResult = new VectorSearchResult<Dictionary<string, object?>>(
new Dictionary<string, object?>
{
["Name"] = function.Name,
["Description"] = function.Description
},
0.99f
);
this._collectionMock
.Setup(c => c.SearchAsync(It.IsAny<string>(), It.IsAny<int>(), null, It.IsAny<CancellationToken>()))
.Returns(new[] { searchResult }.ToAsyncEnumerable());
var provider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5);
var messages = new List<ChatMessage> { new() { Contents = [new TextContent("context")] } };
var context = new AIContextProvider.InvokingContext(messages);
// Act
var result = await provider.InvokingAsync(context);
// Assert
Assert.NotNull(result);
Assert.NotNull(result.Tools);
Assert.Single(result.Tools);
Assert.Equal("f1", result.Tools[0].Name);
this._collectionMock.Verify(
c => c.SearchAsync("context", 5, null, It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task BuildContext_ShouldUseContextEmbeddingValueProvider_Async()
{
// Arrange
var functions = new List<AIFunction> { CreateFunction("f1") };
var options = new ContextualFunctionProviderOptions
{
NumberOfRecentMessagesInContext = 3,
ContextEmbeddingValueProvider = (recentMessages, newMessages, _) =>
{
Assert.Equal(3, recentMessages.Count());
Assert.Single(newMessages);
return Task.FromResult("custom context");
}
};
var provider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5,
options: options);
var message1 = new ChatMessage() { Contents = [new TextContent("msg1")] };
var message2 = new ChatMessage() { Contents = [new TextContent("msg2")] };
var message3 = new ChatMessage() { Contents = [new TextContent("msg3")] };
var message4 = new ChatMessage() { Contents = [new TextContent("msg4")] };
var message5 = new ChatMessage() { Contents = [new TextContent("msg5")] };
// Simulate previous invocations to populate recent messages
await provider.InvokedAsync(new AIContextProvider.InvokedContext([message1], null) { ResponseMessages = [] });
await provider.InvokedAsync(new AIContextProvider.InvokedContext([message2], null) { ResponseMessages = [] });
await provider.InvokedAsync(new AIContextProvider.InvokedContext([message3], null) { ResponseMessages = [] });
await provider.InvokedAsync(new AIContextProvider.InvokedContext([message4], null) { ResponseMessages = [] });
var messages = new List<ChatMessage> { message5 };
var context = new AIContextProvider.InvokingContext(messages);
// Act
await provider.InvokingAsync(context);
// Assert
this._collectionMock.Verify(
c => c.SearchAsync("custom context", It.IsAny<int>(), null, It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task BuildContext_ShouldConcatenateMessages_Async()
{
// Arrange
var functions = new List<AIFunction> { CreateFunction("f1") };
var options = new ContextualFunctionProviderOptions
{
NumberOfRecentMessagesInContext = 3
};
var provider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5,
options: options);
var message1 = new ChatMessage() { Contents = [new TextContent("msg1")] };
var message2 = new ChatMessage() { Contents = [new TextContent("msg2")] };
var message3 = new ChatMessage() { Contents = [new TextContent("msg3")] };
var message4 = new ChatMessage() { Contents = [new TextContent("msg4")] };
var message5 = new ChatMessage() { Contents = [new TextContent("msg5")] };
// Simulate previous invocations to populate recent messages
await provider.InvokedAsync(new AIContextProvider.InvokedContext([message1], null) { ResponseMessages = [] });
await provider.InvokedAsync(new AIContextProvider.InvokedContext([message2], null) { ResponseMessages = [] });
await provider.InvokedAsync(new AIContextProvider.InvokedContext([message3], null) { ResponseMessages = [] });
await provider.InvokedAsync(new AIContextProvider.InvokedContext([message4], null) { ResponseMessages = [] });
// Act
var invokingContext = new AIContextProvider.InvokingContext([message5]);
var context = await provider.InvokingAsync(invokingContext);
// Assert
var expected = string.Join(Environment.NewLine, ["msg2", "msg3", "msg4", "msg5"]);
this._collectionMock.Verify(c => c.SearchAsync(expected, It.IsAny<int>(), null, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task BuildContext_ShouldUseEmbeddingValueProvider_Async()
{
// Arrange
List<Dictionary<string, object?>>? upsertedRecords = null;
this._collectionMock
.Setup(c => c.UpsertAsync(It.IsAny<IEnumerable<Dictionary<string, object?>>>(), It.IsAny<CancellationToken>()))
.Callback<IEnumerable<Dictionary<string, object?>>, CancellationToken>((records, _) => upsertedRecords = records.ToList())
.Returns(Task.CompletedTask);
var functions = new List<AIFunction> { CreateFunction("f1", "desc1") };
var options = new ContextualFunctionProviderOptions
{
EmbeddingValueProvider = (func, ct) => Task.FromResult($"custom embedding for {func.Name}:{func.Description}")
};
var provider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5,
options: options);
var messages = new List<ChatMessage>
{
new() { Contents = [new TextContent("ignored")] }
};
var context = new AIContextProvider.InvokingContext(messages);
// Act
await provider.InvokingAsync(context);
// Assert
Assert.NotNull(upsertedRecords);
var embeddingSource = upsertedRecords!.SelectMany(r => r).FirstOrDefault(kv => kv.Key == "Embedding").Value as string;
Assert.Equal("custom embedding for f1:desc1", embeddingSource);
}
[Fact]
public async Task ContextEmbeddingValueProvider_ReceivesRecentAndNewMessages_Async()
{
// Arrange
var functions = new List<AIFunction> { CreateFunction("f1") };
IEnumerable<ChatMessage>? capturedRecentMessages = null;
IEnumerable<ChatMessage>? capturedNewMessages = null;
var options = new ContextualFunctionProviderOptions
{
NumberOfRecentMessagesInContext = 2,
ContextEmbeddingValueProvider = (recentMessages, newMessages, ct) =>
{
capturedRecentMessages = recentMessages;
capturedNewMessages = newMessages;
return Task.FromResult("context");
}
};
var provider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5,
options: options);
// Add more messages than the number of messages to keep
await provider.InvokedAsync(new AIContextProvider.InvokedContext([new() { Contents = [new TextContent("msg1")] }], null) { ResponseMessages = [] });
await provider.InvokedAsync(new AIContextProvider.InvokedContext([new() { Contents = [new TextContent("msg2")] }], null) { ResponseMessages = [] });
await provider.InvokedAsync(new AIContextProvider.InvokedContext([new() { Contents = [new TextContent("msg3")] }], null) { ResponseMessages = [] });
// Act
var invokingContext = new AIContextProvider.InvokingContext([
new() { Contents = [new TextContent("msg4")] },
new() { Contents = [new TextContent("msg5")] }
]);
await provider.InvokingAsync(invokingContext);
// Assert
Assert.NotNull(capturedRecentMessages);
Assert.Equal("msg2", capturedRecentMessages.ElementAt(0).Text);
Assert.Equal("msg3", capturedRecentMessages.ElementAt(1).Text);
Assert.NotNull(capturedNewMessages);
Assert.Equal("msg4", capturedNewMessages.ElementAt(0).Text);
Assert.Equal("msg5", capturedNewMessages.ElementAt(1).Text);
}
[Fact]
public async Task Serialize_WithNoRecentMessages_ShouldReturnEmptyStateAsync()
{
// Arrange
var functions = new List<AIFunction> { CreateFunction("f1") };
var options = new ContextualFunctionProviderOptions
{
NumberOfRecentMessagesInContext = 5
};
var provider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5,
options: options);
var message1 = new ChatMessage() { Contents = [new TextContent("msg1")] };
var message2 = new ChatMessage() { Contents = [new TextContent("msg2")] };
var message3 = new ChatMessage() { Contents = [new TextContent("msg3")] };
// Add successful invocations first
await provider.InvokedAsync(new AIContextProvider.InvokedContext([message1], null) { ResponseMessages = [] });
await provider.InvokedAsync(new AIContextProvider.InvokedContext([message2], null) { ResponseMessages = [] });
// Act - Add an invocation with an exception
await provider.InvokedAsync(new AIContextProvider.InvokedContext([message3], null)
{
ResponseMessages = [],
InvokeException = new InvalidOperationException("Test exception")
});
// Assert - The exception-causing message should not be added to recent messages
var invokingContext = new AIContextProvider.InvokingContext([new() { Contents = [new TextContent("new message")] }]);
await provider.InvokingAsync(invokingContext);
var expected = string.Join(Environment.NewLine, ["msg1", "msg2", "new message"]);
this._collectionMock.Verify(c => c.SearchAsync(expected, It.IsAny<int>(), null, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task InvokedAsync_ShouldNotAddMessages_WhenExceptionIsPresent_Async()
{
// Arrange
var functions = new List<AIFunction> { CreateFunction("f1") };
var options = new ContextualFunctionProviderOptions
{
NumberOfRecentMessagesInContext = 3
};
var provider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5,
options: options);
// Act
JsonElement state = provider.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, state.ValueKind);
Assert.False(state.TryGetProperty("recentMessages", out _));
}
[Fact]
public async Task Serialize_WithRecentMessages_ShouldPersistMessagesUpToLimitAsync()
{
// Arrange
var functions = new List<AIFunction> { CreateFunction("f1") };
var options = new ContextualFunctionProviderOptions
{
NumberOfRecentMessagesInContext = 2
};
var provider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5,
options: options);
var messages = new[]
{
new ChatMessage() { Contents = [new TextContent("M1")] },
new ChatMessage() { Contents = [new TextContent("M2")] },
new ChatMessage() { Contents = [new TextContent("M3")] }
};
// Act
await provider.InvokedAsync(new AIContextProvider.InvokedContext(messages, aiContextProviderMessages: null));
JsonElement state = provider.Serialize();
// Assert
Assert.True(state.TryGetProperty("recentMessages", out JsonElement recentProperty));
Assert.Equal(JsonValueKind.Array, recentProperty.ValueKind);
int count = recentProperty.GetArrayLength();
Assert.Equal(2, count);
}
[Fact]
public async Task SerializeAndDeserialize_RoundtripRestoresMessagesAsync()
{
// Arrange
var functions = new List<AIFunction> { CreateFunction("f1") };
var options = new ContextualFunctionProviderOptions
{
NumberOfRecentMessagesInContext = 4
};
var provider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5,
options: options);
var messages = new[]
{
new ChatMessage() { Contents = [new TextContent("A")] },
new ChatMessage() { Contents = [new TextContent("B")] },
new ChatMessage() { Contents = [new TextContent("C")] },
new ChatMessage() { Contents = [new TextContent("D")] }
};
await provider.InvokedAsync(new AIContextProvider.InvokedContext(messages, aiContextProviderMessages: null));
// Act
JsonElement state = provider.Serialize();
var roundTrippedProvider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5,
serializedState: state,
options: new ContextualFunctionProviderOptions
{
NumberOfRecentMessagesInContext = 4
});
// Trigger search to verify messages are used
var invokingContext = new AIContextProvider.InvokingContext(Array.Empty<ChatMessage>());
await roundTrippedProvider.InvokingAsync(invokingContext);
// Assert
string expected = string.Join(Environment.NewLine, ["A", "B", "C", "D"]);
this._collectionMock.Verify(c => c.SearchAsync(expected, It.IsAny<int>(), null, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Deserialize_WithChangedLowerLimit_ShouldTruncateToNewLimitAsync()
{
// Arrange
var functions = new List<AIFunction> { CreateFunction("f1") };
var initialProvider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5,
options: new ContextualFunctionProviderOptions
{
NumberOfRecentMessagesInContext = 5
});
var messages = new[]
{
new ChatMessage() { Contents = [new TextContent("L1")] },
new ChatMessage() { Contents = [new TextContent("L2")] },
new ChatMessage() { Contents = [new TextContent("L3")] },
new ChatMessage() { Contents = [new TextContent("L4")] },
new ChatMessage() { Contents = [new TextContent("L5")] }
};
await initialProvider.InvokedAsync(new AIContextProvider.InvokedContext(messages, aiContextProviderMessages: null));
JsonElement state = initialProvider.Serialize();
// Act
var restoredProvider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5,
serializedState: state,
options: new ContextualFunctionProviderOptions
{
NumberOfRecentMessagesInContext = 3 // Lower limit
});
var invokingContext = new AIContextProvider.InvokingContext(Array.Empty<ChatMessage>());
await restoredProvider.InvokingAsync(invokingContext);
// Assert
string expected = string.Join(Environment.NewLine, ["L1", "L2", "L3"]);
this._collectionMock.Verify(c => c.SearchAsync(expected, It.IsAny<int>(), null, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Deserialize_WithEmptyState_ShouldHaveNoMessagesAsync()
{
// Arrange
var functions = new List<AIFunction> { CreateFunction("f1") };
JsonElement emptyState = JsonSerializer.Deserialize("{}", TestJsonSerializerContext.Default.JsonElement);
// Act
var provider = new ContextualFunctionProvider(
vectorStore: this._vectorStoreMock.Object,
vectorDimensions: 1536,
functions: functions,
maxNumberOfFunctions: 5,
serializedState: emptyState,
options: new ContextualFunctionProviderOptions
{
NumberOfRecentMessagesInContext = 3
});
var invokingContext = new AIContextProvider.InvokingContext(Array.Empty<ChatMessage>());
await provider.InvokingAsync(invokingContext);
// Assert
this._collectionMock.Verify(c => c.SearchAsync(string.Empty, It.IsAny<int>(), null, It.IsAny<CancellationToken>()), Times.Once);
}
private static AIFunction CreateFunction(string name, string description = "")
{
return AIFunctionFactory.Create(() => { }, name, description);
}
}
@@ -0,0 +1,141 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Functions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Moq;
namespace Microsoft.Agents.AI.UnitTests.Functions;
/// <summary>
/// Contains unit tests for the <see cref="FunctionStore"/> class.
/// </summary>
public sealed class FunctionStoreTests
{
private readonly Mock<VectorStore> _vectorStoreMock;
private readonly Mock<VectorStoreCollection<object, Dictionary<string, object?>>> _collectionMock;
public FunctionStoreTests()
{
this._vectorStoreMock = new Mock<VectorStore>(MockBehavior.Strict);
this._collectionMock = new Mock<VectorStoreCollection<object, Dictionary<string, object?>>>(MockBehavior.Strict);
this._vectorStoreMock
.Setup(vs => vs.GetDynamicCollection(It.IsAny<string>(), It.IsAny<VectorStoreCollectionDefinition>()))
.Returns(this._collectionMock.Object);
this._collectionMock
.Setup(c => c.CollectionExistsAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
this._collectionMock
.Setup(c => c.EnsureCollectionExistsAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
this._collectionMock
.Setup(c => c.UpsertAsync(It.IsAny<IEnumerable<Dictionary<string, object?>>>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
this._collectionMock
.Setup(c => c.SearchAsync(It.IsAny<string>(), It.IsAny<int>(), null, It.IsAny<CancellationToken>()))
.Returns(AsyncEnumerable.Empty<VectorSearchResult<Dictionary<string, object?>>>());
}
[Fact]
public void Constructor_ShouldThrowOnInvalidArguments()
{
var functions = new List<AIFunction> { CreateFunction("f1") };
Assert.Throws<ArgumentNullException>(() => new FunctionStore(null!, "col", 1, functions, 3));
Assert.Throws<ArgumentException>(() => new FunctionStore(this._vectorStoreMock.Object, "", 1, functions, 3));
Assert.Throws<ArgumentOutOfRangeException>(() => new FunctionStore(this._vectorStoreMock.Object, "col", 0, functions, 3));
Assert.Throws<ArgumentNullException>(() => new FunctionStore(this._vectorStoreMock.Object, "col", 1, null!, 3));
}
[Fact]
public async Task SaveAsync_ShouldUpsertFunctionsAsync()
{
// Arrange
var functions = new List<AIFunction>
{
CreateFunction("f1", "desc1"),
CreateFunction("f2", "desc2")
};
this._collectionMock.Setup(c => c.UpsertAsync(It.IsAny<IEnumerable<Dictionary<string, object?>>>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask)
.Verifiable();
var store = new FunctionStore(this._vectorStoreMock.Object, "col", 3, functions, 3);
// Act
await store.SaveAsync();
// Assert
this._collectionMock.Verify(c => c.EnsureCollectionExistsAsync(It.IsAny<CancellationToken>()), Times.Once);
this._collectionMock.Verify(c => c.UpsertAsync(It.Is<IEnumerable<Dictionary<string, object?>>>(records =>
records.Count() == 2 &&
records.Any(r => (r["Name"] as string) == "f1") &&
records.Any(r => (r["Name"] as string) == "f2")
), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task SearchAsync_ShouldReturnMatchingFunctionsAsync()
{
// Arrange
var functions = new List<AIFunction>
{
CreateFunction("f1", "desc1"),
CreateFunction("f2", "desc2"),
CreateFunction("f3", "desc3")
};
var searchResults = new List<VectorSearchResult<Dictionary<string, object?>>>
{
new(new Dictionary<string, object?> { ["Name"] = "f3" }, 0.3),
new(new Dictionary<string, object?> { ["Name"] = "f2" }, 0.2),
new(new Dictionary<string, object?> { ["Name"] = "f1" }, 0.1)
};
this._collectionMock.Setup(c => c.SearchAsync(It.IsAny<string>(), It.IsAny<int>(), null, It.IsAny<CancellationToken>()))
.Returns(searchResults.ToAsyncEnumerable());
var store = new FunctionStore(this._vectorStoreMock.Object, "col", 3, functions, 3);
// Act
var result = await store.SearchAsync("desc3");
// Assert
var resultList = result.ToList();
Assert.Equal(3, resultList.Count);
Assert.Equal("f3", resultList[0].Name);
Assert.Equal("f2", resultList[1].Name);
Assert.Equal("f1", resultList[2].Name);
}
[Fact]
public async Task SearchAsync_ShouldThrowIfCollectionDoesNotExistAsync()
{
// Arrange
var functions = new List<AIFunction> { CreateFunction("f1") };
this._collectionMock.Setup(c => c.CollectionExistsAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var store = new FunctionStore(this._vectorStoreMock.Object, "col", 3, functions, 3);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => store.SearchAsync("query"));
}
private static AIFunction CreateFunction(string name, string description = "desc")
{
return AIFunctionFactory.Create(() => { }, name, description);
}
}
@@ -40,12 +40,6 @@ internal sealed class MockAgentProvider : Mock<WorkflowAgentProvider>
It.IsAny<bool>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(testMessages));
this.Setup(provider => provider.CreateMessageAsync(
It.IsAny<string>(),
It.IsAny<ChatMessage>(),
It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(testMessages.First()));
}
private string CreateConversationId()
@@ -1,83 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="AddConversationMessageExecutor"/>.
/// </summary>
public sealed class AddConversationMessageExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Theory]
[InlineData(AgentMessageRole.User)]
[InlineData(AgentMessageRole.Agent)]
public async Task AddMessageSuccessfullyAsync(AgentMessageRole role)
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(AddMessageSuccessfullyAsync),
variableName: "TestMessage",
role: AgentMessageRoleWrapper.Get(role),
messageText: $"Hello from {role}");
}
private async Task ExecuteTestAsync(
string displayName,
string variableName,
AgentMessageRoleWrapper role,
string messageText)
{
// Arrange
MockAgentProvider mockAgentProvider = new();
AddConversationMessage model = this.CreateModel(
this.FormatDisplayName(displayName),
FormatVariablePath(variableName),
"TestConversationId",
role,
messageText);
AddConversationMessageExecutor action = new(model, mockAgentProvider.Object, this.State);
// Act
await this.ExecuteAsync(action);
// Assert
ChatMessage? testMessage = mockAgentProvider.TestMessages?.FirstOrDefault();
Assert.NotNull(testMessage);
VerifyModel(model, action);
this.VerifyState(variableName, testMessage.ToRecord());
}
private AddConversationMessage CreateModel(
string displayName,
string messageVariable,
string conversationId,
AgentMessageRoleWrapper role,
string messageText)
{
AddConversationMessage.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Message = PropertyPath.Create(messageVariable),
ConversationId = StringExpression.Literal(conversationId),
Role = role,
};
actionBuilder.Content.Add(new AddConversationMessageContent.Builder
{
Type = AgentMessageContentType.Text,
Value = TemplateLine.Parse(messageText)
});
return AssignParent<AddConversationMessage>(actionBuilder);
}
}
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
@@ -38,24 +37,14 @@ public class OpenAIAssistantClientExtensionsTests
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [weatherFunction]
}
}),
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [weatherFunction])),
"CreateWithChatClientAgentOptionsSync" => this._assistantClient.CreateAIAgent(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [weatherFunction]
}
}),
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [weatherFunction])),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
instructions: AgentInstructions,
@@ -105,24 +94,14 @@ public class OpenAIAssistantClientExtensionsTests
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = Instructions,
Tools = [codeInterpreterTool]
}
}),
options: new ChatClientAgentOptions(
instructions: Instructions,
tools: [codeInterpreterTool])),
"CreateWithChatClientAgentOptionsSync" => this._assistantClient.CreateAIAgent(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = Instructions,
Tools = [codeInterpreterTool]
}
}),
options: new ChatClientAgentOptions(
instructions: Instructions,
tools: [codeInterpreterTool])),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
instructions: Instructions,
@@ -174,33 +153,20 @@ public class OpenAIAssistantClientExtensionsTests
});
string vectorStoreId = vectorStoreCreate.Value.Id;
// Wait for vector store indexing to complete before using it
await WaitForVectorStoreReadyAsync(vectorStoreClient, vectorStoreId);
var fileSearchTool = new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreId)] };
var agent = createMechanism switch
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = Instructions,
Tools = [fileSearchTool]
}
}),
options: new ChatClientAgentOptions(
instructions: Instructions,
tools: [fileSearchTool])),
"CreateWithChatClientAgentOptionsSync" => this._assistantClient.CreateAIAgent(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = Instructions,
Tools = [fileSearchTool]
}
}),
options: new ChatClientAgentOptions(
instructions: Instructions,
tools: [fileSearchTool])),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
instructions: Instructions,
@@ -223,43 +189,4 @@ public class OpenAIAssistantClientExtensionsTests
File.Delete(searchFilePath);
}
}
/// <summary>
/// Waits for a vector store to complete indexing by polling its status.
/// </summary>
/// <param name="client">The vector store client.</param>
/// <param name="vectorStoreId">The ID of the vector store.</param>
/// <param name="maxWaitSeconds">Maximum time to wait in seconds (default: 30).</param>
/// <returns>A task that completes when the vector store is ready or throws on timeout/failure.</returns>
private static async Task WaitForVectorStoreReadyAsync(
VectorStoreClient client,
string vectorStoreId,
int maxWaitSeconds = 30)
{
Stopwatch sw = Stopwatch.StartNew();
while (sw.Elapsed.TotalSeconds < maxWaitSeconds)
{
VectorStore vectorStore = await client.GetVectorStoreAsync(vectorStoreId);
VectorStoreStatus status = vectorStore.Status;
if (status == VectorStoreStatus.Completed)
{
if (vectorStore.FileCounts.Failed > 0)
{
throw new InvalidOperationException("Vector store indexing failed for some files");
}
return;
}
if (status == VectorStoreStatus.Expired)
{
throw new InvalidOperationException("Vector store has expired");
}
await Task.Delay(1000);
}
throw new TimeoutException($"Vector store did not complete indexing within {maxWaitSeconds}s");
}
}
@@ -47,7 +47,8 @@ public class OpenAIChatCompletionFixture : IChatClientAgentFixture
return Task.FromResult(new ChatClientAgent(chatClient, options: new()
{
Name = name,
ChatOptions = new() { Instructions = instructions, Tools = aiTools }
Instructions = instructions,
ChatOptions = new() { Tools = aiTools }
}));
}
@@ -73,9 +73,9 @@ public class OpenAIResponseFixture(bool store) : IChatClientAgentFixture
options: new()
{
Name = name,
Instructions = instructions,
ChatOptions = new ChatOptions
{
Instructions = instructions,
Tools = aiTools,
RawRepresentationFactory = new Func<IChatClient, object>(_ => new ResponseCreationOptions() { StoredOutputEnabled = store })
},
+1 -34
View File
@@ -7,38 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.0b251204] - 2025-12-04
### Added
- **agent-framework-core**: Add support for Pydantic `BaseModel` as function call result (#2606)
- **agent-framework-core**: Executor events now include I/O data (#2591)
- **samples**: Inline YAML declarative sample (#2582)
- **samples**: Handoff-as-agent with HITL sample (#2534)
### Changed
- **agent-framework-core**: [BREAKING] Support Magentic agent tool call approvals and plan stalling HITL behavior (#2569)
- **agent-framework-core**: [BREAKING] Standardize orchestration outputs as list of `ChatMessage`; allow agent as group chat manager (#2291)
- **agent-framework-core**: [BREAKING] Respond with `AgentRunResponse` including serialized structured output (#2285)
- **observability**: Use `executor_id` and `edge_group_id` as span names for clearer traces (#2538)
- **agent-framework-devui**: Add multimodal input support for workflows and refactor chat input (#2593)
- **docs**: Update Python orchestration documentation (#2087)
### Fixed
- **observability**: Resolve mypy error in observability module (#2641)
- **agent-framework-core**: Fix `AgentRunResponse.created_at` returning local datetime labeled as UTC (#2590)
- **agent-framework-core**: Emit `ExecutorFailedEvent` before `WorkflowFailedEvent` when executor throws (#2537)
- **agent-framework-core**: Fix MagenticAgentExecutor producing `repr` string for tool call content (#2566)
- **agent-framework-core**: Fixed empty text content Pydantic validation failure (#2539)
- **agent-framework-azure-ai**: Added support for application endpoints in Azure AI client (#2460)
- **agent-framework-azurefunctions**: Add MCP tool support (#2385)
- **agent-framework-core**: Preserve MCP array items schema in Pydantic field generation (#2382)
- **agent-framework-devui**: Make tool call view optional and fix links (#2243)
- **agent-framework-core**: Always include output in function call result messages (#2414)
- **agent-framework-redis**: Fix TypeError (#2411)
## [1.0.0b251120] - 2025-11-20
### Added
@@ -322,8 +290,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/).
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251204...HEAD
[1.0.0b251204]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251120...python-1.0.0b251204
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251120...HEAD
[1.0.0b251120]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251117...python-1.0.0b251120
[1.0.0b251117]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251114...python-1.0.0b251117
[1.0.0b251114]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251112.post1...python-1.0.0b251114
+1 -1
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.0b251204"
version = "1.0.0b251120"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -134,13 +134,6 @@ class AgentFrameworkEventBridge:
logger.info(f" Suppressed summary length={len(self.suppressed_summary)}")
return events
# Skip empty text chunks to avoid emitting
# TextMessageContentEvent with an empty `delta` which fails
# Pydantic validation (AG-UI requires non-empty strings).
if not content.text:
logger.info(" SKIPPING TextContent: empty chunk")
return events
if not self.current_message_id:
self.current_message_id = generate_event_id()
start_event = TextMessageStartEvent(
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "agent-framework-ag-ui"
version = "1.0.0b251204"
version = "1.0.0b251120"
description = "AG-UI protocol integration for Agent Framework"
readme = "README.md"
license-files = ["LICENSE"]
@@ -68,37 +68,6 @@ async def test_skip_text_content_for_structured_outputs():
assert len(events) == 0
async def test_skip_text_content_for_empty_text():
"""Test streaming TextContent with empty chunks."""
from agent_framework_ag_ui._events import AgentFrameworkEventBridge
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
update1 = AgentRunResponseUpdate(contents=[TextContent(text="Hello ")])
update2 = AgentRunResponseUpdate(contents=[TextContent(text="")]) # Empty chunk
update3 = AgentRunResponseUpdate(contents=[TextContent(text="world")])
events1 = await bridge.from_agent_run_update(update1)
events2 = await bridge.from_agent_run_update(update2)
events3 = await bridge.from_agent_run_update(update3)
# First update: START + CONTENT
assert len(events1) == 2
assert events1[0].type == "TEXT_MESSAGE_START"
assert events1[1].delta == "Hello "
# Second update: should skip empty chunk, no events
assert len(events2) == 0
# Third update: just CONTENT (same message)
assert len(events3) == 1
assert events3[0].type == "TEXT_MESSAGE_CONTENT"
assert events3[0].delta == "world"
# Both content events should have same message_id
assert events1[1].message_id == events3[0].message_id
async def test_tool_call_with_name():
"""Test FunctionCallContent with name emits ToolCallStartEvent."""
from agent_framework_ag_ui._events import AgentFrameworkEventBridge
+1 -1
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.0b251204"
version = "1.0.0b251120"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -5,7 +5,7 @@ import sys
from collections.abc import Awaitable, Callable, MutableSequence
from typing import TYPE_CHECKING, Any, ClassVar, Literal
from agent_framework import AGENT_FRAMEWORK_USER_AGENT, ChatMessage, Context, ContextProvider, Role
from agent_framework import ChatMessage, Context, ContextProvider, Role
from agent_framework._logging import get_logger
from agent_framework._pydantic import AFBaseSettings
from agent_framework.exceptions import ServiceInitializationError
@@ -129,8 +129,6 @@ class AzureAISearchSettings(AFBaseSettings):
Can be set via environment variable AZURE_SEARCH_ENDPOINT.
index_name: Name of the search index.
Can be set via environment variable AZURE_SEARCH_INDEX_NAME.
knowledge_base_name: Name of an existing Knowledge Base (for agentic mode).
Can be set via environment variable AZURE_SEARCH_KNOWLEDGE_BASE_NAME.
api_key: API key for authentication (optional, use managed identity if not provided).
Can be set via environment variable AZURE_SEARCH_API_KEY.
env_file_path: If provided, the .env settings are read from this file path location.
@@ -160,7 +158,6 @@ class AzureAISearchSettings(AFBaseSettings):
endpoint: str | None = None
index_name: str | None = None
knowledge_base_name: str | None = None
api_key: SecretStr | None = None
@@ -242,6 +239,7 @@ class AzureAISearchContextProvider(ContextProvider):
embedding_function: Callable[[str], Awaitable[list[float]]] | None = None,
context_prompt: str | None = None,
# Agentic mode parameters (Knowledge Base)
azure_ai_project_endpoint: str | None = None,
azure_openai_resource_url: str | None = None,
model_deployment_name: str | None = None,
model_name: str | None = None,
@@ -279,18 +277,22 @@ class AzureAISearchContextProvider(ContextProvider):
Required if vector_field_name is specified and no server-side vectorization.
context_prompt: Custom prompt to prepend to retrieved context.
Default: "Use the following context to answer the question:"
azure_ai_project_endpoint: Azure AI Foundry project endpoint URL.
This is NOT the same as azure_openai_resource_url - the project endpoint is used
for Azure AI Foundry services, while the OpenAI endpoint is used by the Knowledge
Base to call the model for query planning. Required for agentic mode.
Example: "https://myproject.services.ai.azure.com/api/projects/myproject"
azure_openai_resource_url: Azure OpenAI resource URL for Knowledge Base model calls.
Required when using agentic mode with index_name (to auto-create Knowledge Base).
Not required when using an existing knowledge_base_name.
Example: "https://myresource.openai.azure.com"
This is the OpenAI endpoint used by the Knowledge Base to call the LLM for
query planning and reasoning. This is separate from the project endpoint because
the Knowledge Base directly calls Azure OpenAI for its internal operations.
Required for agentic mode. Example: "https://myresource.openai.azure.com"
model_deployment_name: Model deployment name in Azure OpenAI for Knowledge Base.
Required when using agentic mode with index_name (to auto-create Knowledge Base).
Not required when using an existing knowledge_base_name.
This is the deployment name the Knowledge Base uses to call the LLM.
Required for agentic mode.
model_name: The underlying model name (e.g., "gpt-4o", "gpt-4o-mini").
If not provided, defaults to model_deployment_name. Used for Knowledge Base configuration.
knowledge_base_name: Name of an existing Knowledge Base to use.
Required for agentic mode if not providing index_name.
Supports KBs with any source type (web, blob, index, etc.).
knowledge_base_name: Name for the Knowledge Base. Required for agentic mode.
retrieval_instructions: Custom instructions for the Knowledge Base's
retrieval planning. Only used in agentic mode.
azure_openai_api_key: Azure OpenAI API key for Knowledge Base to call the model.
@@ -338,7 +340,6 @@ class AzureAISearchContextProvider(ContextProvider):
settings = AzureAISearchSettings(
endpoint=endpoint,
index_name=index_name,
knowledge_base_name=knowledge_base_name,
api_key=api_key if isinstance(api_key, str) else None,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
@@ -352,36 +353,11 @@ class AzureAISearchContextProvider(ContextProvider):
"Azure AI Search endpoint is required. Set via 'endpoint' parameter "
"or 'AZURE_SEARCH_ENDPOINT' environment variable."
)
# Validate index_name and knowledge_base_name based on mode
# Note: settings.* contains the resolved value (explicit param OR env var)
if mode == "semantic":
# Semantic mode: always requires index_name
if not settings.index_name:
raise ServiceInitializationError(
"Azure AI Search index name is required for semantic mode. "
"Set via 'index_name' parameter or 'AZURE_SEARCH_INDEX_NAME' environment variable."
)
elif mode == "agentic":
# Agentic mode: requires exactly ONE of index_name or knowledge_base_name
if settings.index_name and settings.knowledge_base_name:
raise ServiceInitializationError(
"For agentic mode, provide either 'index_name' OR 'knowledge_base_name', not both. "
"Use 'index_name' to auto-create a Knowledge Base, or 'knowledge_base_name' to use an existing one."
)
if not settings.index_name and not settings.knowledge_base_name:
raise ServiceInitializationError(
"For agentic mode, provide either 'index_name' (to auto-create Knowledge Base) "
"or 'knowledge_base_name' (to use existing Knowledge Base). "
"Set via parameters or environment variables "
"AZURE_SEARCH_INDEX_NAME / AZURE_SEARCH_KNOWLEDGE_BASE_NAME."
)
# If using index_name to create KB, model config is required
if settings.index_name and not model_deployment_name:
raise ServiceInitializationError(
"model_deployment_name is required for agentic mode when creating Knowledge Base from index. "
"This is the Azure OpenAI deployment used by the Knowledge Base for query planning."
)
if not settings.index_name:
raise ServiceInitializationError(
"Azure AI Search index name is required. Set via 'index_name' parameter "
"or 'AZURE_SEARCH_INDEX_NAME' environment variable."
)
# Determine the credential to use
resolved_credential: AzureKeyCredential | AsyncTokenCredential
@@ -413,27 +389,14 @@ class AzureAISearchContextProvider(ContextProvider):
self.azure_openai_deployment_name = model_deployment_name
# If model_name not provided, default to deployment name
self.model_name = model_name or model_deployment_name
# Use resolved KB name (from explicit param or env var)
self.knowledge_base_name = settings.knowledge_base_name
self.knowledge_base_name = knowledge_base_name
self.retrieval_instructions = retrieval_instructions
self.azure_openai_api_key = azure_openai_api_key
self.azure_ai_project_endpoint = azure_ai_project_endpoint
self.knowledge_base_output_mode = knowledge_base_output_mode
self.retrieval_reasoning_effort = retrieval_reasoning_effort
self.agentic_message_history_count = agentic_message_history_count
# Determine if using existing Knowledge Base or auto-creating from index
# Since validation ensures exactly one of index_name/knowledge_base_name for agentic mode:
# - knowledge_base_name provided: use existing KB
# - index_name provided: auto-create KB from index
self._use_existing_knowledge_base = False
if mode == "agentic":
if settings.knowledge_base_name:
# Use existing KB directly (supports any source type: web, blob, index, etc.)
self._use_existing_knowledge_base = True
else:
# Auto-generate KB name from index name
self.knowledge_base_name = f"{settings.index_name}-kb"
# Auto-discover vector field if not specified
self._auto_discovered_vector_field = False
self._use_vectorizable_query = False # Will be set to True if server-side vectorization detected
@@ -452,24 +415,22 @@ class AzureAISearchContextProvider(ContextProvider):
"Agentic retrieval requires azure-search-documents >= 11.7.0b1 with Knowledge Base support. "
"Please upgrade: pip install azure-search-documents>=11.7.0b1"
)
# Only require OpenAI resource URL if NOT using existing KB
# (existing KB already has its model configuration)
# Note: model_deployment_name is already validated at initialization
if not self._use_existing_knowledge_base and not self.azure_openai_resource_url:
if not self.azure_openai_resource_url:
raise ValueError(
"azure_openai_resource_url is required for agentic mode when creating Knowledge Base from index. "
"azure_openai_resource_url is required for agentic mode. "
"This should be your Azure OpenAI endpoint (e.g., 'https://myresource.openai.azure.com')"
)
if not self.azure_openai_deployment_name:
raise ValueError("model_deployment_name is required for agentic mode")
if not knowledge_base_name:
raise ValueError("knowledge_base_name is required for agentic mode")
# Create search client for semantic mode (only if index_name is available)
self._search_client: SearchClient | None = None
if self.index_name:
self._search_client = SearchClient(
endpoint=self.endpoint,
index_name=self.index_name,
credential=self.credential,
user_agent=AGENT_FRAMEWORK_USER_AGENT,
)
# Create search client for semantic mode
self._search_client = SearchClient(
endpoint=self.endpoint,
index_name=self.index_name,
credential=self.credential,
)
# Create index client and retrieval client for agentic mode (Knowledge Base)
self._index_client: SearchIndexClient | None = None
@@ -478,7 +439,6 @@ class AzureAISearchContextProvider(ContextProvider):
self._index_client = SearchIndexClient(
endpoint=self.endpoint,
credential=self.credential,
user_agent=AGENT_FRAMEWORK_USER_AGENT,
)
# Retrieval client will be created after Knowledge Base initialization
@@ -614,19 +574,10 @@ class AzureAISearchContextProvider(ContextProvider):
try:
# Use existing index client or create temporary one
if not self._index_client:
self._index_client = SearchIndexClient(
endpoint=self.endpoint,
credential=self.credential,
user_agent=AGENT_FRAMEWORK_USER_AGENT,
)
self._index_client = SearchIndexClient(endpoint=self.endpoint, credential=self.credential)
index_client = self._index_client
# Get index schema (index_name is guaranteed to be set for semantic mode)
if not self.index_name:
logger.warning("Cannot auto-discover vector field: index_name is not set.")
self._auto_discovered_vector_field = True
return
# Get index schema
index = await index_client.get_index(self.index_name)
# Step 1: Find all vector fields
@@ -743,10 +694,7 @@ class AzureAISearchContextProvider(ContextProvider):
search_params["semantic_configuration_name"] = self.semantic_configuration_name
search_params["query_caption"] = QueryCaptionType.EXTRACTIVE
# Execute search (search client is guaranteed to exist for semantic mode)
if not self._search_client:
raise RuntimeError("Search client is not initialized. This should not happen in semantic mode.")
# Execute search
results = await self._search_client.search(**search_params) # type: ignore[reportUnknownVariableType]
# Format results with citations
@@ -763,48 +711,27 @@ class AzureAISearchContextProvider(ContextProvider):
return formatted_results
async def _ensure_knowledge_base(self) -> None:
"""Ensure Knowledge Base and knowledge source are created or use existing KB.
"""Ensure Knowledge Base and knowledge source are created.
This method is idempotent - it will only create resources if they don't exist.
Note: Azure SDK uses KnowledgeAgent classes internally, but the feature
is marketed as "Knowledge Bases" in Azure AI Search.
"""
if self._knowledge_base_initialized:
if self._knowledge_base_initialized or not self._index_client:
return
# Runtime validation
# Runtime validation for agentic mode parameters
if not self.knowledge_base_name:
raise ValueError("knowledge_base_name is required for agentic mode")
if not self.azure_openai_resource_url:
raise ValueError("azure_openai_resource_url is required for agentic mode")
if not self.azure_openai_deployment_name:
raise ValueError("model_deployment_name is required for agentic mode")
knowledge_base_name = self.knowledge_base_name
# Path 1: Use existing Knowledge Base directly (no index needed)
# This supports KB with any source type (web, blob, index, etc.)
if self._use_existing_knowledge_base:
# Just create the retrieval client - KB already exists with its own sources
if _agentic_retrieval_available and self._retrieval_client is None:
self._retrieval_client = KnowledgeBaseRetrievalClient(
endpoint=self.endpoint,
knowledge_base_name=knowledge_base_name,
credential=self.credential,
user_agent=AGENT_FRAMEWORK_USER_AGENT,
)
self._knowledge_base_initialized = True
return
# Path 2: Auto-create Knowledge Base from search index
# Requires index_client and OpenAI configuration
if not self._index_client:
raise ValueError("Index client is required when creating Knowledge Base from index")
if not self.azure_openai_resource_url:
raise ValueError("azure_openai_resource_url is required when creating Knowledge Base from index")
if not self.azure_openai_deployment_name:
raise ValueError("model_deployment_name is required when creating Knowledge Base from index")
if not self.index_name:
raise ValueError("index_name is required when creating Knowledge Base from index")
# Step 1: Create or get knowledge source from index
# Step 1: Create or get knowledge source
knowledge_source_name = f"{self.index_name}-source"
try:
@@ -867,7 +794,6 @@ class AzureAISearchContextProvider(ContextProvider):
endpoint=self.endpoint,
knowledge_base_name=knowledge_base_name,
credential=self.credential,
user_agent=AGENT_FRAMEWORK_USER_AGENT,
)
async def _agentic_search(self, messages: list[ChatMessage]) -> list[str]:
@@ -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.0b251204"
version = "1.0.0b251118"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -148,105 +148,74 @@ class TestSearchProviderInitialization:
vector_field_name="embedding",
)
def test_init_agentic_mode_with_kb_only(self) -> None:
"""Test agentic mode with existing knowledge_base_name (simplest path)."""
# Clear environment to ensure no env vars interfere
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
with patch.dict(os.environ, clean_env, clear=True):
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
api_key="test-key",
mode="agentic",
knowledge_base_name="test-kb",
env_file_path="", # Disable .env file loading
)
assert provider.mode == "agentic"
assert provider.knowledge_base_name == "test-kb"
assert provider._use_existing_knowledge_base is True
def test_init_agentic_mode_with_index_requires_model(self) -> None:
"""Test that agentic mode with index_name requires model_deployment_name."""
# Clear environment to ensure no env vars interfere
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
with (
patch.dict(os.environ, clean_env, clear=True),
pytest.raises(ServiceInitializationError, match="model_deployment_name"),
):
def test_init_agentic_mode_requires_azure_openai_resource_url(self) -> None:
"""Test that agentic mode requires azure_openai_resource_url."""
with pytest.raises(ValueError, match="azure_openai_resource_url"):
AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
env_file_path="", # Disable .env file loading
)
def test_init_agentic_mode_with_index_and_model(self) -> None:
"""Test agentic mode with index_name (auto-create KB path)."""
# Clear environment to ensure no env vars interfere
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
with patch.dict(os.environ, clean_env, clear=True):
provider = AzureAISearchContextProvider(
def test_init_agentic_mode_requires_model_deployment_name(self) -> None:
"""Test that agentic mode requires model_deployment_name."""
with pytest.raises(ValueError, match="model_deployment_name"):
AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
azure_ai_project_endpoint="https://test.services.ai.azure.com",
azure_openai_resource_url="https://test.openai.azure.com",
)
def test_init_agentic_mode_requires_knowledge_base_name(self) -> None:
"""Test that agentic mode requires knowledge_base_name."""
with pytest.raises(ValueError, match="knowledge_base_name"):
AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
azure_ai_project_endpoint="https://test.services.ai.azure.com",
model_deployment_name="gpt-4o",
azure_openai_resource_url="https://test.openai.azure.com",
env_file_path="", # Disable .env file loading
)
assert provider.mode == "agentic"
assert provider.index_name == "test-index"
assert provider.knowledge_base_name == "test-index-kb" # Auto-generated
assert provider._use_existing_knowledge_base is False
def test_init_agentic_mode_rejects_both_index_and_kb(self) -> None:
"""Test that agentic mode rejects both index_name AND knowledge_base_name."""
# Clear environment to ensure no env vars interfere
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
with (
patch.dict(os.environ, clean_env, clear=True),
pytest.raises(ServiceInitializationError, match="either 'index_name' OR 'knowledge_base_name', not both"),
):
AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
knowledge_base_name="test-kb",
model_deployment_name="gpt-4o",
azure_openai_resource_url="https://test.openai.azure.com",
env_file_path="", # Disable .env file loading
)
def test_init_agentic_mode_requires_index_or_kb(self) -> None:
"""Test that agentic mode requires either index_name or knowledge_base_name."""
# Clear environment to ensure no env vars interfere
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
with (
patch.dict(os.environ, clean_env, clear=True),
pytest.raises(ServiceInitializationError, match="provide either 'index_name'.*or 'knowledge_base_name'"),
):
AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
api_key="test-key",
mode="agentic",
env_file_path="", # Disable .env file loading
)
def test_init_agentic_mode_with_all_params(self) -> None:
"""Test initialization with all agentic mode parameters."""
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
azure_ai_project_endpoint="https://test.services.ai.azure.com",
model_deployment_name="my-gpt-4o-deployment",
model_name="gpt-4o",
knowledge_base_name="test-kb",
azure_openai_resource_url="https://test.openai.azure.com",
)
assert provider.mode == "agentic"
assert provider.azure_ai_project_endpoint == "https://test.services.ai.azure.com"
assert provider.azure_openai_resource_url == "https://test.openai.azure.com"
assert provider.azure_openai_deployment_name == "my-gpt-4o-deployment"
assert provider.model_name == "gpt-4o"
assert provider.knowledge_base_name == "test-kb"
def test_init_model_name_defaults_to_deployment_name(self) -> None:
"""Test that model_name defaults to deployment_name if not provided."""
# Clear environment to ensure no env vars interfere
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
with patch.dict(os.environ, clean_env, clear=True):
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
api_key="test-key",
mode="agentic",
knowledge_base_name="test-kb",
model_deployment_name="gpt-4o",
env_file_path="", # Disable .env file loading
)
assert provider.model_name == "gpt-4o"
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
azure_ai_project_endpoint="https://test.services.ai.azure.com",
model_deployment_name="gpt-4o",
knowledge_base_name="test-kb",
azure_openai_resource_url="https://test.openai.azure.com",
)
assert provider.model_name == "gpt-4o"
def test_init_with_custom_context_prompt(self) -> None:
"""Test initialization with custom context prompt."""
@@ -366,7 +335,7 @@ class TestKnowledgeBaseSetup:
async def test_ensure_knowledge_base_creates_when_not_exists(
self, mock_search_class: MagicMock, mock_index_class: MagicMock
) -> None:
"""Test that Knowledge Base is created when it doesn't exist (index_name path)."""
"""Test that Knowledge Base is created when it doesn't exist."""
# Setup mocks
mock_index_client = AsyncMock()
mock_index_client.get_knowledge_source.side_effect = ResourceNotFoundError("Not found")
@@ -378,58 +347,57 @@ class TestKnowledgeBaseSetup:
mock_search_client = AsyncMock()
mock_search_class.return_value = mock_search_client
# Clear environment to ensure no env vars interfere
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
with patch.dict(os.environ, clean_env, clear=True):
# Use index_name path (auto-create KB)
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
model_deployment_name="gpt-4o",
azure_openai_resource_url="https://test.openai.azure.com",
env_file_path="", # Disable .env file loading
)
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
azure_ai_project_endpoint="https://test.services.ai.azure.com",
model_deployment_name="gpt-4o",
model_name="gpt-4o",
knowledge_base_name="test-kb",
azure_openai_resource_url="https://test.openai.azure.com",
)
await provider._ensure_knowledge_base()
await provider._ensure_knowledge_base()
# Verify knowledge source was created
mock_index_client.create_knowledge_source.assert_called_once()
# Verify Knowledge Base was created
mock_index_client.create_or_update_knowledge_base.assert_called_once()
# Verify knowledge source was created
mock_index_client.create_knowledge_source.assert_called_once()
# Verify Knowledge Base was created
mock_index_client.create_or_update_knowledge_base.assert_called_once()
@pytest.mark.asyncio
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_ensure_knowledge_base_skips_when_using_existing_kb(
async def test_ensure_knowledge_base_skips_when_exists(
self, mock_search_class: MagicMock, mock_index_class: MagicMock
) -> None:
"""Test that KB setup is skipped when using existing knowledge_base_name."""
"""Test that Knowledge Base setup is skipped when already exists."""
# Setup mocks
mock_index_client = AsyncMock()
mock_index_client.get_knowledge_source.return_value = MagicMock() # Exists
mock_index_client.get_knowledge_base.return_value = MagicMock() # Exists
mock_index_class.return_value = mock_index_client
mock_search_client = AsyncMock()
mock_search_class.return_value = mock_search_client
# Clear environment to ensure no env vars interfere
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
with patch.dict(os.environ, clean_env, clear=True):
# Use knowledge_base_name path (existing KB)
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
api_key="test-key",
mode="agentic",
knowledge_base_name="test-kb",
env_file_path="", # Disable .env file loading
)
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
azure_ai_project_endpoint="https://test.services.ai.azure.com",
model_deployment_name="gpt-4o",
knowledge_base_name="test-kb",
azure_openai_resource_url="https://test.openai.azure.com",
)
await provider._ensure_knowledge_base()
await provider._ensure_knowledge_base()
# Verify nothing was created (using existing KB)
mock_index_client.create_knowledge_source.assert_not_called()
mock_index_client.create_or_update_knowledge_base.assert_not_called()
# Verify nothing was created
mock_index_client.create_knowledge_source.assert_not_called()
mock_index_client.create_agent.assert_not_called()
class TestContextProviderLifecycle:
@@ -469,22 +437,21 @@ class TestContextProviderLifecycle:
mock_retrieval_client.close = AsyncMock()
mock_retrieval_class.return_value = mock_retrieval_client
# Clear environment to ensure no env vars interfere
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
with patch.dict(os.environ, clean_env, clear=True):
# Use knowledge_base_name path (existing KB)
async with AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
api_key="test-key",
mode="agentic",
knowledge_base_name="test-kb",
env_file_path="", # Disable .env file loading
) as provider:
# Simulate retrieval client being created
provider._retrieval_client = mock_retrieval_client
async with AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
azure_ai_project_endpoint="https://test.services.ai.azure.com",
model_deployment_name="gpt-4o",
knowledge_base_name="test-kb",
azure_openai_resource_url="https://test.openai.azure.com",
) as provider:
# Simulate retrieval client being created
provider._retrieval_client = mock_retrieval_client
# Verify cleanup was called
mock_retrieval_client.close.assert_called_once()
# Verify cleanup was called
mock_retrieval_client.close.assert_called_once()
def test_string_api_key_conversion(self) -> None:
"""Test that string api_key is converted to AzureKeyCredential."""
@@ -612,6 +579,9 @@ class TestAgenticSearch:
# Setup index client mock
mock_index_client = AsyncMock()
mock_index_client.get_knowledge_source.side_effect = ResourceNotFoundError("Not found")
mock_index_client.create_knowledge_source = AsyncMock()
mock_index_client.create_or_update_knowledge_base = AsyncMock()
mock_index_class.return_value = mock_index_client
# Setup retrieval client mock with response
@@ -633,23 +603,22 @@ class TestAgenticSearch:
mock_retrieval_client.close = AsyncMock()
mock_retrieval_class.return_value = mock_retrieval_client
# Clear environment to ensure no env vars interfere
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
with patch.dict(os.environ, clean_env, clear=True):
# Use knowledge_base_name path (existing KB)
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
api_key="test-key",
mode="agentic",
knowledge_base_name="test-kb",
env_file_path="", # Disable .env file loading
)
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
azure_ai_project_endpoint="https://test.services.ai.azure.com",
model_deployment_name="gpt-4o",
knowledge_base_name="test-kb",
azure_openai_resource_url="https://test.openai.azure.com",
)
context = await provider.invoking(sample_messages)
context = await provider.invoking(sample_messages)
assert isinstance(context, Context)
# Should have at least the prompt message
assert len(context.messages) >= 1
assert isinstance(context, Context)
# Should have at least the prompt message
assert len(context.messages) >= 1
@pytest.mark.asyncio
@patch("agent_framework_azure_ai_search._search_provider.KnowledgeBaseRetrievalClient")
@@ -668,6 +637,9 @@ class TestAgenticSearch:
mock_search_class.return_value = mock_search_client
mock_index_client = AsyncMock()
mock_index_client.get_knowledge_source.side_effect = ResourceNotFoundError("Not found")
mock_index_client.create_knowledge_source = AsyncMock()
mock_index_client.create_or_update_knowledge_base = AsyncMock()
mock_index_class.return_value = mock_index_client
# Empty response
@@ -678,23 +650,22 @@ class TestAgenticSearch:
mock_retrieval_client.close = AsyncMock()
mock_retrieval_class.return_value = mock_retrieval_client
# Clear environment to ensure no env vars interfere
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
with patch.dict(os.environ, clean_env, clear=True):
# Use knowledge_base_name path (existing KB)
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
api_key="test-key",
mode="agentic",
knowledge_base_name="test-kb",
env_file_path="", # Disable .env file loading
)
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
azure_ai_project_endpoint="https://test.services.ai.azure.com",
model_deployment_name="gpt-4o",
knowledge_base_name="test-kb",
azure_openai_resource_url="https://test.openai.azure.com",
)
context = await provider.invoking(sample_messages)
context = await provider.invoking(sample_messages)
assert isinstance(context, Context)
# Should have fallback message
assert len(context.messages) >= 1
assert isinstance(context, Context)
# Should have fallback message
assert len(context.messages) >= 1
@pytest.mark.asyncio
@patch("agent_framework_azure_ai_search._search_provider.KnowledgeBaseRetrievalClient")
@@ -713,6 +684,9 @@ class TestAgenticSearch:
mock_search_class.return_value = mock_search_client
mock_index_client = AsyncMock()
mock_index_client.get_knowledge_source.side_effect = ResourceNotFoundError("Not found")
mock_index_client.create_knowledge_source = AsyncMock()
mock_index_client.create_or_update_knowledge_base = AsyncMock()
mock_index_class.return_value = mock_index_client
mock_retrieval_client = AsyncMock()
@@ -732,23 +706,22 @@ class TestAgenticSearch:
mock_retrieval_client.close = AsyncMock()
mock_retrieval_class.return_value = mock_retrieval_client
# Clear environment to ensure no env vars interfere
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
with patch.dict(os.environ, clean_env, clear=True):
# Use knowledge_base_name path (existing KB)
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
api_key="test-key",
mode="agentic",
knowledge_base_name="test-kb",
retrieval_reasoning_effort="medium", # Test medium reasoning
env_file_path="", # Disable .env file loading
)
provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
azure_ai_project_endpoint="https://test.services.ai.azure.com",
model_deployment_name="gpt-4o",
knowledge_base_name="test-kb",
azure_openai_resource_url="https://test.openai.azure.com",
retrieval_reasoning_effort="medium", # Test medium reasoning
)
context = await provider.invoking(sample_messages)
context = await provider.invoking(sample_messages)
assert isinstance(context, Context)
assert len(context.messages) >= 1
assert isinstance(context, Context)
assert len(context.messages) >= 1
class TestVectorFieldAutoDiscovery:
@@ -118,7 +118,6 @@ class AzureAIAgentClient(BaseChatClient):
agents_client: AgentsClient | None = None,
agent_id: str | None = None,
agent_name: str | None = None,
agent_description: str | None = None,
thread_id: str | None = None,
project_endpoint: str | None = None,
model_deployment_name: str | None = None,
@@ -136,7 +135,6 @@ class AzureAIAgentClient(BaseChatClient):
a new agent will be created (and deleted after the request). If neither agents_client
nor agent_id is provided, both will be created and managed automatically.
agent_name: The name to use when creating new agents.
agent_description: The description to use when creating new agents.
thread_id: Default thread ID to use for conversations. Can be overridden by
conversation_id property when making a request.
project_endpoint: The Azure AI Project endpoint URL.
@@ -217,7 +215,6 @@ class AzureAIAgentClient(BaseChatClient):
self.credential = async_credential
self.agent_id = agent_id
self.agent_name = agent_name
self.agent_description = agent_description
self.model_id = azure_ai_settings.model_deployment_name
self.thread_id = thread_id
self.should_cleanup_agent = should_cleanup_agent # Track whether we should delete the agent
@@ -314,7 +311,6 @@ class AzureAIAgentClient(BaseChatClient):
args: dict[str, Any] = {
"model": run_options["model"],
"name": agent_name,
"description": self.agent_description,
}
if "tools" in run_options:
args["tools"] = run_options["tools"]
@@ -1042,19 +1038,16 @@ class AzureAIAgentClient(BaseChatClient):
return run_id, tool_outputs, tool_approvals
def _update_agent_name_and_description(self, agent_name: str | None, description: str | None) -> None:
def _update_agent_name(self, agent_name: str | None) -> None:
"""Update the agent name in the chat client.
Args:
agent_name: The new name for the agent.
description: The new description for the agent.
"""
# This is a no-op in the base class, but can be overridden by subclasses
# to update the agent name in the client.
if agent_name and not self.agent_name:
self.agent_name = agent_name
if description and not self.agent_description:
self.agent_description = description
def service_url(self) -> str:
"""Get the service URL for the chat client.
@@ -62,7 +62,6 @@ class AzureAIClient(OpenAIBaseResponsesClient):
project_client: AIProjectClient | None = None,
agent_name: str | None = None,
agent_version: str | None = None,
agent_description: str | None = None,
conversation_id: str | None = None,
project_endpoint: str | None = None,
model_deployment_name: str | None = None,
@@ -78,7 +77,6 @@ class AzureAIClient(OpenAIBaseResponsesClient):
project_client: An existing AIProjectClient to use. If not provided, one will be created.
agent_name: The name to use when creating new agents or using existing agents.
agent_version: The version of the agent to use.
agent_description: The description to use when creating new agents.
conversation_id: Default conversation ID to use for conversations. Can be overridden by
conversation_id property when making a request.
project_endpoint: The Azure AI Project endpoint URL.
@@ -152,7 +150,6 @@ class AzureAIClient(OpenAIBaseResponsesClient):
# Initialize instance variables
self.agent_name = agent_name
self.agent_version = agent_version
self.agent_description = agent_description
self.use_latest_version = use_latest_version
self.project_client = project_client
self.credential = async_credential
@@ -283,9 +280,7 @@ class AzureAIClient(OpenAIBaseResponsesClient):
args["instructions"] = "".join(combined_instructions)
created_agent = await self.project_client.agents.create_version(
agent_name=self.agent_name,
definition=PromptAgentDefinition(**args),
description=self.agent_description,
agent_name=self.agent_name, definition=PromptAgentDefinition(**args)
)
self.agent_version = created_agent.version
@@ -357,19 +352,16 @@ class AzureAIClient(OpenAIBaseResponsesClient):
"""Initialize OpenAI client."""
self.client = self.project_client.get_openai_client() # type: ignore
def _update_agent_name_and_description(self, agent_name: str | None, description: str | None = None) -> None:
def _update_agent_name(self, agent_name: str | None) -> None:
"""Update the agent name in the chat client.
Args:
agent_name: The new name for the agent.
description: The new description for the agent.
"""
# This is a no-op in the base class, but can be overridden by subclasses
# to update the agent name in the client.
if agent_name and not self.agent_name:
self.agent_name = agent_name
if description and not self.agent_description:
self.agent_description = description
def get_mcp_tool(self, tool: HostedMCPTool) -> Any:
"""Get MCP tool from HostedMCPTool."""
+1 -1
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.0b251204"
version = "1.0.0b251120"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -86,7 +86,6 @@ def create_test_azure_ai_chat_client(
client.credential = None
client.agent_id = agent_id
client.agent_name = agent_name
client.agent_description = None
client.model_id = azure_ai_settings.model_deployment_name
client.thread_id = thread_id
client.should_cleanup_agent = should_cleanup_agent
@@ -442,43 +441,34 @@ async def test_azure_ai_chat_client_close_client_when_should_close_false(mock_ag
mock_agents_client.close.assert_not_called()
def test_azure_ai_chat_client_update_agent_name_and_description_when_current_is_none(
mock_agents_client: MagicMock,
) -> None:
"""Test _update_agent_name_and_description updates name when current agent_name is None."""
def test_azure_ai_chat_client_update_agent_name_when_current_is_none(mock_agents_client: MagicMock) -> None:
"""Test _update_agent_name updates name when current agent_name is None."""
chat_client = create_test_azure_ai_chat_client(mock_agents_client)
chat_client.agent_name = None # type: ignore
chat_client._update_agent_name_and_description("NewAgentName", "description") # type: ignore
chat_client._update_agent_name("NewAgentName") # type: ignore
assert chat_client.agent_name == "NewAgentName"
assert chat_client.agent_description == "description"
def test_azure_ai_chat_client_update_agent_name_and_description_when_current_exists(
mock_agents_client: MagicMock,
) -> None:
"""Test _update_agent_name_and_description does not update when current agent_name exists."""
def test_azure_ai_chat_client_update_agent_name_when_current_exists(mock_agents_client: MagicMock) -> None:
"""Test _update_agent_name does not update when current agent_name exists."""
chat_client = create_test_azure_ai_chat_client(mock_agents_client)
chat_client.agent_name = "ExistingName" # type: ignore
chat_client.agent_description = "ExistingDescription" # type: ignore
chat_client._update_agent_name_and_description("NewAgentName", "description") # type: ignore
chat_client._update_agent_name("NewAgentName") # type: ignore
assert chat_client.agent_name == "ExistingName"
assert chat_client.agent_description == "ExistingDescription"
def test_azure_ai_chat_client_update_agent_name_and_description_with_none_input(mock_agents_client: MagicMock) -> None:
"""Test _update_agent_name_and_description with None input."""
def test_azure_ai_chat_client_update_agent_name_with_none_input(mock_agents_client: MagicMock) -> None:
"""Test _update_agent_name with None input."""
chat_client = create_test_azure_ai_chat_client(mock_agents_client)
chat_client.agent_name = None # type: ignore
chat_client.agent_description = None # type: ignore
chat_client._update_agent_name_and_description(None, None) # type: ignore
chat_client._update_agent_name(None) # type: ignore
assert chat_client.agent_name is None
assert chat_client.agent_description is None
async def test_azure_ai_chat_client_create_run_options_with_messages(mock_agents_client: MagicMock) -> None:
@@ -84,7 +84,6 @@ def create_test_azure_ai_client(
client.credential = None
client.agent_name = agent_name
client.agent_version = agent_version
client.agent_description = None
client.use_latest_version = use_latest_version
client.model_id = azure_ai_settings.model_deployment_name
client.conversation_id = conversation_id
@@ -398,14 +397,14 @@ async def test_azure_ai_client_initialize_client(mock_project_client: MagicMock)
mock_project_client.get_openai_client.assert_called_once()
def test_azure_ai_client_update_agent_name_and_description(mock_project_client: MagicMock) -> None:
"""Test _update_agent_name_and_description method."""
def test_azure_ai_client_update_agent_name(mock_project_client: MagicMock) -> None:
"""Test _update_agent_name method."""
client = create_test_azure_ai_client(mock_project_client)
# Test updating agent name when current is None
with patch.object(client, "_update_agent_name_and_description") as mock_update:
with patch.object(client, "_update_agent_name") as mock_update:
mock_update.return_value = None
client._update_agent_name_and_description("new-agent") # type: ignore
client._update_agent_name("new-agent") # type: ignore
mock_update.assert_called_once_with("new-agent")
# Test behavior when agent name is updated
@@ -413,9 +412,9 @@ def test_azure_ai_client_update_agent_name_and_description(mock_project_client:
client.agent_name = "test-agent" # Manually set for the test
# Test with None input
with patch.object(client, "_update_agent_name_and_description") as mock_update:
with patch.object(client, "_update_agent_name") as mock_update:
mock_update.return_value = None
client._update_agent_name_and_description(None) # type: ignore
client._update_agent_name(None) # type: ignore
mock_update.assert_called_once_with(None)
@@ -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.0b251204"
version = "1.0.0b251120"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
-31
View File
@@ -20,37 +20,6 @@ pip install agent-framework-chatkit --pre
This will install `agent-framework-core` and `openai-chatkit` as dependencies.
## Requirements and Limitations
### Frontend Requirements
The ChatKit integration requires the OpenAI ChatKit frontend library, which has the following requirements:
1. **Internet Connectivity Required**: The ChatKit UI is loaded from OpenAI's CDN (`cdn.platform.openai.com`). This library cannot be self-hosted or bundled locally.
2. **External Network Requests**: The ChatKit frontend makes requests to:
- `cdn.platform.openai.com` - UI library (required)
- `chatgpt.com/ces/v1/projects/oai/settings` - Configuration
- `api-js.mixpanel.com` - Telemetry (metadata only, not user messages)
3. **Domain Registration for Production**: Production deployments require registering your domain at [platform.openai.com](https://platform.openai.com/settings/organization/security/domain-allowlist) and configuring a domain key.
### Air-Gapped / Regulated Environments
**The ChatKit frontend is not suitable for air-gapped or highly-regulated environments** where outbound connections to OpenAI domains are restricted.
**What IS self-hostable:**
- The backend components (`chatkit-python`, `agent-framework-chatkit`) are fully open source and have no external dependencies
**What is NOT self-hostable:**
- The frontend UI (`chatkit.js`) requires connectivity to OpenAI's CDN
For environments with network restrictions, consider building a custom frontend that consumes the ChatKit server protocol, or using alternative UI libraries like `ai-sdk`.
See [openai/chatkit-js#57](https://github.com/openai/chatkit-js/issues/57) for tracking self-hosting feature requests.
## Example Usage
Here's a minimal example showing how to integrate Agent Framework with ChatKit:
+1 -1
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.0b251204"
version = "1.0.0b251120"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"

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