mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f109a5fc9a | ||
|
|
c5f003498b | ||
|
|
6c8ef728fa | ||
|
|
7965285808 | ||
|
|
9008bb8cb7 | ||
|
|
938961d2cc | ||
|
|
d9ce6ee292 | ||
|
|
2a3e41dbdd |
@@ -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 |
|
||||
@@ -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" />
|
||||
|
||||
+5
@@ -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
-6
@@ -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>
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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() })
|
||||
|
||||
+1
-1
@@ -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)
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+4
@@ -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)
|
||||
});
|
||||
|
||||
+2
-6
@@ -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>()
|
||||
}
|
||||
});
|
||||
|
||||
+4
@@ -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>
|
||||
|
||||
+2
-2
@@ -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>()
|
||||
}
|
||||
};
|
||||
|
||||
+2
-4
@@ -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>()
|
||||
}
|
||||
});
|
||||
|
||||
+3
-6
@@ -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>()
|
||||
}
|
||||
});
|
||||
|
||||
+11
-11
@@ -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);
|
||||
|
||||
+4
-14
@@ -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),
|
||||
|
||||
+2
@@ -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" />
|
||||
|
||||
+5
-4
@@ -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" />
|
||||
|
||||
+2
@@ -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>
|
||||
|
||||
+8
-18
@@ -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; }
|
||||
}
|
||||
+2
@@ -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>
|
||||
|
||||
|
||||
+2
-1
@@ -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}")
|
||||
};
|
||||
|
||||
|
||||
+27
-102
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -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>
|
||||
|
||||
+3
@@ -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>
|
||||
|
||||
|
||||
+2
@@ -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>
|
||||
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ public sealed class AnthropicBetaServiceExtensionsTests
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "Test description",
|
||||
ChatOptions = new() { Instructions = "Test instructions" }
|
||||
Instructions = "Test instructions"
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
+1
-1
@@ -158,7 +158,7 @@ public sealed class AnthropicClientExtensionsTests
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "Test description",
|
||||
ChatOptions = new() { Instructions = "Test instructions" }
|
||||
Instructions = "Test instructions"
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
+6
-6
@@ -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
|
||||
|
||||
+8
-7
@@ -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
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+4
@@ -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" />
|
||||
|
||||
+2
@@ -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>
|
||||
|
||||
|
||||
+1
@@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
<PackageReference Include="System.Net.ServerSentEvents" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+6
-6
@@ -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
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ public sealed class OpenAIChatClientExtensionsTests
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "Test description",
|
||||
ChatOptions = new() { Instructions = "Test instructions" }
|
||||
Instructions = "Test instructions"
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
+1
-1
@@ -208,7 +208,7 @@ public sealed class OpenAIResponseClientExtensionsTests
|
||||
var options = new ChatClientAgentOptions
|
||||
{
|
||||
Name = "Test Agent",
|
||||
ChatOptions = new() { Instructions = "Test instructions" }
|
||||
Instructions = "Test instructions"
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
+84
-45
@@ -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 () =>
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+4
-4
@@ -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" }));
|
||||
}
|
||||
}
|
||||
|
||||
+533
@@ -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()
|
||||
|
||||
-83
@@ -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);
|
||||
}
|
||||
}
|
||||
+18
-91
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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"
|
||||
|
||||
+44
-118
@@ -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."""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user