diff --git a/dotnet/.github/skills/project-structure/SKILL.md b/dotnet/.github/skills/project-structure/SKILL.md index 01dcafabf8..6ec9476039 100644 --- a/dotnet/.github/skills/project-structure/SKILL.md +++ b/dotnet/.github/skills/project-structure/SKILL.md @@ -12,8 +12,8 @@ dotnet/ │ ├── Microsoft.Agents.AI.Abstractions/ # Core AI agent abstractions │ ├── Microsoft.Agents.AI.A2A/ # Agent-to-Agent (A2A) provider │ ├── Microsoft.Agents.AI.OpenAI/ # OpenAI provider -│ ├── Microsoft.Agents.AI.AzureAI/ # Azure AI Foundry Agents (v2) provider -│ ├── Microsoft.Agents.AI.AzureAI.Persistent/ # Legacy Azure AI Foundry Agents (v1) provider +│ ├── Microsoft.Agents.AI.Foundry/ # Microsoft Foundry Agents (v2) provider +│ ├── Microsoft.Agents.AI.AzureAI.Persistent/ # Legacy Microsoft Foundry Agents (v1) provider │ ├── Microsoft.Agents.AI.Anthropic/ # Anthropic provider │ ├── Microsoft.Agents.AI.Workflows/ # Workflow orchestration │ └── ... # Other packages diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index f16a580519..748bed8017 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -478,13 +478,13 @@ - + - + @@ -495,7 +495,7 @@ - + @@ -506,11 +506,11 @@ - + - + @@ -526,12 +526,12 @@ - + - + diff --git a/dotnet/agent-framework-release.slnf b/dotnet/agent-framework-release.slnf index 1c8f477b16..98f9dcf887 100644 --- a/dotnet/agent-framework-release.slnf +++ b/dotnet/agent-framework-release.slnf @@ -8,13 +8,13 @@ "src\\Microsoft.Agents.AI.Anthropic\\Microsoft.Agents.AI.Anthropic.csproj", "src\\Microsoft.Agents.AI.GitHub.Copilot\\Microsoft.Agents.AI.GitHub.Copilot.csproj", "src\\Microsoft.Agents.AI.AzureAI.Persistent\\Microsoft.Agents.AI.AzureAI.Persistent.csproj", - "src\\Microsoft.Agents.AI.AzureAI\\Microsoft.Agents.AI.AzureAI.csproj", + "src\\Microsoft.Agents.AI.Foundry\\Microsoft.Agents.AI.Foundry.csproj", "src\\Microsoft.Agents.AI.CopilotStudio\\Microsoft.Agents.AI.CopilotStudio.csproj", "src\\Microsoft.Agents.AI.CosmosNoSql\\Microsoft.Agents.AI.CosmosNoSql.csproj", "src\\Microsoft.Agents.AI.Declarative\\Microsoft.Agents.AI.Declarative.csproj", "src\\Microsoft.Agents.AI.DevUI\\Microsoft.Agents.AI.DevUI.csproj", "src\\Microsoft.Agents.AI.DurableTask\\Microsoft.Agents.AI.DurableTask.csproj", - "src\\Microsoft.Agents.AI.FoundryMemory\\Microsoft.Agents.AI.FoundryMemory.csproj", + "src\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj", "src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj", "src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj", @@ -24,7 +24,7 @@ "src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj", "src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj", "src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj", - "src\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj", + "src\\Microsoft.Agents.AI.Workflows.Declarative.Foundry\\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj", "src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj", "src\\Microsoft.Agents.AI.Workflows.Generators\\Microsoft.Agents.AI.Workflows.Generators.csproj", "src\\Microsoft.Agents.AI.Workflows\\Microsoft.Agents.AI.Workflows.csproj", diff --git a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Agent_With_AzureAIProject.csproj b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Agent_With_AzureAIProject.csproj index a8deaa57b5..562ce0c37e 100644 --- a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Agent_With_AzureAIProject.csproj +++ b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Agent_With_AzureAIProject.csproj @@ -15,7 +15,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Program.cs b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Program.cs index 8265623904..355f4e1380 100644 --- a/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Program.cs +++ b/dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Program.cs @@ -6,7 +6,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj index 0b6c06a5a8..4c83380f90 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj @@ -1,4 +1,4 @@ - + Exe @@ -14,8 +14,7 @@ - - + diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs index 9a8c643bb9..1132e9ef1d 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs @@ -11,8 +11,7 @@ using System.Text.Json; using Azure.AI.Projects; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; -using Microsoft.Agents.AI.FoundryMemory; +using Microsoft.Agents.AI.Foundry; string foundryEndpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); string memoryStoreName = Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID") ?? "memory-store-sample"; @@ -37,7 +36,7 @@ FoundryMemoryProvider memoryProvider = new( memoryStoreName, stateInitializer: _ => new(new FoundryMemoryProviderScope("sample-user-123"))); -FoundryAgent agent = projectClient.AsAIAgent( +ChatClientAgent agent = projectClient.AsAIAgent( new ChatClientAgentOptions() { Name = "TravelAssistantWithFoundryMemory", diff --git a/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj b/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj index d90e1c394b..6a2bd7618c 100644 --- a/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj +++ b/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj @@ -14,7 +14,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs b/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs index e049618a72..7eb5bd5a39 100644 --- a/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs +++ b/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs @@ -7,7 +7,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; using OpenAI; using OpenAI.Files; using OpenAI.Responses; diff --git a/dotnet/samples/02-agents/Agents/Agent_Step07_AsMcpTool/Agent_Step07_AsMcpTool.csproj b/dotnet/samples/02-agents/Agents/Agent_Step07_AsMcpTool/Agent_Step07_AsMcpTool.csproj index 5239225499..a7df53251d 100644 --- a/dotnet/samples/02-agents/Agents/Agent_Step07_AsMcpTool/Agent_Step07_AsMcpTool.csproj +++ b/dotnet/samples/02-agents/Agents/Agent_Step07_AsMcpTool/Agent_Step07_AsMcpTool.csproj @@ -17,7 +17,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Agent_Step00_FoundryAgentLifecycle.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Agent_Step00_FoundryAgentLifecycle.csproj index d861331d9f..4c83380f90 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Agent_Step00_FoundryAgentLifecycle.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Agent_Step00_FoundryAgentLifecycle.csproj @@ -14,7 +14,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Program.cs index 691af1fc16..16c219e144 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Program.cs +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Program.cs @@ -7,7 +7,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Agent_Step01_Basics.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Agent_Step01_Basics.csproj index 7367c1d2f8..6b4cb8f43e 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Agent_Step01_Basics.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Agent_Step01_Basics.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Agent_Step02.1_MultiturnConversation.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Agent_Step02.1_MultiturnConversation.csproj index 5e73fd236a..6b4cb8f43e 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Agent_Step02.1_MultiturnConversation.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Agent_Step02.1_MultiturnConversation.csproj @@ -9,7 +9,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Agent_Step02.2_MultiturnWithServerConversations.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Agent_Step02.2_MultiturnWithServerConversations.csproj index 7367c1d2f8..6b4cb8f43e 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Agent_Step02.2_MultiturnWithServerConversations.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Agent_Step02.2_MultiturnWithServerConversations.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Program.cs index 7a66555c18..6057d71895 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Program.cs +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Program.cs @@ -4,10 +4,10 @@ // Server-side conversations persist on the Foundry service and are visible in the Foundry Project UI. // Use this when you need conversation history to be stored and accessible server-side. +using Azure.AI.Extensions.OpenAI; using Azure.AI.Projects; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; @@ -15,12 +15,20 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLO // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -FoundryAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential()) +AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential()); + +ChatClientAgent agent = aiProjectClient .AsAIAgent(deploymentName, instructions: "You are good at telling jokes.", name: "JokerAgent"); +ProjectConversationsClient conversationsClient = aiProjectClient + .GetProjectOpenAIClient() + .GetProjectConversationsClient(); + +ProjectConversation conversation = (await conversationsClient.CreateProjectConversationAsync().ConfigureAwait(false)).Value; + // CreateConversationSessionAsync creates a server-side ProjectConversation // that persists on the Foundry service and is visible in the Foundry Project UI. -AgentSession session = await agent.CreateConversationSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(conversation.Id); Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session)); Console.WriteLine(await agent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", session)); diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj index 7367c1d2f8..6b4cb8f43e 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj index 7367c1d2f8..6b4cb8f43e 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj index 7367c1d2f8..6b4cb8f43e 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj index 7367c1d2f8..6b4cb8f43e 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Agent_Step07_Observability.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Agent_Step07_Observability.csproj index 1189939bc0..60190545a1 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Agent_Step07_Observability.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Agent_Step07_Observability.csproj @@ -1,4 +1,4 @@ - + Exe @@ -15,7 +15,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Agent_Step08_DependencyInjection.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Agent_Step08_DependencyInjection.csproj index 72af634725..fd54882035 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Agent_Step08_DependencyInjection.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Agent_Step08_DependencyInjection.csproj @@ -1,4 +1,4 @@ - + Exe @@ -15,7 +15,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Agent_Step09_UsingMcpClientAsTools.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Agent_Step09_UsingMcpClientAsTools.csproj index 96cdf948fe..1f596a94a1 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Agent_Step09_UsingMcpClientAsTools.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Agent_Step09_UsingMcpClientAsTools.csproj @@ -1,4 +1,4 @@ - + Exe @@ -15,7 +15,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Agent_Step10_UsingImages.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Agent_Step10_UsingImages.csproj index 6064cf9334..c2bb03bffd 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Agent_Step10_UsingImages.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Agent_Step10_UsingImages.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Agent_Step11_AsFunctionTool.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Agent_Step11_AsFunctionTool.csproj index 7367c1d2f8..6b4cb8f43e 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Agent_Step11_AsFunctionTool.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Agent_Step11_AsFunctionTool.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Agent_Step12_Middleware.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Agent_Step12_Middleware.csproj index b30baccd54..d1cfe0da4a 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Agent_Step12_Middleware.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Agent_Step12_Middleware.csproj @@ -1,4 +1,4 @@ - + Exe @@ -13,7 +13,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Agent_Step13_Plugins.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Agent_Step13_Plugins.csproj index 1f5e37c1a3..5dfa730c8a 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Agent_Step13_Plugins.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Agent_Step13_Plugins.csproj @@ -1,4 +1,4 @@ - + Exe @@ -15,7 +15,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Agent_Step14_CodeInterpreter.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Agent_Step14_CodeInterpreter.csproj index e11688b6ba..7d91cedca5 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Agent_Step14_CodeInterpreter.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Agent_Step14_CodeInterpreter.csproj @@ -1,4 +1,4 @@ - + Exe @@ -13,7 +13,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj index f739f56123..7bd94bd716 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj @@ -1,4 +1,4 @@ - + Exe @@ -15,7 +15,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Program.cs index 22f03e27b3..d79c1122b7 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Program.cs +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Program.cs @@ -5,7 +5,7 @@ using Azure.AI.Projects; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; using Microsoft.Extensions.AI; using OpenAI.Responses; diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Agent_Step16_FileSearch.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Agent_Step16_FileSearch.csproj index e11688b6ba..7d91cedca5 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Agent_Step16_FileSearch.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Agent_Step16_FileSearch.csproj @@ -1,4 +1,4 @@ - + Exe @@ -13,7 +13,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Agent_Step17_OpenAPITools.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Agent_Step17_OpenAPITools.csproj index 4602e9c9e0..8671b3027c 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Agent_Step17_OpenAPITools.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Agent_Step17_OpenAPITools.csproj @@ -1,4 +1,4 @@ - + Exe @@ -14,7 +14,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Program.cs index 5cc2720dd9..1968b89ca1 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Program.cs +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Program.cs @@ -6,7 +6,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; using Microsoft.Extensions.AI; string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Agent_Step18_BingCustomSearch.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Agent_Step18_BingCustomSearch.csproj index e11688b6ba..7d91cedca5 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Agent_Step18_BingCustomSearch.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Agent_Step18_BingCustomSearch.csproj @@ -1,4 +1,4 @@ - + Exe @@ -13,7 +13,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Program.cs index 4ab548403d..e244bb3cb0 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Program.cs +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Program.cs @@ -6,7 +6,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; string connectionId = Environment.GetEnvironmentVariable("AZURE_AI_CUSTOM_SEARCH_CONNECTION_ID") ?? throw new InvalidOperationException("AZURE_AI_CUSTOM_SEARCH_CONNECTION_ID is not set."); string instanceName = Environment.GetEnvironmentVariable("AZURE_AI_CUSTOM_SEARCH_INSTANCE_NAME") ?? throw new InvalidOperationException("AZURE_AI_CUSTOM_SEARCH_INSTANCE_NAME is not set."); diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Agent_Step19_SharePoint.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Agent_Step19_SharePoint.csproj index e11688b6ba..7d91cedca5 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Agent_Step19_SharePoint.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Agent_Step19_SharePoint.csproj @@ -1,4 +1,4 @@ - + Exe @@ -13,7 +13,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Program.cs index aafca6b8bd..a5f8bf845a 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Program.cs +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Program.cs @@ -6,7 +6,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; string sharepointConnectionId = Environment.GetEnvironmentVariable("SHAREPOINT_PROJECT_CONNECTION_ID") ?? throw new InvalidOperationException("SHAREPOINT_PROJECT_CONNECTION_ID is not set."); diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Agent_Step20_MicrosoftFabric.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Agent_Step20_MicrosoftFabric.csproj index e11688b6ba..7d91cedca5 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Agent_Step20_MicrosoftFabric.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Agent_Step20_MicrosoftFabric.csproj @@ -1,4 +1,4 @@ - + Exe @@ -13,7 +13,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Program.cs index 7c49afa7ee..69954b9483 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Program.cs +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Program.cs @@ -6,7 +6,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; string fabricConnectionId = Environment.GetEnvironmentVariable("FABRIC_PROJECT_CONNECTION_ID") ?? throw new InvalidOperationException("FABRIC_PROJECT_CONNECTION_ID is not set."); diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Agent_Step21_WebSearch.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Agent_Step21_WebSearch.csproj index e11688b6ba..7d91cedca5 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Agent_Step21_WebSearch.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Agent_Step21_WebSearch.csproj @@ -1,4 +1,4 @@ - + Exe @@ -13,7 +13,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Agent_Step22_MemorySearch.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Agent_Step22_MemorySearch.csproj index 4602e9c9e0..8671b3027c 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Agent_Step22_MemorySearch.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Agent_Step22_MemorySearch.csproj @@ -1,4 +1,4 @@ - + Exe @@ -14,7 +14,7 @@ - + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Program.cs index 9e1a902f29..f2b447e52e 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Program.cs +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Program.cs @@ -9,7 +9,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; using Microsoft.Extensions.AI; using OpenAI.Responses; diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj index e51f57c439..53b8a3af34 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj @@ -1,4 +1,4 @@ - + Exe @@ -14,7 +14,7 @@ - + diff --git a/dotnet/samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj b/dotnet/samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj index d861331d9f..4c83380f90 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj +++ b/dotnet/samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj @@ -14,7 +14,7 @@ - + diff --git a/dotnet/samples/03-workflows/Agents/FoundryAgent/FoundryAgent.csproj b/dotnet/samples/03-workflows/Agents/FoundryAgent/FoundryAgent.csproj index a7648b7a10..dd6854fbfe 100644 --- a/dotnet/samples/03-workflows/Agents/FoundryAgent/FoundryAgent.csproj +++ b/dotnet/samples/03-workflows/Agents/FoundryAgent/FoundryAgent.csproj @@ -15,7 +15,7 @@ - + diff --git a/dotnet/samples/03-workflows/Agents/FoundryAgent/Program.cs b/dotnet/samples/03-workflows/Agents/FoundryAgent/Program.cs index e4fb1378b1..1ecbbae08e 100644 --- a/dotnet/samples/03-workflows/Agents/FoundryAgent/Program.cs +++ b/dotnet/samples/03-workflows/Agents/FoundryAgent/Program.cs @@ -4,7 +4,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; diff --git a/dotnet/samples/03-workflows/Declarative/ConfirmInput/ConfirmInput.csproj b/dotnet/samples/03-workflows/Declarative/ConfirmInput/ConfirmInput.csproj index dac2f49921..e7a4c3a774 100644 --- a/dotnet/samples/03-workflows/Declarative/ConfirmInput/ConfirmInput.csproj +++ b/dotnet/samples/03-workflows/Declarative/ConfirmInput/ConfirmInput.csproj @@ -26,7 +26,7 @@ - + diff --git a/dotnet/samples/03-workflows/Declarative/CustomerSupport/CustomerSupport.csproj b/dotnet/samples/03-workflows/Declarative/CustomerSupport/CustomerSupport.csproj index 0bc83997d0..ec6ca6093c 100644 --- a/dotnet/samples/03-workflows/Declarative/CustomerSupport/CustomerSupport.csproj +++ b/dotnet/samples/03-workflows/Declarative/CustomerSupport/CustomerSupport.csproj @@ -26,7 +26,7 @@ - + diff --git a/dotnet/samples/03-workflows/Declarative/DeepResearch/DeepResearch.csproj b/dotnet/samples/03-workflows/Declarative/DeepResearch/DeepResearch.csproj index cd533a0707..60928aaa52 100644 --- a/dotnet/samples/03-workflows/Declarative/DeepResearch/DeepResearch.csproj +++ b/dotnet/samples/03-workflows/Declarative/DeepResearch/DeepResearch.csproj @@ -26,7 +26,7 @@ - + diff --git a/dotnet/samples/03-workflows/Declarative/ExecuteCode/ExecuteCode.csproj b/dotnet/samples/03-workflows/Declarative/ExecuteCode/ExecuteCode.csproj index 6a9c4957c2..c92a9ffbf1 100644 --- a/dotnet/samples/03-workflows/Declarative/ExecuteCode/ExecuteCode.csproj +++ b/dotnet/samples/03-workflows/Declarative/ExecuteCode/ExecuteCode.csproj @@ -27,7 +27,7 @@ - + diff --git a/dotnet/samples/03-workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj b/dotnet/samples/03-workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj index fce40b64d4..243d6d3d52 100644 --- a/dotnet/samples/03-workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj +++ b/dotnet/samples/03-workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj @@ -26,7 +26,7 @@ - + diff --git a/dotnet/samples/03-workflows/Declarative/FunctionTools/FunctionTools.csproj b/dotnet/samples/03-workflows/Declarative/FunctionTools/FunctionTools.csproj index f890fb30a8..5ed2187e2f 100644 --- a/dotnet/samples/03-workflows/Declarative/FunctionTools/FunctionTools.csproj +++ b/dotnet/samples/03-workflows/Declarative/FunctionTools/FunctionTools.csproj @@ -26,7 +26,7 @@ - + diff --git a/dotnet/samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj b/dotnet/samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj index f9379f38a3..31e88d54e7 100644 --- a/dotnet/samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj +++ b/dotnet/samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj @@ -27,7 +27,7 @@ - + diff --git a/dotnet/samples/03-workflows/Declarative/HostedWorkflow/Program.cs b/dotnet/samples/03-workflows/Declarative/HostedWorkflow/Program.cs index 7852a8730f..1d688531df 100644 --- a/dotnet/samples/03-workflows/Declarative/HostedWorkflow/Program.cs +++ b/dotnet/samples/03-workflows/Declarative/HostedWorkflow/Program.cs @@ -8,7 +8,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; using Shared.Foundry; diff --git a/dotnet/samples/03-workflows/Declarative/InputArguments/InputArguments.csproj b/dotnet/samples/03-workflows/Declarative/InputArguments/InputArguments.csproj index 45bc44eaf3..ae6a0046ef 100644 --- a/dotnet/samples/03-workflows/Declarative/InputArguments/InputArguments.csproj +++ b/dotnet/samples/03-workflows/Declarative/InputArguments/InputArguments.csproj @@ -26,7 +26,7 @@ - + diff --git a/dotnet/samples/03-workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj b/dotnet/samples/03-workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj index 67229da4b8..f2d23835a1 100644 --- a/dotnet/samples/03-workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj +++ b/dotnet/samples/03-workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj @@ -26,7 +26,7 @@ - + diff --git a/dotnet/samples/03-workflows/Declarative/InvokeMcpTool/InvokeMcpTool.csproj b/dotnet/samples/03-workflows/Declarative/InvokeMcpTool/InvokeMcpTool.csproj index 317d93c4e9..ea122bd971 100644 --- a/dotnet/samples/03-workflows/Declarative/InvokeMcpTool/InvokeMcpTool.csproj +++ b/dotnet/samples/03-workflows/Declarative/InvokeMcpTool/InvokeMcpTool.csproj @@ -26,7 +26,7 @@ - + diff --git a/dotnet/samples/03-workflows/Declarative/Marketing/Marketing.csproj b/dotnet/samples/03-workflows/Declarative/Marketing/Marketing.csproj index 20e5843554..23d1224a5e 100644 --- a/dotnet/samples/03-workflows/Declarative/Marketing/Marketing.csproj +++ b/dotnet/samples/03-workflows/Declarative/Marketing/Marketing.csproj @@ -26,7 +26,7 @@ - + diff --git a/dotnet/samples/03-workflows/Declarative/StudentTeacher/StudentTeacher.csproj b/dotnet/samples/03-workflows/Declarative/StudentTeacher/StudentTeacher.csproj index 8136706b8d..2785c5930b 100644 --- a/dotnet/samples/03-workflows/Declarative/StudentTeacher/StudentTeacher.csproj +++ b/dotnet/samples/03-workflows/Declarative/StudentTeacher/StudentTeacher.csproj @@ -26,7 +26,7 @@ - + diff --git a/dotnet/samples/03-workflows/Declarative/ToolApproval/ToolApproval.csproj b/dotnet/samples/03-workflows/Declarative/ToolApproval/ToolApproval.csproj index a44e140f1f..e5cf939e15 100644 --- a/dotnet/samples/03-workflows/Declarative/ToolApproval/ToolApproval.csproj +++ b/dotnet/samples/03-workflows/Declarative/ToolApproval/ToolApproval.csproj @@ -26,7 +26,7 @@ - + diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.csproj b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.csproj index 5a7ef20208..98b7b293c8 100644 --- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.csproj +++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.csproj @@ -1,4 +1,4 @@ - + Exe @@ -23,7 +23,7 @@ - + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs index 5173578ee8..185afd186f 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs @@ -70,17 +70,19 @@ string GetAvailableHotels( // Build response var result = new StringBuilder(); - result.AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):"); - result.AppendLine(); + result + .AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):") + .AppendLine(); foreach (var hotel in availableHotels) { var totalCost = hotel.PricePerNight * nights; - result.AppendLine($"**{hotel.Name}**"); - result.AppendLine($" Location: {hotel.Location}"); - result.AppendLine($" Rating: {hotel.Rating}/5"); - result.AppendLine($" ${hotel.PricePerNight}/night (Total: ${totalCost})"); - result.AppendLine(); + result + .AppendLine($"**{hotel.Name}**") + .AppendLine($" Location: {hotel.Location}") + .AppendLine($" Rating: {hotel.Rating}/5") + .AppendLine($" ${hotel.PricePerNight}/night (Total: ${totalCost})") + .AppendLine(); } return result.ToString(); diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs deleted file mode 100644 index 479ab894ae..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs +++ /dev/null @@ -1,935 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel; -using System.ClientModel.Primitives; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using System.Text.RegularExpressions; -using Azure.AI.Extensions.OpenAI; -using Azure.AI.Projects.Agents; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging; -using Microsoft.Shared.DiagnosticIds; -using Microsoft.Shared.Diagnostics; -using OpenAI; -using OpenAI.Responses; - -namespace Azure.AI.Projects; - -/// -/// Provides extension methods for . -/// -[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] -public static partial class AzureAIProjectChatClientExtensions -{ - /// - /// Uses an existing server side agent, wrapped as a using the provided and . - /// - /// The to create the with. Cannot be . - /// The representing the name and version of the server side agent to create a for. Cannot be . - /// The tools to use when interacting with the agent. This is required when using prompt agent definitions with tools. - /// Provides a way to customize the creation of the underlying used by the agent. - /// An optional to use for resolving services required by the instances being invoked. - /// A instance that can be used to perform operations based on the latest version of the named Azure AI Agent. - /// Thrown when or is . - /// The agent with the specified name was not found. - /// - /// When instantiating a by using an , minimal information will be available about the agent in the instance level, and any logic that relies - /// on to retrieve information about the agent like will receive as the result. - /// - public static FoundryAgent AsAIAgent( - this AIProjectClient aiProjectClient, - AgentReference agentReference, - IList? tools = null, - Func? clientFactory = null, - IServiceProvider? services = null) - { - Throw.IfNull(aiProjectClient); - Throw.IfNull(agentReference); - ThrowIfInvalidAgentName(agentReference.Name); - - var innerAgent = AsChatClientAgent( - aiProjectClient, - agentReference, - new ChatClientAgentOptions() - { - Id = $"{agentReference.Name}:{agentReference.Version}", - Name = agentReference.Name, - ChatOptions = new() { Tools = tools }, - }, - clientFactory, - services); - - return new FoundryAgent(aiProjectClient, innerAgent); - } - - /// - /// Asynchronously retrieves an existing server side agent, wrapped as a using the provided . - /// - /// The to create the with. Cannot be . - /// The name of the server side agent to create a for. Cannot be or whitespace. - /// The tools to use when interacting with the agent. This is required when using prompt agent definitions with tools. - /// Provides a way to customize the creation of the underlying used by the agent. - /// An optional to use for resolving services required by the instances being invoked. - /// The to monitor for cancellation requests. The default is . - /// A instance that can be used to perform operations based on the latest version of the named Azure AI Agent. - /// Thrown when or is . - /// Thrown when is empty or whitespace, or when the agent with the specified name was not found. - /// The agent with the specified name was not found. - [Obsolete("Use native AIProjectClient agent APIs and AsAIAgent(AgentRecord/AgentVersion) instead.")] - public static async Task GetAIAgentAsync( - this AIProjectClient aiProjectClient, - string name, - IList? tools = null, - Func? clientFactory = null, - IServiceProvider? services = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(aiProjectClient); - ThrowIfInvalidAgentName(name); - - AgentRecord agentRecord = await GetAgentRecordByNameAsync(aiProjectClient, name, cancellationToken).ConfigureAwait(false); - - return AsAIAgent( - aiProjectClient, - agentRecord, - tools, - clientFactory, - services); - } - - /// - /// Uses an existing server side agent, wrapped as a using the provided and . - /// - /// The client used to interact with Azure AI Agents. Cannot be . - /// The agent record to be converted. The latest version will be used. Cannot be . - /// The tools to use when interacting with the agent. This is required when using prompt agent definitions with tools. - /// Provides a way to customize the creation of the underlying used by the agent. - /// An optional to use for resolving services required by the instances being invoked. - /// A instance that can be used to perform operations based on the latest version of the Azure AI Agent. - /// Thrown when or is . - public static FoundryAgent AsAIAgent( - this AIProjectClient aiProjectClient, - AgentRecord agentRecord, - IList? tools = null, - Func? clientFactory = null, - IServiceProvider? services = null) - { - Throw.IfNull(aiProjectClient); - Throw.IfNull(agentRecord); - - var allowDeclarativeMode = tools is not { Count: > 0 }; - - var innerAgent = AsChatClientAgent( - aiProjectClient, - agentRecord, - tools, - clientFactory, - !allowDeclarativeMode, - services); - - return new FoundryAgent(aiProjectClient, innerAgent); - } - - /// - /// Uses an existing server side agent, wrapped as a using the provided and . - /// - /// The client used to interact with Azure AI Agents. Cannot be . - /// The agent version to be converted. Cannot be . - /// In-process invocable tools to be provided. If no tools are provided manual handling will be necessary to invoke in-process tools. - /// Provides a way to customize the creation of the underlying used by the agent. - /// An optional to use for resolving services required by the instances being invoked. - /// A instance that can be used to perform operations based on the provided version of the Azure AI Agent. - /// Thrown when or is . - public static FoundryAgent AsAIAgent( - this AIProjectClient aiProjectClient, - AgentVersion agentVersion, - IList? tools = null, - Func? clientFactory = null, - IServiceProvider? services = null) - { - Throw.IfNull(aiProjectClient); - Throw.IfNull(agentVersion); - - var allowDeclarativeMode = tools is not { Count: > 0 }; - - var innerAgent = AsChatClientAgent( - aiProjectClient, - agentVersion, - tools, - clientFactory, - !allowDeclarativeMode, - services); - - return new FoundryAgent(aiProjectClient, innerAgent); - } - - /// - /// Asynchronously retrieves an existing server side agent, wrapped as a using the provided . - /// - /// The client used to manage and interact with AI agents. Cannot be . - /// The options for creating the agent. Cannot be . - /// A factory function to customize the creation of the chat client used by the agent. - /// An optional to use for resolving services required by the instances being invoked. - /// A to cancel the operation if needed. - /// A instance that can be used to perform operations on the newly created agent. - /// Thrown when or is . - [Obsolete("Use native AIProjectClient agent APIs and AsAIAgent(AgentRecord/AgentVersion) instead.")] - public static async Task GetAIAgentAsync( - this AIProjectClient aiProjectClient, - ChatClientAgentOptions options, - Func? clientFactory = null, - IServiceProvider? services = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(aiProjectClient); - Throw.IfNull(options); - - if (string.IsNullOrWhiteSpace(options.Name)) - { - throw new ArgumentException("Agent name must be provided in the options.Name property", nameof(options)); - } - - ThrowIfInvalidAgentName(options.Name); - - AgentRecord agentRecord = await GetAgentRecordByNameAsync(aiProjectClient, options.Name, cancellationToken).ConfigureAwait(false); - var agentVersion = agentRecord.GetLatestVersion(); - - var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: !options.UseProvidedChatClientAsIs); - - return new FoundryAgent( - aiProjectClient, - AsChatClientAgent(aiProjectClient, agentVersion, agentOptions, clientFactory, services)); - } - - /// - /// Creates a new Prompt AI agent in the Foundry service using the specified configuration parameters, and exposes it as a . - /// - /// The client used to manage and interact with AI agents. Cannot be . - /// The name for the agent. - /// The name of the model to use for the agent. Cannot be or whitespace. - /// The instructions that guide the agent's behavior. Cannot be or whitespace. - /// The description for the agent. - /// The tools to use when interacting with the agent, this is required when using prompt agent definitions with tools. - /// A factory function to customize the creation of the chat client used by the agent. - /// An optional to use for resolving services required by the instances being invoked. - /// A token to monitor for cancellation requests. - /// A instance that can be used to perform operations on the newly created agent. - /// Thrown when , , or is . - /// Thrown when or is empty or whitespace. - /// When using prompt agent definitions with tools the parameter needs to be provided. - [Obsolete("Use native AIProjectClient.Agents APIs instead.")] - public static Task CreateAIAgentAsync( - this AIProjectClient aiProjectClient, - string name, - string model, - string instructions, - string? description = null, - IList? tools = null, - Func? clientFactory = null, - IServiceProvider? services = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(aiProjectClient); - ThrowIfInvalidAgentName(name); - Throw.IfNullOrWhitespace(model); - Throw.IfNullOrWhitespace(instructions); - - return CreateAIAgentAsync( - aiProjectClient, - name, - tools, - new AgentVersionCreationOptions(new PromptAgentDefinition(model) { Instructions = instructions }) { Description = description }, - clientFactory, - services, - cancellationToken); - } - - /// - /// Creates a new Prompt AI agent in the Foundry service using the specified configuration parameters, and exposes it as a . - /// - /// The client used to manage and interact with AI agents. Cannot be . - /// The name of the model to use for the agent. Cannot be or whitespace. - /// The options for creating the agent. Cannot be . - /// A factory function to customize the creation of the chat client used by the agent. - /// An optional to use for resolving services required by the instances being invoked. - /// A to cancel the operation if needed. - /// A instance that can be used to perform operations on the newly created agent. - /// Thrown when or is . - /// Thrown when is empty or whitespace, or when the agent name is not provided in the options. - [Obsolete("Use native AIProjectClient.Agents APIs instead.")] - public static async Task CreateAIAgentAsync( - this AIProjectClient aiProjectClient, - string model, - ChatClientAgentOptions options, - Func? clientFactory = null, - IServiceProvider? services = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(aiProjectClient); - Throw.IfNull(options); - Throw.IfNullOrWhitespace(model); - - if (string.IsNullOrWhiteSpace(options.Name)) - { - throw new ArgumentException("Agent name must be provided in the options.Name property", nameof(options)); - } - - ThrowIfInvalidAgentName(options.Name); - - AgentVersion agentVersion = await CreateAgentVersionFromOptionsAsync(aiProjectClient, model, options, cancellationToken).ConfigureAwait(false); - - var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: true); - - return new FoundryAgent( - aiProjectClient, - AsChatClientAgent(aiProjectClient, agentVersion, agentOptions, clientFactory, services)); - } - - /// - /// Creates a new Prompt AI agent in the Foundry service using the specified configuration parameters, and exposes it as a . - /// parameters. - /// - /// The client used to manage and interact with AI agents. Cannot be . - /// The name for the agent. - /// Settings that control the creation of the agent. - /// A factory function to customize the creation of the chat client used by the agent. - /// A token to monitor for cancellation requests. - /// A instance that can be used to perform operations on the newly created agent. - /// Thrown when or is . - /// - /// When using this extension method with a the tools are only declarative and not invocable. - /// Invocation of any in-process tools will need to be handled manually. - /// - [Obsolete("Use native AIProjectClient.Agents APIs instead.")] - public static Task CreateAIAgentAsync( - this AIProjectClient aiProjectClient, - string name, - AgentVersionCreationOptions creationOptions, - Func? clientFactory = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(aiProjectClient); - ThrowIfInvalidAgentName(name); - Throw.IfNull(creationOptions); - - return CreateAIAgentAsync( - aiProjectClient, - name, - tools: null, - creationOptions, - clientFactory, - services: null, - cancellationToken); - } - - /// - /// Creates a non-versioned backed by the project's Responses API using the specified model and instructions. - /// - /// The to use for Responses API calls. Cannot be . - /// The model deployment name to use for the agent. Cannot be or whitespace. - /// The instructions that guide the agent's behavior. Cannot be or whitespace. - /// Optional name for the agent. - /// Optional human-readable description for the agent. - /// Optional collection of tools that the agent can invoke during conversations. - /// Provides a way to customize the creation of the underlying used by the agent. - /// Optional logger factory for creating loggers used by the agent. - /// An optional to use for resolving services required by the instances being invoked. - /// A backed by the project's Responses API. - /// Thrown when is . - /// Thrown when or is empty or whitespace. - public static FoundryAgent AsAIAgent( - this AIProjectClient aiProjectClient, - string model, - string instructions, - string? name = null, - string? description = null, - IList? tools = null, - Func? clientFactory = null, - ILoggerFactory? loggerFactory = null, - IServiceProvider? services = null) - { - Throw.IfNull(aiProjectClient); - Throw.IfNullOrWhitespace(model); - Throw.IfNullOrWhitespace(instructions); - - ChatClientAgentOptions options = new() - { - Name = name, - Description = description, - ChatOptions = new ChatOptions - { - ModelId = model, - Instructions = instructions, - Tools = tools, - }, - }; - - return new FoundryAgent(aiProjectClient, CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services)); - } - - /// - /// Creates a non-versioned backed by the project's Responses API using the specified options. - /// - /// The to use for Responses API calls. Cannot be . - /// Configuration options that control the agent's behavior. is required. - /// Provides a way to customize the creation of the underlying used by the agent. - /// Optional logger factory for creating loggers used by the agent. - /// An optional to use for resolving services required by the instances being invoked. - /// A backed by the project's Responses API. - /// Thrown when or is . - /// Thrown when does not specify . - public static FoundryAgent AsAIAgent( - this AIProjectClient aiProjectClient, - ChatClientAgentOptions options, - Func? clientFactory = null, - ILoggerFactory? loggerFactory = null, - IServiceProvider? services = null) - { - Throw.IfNull(aiProjectClient); - Throw.IfNull(options); - - return new FoundryAgent(aiProjectClient, CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services)); - } - - #region Private - - private static readonly ModelReaderWriterOptions s_modelWriterOptionsWire = new("W"); - - /// - /// Asynchronously retrieves an agent record by name using the protocol method to inject user-agent headers. - /// - internal static async Task GetAgentRecordByNameAsync(AIProjectClient aiProjectClient, string agentName, CancellationToken cancellationToken) - { - ClientResult protocolResponse = await aiProjectClient.Agents.GetAgentAsync(agentName, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false); - var rawResponse = protocolResponse.GetRawResponse(); - AgentRecord? result = ModelReaderWriter.Read(rawResponse.Content, s_modelWriterOptionsWire, AzureAIProjectsAgentsContext.Default); - return result ?? throw new InvalidOperationException($"Agent with name '{agentName}' not found."); - } - - /// - /// Asynchronously creates an agent version using the protocol method to inject user-agent headers. - /// - internal static async Task CreateAgentVersionWithProtocolAsync(AIProjectClient aiProjectClient, string agentName, AgentVersionCreationOptions creationOptions, CancellationToken cancellationToken) - { - BinaryData serializedOptions = ModelReaderWriter.Write(creationOptions, s_modelWriterOptionsWire, AzureAIProjectsAgentsContext.Default); - BinaryContent content = BinaryContent.Create(serializedOptions); - ClientResult protocolResponse = await aiProjectClient.Agents.CreateAgentVersionAsync(agentName, content, foundryFeatures: null, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false); - var rawResponse = protocolResponse.GetRawResponse(); - AgentVersion? result = ModelReaderWriter.Read(rawResponse.Content, s_modelWriterOptionsWire, AzureAIProjectsAgentsContext.Default); - return result ?? throw new InvalidOperationException($"Failed to create agent version for agent '{agentName}'."); - } - - private static async Task CreateAIAgentAsync( - this AIProjectClient aiProjectClient, - string name, - IList? tools, - AgentVersionCreationOptions creationOptions, - Func? clientFactory, - IServiceProvider? services, - CancellationToken cancellationToken) - { - var allowDeclarativeMode = tools is not { Count: > 0 }; - - if (!allowDeclarativeMode) - { - ApplyToolsToAgentDefinition(creationOptions.Definition, tools); - } - - AgentVersion agentVersion = await CreateAgentVersionWithProtocolAsync(aiProjectClient, name, creationOptions, cancellationToken).ConfigureAwait(false); - - return new FoundryAgent(aiProjectClient, AsChatClientAgent(aiProjectClient, agentVersion, tools, clientFactory, !allowDeclarativeMode, services)); - } - - /// - /// Creates an agent version with optional tool application, using the protocol method to inject user-agent headers. - /// - internal static async Task CreateAgentVersionWithProtocolAsync(AIProjectClient aiProjectClient, string agentName, AgentVersionCreationOptions creationOptions, IList? tools, CancellationToken cancellationToken) - { - if (tools is { Count: > 0 }) - { - ApplyToolsToAgentDefinition(creationOptions.Definition, tools); - } - - return await CreateAgentVersionWithProtocolAsync(aiProjectClient, agentName, creationOptions, cancellationToken).ConfigureAwait(false); - } - - /// - /// Creates an agent version from , mapping options to a . - /// - internal static async Task CreateAgentVersionFromOptionsAsync( - AIProjectClient aiProjectClient, - string model, - ChatClientAgentOptions options, - CancellationToken cancellationToken) - { - PromptAgentDefinition agentDefinition = new(model) - { - Instructions = options.ChatOptions?.Instructions, - Temperature = options.ChatOptions?.Temperature, - TopP = options.ChatOptions?.TopP, - TextOptions = new() { TextFormat = ToOpenAIResponseTextFormat(options.ChatOptions?.ResponseFormat, options.ChatOptions) } - }; - - if (options.ChatOptions?.Reasoning is { } reasoning) - { - agentDefinition.ReasoningOptions = ToResponseReasoningOptions(reasoning); - } - else if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is CreateResponseOptions respCreationOptions) - { - agentDefinition.ReasoningOptions = respCreationOptions.ReasoningOptions; - } - - ApplyToolsToAgentDefinition(agentDefinition, options.ChatOptions?.Tools); - - AgentVersionCreationOptions creationOptions = new(agentDefinition); - if (!string.IsNullOrWhiteSpace(options.Description)) - { - creationOptions.Description = options.Description; - } - - return await CreateAgentVersionWithProtocolAsync(aiProjectClient, options.Name!, creationOptions, cancellationToken).ConfigureAwait(false); - } - - /// Creates a with the specified options. - internal static ChatClientAgent CreateChatClientAgent( - AIProjectClient aiProjectClient, - AgentVersion agentVersion, - ChatClientAgentOptions agentOptions, - Func? clientFactory, - IServiceProvider? services) - { - IChatClient chatClient = new AzureAIProjectChatClient(aiProjectClient, agentVersion, agentOptions.ChatOptions); - - if (clientFactory is not null) - { - chatClient = clientFactory(chatClient); - } - - return new ChatClientAgent(chatClient, agentOptions, services: services); - } - - internal static ChatClientAgent CreateResponsesChatClientAgent( - AIProjectClient aiProjectClient, - ChatClientAgentOptions agentOptions, - Func? clientFactory, - ILoggerFactory? loggerFactory, - IServiceProvider? services) - { - Throw.IfNull(aiProjectClient); - Throw.IfNull(agentOptions); - Throw.IfNull(agentOptions.ChatOptions); - Throw.IfNullOrWhitespace(agentOptions.ChatOptions.ModelId); - - IChatClient chatClient = new AzureAIProjectResponsesChatClient(aiProjectClient, agentOptions.ChatOptions.ModelId); - - if (clientFactory is not null) - { - chatClient = clientFactory(chatClient); - } - - return new ChatClientAgent(chatClient, agentOptions, loggerFactory, services); - } - - /// This method creates an with the specified ChatClientAgentOptions. - private static ChatClientAgent AsChatClientAgent( - AIProjectClient aiProjectClient, - AgentVersion agentVersion, - ChatClientAgentOptions agentOptions, - Func? clientFactory, - IServiceProvider? services) - => CreateChatClientAgent(aiProjectClient, agentVersion, agentOptions, clientFactory, services); - - /// This method creates an with the specified ChatClientAgentOptions. - private static ChatClientAgent AsChatClientAgent( - AIProjectClient aiProjectClient, - AgentRecord agentRecord, - ChatClientAgentOptions agentOptions, - Func? clientFactory, - IServiceProvider? services) - { - IChatClient chatClient = new AzureAIProjectChatClient(aiProjectClient, agentRecord, agentOptions.ChatOptions); - - if (clientFactory is not null) - { - chatClient = clientFactory(chatClient); - } - - return new ChatClientAgent(chatClient, agentOptions, services: services); - } - - /// This method creates an with the specified ChatClientAgentOptions. - private static ChatClientAgent AsChatClientAgent( - AIProjectClient aiProjectClient, - AgentReference agentReference, - ChatClientAgentOptions agentOptions, - Func? clientFactory, - IServiceProvider? services) - { - IChatClient chatClient = new AzureAIProjectChatClient(aiProjectClient, agentReference, defaultModelId: null, agentOptions.ChatOptions); - - if (clientFactory is not null) - { - chatClient = clientFactory(chatClient); - } - - return new ChatClientAgent(chatClient, agentOptions, services: services); - } - - /// This method creates an with a auto-generated ChatClientAgentOptions from the specified configuration parameters. - private static ChatClientAgent AsChatClientAgent( - AIProjectClient AIProjectClient, - AgentVersion agentVersion, - IList? tools, - Func? clientFactory, - bool requireInvocableTools, - IServiceProvider? services) - => AsChatClientAgent( - AIProjectClient, - agentVersion, - CreateChatClientAgentOptions(agentVersion, new ChatOptions() { Tools = tools }, requireInvocableTools), - clientFactory, - services); - - /// This method creates an with a auto-generated ChatClientAgentOptions from the specified configuration parameters. - private static ChatClientAgent AsChatClientAgent( - AIProjectClient AIProjectClient, - AgentRecord agentRecord, - IList? tools, - Func? clientFactory, - bool requireInvocableTools, - IServiceProvider? services) - => AsChatClientAgent( - AIProjectClient, - agentRecord, - CreateChatClientAgentOptions(agentRecord.GetLatestVersion(), new ChatOptions() { Tools = tools }, requireInvocableTools), - clientFactory, - services); - - /// - /// This method creates for the specified and the provided tools. - /// - /// The agent version. - /// The to use when interacting with the agent. - /// Indicates whether to enforce the presence of invocable tools when the AIAgent is created with an agent definition that uses them. - /// The created . - /// Thrown when the agent definition requires in-process tools but none were provided. - /// Thrown when the agent definition required tools were not provided. - /// - /// This method rebuilds the agent options from the agent definition returned by the version and combine with the in-proc tools when provided - /// this ensures that all required tools are provided and the definition of the agent options are consistent with the agent definition coming from the server. - /// - internal static ChatClientAgentOptions CreateChatClientAgentOptions(AgentVersion agentVersion, ChatOptions? chatOptions, bool requireInvocableTools) - { - var agentDefinition = agentVersion.Definition; - - List? agentTools = null; - if (agentDefinition is PromptAgentDefinition { Tools: { Count: > 0 } definitionTools }) - { - // Check if no tools were provided while the agent definition requires in-proc tools. - if (requireInvocableTools && chatOptions?.Tools is not { Count: > 0 } && definitionTools.Any(t => t is FunctionTool)) - { - throw new ArgumentException("The agent definition in-process tools must be provided in the extension method tools parameter."); - } - - // Agregate all missing tools for a single error message. - List? missingTools = null; - - // Check function tools - foreach (ResponseTool responseTool in definitionTools) - { - if (responseTool is FunctionTool functionTool) - { - // Check if a tool with the same type and name exists in the provided tools. - // Always prefer matching AIFunction when available, regardless of requireInvocableTools. - var matchingTool = chatOptions?.Tools?.FirstOrDefault(t => t is AIFunction tf && functionTool.FunctionName == tf.Name); - - if (matchingTool is not null) - { - (agentTools ??= []).Add(matchingTool!); - continue; - } - - if (requireInvocableTools) - { - (missingTools ??= []).Add($"Function tool: {functionTool.FunctionName}"); - continue; - } - } - - (agentTools ??= []).Add(responseTool.AsAITool()); - } - - if (requireInvocableTools && missingTools is { Count: > 0 }) - { - throw new InvalidOperationException($"The following prompt agent definition required tools were not provided: {string.Join(", ", missingTools)}"); - } - } - - // Use the agent version's ID if available, otherwise generate one from name and version. - // This handles cases where hosted agents (like MCP agents) may not have an ID assigned. - var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version; - var agentId = string.IsNullOrWhiteSpace(agentVersion.Id) - ? $"{agentVersion.Name}:{version}" - : agentVersion.Id; - - var agentOptions = new ChatClientAgentOptions() - { - Id = agentId, - Name = agentVersion.Name, - Description = agentVersion.Description, - }; - - if (agentDefinition is PromptAgentDefinition promptAgentDefinition) - { - agentOptions.ChatOptions ??= chatOptions?.Clone() ?? new(); - agentOptions.ChatOptions.Instructions = promptAgentDefinition.Instructions; - agentOptions.ChatOptions.Temperature = promptAgentDefinition.Temperature; - agentOptions.ChatOptions.TopP = promptAgentDefinition.TopP; - } - - if (agentTools is { Count: > 0 }) - { - agentOptions.ChatOptions ??= chatOptions?.Clone() ?? new(); - agentOptions.ChatOptions.Tools = agentTools; - } - - return agentOptions; - } - - /// - /// Creates a new instance of configured for the specified agent version and - /// optional base options. - /// - /// The agent version to use when configuring the chat client agent options. - /// An optional instance whose relevant properties will be copied to the - /// returned options. If , only default values are used. - /// Specifies whether the returned options must include invocable tools. Set to to require - /// invocable tools; otherwise, . - /// A instance configured according to the specified parameters. - internal static ChatClientAgentOptions CreateChatClientAgentOptions(AgentVersion agentVersion, ChatClientAgentOptions? options, bool requireInvocableTools) - { - var agentOptions = CreateChatClientAgentOptions(agentVersion, options?.ChatOptions, requireInvocableTools); - if (options is not null) - { - agentOptions.AIContextProviders = options.AIContextProviders; - agentOptions.ChatHistoryProvider = options.ChatHistoryProvider; - agentOptions.UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs; - } - - return agentOptions; - } - - /// - /// Adds the specified AI tools to a prompt agent definition, while also ensuring that all invocable tools are provided. - /// - /// The agent definition to which the tools will be applied. Must be a PromptAgentDefinition to support tools. - /// A list of AI tools to add to the agent definition. If null or empty, no tools are added. - /// Thrown if tools were provided but is not a . - /// When providing functions, they need to be invokable AIFunctions. - private static void ApplyToolsToAgentDefinition(AgentDefinition agentDefinition, IList? tools) - { - if (tools is { Count: > 0 }) - { - if (agentDefinition is not PromptAgentDefinition promptAgentDefinition) - { - throw new ArgumentException("Only prompt agent definitions support tools.", nameof(agentDefinition)); - } - - // When tools are provided, those should represent the complete set of tools for the agent definition. - // This is particularly important for existing agents so no duplication happens for what was already defined. - promptAgentDefinition.Tools.Clear(); - - foreach (var tool in tools) - { - // Ensure that any AIFunctions provided are In-Proc, not just the declarations. - if (tool is not AIFunction && ( - tool.GetService() is not null // Declarative FunctionTool converted as AsAITool() - || tool is AIFunctionDeclaration)) // AIFunctionDeclaration type - { - throw new InvalidOperationException("When providing functions, they need to be invokable AIFunctions. AIFunctions can be created correctly using AIFunctionFactory.Create"); - } - - promptAgentDefinition.Tools.Add( - // If this is a converted ResponseTool as AITool, we can directly retrieve the ResponseTool instance from GetService. - tool.GetService() - // Otherwise we should be able to convert existing MEAI Tool abstractions into OpenAI ResponseTools - ?? tool.AsOpenAIResponseTool() - ?? throw new InvalidOperationException("The provided AITool could not be converted to a ResponseTool, ensure that the AITool was created using responseTool.AsAITool() extension.")); - } - } - } - - private static ResponseTextFormat? ToOpenAIResponseTextFormat(ChatResponseFormat? format, ChatOptions? options = null) => - format switch - { - ChatResponseFormatText => ResponseTextFormat.CreateTextFormat(), - - ChatResponseFormatJson jsonFormat when StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema => - ResponseTextFormat.CreateJsonSchemaFormat( - jsonFormat.SchemaName ?? "json_schema", - BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, AgentClientJsonContext.Default.JsonElement)), - jsonFormat.SchemaDescription, - HasStrict(options?.AdditionalProperties)), - - ChatResponseFormatJson => ResponseTextFormat.CreateJsonObjectFormat(), - - _ => null, - }; - - /// Key into AdditionalProperties used to store a strict option. - private const string StrictKey = "strictJsonSchema"; - - /// Gets whether the properties specify that strict schema handling is desired. - private static bool? HasStrict(IReadOnlyDictionary? additionalProperties) => - additionalProperties?.TryGetValue(StrictKey, out object? strictObj) is true && - strictObj is bool strictValue ? - strictValue : null; - - /// - /// Gets the JSON schema transformer cache conforming to OpenAI strict / structured output restrictions per - /// https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas. - /// - private static AIJsonSchemaTransformCache StrictSchemaTransformCache { get; } = new(new() - { - DisallowAdditionalProperties = true, - ConvertBooleanSchemas = true, - MoveDefaultKeywordToDescription = true, - RequireAllProperties = true, - TransformSchemaNode = (ctx, node) => - { - // Move content from common but unsupported properties to description. In particular, we focus on properties that - // the AIJsonUtilities schema generator might produce and/or that are explicitly mentioned in the OpenAI documentation. - - if (node is JsonObject schemaObj) - { - StringBuilder? additionalDescription = null; - - ReadOnlySpan unsupportedProperties = - [ - // Produced by AIJsonUtilities but not in allow list at https://platform.openai.com/docs/guides/structured-outputs#supported-properties: - "contentEncoding", "contentMediaType", "not", - - // Explicitly mentioned at https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#key-ordering as being unsupported with some models: - "minLength", "maxLength", "pattern", "format", - "minimum", "maximum", "multipleOf", - "patternProperties", - "minItems", "maxItems", - - // Explicitly mentioned at https://learn.microsoft.com/azure/ai-services/openai/how-to/structured-outputs?pivots=programming-language-csharp&tabs=python-secure%2Cdotnet-entra-id#unsupported-type-specific-keywords - // as being unsupported with Azure OpenAI: - "unevaluatedProperties", "propertyNames", "minProperties", "maxProperties", - "unevaluatedItems", "contains", "minContains", "maxContains", "uniqueItems", - ]; - - foreach (string propName in unsupportedProperties) - { - if (schemaObj[propName] is { } propNode) - { - _ = schemaObj.Remove(propName); - AppendLine(ref additionalDescription, propName, propNode); - } - } - - if (additionalDescription is not null) - { - schemaObj["description"] = schemaObj["description"] is { } descriptionNode && descriptionNode.GetValueKind() == JsonValueKind.String ? - $"{descriptionNode.GetValue()}{Environment.NewLine}{additionalDescription}" : - additionalDescription.ToString(); - } - - return node; - - static void AppendLine(ref StringBuilder? sb, string propName, JsonNode propNode) - { - sb ??= new(); - - if (sb.Length > 0) - { - _ = sb.AppendLine(); - } - - _ = sb.Append(propName).Append(": ").Append(propNode); - } - } - - return node; - }, - }); - - /// - /// This class is a no-op implementation of to be used to honor the argument passed - /// while triggering avoiding any unexpected exception on the caller implementation. - /// - private sealed class NoOpChatClient : IChatClient - { - public void Dispose() { } - - public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - => Task.FromResult(new ChatResponse()); - - public object? GetService(Type serviceType, object? serviceKey = null) => null; - - public async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - yield return new ChatResponseUpdate(); - } - } - #endregion - -#if NET - [GeneratedRegex("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$")] - private static partial Regex AgentNameValidationRegex(); -#else - private static Regex AgentNameValidationRegex() => new("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$"); -#endif - - internal static string ThrowIfInvalidAgentName(string? name) - { - Throw.IfNullOrWhitespace(name); - if (!AgentNameValidationRegex().IsMatch(name)) - { - throw new ArgumentException("Agent name must be 1-63 characters long, start and end with an alphanumeric character, and can only contain alphanumeric characters or hyphens.", nameof(name)); - } - return name; - } - - private static ResponseReasoningOptions? ToResponseReasoningOptions(ReasoningOptions reasoning) - { - ResponseReasoningEffortLevel? effortLevel = reasoning.Effort switch - { - ReasoningEffort.Low => ResponseReasoningEffortLevel.Low, - ReasoningEffort.Medium => ResponseReasoningEffortLevel.Medium, - ReasoningEffort.High => ResponseReasoningEffortLevel.High, - ReasoningEffort.ExtraHigh => ResponseReasoningEffortLevel.High, - _ => null, - }; - - ResponseReasoningSummaryVerbosity? summary = reasoning.Output switch - { - ReasoningOutput.Summary => ResponseReasoningSummaryVerbosity.Concise, - ReasoningOutput.Full => ResponseReasoningSummaryVerbosity.Detailed, - _ => null, - }; - - if (effortLevel is null && summary is null) - { - return null; - } - - return new ResponseReasoningOptions - { - ReasoningEffortLevel = effortLevel, - ReasoningSummaryVerbosity = summary, - }; - } -} - -[JsonSerializable(typeof(JsonElement))] -internal sealed partial class AgentClientJsonContext : JsonSerializerContext; diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClient.cs similarity index 98% rename from dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClient.cs index ec788233ed..b33af228b2 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClient.cs @@ -1,7 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; using Azure.AI.Extensions.OpenAI; using Azure.AI.Projects; using Azure.AI.Projects.Agents; @@ -10,7 +14,7 @@ using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; using OpenAI.Responses; -namespace Microsoft.Agents.AI.AzureAI; +namespace Microsoft.Agents.AI.Foundry; /// /// Provides a chat client implementation that integrates with Azure AI Agents, enabling chat interactions using diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs new file mode 100644 index 0000000000..d83299de90 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs @@ -0,0 +1,436 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects.Agents; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; +using OpenAI.Responses; + +namespace Azure.AI.Projects; + +/// +/// Provides extension methods for . +/// +[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] +public static partial class AzureAIProjectChatClientExtensions +{ + /// + /// Uses an existing server side agent, wrapped as a using the provided and . + /// + /// The to create the with. Cannot be . + /// The representing the name and version of the server side agent to create a for. Cannot be . + /// The tools to use when interacting with the agent. This is required when using prompt agent definitions with tools. + /// Provides a way to customize the creation of the underlying used by the agent. + /// An optional to use for resolving services required by the instances being invoked. + /// A instance that can be used to perform operations based on the latest version of the named Azure AI Agent. + /// Thrown when or is . + /// The agent with the specified name was not found. + /// + /// When instantiating a by using an , minimal information will be available about the agent in the instance level, and any logic that relies + /// on to retrieve information about the agent like will receive as the result. + /// + public static FoundryAgent AsAIAgent( + this AIProjectClient aiProjectClient, + AgentReference agentReference, + IList? tools = null, + Func? clientFactory = null, + IServiceProvider? services = null) + { + Throw.IfNull(aiProjectClient); + Throw.IfNull(agentReference); + ThrowIfInvalidAgentName(agentReference.Name); + + var innerAgent = AsChatClientAgent( + aiProjectClient, + agentReference, + new ChatClientAgentOptions() + { + Id = $"{agentReference.Name}:{agentReference.Version}", + Name = agentReference.Name, + ChatOptions = new() { Tools = tools }, + }, + clientFactory, + services); + + return new FoundryAgent(aiProjectClient, innerAgent); + } + + /// + /// Uses an existing server side agent, wrapped as a using the provided and . + /// + /// The client used to interact with Azure AI Agents. Cannot be . + /// The agent record to be converted. The latest version will be used. Cannot be . + /// The tools to use when interacting with the agent. This is required when using prompt agent definitions with tools. + /// Provides a way to customize the creation of the underlying used by the agent. + /// An optional to use for resolving services required by the instances being invoked. + /// A instance that can be used to perform operations based on the latest version of the Azure AI Agent. + /// Thrown when or is . + public static FoundryAgent AsAIAgent( + this AIProjectClient aiProjectClient, + AgentRecord agentRecord, + IList? tools = null, + Func? clientFactory = null, + IServiceProvider? services = null) + { + Throw.IfNull(aiProjectClient); + Throw.IfNull(agentRecord); + + var allowDeclarativeMode = tools is not { Count: > 0 }; + + var innerAgent = AsChatClientAgent( + aiProjectClient, + agentRecord, + tools, + clientFactory, + !allowDeclarativeMode, + services); + + return new FoundryAgent(aiProjectClient, innerAgent); + } + + /// + /// Uses an existing server side agent, wrapped as a using the provided and . + /// + /// The client used to interact with Azure AI Agents. Cannot be . + /// The agent version to be converted. Cannot be . + /// In-process invocable tools to be provided. If no tools are provided manual handling will be necessary to invoke in-process tools. + /// Provides a way to customize the creation of the underlying used by the agent. + /// An optional to use for resolving services required by the instances being invoked. + /// A instance that can be used to perform operations based on the provided version of the Azure AI Agent. + /// Thrown when or is . + public static FoundryAgent AsAIAgent( + this AIProjectClient aiProjectClient, + AgentVersion agentVersion, + IList? tools = null, + Func? clientFactory = null, + IServiceProvider? services = null) + { + Throw.IfNull(aiProjectClient); + Throw.IfNull(agentVersion); + + var allowDeclarativeMode = tools is not { Count: > 0 }; + + var innerAgent = AsChatClientAgent( + aiProjectClient, + agentVersion, + tools, + clientFactory, + !allowDeclarativeMode, + services); + + return new FoundryAgent(aiProjectClient, innerAgent); + } + + /// + /// Creates a non-versioned backed by the project's Responses API using the specified model and instructions. + /// + /// The to use for Responses API calls. Cannot be . + /// The model deployment name to use for the agent. Cannot be or whitespace. + /// The instructions that guide the agent's behavior. Cannot be or whitespace. + /// Optional name for the agent. + /// Optional human-readable description for the agent. + /// Optional collection of tools that the agent can invoke during conversations. + /// Provides a way to customize the creation of the underlying used by the agent. + /// Optional logger factory for creating loggers used by the agent. + /// An optional to use for resolving services required by the instances being invoked. + /// A backed by the project's Responses API. + /// Thrown when is . + /// Thrown when or is empty or whitespace. + public static ChatClientAgent AsAIAgent( + this AIProjectClient aiProjectClient, + string model, + string instructions, + string? name = null, + string? description = null, + IList? tools = null, + Func? clientFactory = null, + ILoggerFactory? loggerFactory = null, + IServiceProvider? services = null) + { + Throw.IfNull(aiProjectClient); + Throw.IfNullOrWhitespace(model); + Throw.IfNullOrWhitespace(instructions); + + ChatClientAgentOptions options = new() + { + Name = name, + Description = description, + ChatOptions = new ChatOptions + { + ModelId = model, + Instructions = instructions, + Tools = tools, + }, + }; + + return CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services); + } + + /// + /// Creates a non-versioned backed by the project's Responses API using the specified options. + /// + /// The to use for Responses API calls. Cannot be . + /// Configuration options that control the agent's behavior. is required. + /// Provides a way to customize the creation of the underlying used by the agent. + /// Optional logger factory for creating loggers used by the agent. + /// An optional to use for resolving services required by the instances being invoked. + /// A backed by the project's Responses API. + /// Thrown when or is . + /// Thrown when does not specify . + public static ChatClientAgent AsAIAgent( + this AIProjectClient aiProjectClient, + ChatClientAgentOptions options, + Func? clientFactory = null, + ILoggerFactory? loggerFactory = null, + IServiceProvider? services = null) + { + Throw.IfNull(aiProjectClient); + Throw.IfNull(options); + + return CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services); + } + + #region Private + + /// Creates a with the specified options. + private static ChatClientAgent CreateChatClientAgent( + AIProjectClient aiProjectClient, + AgentVersion agentVersion, + ChatClientAgentOptions agentOptions, + Func? clientFactory, + IServiceProvider? services) + { + IChatClient chatClient = new AzureAIProjectChatClient(aiProjectClient, agentVersion, agentOptions.ChatOptions); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + return new ChatClientAgent(chatClient, agentOptions, services: services); + } + + private static ChatClientAgent CreateResponsesChatClientAgent( + AIProjectClient aiProjectClient, + ChatClientAgentOptions agentOptions, + Func? clientFactory, + ILoggerFactory? loggerFactory, + IServiceProvider? services) + { + Throw.IfNull(aiProjectClient); + Throw.IfNull(agentOptions); + Throw.IfNull(agentOptions.ChatOptions); + Throw.IfNullOrWhitespace(agentOptions.ChatOptions.ModelId); + + IChatClient chatClient = aiProjectClient + .GetProjectOpenAIClient() + .GetResponsesClient() + .AsIChatClient(agentOptions.ChatOptions.ModelId); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + return new ChatClientAgent(chatClient, agentOptions, loggerFactory, services); + } + + /// This method creates an with the specified ChatClientAgentOptions. + private static ChatClientAgent AsChatClientAgent( + AIProjectClient aiProjectClient, + AgentVersion agentVersion, + ChatClientAgentOptions agentOptions, + Func? clientFactory, + IServiceProvider? services) + => CreateChatClientAgent(aiProjectClient, agentVersion, agentOptions, clientFactory, services); + + /// This method creates an with the specified ChatClientAgentOptions. + private static ChatClientAgent AsChatClientAgent( + AIProjectClient aiProjectClient, + AgentRecord agentRecord, + ChatClientAgentOptions agentOptions, + Func? clientFactory, + IServiceProvider? services) + { + IChatClient chatClient = new AzureAIProjectChatClient(aiProjectClient, agentRecord, agentOptions.ChatOptions); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + return new ChatClientAgent(chatClient, agentOptions, services: services); + } + + /// This method creates an with the specified ChatClientAgentOptions. + private static ChatClientAgent AsChatClientAgent( + AIProjectClient aiProjectClient, + AgentReference agentReference, + ChatClientAgentOptions agentOptions, + Func? clientFactory, + IServiceProvider? services) + { + IChatClient chatClient = new AzureAIProjectChatClient(aiProjectClient, agentReference, defaultModelId: null, agentOptions.ChatOptions); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + return new ChatClientAgent(chatClient, agentOptions, services: services); + } + + /// This method creates an with a auto-generated ChatClientAgentOptions from the specified configuration parameters. + private static ChatClientAgent AsChatClientAgent( + AIProjectClient AIProjectClient, + AgentVersion agentVersion, + IList? tools, + Func? clientFactory, + bool requireInvocableTools, + IServiceProvider? services) + => AsChatClientAgent( + AIProjectClient, + agentVersion, + CreateChatClientAgentOptions(agentVersion, new ChatOptions() { Tools = tools }, requireInvocableTools), + clientFactory, + services); + + /// This method creates an with a auto-generated ChatClientAgentOptions from the specified configuration parameters. + private static ChatClientAgent AsChatClientAgent( + AIProjectClient AIProjectClient, + AgentRecord agentRecord, + IList? tools, + Func? clientFactory, + bool requireInvocableTools, + IServiceProvider? services) + => AsChatClientAgent( + AIProjectClient, + agentRecord, + CreateChatClientAgentOptions(agentRecord.GetLatestVersion(), new ChatOptions() { Tools = tools }, requireInvocableTools), + clientFactory, + services); + + /// + /// This method creates for the specified and the provided tools. + /// + /// The agent version. + /// The to use when interacting with the agent. + /// Indicates whether to enforce the presence of invocable tools when the AIAgent is created with an agent definition that uses them. + /// The created . + /// Thrown when the agent definition requires in-process tools but none were provided. + /// Thrown when the agent definition required tools were not provided. + /// + /// This method rebuilds the agent options from the agent definition returned by the version and combine with the in-proc tools when provided + /// this ensures that all required tools are provided and the definition of the agent options are consistent with the agent definition coming from the server. + /// + private static ChatClientAgentOptions CreateChatClientAgentOptions(AgentVersion agentVersion, ChatOptions? chatOptions, bool requireInvocableTools) + { + var agentDefinition = agentVersion.Definition; + + List? agentTools = null; + if (agentDefinition is PromptAgentDefinition { Tools: { Count: > 0 } definitionTools }) + { + // Check if no tools were provided while the agent definition requires in-proc tools. + if (requireInvocableTools && chatOptions?.Tools is not { Count: > 0 } && definitionTools.Any(t => t is FunctionTool)) + { + throw new ArgumentException("The agent definition in-process tools must be provided in the extension method tools parameter."); + } + + // Agregate all missing tools for a single error message. + List? missingTools = null; + + // Check function tools + foreach (ResponseTool responseTool in definitionTools) + { + if (responseTool is FunctionTool functionTool) + { + // Check if a tool with the same type and name exists in the provided tools. + // Always prefer matching AIFunction when available, regardless of requireInvocableTools. + var matchingTool = chatOptions?.Tools?.FirstOrDefault(t => t is AIFunction tf && functionTool.FunctionName == tf.Name); + + if (matchingTool is not null) + { + (agentTools ??= []).Add(matchingTool!); + continue; + } + + if (requireInvocableTools) + { + (missingTools ??= []).Add($"Function tool: {functionTool.FunctionName}"); + continue; + } + } + + (agentTools ??= []).Add(responseTool.AsAITool()); + } + + if (requireInvocableTools && missingTools is { Count: > 0 }) + { + throw new InvalidOperationException($"The following prompt agent definition required tools were not provided: {string.Join(", ", missingTools)}"); + } + } + + // Use the agent version's ID if available, otherwise generate one from name and version. + // This handles cases where hosted agents (like MCP agents) may not have an ID assigned. + var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version; + var agentId = string.IsNullOrWhiteSpace(agentVersion.Id) + ? $"{agentVersion.Name}:{version}" + : agentVersion.Id; + + var agentOptions = new ChatClientAgentOptions() + { + Id = agentId, + Name = agentVersion.Name, + Description = agentVersion.Description, + }; + + if (agentDefinition is PromptAgentDefinition promptAgentDefinition) + { + agentOptions.ChatOptions ??= chatOptions?.Clone() ?? new(); + agentOptions.ChatOptions.Instructions = promptAgentDefinition.Instructions; + agentOptions.ChatOptions.Temperature = promptAgentDefinition.Temperature; + agentOptions.ChatOptions.TopP = promptAgentDefinition.TopP; + } + + if (agentTools is { Count: > 0 }) + { + agentOptions.ChatOptions ??= chatOptions?.Clone() ?? new(); + agentOptions.ChatOptions.Tools = agentTools; + } + + return agentOptions; + } + +#if NET + [GeneratedRegex("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$")] + private static partial Regex AgentNameValidationRegex(); +#else + private static Regex AgentNameValidationRegex() => new("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$"); +#endif + + internal static string ThrowIfInvalidAgentName(string? name) + { + Throw.IfNullOrWhitespace(name); + if (!AgentNameValidationRegex().IsMatch(name)) + { + throw new ArgumentException("Agent name must be 1-63 characters long, start and end with an alphanumeric character, and can only contain alphanumeric characters or hyphens.", nameof(name)); + } + return name; + } +} + +[JsonSerializable(typeof(JsonElement))] +internal sealed partial class AgentClientJsonContext : JsonSerializerContext; + +#endregion diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectResponsesChatClient.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectResponsesChatClient.cs similarity index 95% rename from dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectResponsesChatClient.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectResponsesChatClient.cs index 48bb20d766..a768d102f8 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectResponsesChatClient.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectResponsesChatClient.cs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using Azure.AI.Projects; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; -namespace Microsoft.Agents.AI.AzureAI; +namespace Microsoft.Agents.AI.Foundry; #pragma warning disable OPENAI001 internal sealed class AzureAIProjectResponsesChatClient : DelegatingChatClient diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/CompatibilitySuppressions.xml b/dotnet/src/Microsoft.Agents.AI.Foundry/CompatibilitySuppressions.xml similarity index 100% rename from dotnet/src/Microsoft.Agents.AI.AzureAI/CompatibilitySuppressions.xml rename to dotnet/src/Microsoft.Agents.AI.Foundry/CompatibilitySuppressions.xml diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/FoundryAITool.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs similarity index 99% rename from dotnet/src/Microsoft.Agents.AI.AzureAI/FoundryAITool.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs index 80ed48e1df..79e221f02f 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/FoundryAITool.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAITool.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Azure.AI.Projects.Agents; using Microsoft.Extensions.AI; @@ -8,7 +10,7 @@ using OpenAI.Responses; #pragma warning disable OPENAI001 -namespace Microsoft.Agents.AI.AzureAI; +namespace Microsoft.Agents.AI.Foundry; /// /// Provides factory methods for creating instances from Microsoft Foundry and OpenAI response tools. diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/FoundryAgent.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs similarity index 89% rename from dotnet/src/Microsoft.Agents.AI.AzureAI/FoundryAgent.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs index 66fa93e39f..b1d97a5c9c 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/FoundryAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs @@ -1,7 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.ClientModel; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; using Azure.AI.Extensions.OpenAI; using Azure.AI.Projects; using Microsoft.Extensions.AI; @@ -9,7 +13,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; -namespace Microsoft.Agents.AI.AzureAI; +namespace Microsoft.Agents.AI.Foundry; /// /// Provides an that uses Microsoft Foundry for AI agent capabilities. @@ -163,7 +167,29 @@ public sealed class FoundryAgent : DelegatingAIAgent }, }; - return AzureAIProjectChatClientExtensions.CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services); + return CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services); + } + + private static ChatClientAgent CreateResponsesChatClientAgent( + AIProjectClient aiProjectClient, + ChatClientAgentOptions agentOptions, + Func? clientFactory, + ILoggerFactory? loggerFactory, + IServiceProvider? services) + { + Throw.IfNull(aiProjectClient); + Throw.IfNull(agentOptions); + Throw.IfNull(agentOptions.ChatOptions); + Throw.IfNullOrWhitespace(agentOptions.ChatOptions.ModelId); + + IChatClient chatClient = new AzureAIProjectResponsesChatClient(aiProjectClient, agentOptions.ChatOptions.ModelId); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + return new ChatClientAgent(chatClient, agentOptions, loggerFactory, services); } private static ChatClientAgent CreateInnerAgentFromEndpoint( diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryJsonUtilities.cs similarity index 84% rename from dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryJsonUtilities.cs index 1a0dd4f4e2..1ed4046de0 100644 --- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryJsonUtilities.cs @@ -1,13 +1,16 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Shared.DiagnosticIds; -namespace Microsoft.Agents.AI.FoundryMemory; +namespace Microsoft.Agents.AI.Foundry; /// /// Provides JSON serialization utilities for the Foundry Memory provider. /// +[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] internal static class FoundryMemoryJsonUtilities { /// @@ -33,4 +36,5 @@ internal static class FoundryMemoryJsonUtilities WriteIndented = false)] [JsonSerializable(typeof(FoundryMemoryProviderScope))] [JsonSerializable(typeof(FoundryMemoryProvider.State))] +[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] internal partial class FoundryMemoryJsonContext : JsonSerializerContext; diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryProvider.cs similarity index 99% rename from dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryProvider.cs index 93b343b2de..cc92c40a41 100644 --- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryProvider.cs @@ -16,7 +16,7 @@ using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; using OpenAI.Responses; -namespace Microsoft.Agents.AI.FoundryMemory; +namespace Microsoft.Agents.AI.Foundry; /// /// Provides a Microsoft Foundry Memory backed that persists conversation messages as memories @@ -27,7 +27,7 @@ namespace Microsoft.Agents.AI.FoundryMemory; /// for new invocations using the memory search endpoint. Retrieved memories are injected as user messages /// to the model, prefixed by a configurable context prompt. /// -[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] +[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class FoundryMemoryProvider : AIContextProvider { private const string DefaultContextPrompt = "## Memories\nConsider the following memories when answering user questions:"; diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryProviderOptions.cs similarity index 95% rename from dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryProviderOptions.cs index cf4fb5ab15..668db44112 100644 --- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryProviderOptions.cs @@ -2,14 +2,17 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.AI; using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Shared.DiagnosticIds; -namespace Microsoft.Agents.AI.FoundryMemory; +namespace Microsoft.Agents.AI.Foundry; /// /// Options for configuring the . /// +[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class FoundryMemoryProviderOptions { /// diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryProviderScope.cs similarity index 89% rename from dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryProviderScope.cs index 6646c482a3..769aff7370 100644 --- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryProviderScope.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; -namespace Microsoft.Agents.AI.FoundryMemory; +namespace Microsoft.Agents.AI.Foundry; /// /// Allows scoping of memories for the . @@ -13,6 +15,7 @@ namespace Microsoft.Agents.AI.FoundryMemory; /// Common patterns include using a user ID, team ID, or other unique identifier /// to partition memories across different contexts. /// +[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class FoundryMemoryProviderScope { /// diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Memory/MemoryStoreExtensions.cs similarity index 93% rename from dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Memory/MemoryStoreExtensions.cs index 9e24703d92..a696a33e5a 100644 --- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Memory/MemoryStoreExtensions.cs @@ -5,12 +5,12 @@ using System.Threading; using System.Threading.Tasks; using Azure.AI.Projects; -namespace Microsoft.Agents.AI.FoundryMemory; +namespace Microsoft.Agents.AI.Foundry; /// /// Internal extension methods for to provide MemoryStores helper operations. /// -internal static class AIProjectClientExtensions +internal static class MemoryStoreExtensions { /// /// Creates a memory store if it doesn't already exist. diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj b/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj similarity index 63% rename from dotnet/src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj rename to dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj index 0cd8690126..03d84c9d47 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj @@ -2,21 +2,29 @@ true - enable true + $(NoWarn);OPENAI001 + + + + false + + true true + true + @@ -30,4 +38,9 @@ Provides Microsoft Agent Framework support for Foundry Agents. + + + + + diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/ProjectResponsesClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/ProjectResponsesClientExtensions.cs similarity index 99% rename from dotnet/src/Microsoft.Agents.AI.AzureAI/ProjectResponsesClientExtensions.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/ProjectResponsesClientExtensions.cs index 5a899d5076..cd253776a8 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/ProjectResponsesClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/ProjectResponsesClientExtensions.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.AI; using Microsoft.Shared.DiagnosticIds; diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/RequestOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/RequestOptionsExtensions.cs similarity index 96% rename from dotnet/src/Microsoft.Agents.AI.AzureAI/RequestOptionsExtensions.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/RequestOptionsExtensions.cs index 2705611b57..03e48e293b 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/RequestOptionsExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/RequestOptionsExtensions.cs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System.ClientModel.Primitives; +using System.Collections.Generic; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.Agents.AI; diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj deleted file mode 100644 index 7abc3d0bcc..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - preview - $(NoWarn);OPENAI001 - - - - true - true - true - true - true - - - - - - - - - - - - - - - - - Microsoft Agent Framework - Azure AI Foundry Memory integration - Provides Azure AI Foundry Memory integration for Microsoft Agent Framework. - - - - - - - - diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/AzureAgentProvider.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/AzureAgentProvider.cs similarity index 100% rename from dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/AzureAgentProvider.cs rename to dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/AzureAgentProvider.cs diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/CompatibilitySuppressions.xml b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/CompatibilitySuppressions.xml similarity index 80% rename from dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/CompatibilitySuppressions.xml rename to dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/CompatibilitySuppressions.xml index 3454984fae..fbf1db84c7 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/CompatibilitySuppressions.xml +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/CompatibilitySuppressions.xml @@ -1,39 +1,39 @@ - + CP0002 M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions - lib/net10.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll - lib/net10.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll + lib/net10.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll + lib/net10.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll true CP0002 M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions - lib/net472/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll - lib/net472/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll + lib/net472/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll + lib/net472/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll true CP0002 M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions - lib/net8.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll - lib/net8.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll + lib/net8.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll + lib/net8.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll true CP0002 M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions - lib/net9.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll - lib/net9.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll + lib/net9.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll + lib/net9.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll true CP0002 M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions - lib/netstandard2.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll - lib/netstandard2.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll + lib/netstandard2.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll + lib/netstandard2.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll true \ No newline at end of file diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj similarity index 70% rename from dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj rename to dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj index 5bf9f6d29e..407593536e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj @@ -13,10 +13,16 @@ + + + + false + + - Microsoft Agent Framework Declarative Workflows Azure AI - Provides Microsoft Agent Framework support for declarative workflows for Azure AI Agents. + Microsoft Agent Framework Declarative Workflows Foundry + Provides Microsoft Agent Framework support for declarative workflows for Microsoft Foundry Agents. @@ -24,7 +30,7 @@ - + diff --git a/dotnet/src/Shared/IntegrationTests/TestSettings.cs b/dotnet/src/Shared/IntegrationTests/TestSettings.cs index 880db9d1cd..de5757314c 100644 --- a/dotnet/src/Shared/IntegrationTests/TestSettings.cs +++ b/dotnet/src/Shared/IntegrationTests/TestSettings.cs @@ -16,6 +16,7 @@ internal static class TestSettings // Azure AI (Foundry) public const string AzureAIBingConnectionId = "AZURE_AI_BING_CONNECTION_ID"; + public const string AzureAIEmbeddingDeploymentName = "AZURE_AI_EMBEDDING_DEPLOYMENT_NAME"; public const string AzureAIMemoryStoreId = "AZURE_AI_MEMORY_STORE_ID"; public const string AzureAIModelDeploymentName = "AZURE_AI_MODEL_DEPLOYMENT_NAME"; public const string AzureAIProjectEndpoint = "AZURE_AI_PROJECT_ENDPOINT"; diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunStreamingTests.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunStreamingTests.cs deleted file mode 100644 index 0e507e44c1..0000000000 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunStreamingTests.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading.Tasks; -using AgentConformance.IntegrationTests; - -namespace AzureAI.IntegrationTests; - -#pragma warning disable CS0618 // Tests intentionally exercise obsolete AIProjectClientFixture -[Obsolete("Use FoundryVersionedAgentRunTests instead. These tests exercise obsolete AIProjectClient extension methods.")] -public class AIProjectClientChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests(() => new()) -{ - public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() - { - Assert.Skip("No messages is not supported"); - return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); - } -} diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunTests.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunTests.cs deleted file mode 100644 index a0ee72ebd4..0000000000 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunTests.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading.Tasks; -using AgentConformance.IntegrationTests; - -namespace AzureAI.IntegrationTests; - -#pragma warning disable CS0618 // Tests intentionally exercise obsolete AIProjectClientFixture -[Obsolete("Use FoundryVersionedAgentRunTests instead. These tests exercise obsolete AIProjectClient extension methods.")] -public class AIProjectClientChatClientAgentRunTests() : ChatClientAgentRunTests(() => new()) -{ - public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() - { - Assert.Skip("No messages is not supported"); - return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); - } -} diff --git a/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj b/dotnet/tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj similarity index 83% rename from dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj rename to dotnet/tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj index 2703360cb2..dbff50104c 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj +++ b/dotnet/tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj @@ -7,7 +7,7 @@ - + diff --git a/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentChatClientRunStreamingTests.cs b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentChatClientRunStreamingTests.cs new file mode 100644 index 0000000000..c9128050fc --- /dev/null +++ b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentChatClientRunStreamingTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using AgentConformance.IntegrationTests; + +namespace Foundry.IntegrationTests; + +public class FoundryVersionedAgentChatClientRunStreamingTests() : ChatClientAgentRunStreamingTests(() => new()) +{ + public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() + { + Assert.Skip("No messages is not supported"); + return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); + } +} diff --git a/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentChatClientRunTests.cs b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentChatClientRunTests.cs new file mode 100644 index 0000000000..fff2ad529f --- /dev/null +++ b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentChatClientRunTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using AgentConformance.IntegrationTests; + +namespace Foundry.IntegrationTests; + +public class FoundryVersionedAgentChatClientRunTests() : ChatClientAgentRunTests(() => new()) +{ + public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() + { + Assert.Skip("No messages is not supported"); + return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); + } +} diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentCreateTests.cs similarity index 70% rename from dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs rename to dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentCreateTests.cs index bc5f38acf2..160ab697f7 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs +++ b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentCreateTests.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CS0618 // Tests intentionally exercise obsolete extension methods - using System; using System.IO; using System.Threading.Tasks; @@ -9,45 +7,43 @@ using AgentConformance.IntegrationTests.Support; using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; using Microsoft.Extensions.AI; using OpenAI.Files; using OpenAI.Responses; using Shared.IntegrationTests; -namespace AzureAI.IntegrationTests; +namespace Foundry.IntegrationTests; -[Obsolete("Use FoundryVersionedAgentCreateTests instead. These tests exercise obsolete AIProjectClient extension methods.")] -public class AIProjectClientCreateTests +/// +/// Integration tests for versioned creation via +/// AIProjectClient.Agents.CreateAgentVersionAsync and AIProjectClient.AsAIAgent(AgentVersion). +/// +public class FoundryVersionedAgentCreateTests { private readonly AIProjectClient _client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential()); - [Theory] - [InlineData("CreateWithChatClientAgentOptionsAsync")] - [InlineData("CreateWithFoundryOptionsAsync")] - public async Task CreateAgent_CreatesAgentWithCorrectMetadataAsync(string createMechanism) + [Fact] + public async Task CreateAgent_CreatesAgentWithCorrectMetadataAsync() { // Arrange. - string AgentName = AIProjectClientFixture.GenerateUniqueAgentName("IntegrationTestAgent"); + string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("IntegrationTestAgent"); const string AgentDescription = "An agent created during integration tests"; const string AgentInstructions = "You are an integration test agent"; // Act. - var agent = createMechanism switch - { - "CreateWithChatClientAgentOptionsAsync" => await this._client.CreateAIAgentAsync( - model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), - options: new ChatClientAgentOptions() + var agentVersion = await this._client.Agents.CreateAgentVersionAsync( + AgentName, + new AgentVersionCreationOptions( + new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName)) { - Name = AgentName, - Description = AgentDescription, - ChatOptions = new() { Instructions = AgentInstructions } - }), - "CreateWithFoundryOptionsAsync" => await this._client.CreateAIAgentAsync( - name: AgentName, - creationOptions: new AgentVersionCreationOptions(new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName)) { Instructions = AgentInstructions }) { Description = AgentDescription }), - _ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}") - }; + Instructions = AgentInstructions + }) + { + Description = AgentDescription + }); + + var agent = this._client.AsAIAgent(agentVersion); try { @@ -72,12 +68,11 @@ public class AIProjectClientCreateTests } [Theory(Skip = "For manual testing only")] - [InlineData("CreateWithChatClientAgentOptionsAsync")] - [InlineData("CreateWithFoundryOptionsAsync")] - public async Task CreateAgent_CreatesAgentWithVectorStoresAsync(string createMechanism) + [InlineData("FileSearchTool")] + public async Task CreateAgent_CreatesAgentWithVectorStoresAsync(string _) { // Arrange. - string AgentName = AIProjectClientFixture.GenerateUniqueAgentName("VectorStoreAgent"); + string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("VectorStoreAgent"); const string AgentInstructions = """ You are a helpful agent that can help fetch data from files you know about. Use the File Search Tool to look up codes for words. @@ -99,22 +94,19 @@ public class AIProjectClientCreateTests ); var vectorStoreMetadata = await projectOpenAIClient.GetProjectVectorStoresClient().CreateVectorStoreAsync(options: new() { FileIds = { uploadedAgentFile.Id }, Name = "WordCodeLookup_VectorStore" }); - // Act. - var agent = createMechanism switch + // Act — create agent version with FileSearch tool via native SDK, then wrap with AsAIAgent. + var definition = new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName)) { - "CreateWithChatClientAgentOptionsAsync" => await this._client.CreateAIAgentAsync( - model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), - name: AgentName, - instructions: AgentInstructions, - tools: [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }]), - "CreateWithFoundryOptionsAsync" => await this._client.CreateAIAgentAsync( - model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), - name: AgentName, - instructions: AgentInstructions, - tools: [ResponseTool.CreateFileSearchTool(vectorStoreIds: [vectorStoreMetadata.Value.Id]).AsAITool()]), - _ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}") + Instructions = AgentInstructions, + Tools = { ResponseTool.CreateFileSearchTool(vectorStoreIds: [vectorStoreMetadata.Value.Id]) } }; + var agentVersion = await this._client.Agents.CreateAgentVersionAsync( + AgentName, + new AgentVersionCreationOptions(definition)); + + var agent = this._client.AsAIAgent(agentVersion); + try { // Assert. @@ -132,13 +124,11 @@ public class AIProjectClientCreateTests } } - [Theory] - [InlineData("CreateWithChatClientAgentOptionsAsync")] - [InlineData("CreateWithFoundryOptionsAsync")] - public async Task CreateAgent_CreatesAgentWithCodeInterpreterAsync(string createMechanism) + [Fact] + public async Task CreateAgent_CreatesAgentWithCodeInterpreterAsync() { // Arrange. - string AgentName = AIProjectClientFixture.GenerateUniqueAgentName("CodeInterpreterAgent"); + string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("CodeInterpreterAgent"); const string AgentInstructions = """ You are a helpful coding agent. A Python file is provided. Use the Code Interpreter Tool to run the file and report the SECRET_NUMBER value it prints. Respond only with the number. @@ -158,24 +148,19 @@ public class AIProjectClientCreateTests purpose: FileUploadPurpose.Assistants ); - // Act. - var agent = createMechanism switch + // Act — create agent version with CodeInterpreter tool via native SDK, then wrap with AsAIAgent. + var definition = new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName)) { - // Hosted tool path (tools supplied via ChatClientAgentOptions) - "CreateWithChatClientAgentOptionsAsync" => await this._client.CreateAIAgentAsync( - model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), - name: AgentName, - instructions: AgentInstructions, - tools: [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }]), - // Foundry (definitions + resources provided directly) - "CreateWithFoundryOptionsAsync" => await this._client.CreateAIAgentAsync( - model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), - name: AgentName, - instructions: AgentInstructions, - tools: [ResponseTool.CreateCodeInterpreterTool(new CodeInterpreterToolContainer(CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration([uploadedCodeFile.Id]))).AsAITool()]), - _ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}") + Instructions = AgentInstructions, + Tools = { ResponseTool.CreateCodeInterpreterTool(new CodeInterpreterToolContainer(CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration([uploadedCodeFile.Id]))) } }; + var agentVersion = await this._client.Agents.CreateAgentVersionAsync( + AgentName, + new AgentVersionCreationOptions(definition)); + + var agent = this._client.AsAIAgent(agentVersion); + try { // Assert. @@ -202,7 +187,7 @@ public class AIProjectClientCreateTests public async Task AsAIAgent_WithOpenAPITool_NativeSDKCreation_InvokesServerSideToolAsync() { // Arrange — create agent version with OpenAPI tool using native Azure.AI.Projects SDK types. - string AgentName = AIProjectClientFixture.GenerateUniqueAgentName("OpenAPITestAgent"); + string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("OpenAPITestAgent"); const string AgentInstructions = "You are a helpful assistant that can use the countries API to retrieve information about countries by their currency code."; const string CountriesOpenApiSpec = """ @@ -320,28 +305,29 @@ public class AIProjectClientCreateTests } } - [Theory] - [InlineData("CreateWithChatClientAgentOptionsAsync")] - public async Task CreateAgent_CreatesAgentWithAIFunctionToolsAsync(string createMechanism) + [Fact] + public async Task CreateAgent_CreatesAgentWithAIFunctionToolsAsync() { // Arrange. - string AgentName = AIProjectClientFixture.GenerateUniqueAgentName("WeatherAgent"); + string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("WeatherAgent"); const string AgentInstructions = "You are a helpful weather assistant. Always call the GetWeather function to answer questions about weather."; static string GetWeather(string location) => $"The weather in {location} is sunny with a high of 23C."; var weatherFunction = AIFunctionFactory.Create(GetWeather); - FoundryAgent agent = createMechanism switch + // Create agent version with the function tool registered in the server-side definition, + // then wrap with AsAIAgent passing the local AIFunction implementation. + var definition = new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName)) { - "CreateWithChatClientAgentOptionsAsync" => await this._client.CreateAIAgentAsync( - model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), - options: new ChatClientAgentOptions() - { - Name = AgentName, - ChatOptions = new() { Instructions = AgentInstructions, Tools = [weatherFunction] } - }), - _ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}") + Instructions = AgentInstructions, }; + definition.Tools.Add(weatherFunction.AsOpenAIResponseTool()); + + var agentVersion = await this._client.Agents.CreateAgentVersionAsync( + AgentName, + new AgentVersionCreationOptions(definition)); + + FoundryAgent agent = this._client.AsAIAgent(agentVersion, tools: [weatherFunction]); try { diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentFixture.cs similarity index 70% rename from dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs rename to dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentFixture.cs index 112c76571b..2c06404eb6 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs +++ b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentFixture.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CS0618 // Tests intentionally exercise obsolete extension methods - using System; using System.Collections.Generic; using System.Linq; @@ -10,16 +8,21 @@ using AgentConformance.IntegrationTests; using AgentConformance.IntegrationTests.Support; using Azure.AI.Extensions.OpenAI; using Azure.AI.Projects; +using Azure.AI.Projects.Agents; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; +using Microsoft.Agents.AI.Foundry; using Microsoft.Extensions.AI; using OpenAI.Responses; using Shared.IntegrationTests; -namespace AzureAI.IntegrationTests; +namespace Foundry.IntegrationTests; -[Obsolete("Use FoundryVersionedAgentFixture instead. These tests exercise obsolete AIProjectClient extension methods.")] -public class AIProjectClientFixture : IChatClientAgentFixture +/// +/// Integration test fixture that creates versioned Foundry agents via +/// AIProjectClient.Agents.CreateAgentVersionAsync and wraps them +/// with AIProjectClient.AsAIAgent(AgentVersion). +/// +public class FoundryVersionedAgentFixture : IChatClientAgentFixture { private FoundryAgent _agent = null!; private AIProjectClient _client = null!; @@ -40,7 +43,6 @@ public class AIProjectClientFixture : IChatClientAgentFixture if (chatClientSession.ConversationId?.StartsWith("conv_", StringComparison.OrdinalIgnoreCase) == true) { - // Conversation sessions do not persist message history. return await this.GetChatHistoryFromConversationAsync(chatClientSession.ConversationId); } @@ -119,14 +121,48 @@ public class AIProjectClientFixture : IChatClientAgentFixture string instructions = "You are a helpful assistant.", IList? aiTools = null) { - return (await this._client.CreateAIAgentAsync(GenerateUniqueAgentName(name), model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), instructions: instructions, tools: aiTools)).GetService()!; + var definition = new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName)) + { + Instructions = instructions + }; + + // Register AIFunction tool definitions in the server-side agent definition so the model + // can invoke them. The local AIFunction implementations are matched by name via AsAIAgent. + if (aiTools is not null) + { + foreach (var tool in aiTools) + { + if (tool.AsOpenAIResponseTool() is ResponseTool responseTool) + { + definition.Tools.Add(responseTool); + } + } + } + + var agentVersion = await this._client.Agents.CreateAgentVersionAsync( + GenerateUniqueAgentName(name), + new AgentVersionCreationOptions(definition)); + + return this._client.AsAIAgent(agentVersion, tools: aiTools).GetService()!; } public async Task CreateChatClientAgentAsync(ChatClientAgentOptions options) { options.Name ??= GenerateUniqueAgentName("HelpfulAssistant"); - return (await this._client.CreateAIAgentAsync(model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), options)).GetService()!; + var definition = new PromptAgentDefinition( + options.ChatOptions?.ModelId ?? TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName)) + { + Instructions = options.ChatOptions?.Instructions + }; + + var agentVersion = await this._client.Agents.CreateAgentVersionAsync( + options.Name, + new AgentVersionCreationOptions(definition) { Description = options.Description }); + + var agent = this._client.AsAIAgent(agentVersion, tools: options.ChatOptions?.Tools); + + return agent.GetService()!; } public static string GenerateUniqueAgentName(string baseName) => @@ -174,13 +210,33 @@ public class AIProjectClientFixture : IChatClientAgentFixture public virtual async ValueTask InitializeAsync() { this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential()); - this._agent = await this._client.CreateAIAgentAsync(GenerateUniqueAgentName("HelpfulAssistant"), model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), instructions: "You are a helpful assistant."); + + var agentVersion = await this._client.Agents.CreateAgentVersionAsync( + GenerateUniqueAgentName("HelpfulAssistant"), + new AgentVersionCreationOptions( + new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName)) + { + Instructions = "You are a helpful assistant." + })); + + this._agent = this._client.AsAIAgent(agentVersion); } public async Task InitializeAsync(ChatClientAgentOptions options) { this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential()); options.Name ??= GenerateUniqueAgentName("HelpfulAssistant"); - this._agent = await this._client.CreateAIAgentAsync(model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), options); + + var definition = new PromptAgentDefinition( + options.ChatOptions?.ModelId ?? TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName)) + { + Instructions = options.ChatOptions?.Instructions + }; + + var agentVersion = await this._client.Agents.CreateAgentVersionAsync( + options.Name, + new AgentVersionCreationOptions(definition) { Description = options.Description }); + + this._agent = this._client.AsAIAgent(agentVersion, tools: options.ChatOptions?.Tools); } } diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentRunTests.cs b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentRunStreamingTests.cs similarity index 57% rename from dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentRunTests.cs rename to dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentRunStreamingTests.cs index ad10ec5b7a..f5df9f1f42 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentRunTests.cs +++ b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentRunStreamingTests.cs @@ -5,11 +5,9 @@ using System.Threading.Tasks; using AgentConformance.IntegrationTests; using Microsoft.Agents.AI; -namespace AzureAI.IntegrationTests; +namespace Foundry.IntegrationTests; -#pragma warning disable CS0618 // Tests intentionally exercise obsolete AIProjectClientFixture -[Obsolete("Use FoundryVersionedAgentRunTests instead. These tests exercise obsolete AIProjectClient extension methods.")] -public class AIProjectClientAgentRunPreviousResponseTests() : RunTests(() => new()) +public class FoundryVersionedAgentRunStreamingPreviousResponseTests() : RunStreamingTests(() => new()) { public override Task RunWithNoMessageDoesNotFailAsync() { @@ -18,8 +16,7 @@ public class AIProjectClientAgentRunPreviousResponseTests() : RunTests(() => new()) +public class FoundryVersionedAgentRunStreamingConversationTests() : RunStreamingTests(() => new()) { public override Func> AgentRunOptionsFactory => async () => { diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentRunStreamingTests.cs b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentRunTests.cs similarity index 56% rename from dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentRunStreamingTests.cs rename to dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentRunTests.cs index 0f2b123fd9..9cefbd0f46 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentRunStreamingTests.cs +++ b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentRunTests.cs @@ -5,11 +5,9 @@ using System.Threading.Tasks; using AgentConformance.IntegrationTests; using Microsoft.Agents.AI; -namespace AzureAI.IntegrationTests; +namespace Foundry.IntegrationTests; -#pragma warning disable CS0618 // Tests intentionally exercise obsolete AIProjectClientFixture -[Obsolete("Use FoundryVersionedAgentRunTests instead. These tests exercise obsolete AIProjectClient extension methods.")] -public class AIProjectClientAgentRunStreamingPreviousResponseTests() : RunStreamingTests(() => new()) +public class FoundryVersionedAgentRunPreviousResponseTests() : RunTests(() => new()) { public override Task RunWithNoMessageDoesNotFailAsync() { @@ -18,8 +16,7 @@ public class AIProjectClientAgentRunStreamingPreviousResponseTests() : RunStream } } -[Obsolete("Use FoundryVersionedAgentRunTests instead. These tests exercise obsolete AIProjectClient extension methods.")] -public class AIProjectClientAgentRunStreamingConversationTests() : RunStreamingTests(() => new()) +public class FoundryVersionedAgentRunConversationTests() : RunTests(() => new()) { public override Func> AgentRunOptionsFactory => async () => { diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentStructuredOutputRunTests.cs b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentStructuredOutputRunTests.cs similarity index 71% rename from dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentStructuredOutputRunTests.cs rename to dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentStructuredOutputRunTests.cs index b3782a6601..015877df05 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentStructuredOutputRunTests.cs +++ b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentStructuredOutputRunTests.cs @@ -1,25 +1,23 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using AgentConformance.IntegrationTests; using AgentConformance.IntegrationTests.Support; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; -namespace AzureAI.IntegrationTests; +namespace Foundry.IntegrationTests; -#pragma warning disable CS0618 // Tests intentionally exercise obsolete AIProjectClientFixture -[Obsolete("Use FoundryVersionedAgentStructuredOutputRunTests instead. These tests exercise obsolete AIProjectClient extension methods.")] -public class AIProjectClientAgentStructuredOutputRunTests() : StructuredOutputRunTests>(() => new AIProjectClientStructuredOutputFixture()) +public class FoundryVersionedAgentStructuredOutputRunTests() : StructuredOutputRunTests>(() => new FoundryVersionedAgentStructuredOutputFixture()) { - private const string NotSupported = "AIProjectClient does not support specifying structured output type at invocation time."; + private const string NotSupported = "Versioned Foundry agents do not support specifying structured output type at invocation time."; + private const string ResponseFormatNotSupported = "AzureAIProjectChatClient clears ResponseFormat for versioned agents; structured output must be defined in the server-side agent definition."; /// /// Verifies that response format provided at agent initialization is used when invoking RunAsync. /// /// - [RetryFact(Constants.RetryCount, Constants.RetryDelay)] + [RetryFact(Constants.RetryCount, Constants.RetryDelay, Skip = ResponseFormatNotSupported)] public async Task RunWithResponseFormatAtAgentInitializationReturnsExpectedResultAsync() { // Arrange @@ -39,14 +37,14 @@ public class AIProjectClientAgentStructuredOutputRunTests() : StructuredOutputRu } /// - /// Verifies that generic RunAsync works with AIProjectClient when structured output is configured at agent initialization. + /// Verifies that generic RunAsync works with versioned Foundry agents when structured output is configured at agent initialization. /// /// - /// AIProjectClient does not support specifying the structured output type at invocation time yet. + /// Versioned Foundry agents do not support specifying the structured output type at invocation time yet. /// The type T provided to RunAsync<T> is ignored by AzureAIProjectChatClient and is only used /// for deserializing the agent response by AgentResponse<T>.Result. /// - [RetryFact(Constants.RetryCount, Constants.RetryDelay)] + [RetryFact(Constants.RetryCount, Constants.RetryDelay, Skip = ResponseFormatNotSupported)] public async Task RunGenericWithResponseFormatAtAgentInitializationReturnsExpectedResultAsync() { // Arrange @@ -88,10 +86,9 @@ public class AIProjectClientAgentStructuredOutputRunTests() : StructuredOutputRu } /// -/// Represents a fixture for testing AIProjectClient with structured output of type provided at agent initialization. +/// Represents a fixture for testing versioned Foundry agents with structured output of type provided at agent initialization. /// -[Obsolete("Use FoundryVersionedAgentStructuredOutputFixture instead.")] -public class AIProjectClientStructuredOutputFixture : AIProjectClientFixture +public class FoundryVersionedAgentStructuredOutputFixture : FoundryVersionedAgentFixture { public override async ValueTask InitializeAsync() { diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Foundry.IntegrationTests/Memory/FoundryMemoryProviderTests.cs similarity index 57% rename from dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs rename to dotnet/tests/Foundry.IntegrationTests/Memory/FoundryMemoryProviderTests.cs index d6092f5231..9b9b39cbdf 100644 --- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs +++ b/dotnet/tests/Foundry.IntegrationTests/Memory/FoundryMemoryProviderTests.cs @@ -1,14 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CS0618 // Tests intentionally exercise obsolete extension methods - using System; using System.Threading.Tasks; using Azure.AI.Projects; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; +using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; +using OpenAI.Responses; using Shared.IntegrationTests; -namespace Microsoft.Agents.AI.FoundryMemory.IntegrationTests; +namespace Foundry.IntegrationTests.Memory; /// /// Integration tests for against a configured Azure AI Foundry Memory service. @@ -16,7 +19,6 @@ namespace Microsoft.Agents.AI.FoundryMemory.IntegrationTests; /// /// These integration tests are skipped by default and require a live Azure AI Foundry Memory service. /// The tests need to be updated to use the new AIAgent-based API pattern. -/// Set to null to enable them after configuring the service. /// public sealed class FoundryMemoryProviderTests : IDisposable { @@ -25,6 +27,7 @@ public sealed class FoundryMemoryProviderTests : IDisposable private readonly AIProjectClient? _client; private readonly string? _memoryStoreName; private readonly string? _deploymentName; + private readonly string? _embeddingDeploymentName; private bool _disposed; public FoundryMemoryProviderTests() @@ -38,13 +41,15 @@ public sealed class FoundryMemoryProviderTests : IDisposable var endpoint = configuration[TestSettings.AzureAIProjectEndpoint]; var memoryStoreName = configuration[TestSettings.AzureAIMemoryStoreId]; var deploymentName = configuration[TestSettings.AzureAIModelDeploymentName]; + var embeddingDeploymentName = configuration[TestSettings.AzureAIEmbeddingDeploymentName]; if (!string.IsNullOrWhiteSpace(endpoint) && !string.IsNullOrWhiteSpace(memoryStoreName)) { - this._client = new AIProjectClient(new Uri(endpoint), TestAzureCliCredentials.CreateAzureCliCredential()); + this._client = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential()); this._memoryStoreName = memoryStoreName; this._deploymentName = deploymentName ?? "gpt-4.1-mini"; + this._embeddingDeploymentName = embeddingDeploymentName ?? "text-embedding-ada-002"; } } @@ -57,8 +62,17 @@ public sealed class FoundryMemoryProviderTests : IDisposable this._memoryStoreName!, stateInitializer: _ => new(new FoundryMemoryProviderScope("it-user-1"))); - AIAgent agent = await this._client!.CreateAIAgentAsync(this._deploymentName!, - options: new ChatClientAgentOptions { AIContextProviders = [memoryProvider] }); + await memoryProvider.EnsureMemoryStoreCreatedAsync(this._deploymentName!, this._embeddingDeploymentName!); + + AIAgent agent = this._client!.AsAIAgent(new ChatClientAgentOptions + { + ChatOptions = new ChatOptions + { + ModelId = this._deploymentName!, + Instructions = "You are a helpful assistant. Use known memories about the user when responding, and do not invent details." + }, + AIContextProviders = [memoryProvider] + }); AgentSession session = await agent.CreateSessionAsync(); @@ -72,6 +86,15 @@ public sealed class FoundryMemoryProviderTests : IDisposable await memoryProvider.WhenUpdatesCompletedAsync(); await Task.Delay(2000); + // Assert - verify memories were actually created in the store before querying via agent + var searchResult = await this._client!.MemoryStores.SearchMemoriesAsync( + this._memoryStoreName!, + new MemorySearchOptions("it-user-1") + { + Items = { ResponseItem.CreateUserMessageItem("Caoimhe") } + }); + Assert.NotEmpty(searchResult.Value.Memories); + AgentResponse resultAfter = await agent.RunAsync("What is my name?", session); // Cleanup @@ -95,10 +118,27 @@ public sealed class FoundryMemoryProviderTests : IDisposable this._memoryStoreName!, stateInitializer: _ => new(new FoundryMemoryProviderScope("it-scope-b"))); - AIAgent agent1 = await this._client!.CreateAIAgentAsync(this._deploymentName!, - options: new ChatClientAgentOptions { AIContextProviders = [memoryProvider1] }); - AIAgent agent2 = await this._client!.CreateAIAgentAsync(this._deploymentName!, - options: new ChatClientAgentOptions { AIContextProviders = [memoryProvider2] }); + await memoryProvider1.EnsureMemoryStoreCreatedAsync(this._deploymentName!, this._embeddingDeploymentName!); + + AIAgent agent1 = this._client!.AsAIAgent(new ChatClientAgentOptions + { + ChatOptions = new ChatOptions + { + ModelId = this._deploymentName!, + Instructions = "You are a helpful assistant. Use known memories about the user when responding, and do not invent details." + }, + AIContextProviders = [memoryProvider1] + }); + + AIAgent agent2 = this._client!.AsAIAgent(new ChatClientAgentOptions + { + ChatOptions = new ChatOptions + { + ModelId = this._deploymentName!, + Instructions = "You are a helpful assistant. Use known memories about the user when responding, and do not invent details." + }, + AIContextProviders = [memoryProvider2] + }); AgentSession session1 = await agent1.CreateSessionAsync(); AgentSession session2 = await agent2.CreateSessionAsync(); @@ -111,8 +151,25 @@ public sealed class FoundryMemoryProviderTests : IDisposable await memoryProvider1.WhenUpdatesCompletedAsync(); await Task.Delay(2000); - AgentResponse result1 = await agent1.RunAsync("What is your name?", session1); - AgentResponse result2 = await agent2.RunAsync("What is your name?", session2); + // Assert - verify memories were created in scope A but not in scope B + var searchResultA = await this._client!.MemoryStores.SearchMemoriesAsync( + this._memoryStoreName!, + new MemorySearchOptions("it-scope-a") + { + Items = { ResponseItem.CreateUserMessageItem("Caoimhe") } + }); + Assert.NotEmpty(searchResultA.Value.Memories); + + var searchResultB = await this._client.MemoryStores.SearchMemoriesAsync( + this._memoryStoreName!, + new MemorySearchOptions("it-scope-b") + { + Items = { ResponseItem.CreateUserMessageItem("Caoimhe") } + }); + Assert.Empty(searchResultB.Value.Memories); + + AgentResponse result1 = await agent1.RunAsync("What is my name?", session1); + AgentResponse result2 = await agent2.RunAsync("What is my name?", session2); // Assert Assert.Contains("Caoimhe", result1.Text); diff --git a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentChatClientRunStreamingTests.cs b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentChatClientRunStreamingTests.cs similarity index 93% rename from dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentChatClientRunStreamingTests.cs rename to dotnet/tests/Foundry.IntegrationTests/ResponsesAgentChatClientRunStreamingTests.cs index 5895ceb8b9..c07509e04e 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentChatClientRunStreamingTests.cs +++ b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentChatClientRunStreamingTests.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using AgentConformance.IntegrationTests; -namespace AzureAI.IntegrationTests; +namespace Foundry.IntegrationTests; public class ResponsesAgentChatClientRunStreamingTests() : ChatClientAgentRunStreamingTests(() => new()) { diff --git a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentChatClientRunTests.cs b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentChatClientRunTests.cs similarity index 92% rename from dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentChatClientRunTests.cs rename to dotnet/tests/Foundry.IntegrationTests/ResponsesAgentChatClientRunTests.cs index d80b25deb2..100a3c001b 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentChatClientRunTests.cs +++ b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentChatClientRunTests.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using AgentConformance.IntegrationTests; -namespace AzureAI.IntegrationTests; +namespace Foundry.IntegrationTests; public class ResponsesAgentChatClientRunTests() : ChatClientAgentRunTests(() => new()) { diff --git a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentExtensionCreateTests.cs b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentExtensionCreateTests.cs similarity index 79% rename from dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentExtensionCreateTests.cs rename to dotnet/tests/Foundry.IntegrationTests/ResponsesAgentExtensionCreateTests.cs index cd8dc6cb03..af358a9e55 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentExtensionCreateTests.cs +++ b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentExtensionCreateTests.cs @@ -3,13 +3,13 @@ using System; using System.Threading.Tasks; using AgentConformance.IntegrationTests.Support; +using Azure.AI.Extensions.OpenAI; using Azure.AI.Projects; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; using Microsoft.Extensions.AI; using Shared.IntegrationTests; -namespace AzureAI.IntegrationTests; +namespace Foundry.IntegrationTests; /// /// Integration tests for non-versioned creation via extension methods. @@ -30,16 +30,19 @@ public class ResponsesAgentExtensionCreateTests const string AgentDescription = "Integration test agent created from AIProjectClient.AsAIAgent(model, instructions)."; const string VerificationToken = "integration-extension-ok"; - FoundryAgent agent = this._client.AsAIAgent( + ChatClientAgent agent = this._client.AsAIAgent( model: Model, instructions: $"You are a helpful assistant. When asked for verification, reply with exactly '{VerificationToken}'.", name: AgentName, description: AgentDescription); - AgentSession session = await agent.CreateSessionAsync(); + AgentSession? session = null; try { + var conversation = await CreateConversationAsync(this._client); + session = await agent.CreateSessionAsync(conversation.Id); + // Act AgentResponse response = await agent.RunAsync("Return the verification token.", session); @@ -47,7 +50,6 @@ public class ResponsesAgentExtensionCreateTests Assert.NotNull(agent); Assert.Equal(AgentName, agent.Name); Assert.Equal(AgentDescription, agent.Description); - Assert.Same(this._client, agent.GetService()); Assert.NotNull(agent.GetService()); Assert.Contains(VerificationToken, response.Text, StringComparison.OrdinalIgnoreCase); } @@ -73,19 +75,22 @@ public class ResponsesAgentExtensionCreateTests }, }; - FoundryAgent agent = this._client.AsAIAgent(options); - ChatClientAgentSession session = await agent.CreateConversationSessionAsync(); + ChatClientAgent agent = this._client.AsAIAgent(options); + + ChatClientAgentSession? session = null; try { + var conversation = await CreateConversationAsync(this._client); + session = ((await agent.CreateSessionAsync(conversation.Id)) as ChatClientAgentSession)!; + // Act AgentResponse response = await agent.RunAsync("Return the verification token.", session); // Assert - Assert.StartsWith("conv_", session.ConversationId, StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("conv_", session!.ConversationId, StringComparison.OrdinalIgnoreCase); Assert.Equal(options.Name, agent.Name); Assert.Equal(options.Description, agent.Description); - Assert.Same(this._client, agent.GetService()); Assert.Contains(VerificationToken, response.Text, StringComparison.OrdinalIgnoreCase); } finally @@ -94,8 +99,13 @@ public class ResponsesAgentExtensionCreateTests } } - private static async Task DeleteSessionAsync(AIProjectClient client, AgentSession session) + private static async Task DeleteSessionAsync(AIProjectClient client, AgentSession? session) { + if (session is null) + { + return; + } + ChatClientAgentSession typedSession = (ChatClientAgentSession)session; if (typedSession.ConversationId?.StartsWith("conv_", StringComparison.OrdinalIgnoreCase) == true) @@ -119,4 +129,10 @@ public class ResponsesAgentExtensionCreateTests await DeleteResponseChainAsync(client, response.Value.PreviousResponseId); } } + + private static async Task CreateConversationAsync(AIProjectClient client) + { + ProjectConversationsClient conversationsClient = client.GetProjectOpenAIClient().GetProjectConversationsClient(); + return (await conversationsClient.CreateProjectConversationAsync()).Value!; + } } diff --git a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentFixture.cs b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentFixture.cs similarity index 98% rename from dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentFixture.cs rename to dotnet/tests/Foundry.IntegrationTests/ResponsesAgentFixture.cs index ec678a00d9..7bd0da0d95 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentFixture.cs +++ b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentFixture.cs @@ -9,19 +9,18 @@ using AgentConformance.IntegrationTests.Support; using Azure.AI.Extensions.OpenAI; using Azure.AI.Projects; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.AzureAI; using Microsoft.Extensions.AI; using OpenAI.Responses; using Shared.IntegrationTests; -namespace AzureAI.IntegrationTests; +namespace Foundry.IntegrationTests; /// /// Integration test fixture that creates non-versioned Responses agents via the direct AIProjectClient.AsAIAgent(...) path. /// public class ResponsesAgentFixture : IChatClientAgentFixture { - private FoundryAgent _agent = null!; + private ChatClientAgent _agent = null!; private AIProjectClient _client = null!; public IChatClient ChatClient => this._agent.GetService()!.ChatClient; diff --git a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentRunStreamingTests.cs b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentRunStreamingTests.cs similarity index 96% rename from dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentRunStreamingTests.cs rename to dotnet/tests/Foundry.IntegrationTests/ResponsesAgentRunStreamingTests.cs index 5f21c316c4..09f5fe6b2e 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentRunStreamingTests.cs +++ b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentRunStreamingTests.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using AgentConformance.IntegrationTests; using Microsoft.Agents.AI; -namespace AzureAI.IntegrationTests; +namespace Foundry.IntegrationTests; public class ResponsesAgentRunStreamingPreviousResponseTests() : RunStreamingTests(() => new()) { diff --git a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentRunTests.cs b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentRunTests.cs similarity index 96% rename from dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentRunTests.cs rename to dotnet/tests/Foundry.IntegrationTests/ResponsesAgentRunTests.cs index 71460f7737..0635b0f4ac 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentRunTests.cs +++ b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentRunTests.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using AgentConformance.IntegrationTests; using Microsoft.Agents.AI; -namespace AzureAI.IntegrationTests; +namespace Foundry.IntegrationTests; public class ResponsesAgentRunPreviousResponseTests() : RunTests(() => new()) { diff --git a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentStructuredOutputRunTests.cs b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentStructuredOutputRunTests.cs similarity index 99% rename from dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentStructuredOutputRunTests.cs rename to dotnet/tests/Foundry.IntegrationTests/ResponsesAgentStructuredOutputRunTests.cs index 19ebdb4e28..a561fbae1b 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/ResponsesAgentStructuredOutputRunTests.cs +++ b/dotnet/tests/Foundry.IntegrationTests/ResponsesAgentStructuredOutputRunTests.cs @@ -7,7 +7,7 @@ using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Shared.IntegrationTests; -namespace AzureAI.IntegrationTests; +namespace Foundry.IntegrationTests; public class ResponsesAgentStructuredOutputRunTests() : StructuredOutputRunTests>(() => new()) { diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs deleted file mode 100644 index 5c954d30e8..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs +++ /dev/null @@ -1,3472 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ClientModel; -using System.ClientModel.Primitives; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.Extensions.OpenAI; -using Azure.AI.Projects; -using Azure.AI.Projects.Agents; -using Microsoft.Extensions.AI; -using Moq; -using OpenAI.Responses; - -namespace Microsoft.Agents.AI.AzureAI.UnitTests; - -#pragma warning disable CS0618 -/// -/// Unit tests for the class. -/// -[Obsolete("Includes coverage for obsolete AIProjectClient compatibility extension methods.")] -public sealed class AzureAIProjectChatClientExtensionsTests -{ - #region AsAIAgent(AIProjectClient, model, instructions) Tests - - /// - /// Verify that the non-versioned AsAIAgent overload throws ArgumentNullException when AIProjectClient is null. - /// - [Fact] - public void AsAIAgent_WithModelAndInstructions_WithNullClient_ThrowsArgumentNullException() - { - // Arrange - AIProjectClient? client = null; - - // Act & Assert - ArgumentNullException exception = Assert.Throws(() => - client!.AsAIAgent("gpt-4o-mini", "You are helpful.")); - - Assert.Equal("aiProjectClient", exception.ParamName); - } - - /// - /// Verify that the non-versioned AsAIAgent overload creates a valid ChatClientAgent. - /// - [Fact] - public void AsAIAgent_WithModelAndInstructions_CreatesChatClientAgent() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - List tools = - [ - AIFunctionFactory.Create(() => "test", "test_function", "A test function") - ]; - - // Act - FoundryAgent agent = client.AsAIAgent( - "gpt-4o-mini", - "You are helpful.", - name: "test-agent", - description: "A test agent", - tools: tools); - - // Assert - Assert.NotNull(agent); - Assert.Equal("test-agent", agent.Name); - Assert.Equal("A test agent", agent.Description); - Assert.Same(client, agent.GetService()); - Assert.NotNull(agent.GetService()); - } - - /// - /// Verify that the non-versioned AsAIAgent overload applies the clientFactory. - /// - [Fact] - public void AsAIAgent_WithModelAndInstructions_WithClientFactory_AppliesFactoryCorrectly() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - TestChatClient? testChatClient = null; - - // Act - FoundryAgent agent = client.AsAIAgent( - "gpt-4o-mini", - "You are helpful.", - clientFactory: innerClient => testChatClient = new TestChatClient(innerClient)); - - // Assert - Assert.NotNull(agent); - TestChatClient? retrievedTestClient = agent.GetService(); - Assert.NotNull(retrievedTestClient); - Assert.Same(testChatClient, retrievedTestClient); - } - - /// - /// Verify that the options-based non-versioned AsAIAgent overload creates a valid ChatClientAgent. - /// - [Fact] - public void AsAIAgent_WithOptions_CreatesChatClientAgent() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - ChatClientAgentOptions options = new() - { - Name = "options-agent", - Description = "Agent from options", - ChatOptions = new ChatOptions - { - ModelId = "gpt-4o-mini", - Instructions = "You are helpful.", - }, - }; - - // Act - FoundryAgent agent = client.AsAIAgent(options); - - // Assert - Assert.NotNull(agent); - Assert.Equal("options-agent", agent.Name); - Assert.Equal("Agent from options", agent.Description); - Assert.Same(client, agent.GetService()); - } - - /// - /// Verify that the non-versioned AsAIAgent overload adds the MEAI user-agent header to Responses API requests. - /// - [Fact] - public async Task AsAIAgent_WithModelAndInstructions_UserAgentHeaderAddedToResponsesRequestsAsync() - { - // Arrange - bool userAgentFound = false; - using HttpHandlerAssert httpHandler = new(request => - { - if (request.Headers.TryGetValues("User-Agent", out IEnumerable? values)) - { - foreach (string value in values) - { - if (value.Contains("MEAI")) - { - userAgentFound = true; - } - } - } - - if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses")) - { - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent( - TestDataUtil.GetOpenAIDefaultResponseJson(), - Encoding.UTF8, - "application/json") - }; - } - - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("{}", Encoding.UTF8, "application/json") - }; - }); - -#pragma warning disable CA5399 - using HttpClient httpClient = new(httpHandler); -#pragma warning restore CA5399 - - AIProjectClient aiProjectClient = new( - new Uri("https://test.openai.azure.com/"), - new FakeAuthenticationTokenProvider(), - new() { Transport = new HttpClientPipelineTransport(httpClient) }); - - FoundryAgent agent = aiProjectClient.AsAIAgent( - "gpt-4o-mini", - "You are helpful."); - - // Act - AgentSession session = await agent.CreateSessionAsync(); - await agent.RunAsync("Hello", session); - - // Assert - Assert.True(userAgentFound, "MEAI user-agent header was not found in any request"); - } - - #endregion - - #region AsAIAgent(AIProjectClient, AgentRecord) Tests - - /// - /// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null. - /// - [Fact] - public void AsAIAgent_WithAgentRecord_WithNullClient_ThrowsArgumentNullException() - { - // Arrange - AIProjectClient? client = null; - AgentRecord agentRecord = this.CreateTestAgentRecord(); - - // Act & Assert - var exception = Assert.Throws(() => - client!.AsAIAgent(agentRecord)); - - Assert.Equal("aiProjectClient", exception.ParamName); - } - - /// - /// Verify that AsAIAgent throws ArgumentNullException when agentRecord is null. - /// - [Fact] - public void AsAIAgent_WithAgentRecord_WithNullAgentRecord_ThrowsArgumentNullException() - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - var exception = Assert.Throws(() => - mockClient.Object.AsAIAgent((AgentRecord)null!)); - - Assert.Equal("agentRecord", exception.ParamName); - } - - /// - /// Verify that AsAIAgent with AgentRecord creates a valid agent. - /// - [Fact] - public void AsAIAgent_WithAgentRecord_CreatesValidAgent() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentRecord agentRecord = this.CreateTestAgentRecord(); - - // Act - var agent = client.AsAIAgent(agentRecord); - - // Assert - Assert.NotNull(agent); - Assert.Equal("agent_abc123", agent.Name); - } - - /// - /// Verify that AsAIAgent with AgentRecord and clientFactory applies the factory. - /// - [Fact] - public void AsAIAgent_WithAgentRecord_WithClientFactory_AppliesFactoryCorrectly() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentRecord agentRecord = this.CreateTestAgentRecord(); - TestChatClient? testChatClient = null; - - // Act - var agent = client.AsAIAgent( - agentRecord, - clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient)); - - // Assert - Assert.NotNull(agent); - var retrievedTestClient = agent.GetService(); - Assert.NotNull(retrievedTestClient); - Assert.Same(testChatClient, retrievedTestClient); - } - - #endregion - - #region AsAIAgent(AIProjectClient, AgentVersion) Tests - - /// - /// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null. - /// - [Fact] - public void AsAIAgent_WithAgentVersion_WithNullClient_ThrowsArgumentNullException() - { - // Arrange - AIProjectClient? client = null; - AgentVersion agentVersion = this.CreateTestAgentVersion(); - - // Act & Assert - var exception = Assert.Throws(() => - client!.AsAIAgent(agentVersion)); - - Assert.Equal("aiProjectClient", exception.ParamName); - } - - /// - /// Verify that AsAIAgent throws ArgumentNullException when agentVersion is null. - /// - [Fact] - public void AsAIAgent_WithAgentVersion_WithNullAgentVersion_ThrowsArgumentNullException() - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - var exception = Assert.Throws(() => - mockClient.Object.AsAIAgent((AgentVersion)null!)); - - Assert.Equal("agentVersion", exception.ParamName); - } - - /// - /// Verify that AsAIAgent with AgentVersion creates a valid agent. - /// - [Fact] - public void AsAIAgent_WithAgentVersion_CreatesValidAgent() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentVersion agentVersion = this.CreateTestAgentVersion(); - - // Act - var agent = client.AsAIAgent(agentVersion); - - // Assert - Assert.NotNull(agent); - Assert.Equal("agent_abc123", agent.Name); - } - - /// - /// Verify that AsAIAgent with AgentVersion and clientFactory applies the factory. - /// - [Fact] - public void AsAIAgent_WithAgentVersion_WithClientFactory_AppliesFactoryCorrectly() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentVersion agentVersion = this.CreateTestAgentVersion(); - TestChatClient? testChatClient = null; - - // Act - var agent = client.AsAIAgent( - agentVersion, - clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient)); - - // Assert - Assert.NotNull(agent); - var retrievedTestClient = agent.GetService(); - Assert.NotNull(retrievedTestClient); - Assert.Same(testChatClient, retrievedTestClient); - } - - /// - /// Verify that AsAIAgent with requireInvocableTools=true enforces invocable tools. - /// - [Fact] - public void AsAIAgent_WithAgentVersion_WithRequireInvocableToolsTrue_EnforcesInvocableTools() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentVersion agentVersion = this.CreateTestAgentVersion(); - var tools = new List - { - AIFunctionFactory.Create(() => "test", "test_function", "A test function") - }; - - // Act - var agent = client.AsAIAgent(agentVersion, tools: tools); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that AsAIAgent with requireInvocableTools=false allows declarative functions. - /// - [Fact] - public void AsAIAgent_WithAgentVersion_WithRequireInvocableToolsFalse_AllowsDeclarativeFunctions() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentVersion agentVersion = this.CreateTestAgentVersion(); - - // Act - should not throw even without tools when requireInvocableTools is false - var agent = client.AsAIAgent(agentVersion); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - #endregion - - #region GetAIAgentAsync(AIProjectClient, ChatClientAgentOptions) Tests - - /// - /// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentNullException when client is null. - /// - [Fact] - public async Task GetAIAgentAsync_WithOptions_WithNullClient_ThrowsArgumentNullExceptionAsync() - { - // Arrange - AIProjectClient? client = null; - var options = new ChatClientAgentOptions { Name = "test-agent" }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - client!.GetAIAgentAsync(options)); - - Assert.Equal("aiProjectClient", exception.ParamName); - } - - /// - /// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentNullException when options is null. - /// - [Fact] - public async Task GetAIAgentAsync_WithOptions_WithNullOptions_ThrowsArgumentNullExceptionAsync() - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - mockClient.Object.GetAIAgentAsync((ChatClientAgentOptions)null!)); - - Assert.Equal("options", exception.ParamName); - } - - /// - /// Verify that GetAIAgentAsync with ChatClientAgentOptions creates a valid agent. - /// - [Fact] - public async Task GetAIAgentAsync_WithOptions_CreatesValidAgentAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent"); - var options = new ChatClientAgentOptions { Name = "test-agent" }; - - // Act - var agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - Assert.Equal("test-agent", agent.Name); - } - - #endregion - - #region AsAIAgent(AIProjectClient, string) Tests - - /// - /// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null. - /// - [Fact] - public void AsAIAgent_ByName_WithNullClient_ThrowsArgumentNullException() - { - // Arrange - AIProjectClient? client = null; - - // Act & Assert - var exception = Assert.Throws(() => - client!.AsAIAgent("test-agent")); - - Assert.Equal("aiProjectClient", exception.ParamName); - } - - /// - /// Verify that AsAIAgent throws ArgumentNullException when name is null. - /// - [Fact] - public void AsAIAgent_ByName_WithNullName_ThrowsArgumentNullException() - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - var exception = Assert.Throws(() => - mockClient.Object.AsAIAgent((string)null!)); - - Assert.Equal("name", exception.ParamName); - } - - /// - /// Verify that AsAIAgent throws ArgumentException when name is empty. - /// - [Fact] - public void AsAIAgent_ByName_WithEmptyName_ThrowsArgumentException() - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - var exception = Assert.Throws(() => - mockClient.Object.AsAIAgent(string.Empty)); - - Assert.Equal("name", exception.ParamName); - } - - #endregion - - #region GetAIAgentAsync(AIProjectClient, string) Tests - - /// - /// Verify that GetAIAgentAsync throws ArgumentNullException when AIProjectClient is null. - /// - [Fact] - public async Task GetAIAgentAsync_ByName_WithNullClient_ThrowsArgumentNullExceptionAsync() - { - // Arrange - AIProjectClient? client = null; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - client!.GetAIAgentAsync("test-agent")); - - Assert.Equal("aiProjectClient", exception.ParamName); - } - - /// - /// Verify that GetAIAgentAsync throws ArgumentNullException when name is null. - /// - [Fact] - public async Task GetAIAgentAsync_ByName_WithNullName_ThrowsArgumentNullExceptionAsync() - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - mockClient.Object.GetAIAgentAsync(name: null!)); - - Assert.Equal("name", exception.ParamName); - } - - /// - /// Verify that GetAIAgentAsync throws InvalidOperationException when agent is not found. - /// - [Fact] - public async Task GetAIAgentAsync_ByName_WithNonExistentAgent_ThrowsInvalidOperationExceptionAsync() - { - // Arrange - var mockAgentOperations = new Mock(); - mockAgentOperations - .Setup(c => c.GetAgentAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(ClientResult.FromOptionalValue((AgentRecord)null!, new MockPipelineResponse(200, BinaryData.FromString("null")))); - - var mockClient = new Mock(); - mockClient.SetupGet(c => c.Agents).Returns(mockAgentOperations.Object); - mockClient.Setup(x => x.GetConnection(It.IsAny())).Returns(new ClientConnection("fake-connection-id", "http://localhost", ClientPipeline.Create(), CredentialKind.None)); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - mockClient.Object.GetAIAgentAsync("non-existent-agent")); - - Assert.Contains("not found", exception.Message); - } - - #endregion - - #region AsAIAgent(AIProjectClient, AgentRecord) with tools Tests - - /// - /// Verify that AsAIAgent with additional tools when the definition has no tools does not throw and results in an agent with no tools. - /// - [Fact] - public void AsAIAgent_WithAgentRecordAndAdditionalTools_WhenDefinitionHasNoTools_ShouldNotThrow() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentRecord agentRecord = this.CreateTestAgentRecord(); - var tools = new List - { - AIFunctionFactory.Create(() => "test", "test_function", "A test function") - }; - - // Act - var agent = client.AsAIAgent(agentRecord, tools: tools); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - var chatClient = agent.GetService(); - Assert.NotNull(chatClient); - var agentVersion = chatClient.GetService(); - Assert.NotNull(agentVersion); - var definition = Assert.IsType(agentVersion.Definition); - Assert.Empty(definition.Tools); - } - - /// - /// Verify that AsAIAgent with null tools works correctly. - /// - [Fact] - public void AsAIAgent_WithAgentRecordAndNullTools_WorksCorrectly() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentRecord agentRecord = this.CreateTestAgentRecord(); - - // Act - var agent = client.AsAIAgent(agentRecord, tools: null); - - // Assert - Assert.NotNull(agent); - Assert.Equal("agent_abc123", agent.Name); - } - - #endregion - - #region GetAIAgentAsync(AIProjectClient, string) with tools Tests - - /// - /// Verify that GetAIAgentAsync with tools parameter creates an agent. - /// - [Fact] - public async Task GetAIAgentAsync_WithNameAndTools_CreatesAgentAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var tools = new List - { - AIFunctionFactory.Create(() => "test", "test_function", "A test function") - }; - - // Act - var agent = await client.GetAIAgentAsync("test-agent", tools: tools); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgentAsync with model and options creates a valid agent. - /// - [Fact] - public async Task CreateAIAgentAsync_WithModelAndOptions_CreatesValidAgentAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", instructions: "Test instructions"); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new() { Instructions = "Test instructions" } - }; - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - Assert.Equal("test-agent", agent.Name); - Assert.Equal("Test instructions", agent.GetService()!.Instructions); - } - - /// - /// Verify that CreateAIAgentAsync with model and options and clientFactory applies the factory. - /// - [Fact] - public async Task CreateAIAgentAsync_WithModelAndOptions_WithClientFactory_AppliesFactoryCorrectlyAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", instructions: "Test instructions"); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new() { Instructions = "Test instructions" } - }; - TestChatClient? testChatClient = null; - - // Act - var agent = await testClient.Client.CreateAIAgentAsync( - "test-model", - options, - clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient)); - - // Assert - Assert.NotNull(agent); - var retrievedTestClient = agent.GetService(); - Assert.NotNull(retrievedTestClient); - Assert.Same(testChatClient, retrievedTestClient); - } - - #endregion - - #region CreateAIAgentAsync(AIProjectClient, string, AgentDefinition) Tests - - /// - /// Verify that CreateAIAgentAsync throws ArgumentNullException when AIProjectClient is null. - /// - [Fact] - public async Task CreateAIAgentAsync_WithAgentDefinition_WithNullClient_ThrowsArgumentNullExceptionAsync() - { - // Arrange - AIProjectClient? client = null; - var definition = new PromptAgentDefinition("test-model"); - var options = new AgentVersionCreationOptions(definition); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - client!.CreateAIAgentAsync("agent-name", options)); - - Assert.Equal("aiProjectClient", exception.ParamName); - } - - /// - /// Verify that CreateAIAgentAsync throws ArgumentNullException when creationOptions is null. - /// - [Fact] - public async Task CreateAIAgentAsync_WithAgentDefinition_WithNullDefinition_ThrowsArgumentNullExceptionAsync() - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - mockClient.Object.CreateAIAgentAsync(name: "agent-name", null!)); - - Assert.Equal("creationOptions", exception.ParamName); - } - - #endregion - - #region Tool Validation Tests - - /// - /// Verify that CreateAIAgent creates an agent successfully. - /// - [Fact] - public async Task CreateAIAgentAsync_WithDefinition_CreatesAgentSuccessfullyAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" }; - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgent without tools parameter creates an agent successfully. - /// - [Fact] - public async Task CreateAIAgentAsync_WithoutToolsParameter_CreatesAgentSuccessfullyAsync() - { - // Arrange - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" }; - - var definitionResponse = GeneratePromptDefinitionResponse(definition, null); - using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse); - - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgent without tools in definition creates an agent successfully. - /// - [Fact] - public async Task CreateAIAgentAsync_WithoutToolsInDefinition_CreatesAgentSuccessfullyAsync() - { - // Arrange - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" }; - using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definition); - - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgent uses tools from the definition when no separate tools parameter is provided. - /// - [Fact] - public async Task CreateAIAgentAsync_WithDefinitionTools_UsesDefinitionToolsAsync() - { - // Arrange - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" }; - - // Add a function tool to the definition - definition.Tools.Add(ResponseTool.CreateFunctionTool("required_tool", BinaryData.FromString("{}"), strictModeEnabled: false)); - - // Create a response definition with the same tool - var definitionResponse = GeneratePromptDefinitionResponse(definition, definition.Tools.Select(t => t.AsAITool()).ToList()); - using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse); - - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - var agentVersion = agent.GetService(); - Assert.NotNull(agentVersion); - if (agentVersion.Definition is PromptAgentDefinition promptDef) - { - Assert.NotEmpty(promptDef.Tools); - Assert.Single(promptDef.Tools); - Assert.Equal("required_tool", (promptDef.Tools.First() as FunctionTool)?.FunctionName); - } - } - - /// - /// Verify that CreateAIAgent creates an agent successfully when definition has a mix of custom and hosted tools. - /// - [Fact] - public async Task CreateAIAgentAsync_WithMixedToolsInDefinition_CreatesAgentSuccessfullyAsync() - { - // Arrange - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" }; - definition.Tools.Add(ResponseTool.CreateFunctionTool("create_tool", BinaryData.FromString("{}"), strictModeEnabled: false)); - definition.Tools.Add(new HostedWebSearchTool().GetService() ?? new HostedWebSearchTool().AsOpenAIResponseTool()); - definition.Tools.Add(new HostedFileSearchTool().GetService() ?? new HostedFileSearchTool().AsOpenAIResponseTool()); - - // Simulate agent definition response with the tools - var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" }; - foreach (var tool in definition.Tools) - { - definitionResponse.Tools.Add(tool); - } - - using var testClient = CreateTestAgentClientWithHandler(agentDefinitionResponse: definitionResponse); - - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - var agentVersion = agent.GetService(); - Assert.NotNull(agentVersion); - if (agentVersion.Definition is PromptAgentDefinition promptDef) - { - Assert.NotEmpty(promptDef.Tools); - Assert.Equal(3, promptDef.Tools.Count); - } - } - - /// - /// Verify that CreateAIAgentAsync when AI Tools are provided, uses them for the definition via http request. - /// - [Fact] - public async Task CreateAIAgentAsync_WithNameAndAITools_SendsToolDefinitionViaHttpAsync() - { - // Arrange - using var httpHandler = new HttpHandlerAssert(async (request) => - { - if (request.Content is not null) - { - var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false); - - Assert.Contains("required_tool", requestBody); - } - - return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json") }; - }); - -#pragma warning disable CA5399 - using var httpClient = new HttpClient(httpHandler); -#pragma warning restore CA5399 - - var client = new AIProjectClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) }); - - // Act - var agent = await client.CreateAIAgentAsync( - name: "test-agent", - model: "test-model", - instructions: "Test", - tools: [AIFunctionFactory.Create(() => true, "required_tool")]); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - var agentVersion = agent.GetService(); - Assert.NotNull(agentVersion); - Assert.IsType(agentVersion.Definition); - } - - /// - /// Verify that when providing AITools with AsAIAgent, any additional tool that doesn't match the tools in agent definition are ignored. - /// - [Fact] - public void AsAIAgent_AdditionalAITools_WhenNotInTheDefinitionAreIgnored() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var agentVersion = this.CreateTestAgentVersion(); - - // Manually add tools to the definition to simulate inline tools - if (agentVersion.Definition is PromptAgentDefinition promptDef) - { - promptDef.Tools.Add(ResponseTool.CreateFunctionTool("inline_tool", BinaryData.FromString("{}"), strictModeEnabled: false)); - } - - var invocableInlineAITool = AIFunctionFactory.Create(() => "test", "inline_tool", "An invocable AIFunction for the inline function"); - var shouldBeIgnoredTool = AIFunctionFactory.Create(() => "test", "additional_tool", "An additional test function that should be ignored"); - - // Act & Assert - var agent = client.AsAIAgent(agentVersion, tools: [invocableInlineAITool, shouldBeIgnoredTool]); - Assert.NotNull(agent); - var version = agent.GetService(); - Assert.NotNull(version); - var definition = Assert.IsType(version.Definition); - Assert.NotEmpty(definition.Tools); - Assert.NotNull(GetAgentChatOptions(agent)); - Assert.NotNull(GetAgentChatOptions(agent)!.Tools); - Assert.Single(GetAgentChatOptions(agent)!.Tools!); - Assert.Equal("inline_tool", (definition.Tools.First() as FunctionTool)?.FunctionName); - } - - #endregion - - #region Inline Tools vs Parameter Tools Tests - - /// - /// Verify that tools passed as parameters are accepted by AsAIAgent. - /// - [Fact] - public void AsAIAgent_WithParameterTools_AcceptsTools() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentRecord agentRecord = this.CreateTestAgentRecord(); - var tools = new List - { - AIFunctionFactory.Create(() => "tool1", "param_tool_1", "First parameter tool"), - AIFunctionFactory.Create(() => "tool2", "param_tool_2", "Second parameter tool") - }; - - // Act - var agent = client.AsAIAgent(agentRecord, tools: tools); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - var chatClient = agent.GetService(); - Assert.NotNull(chatClient); - var agentVersion = chatClient.GetService(); - Assert.NotNull(agentVersion); - } - - /// - /// Verify that CreateAIAgent with string parameters and tools creates an agent. - /// - [Fact] - public async Task CreateAIAgentAsync_WithStringParamsAndTools_CreatesAgentAsync() - { - // Arrange - var tools = new List - { - AIFunctionFactory.Create(() => "weather", "string_param_tool", "Tool from string params") - }; - - var definitionResponse = GeneratePromptDefinitionResponse(new PromptAgentDefinition("test-model") { Instructions = "Test instructions" }, tools); - - using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync( - "test-agent", - "test-model", - "Test instructions", - tools: tools); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - var agentVersion = agent.GetService(); - Assert.NotNull(agentVersion); - if (agentVersion.Definition is PromptAgentDefinition promptDef) - { - Assert.NotEmpty(promptDef.Tools); - Assert.Single(promptDef.Tools); - } - } - - /// - /// Verify that CreateAIAgentAsync with tools in definition creates an agent. - /// - [Fact] - public async Task CreateAIAgentAsync_WithDefinitionTools_CreatesAgentAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" }; - definition.Tools.Add(ResponseTool.CreateFunctionTool("async_tool", BinaryData.FromString("{}"), strictModeEnabled: false)); - - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that GetAIAgentAsync with tools parameter creates an agent. - /// - [Fact] - public async Task GetAIAgentAsync_WithToolsParameter_CreatesAgentAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var tools = new List - { - AIFunctionFactory.Create(() => "async_get_result", "async_get_tool", "An async get tool") - }; - - // Act - var agent = await client.GetAIAgentAsync("test-agent", tools: tools); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - #endregion - - #region Declarative Function Handling Tests - - /// - /// Verifies that CreateAIAgent uses tools from definition when they are ResponseTool instances, resulting in successful agent creation. - /// - [Fact] - public async Task CreateAIAgentAsync_WithResponseToolsInDefinition_CreatesAgentSuccessfullyAsync() - { - // Arrange - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" }; - - var fabricToolOptions = new FabricDataAgentToolOptions(); - fabricToolOptions.ProjectConnections.Add(new ToolProjectConnection("connection-id")); - - var sharepointOptions = new SharePointGroundingToolOptions(); - sharepointOptions.ProjectConnections.Add(new ToolProjectConnection("connection-id")); - - var structuredOutputs = new StructuredOutputDefinition("name", "description", new Dictionary { ["schema"] = BinaryData.FromString(AIJsonUtilities.CreateJsonSchema(new { id = "test" }.GetType()).ToString()) }, false); - - // Add tools to the definition - definition.Tools.Add(ResponseTool.CreateFunctionTool("create_tool", BinaryData.FromString("{}"), strictModeEnabled: false)); - definition.Tools.Add((ResponseTool)AgentTool.CreateBingCustomSearchTool(new BingCustomSearchToolOptions([new BingCustomSearchConfiguration("connection-id", "instance-name")]))); - definition.Tools.Add((ResponseTool)AgentTool.CreateBrowserAutomationTool(new BrowserAutomationToolOptions(new BrowserAutomationToolConnectionParameters("id")))); - definition.Tools.Add(AgentTool.CreateA2ATool(new Uri("https://test-uri.microsoft.com"))); - definition.Tools.Add((ResponseTool)AgentTool.CreateBingGroundingTool(new BingGroundingSearchToolOptions([new BingGroundingSearchConfiguration("connection-id")]))); - definition.Tools.Add((ResponseTool)AgentTool.CreateMicrosoftFabricTool(fabricToolOptions)); - definition.Tools.Add((ResponseTool)AgentTool.CreateOpenApiTool(new OpenApiFunctionDefinition("name", BinaryData.FromString(OpenAPISpec), new OpenAPIAnonymousAuthenticationDetails()))); - definition.Tools.Add((ResponseTool)AgentTool.CreateSharepointTool(sharepointOptions)); - definition.Tools.Add((ResponseTool)AgentTool.CreateStructuredOutputsTool(structuredOutputs)); - definition.Tools.Add((ResponseTool)AgentTool.CreateAzureAISearchTool(new AzureAISearchToolOptions([new AzureAISearchToolIndex() { IndexName = "name" }]))); - - // Generate agent definition response with the tools - var definitionResponse = GeneratePromptDefinitionResponse(definition, definition.Tools.Select(t => t.AsAITool()).ToList()); - - using var testClient = CreateTestAgentClientWithHandler(agentDefinitionResponse: definitionResponse); - - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - var agentVersion = agent.GetService(); - Assert.NotNull(agentVersion); - if (agentVersion.Definition is PromptAgentDefinition promptDef) - { - Assert.NotEmpty(promptDef.Tools); - Assert.Equal(10, promptDef.Tools.Count); - } - } - - /// - /// Verify that CreateAIAgentAsync accepts FunctionTools from definition. - /// - [Fact] - public async Task CreateAIAgentAsync_WithFunctionToolsInDefinition_AcceptsDeclarativeFunctionAsync() - { - // Arrange - var functionTool = ResponseTool.CreateFunctionTool( - functionName: "get_user_name", - functionParameters: BinaryData.FromString("{}"), - strictModeEnabled: false, - functionDescription: "Gets the user's name, as used for friendly address." - ); - - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" }; - definition.Tools.Add(functionTool); - - // Generate response with the declarative function - var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test" }; - definitionResponse.Tools.Add(functionTool); - - using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse); - - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgentAsync accepts declarative functions from definition. - /// - [Fact] - public async Task CreateAIAgentAsync_WithDeclarativeFunctionFromDefinition_AcceptsDeclarativeFunctionAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" }; - - // Create a declarative function (not invocable) using AIFunctionFactory.CreateDeclaration - using var doc = JsonDocument.Parse("{}"); - var declarativeFunction = AIFunctionFactory.CreateDeclaration("test_function", "A test function", doc.RootElement); - - // Add to definition - definition.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException()); - - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgentAsync accepts declarative functions from definition. - /// - [Fact] - public async Task CreateAIAgentAsync_WithDeclarativeFunctionInDefinition_AcceptsDeclarativeFunctionAsync() - { - // Arrange - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" }; - - // Create a declarative function (not invocable) using AIFunctionFactory.CreateDeclaration - using var doc = JsonDocument.Parse("{}"); - var declarativeFunction = AIFunctionFactory.CreateDeclaration("test_function", "A test function", doc.RootElement); - - // Add to definition - definition.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException()); - - // Generate response with the declarative function - var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test" }; - definitionResponse.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException()); - - using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse); - - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - #endregion - - #region Options Generation Validation Tests - - /// - /// Verify that ChatClientAgentOptions are generated correctly without tools. - /// - [Fact] - public async Task CreateAIAgentAsync_GeneratesCorrectChatClientAgentOptionsAsync() - { - // Arrange - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" }; - - var definitionResponse = GeneratePromptDefinitionResponse(definition, null); - using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse); - - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options); - - // Assert - Assert.NotNull(agent); - var agentVersion = agent.GetService(); - Assert.NotNull(agentVersion); - Assert.Equal("test-agent", agentVersion.Name); - Assert.Equal("Test instructions", (agentVersion.Definition as PromptAgentDefinition)?.Instructions); - } - - /// - /// Verify that GetAIAgentAsync with options preserves custom properties from input options. - /// - [Fact] - public async Task GetAIAgentAsync_WithOptions_PreservesCustomPropertiesAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", instructions: "Custom instructions", description: "Custom description"); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - Description = "Custom description", - ChatOptions = new ChatOptions { Instructions = "Custom instructions" } - }; - - // Act - var agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - Assert.Equal("test-agent", agent.Name); - Assert.Equal("Custom instructions", agent.GetService()!.Instructions); - Assert.Equal("Custom description", agent.Description); - } - - /// - /// Verify that CreateAIAgentAsync with options and tools generates correct ChatClientAgentOptions. - /// - [Fact] - public async Task CreateAIAgentAsync_WithOptionsAndTools_GeneratesCorrectOptionsAsync() - { - // Arrange - var tools = new List - { - AIFunctionFactory.Create(() => "result", "option_tool", "A tool from options") - }; - - var definitionResponse = GeneratePromptDefinitionResponse( - new PromptAgentDefinition("test-model") { Instructions = "Test" }, - tools); - - using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse); - - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions { Instructions = "Test", Tools = tools } - }; - - // Act - var agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - var agentVersion = agent.GetService(); - Assert.NotNull(agentVersion); - if (agentVersion.Definition is PromptAgentDefinition promptDef) - { - Assert.NotEmpty(promptDef.Tools); - Assert.Single(promptDef.Tools); - } - } - - #endregion - - #region AgentName Validation Tests - - /// - /// Verify that AsAIAgent throws ArgumentException when agent name is invalid. - /// - [Theory] - [MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))] - public void AsAIAgent_ByName_WithInvalidAgentName_ThrowsArgumentException(string invalidName) - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - var exception = Assert.Throws(() => - mockClient.Object.AsAIAgent(invalidName)); - - Assert.Equal("name", exception.ParamName); - Assert.Contains("Agent name must be 1-63 characters long", exception.Message); - } - - /// - /// Verify that GetAIAgentAsync throws ArgumentException when agent name is invalid. - /// - [Theory] - [MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))] - public async Task GetAIAgentAsync_ByName_WithInvalidAgentName_ThrowsArgumentExceptionAsync(string invalidName) - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - mockClient.Object.GetAIAgentAsync(invalidName)); - - Assert.Equal("name", exception.ParamName); - Assert.Contains("Agent name must be 1-63 characters long", exception.Message); - } - - /// - /// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentException when agent name is invalid. - /// - [Theory] - [MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))] - public async Task GetAIAgentAsync_WithOptions_WithInvalidAgentName_ThrowsArgumentExceptionAsync(string invalidName) - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var options = new ChatClientAgentOptions { Name = invalidName }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - client.GetAIAgentAsync(options)); - - Assert.Equal("name", exception.ParamName); - Assert.Contains("Agent name must be 1-63 characters long", exception.Message); - } - - /// - /// Verify that CreateAIAgentAsync throws ArgumentException when agent name is invalid. - /// - [Theory] - [MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))] - public async Task CreateAIAgentAsync_WithBasicParams_WithInvalidAgentName_ThrowsArgumentExceptionAsync(string invalidName) - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - mockClient.Object.CreateAIAgentAsync(invalidName, "model", "instructions")); - - Assert.Equal("name", exception.ParamName); - Assert.Contains("Agent name must be 1-63 characters long", exception.Message); - } - - /// - /// Verify that CreateAIAgentAsync with AgentVersionCreationOptions throws ArgumentException when agent name is invalid. - /// - [Theory] - [MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))] - public async Task CreateAIAgentAsync_WithAgentDefinition_WithInvalidAgentName_ThrowsArgumentExceptionAsync(string invalidName) - { - // Arrange - var mockClient = new Mock(); - var definition = new PromptAgentDefinition("test-model"); - var options = new AgentVersionCreationOptions(definition); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - mockClient.Object.CreateAIAgentAsync(invalidName, options)); - - Assert.Equal("name", exception.ParamName); - Assert.Contains("Agent name must be 1-63 characters long", exception.Message); - } - - /// - /// Verify that CreateAIAgentAsync with ChatClientAgentOptions throws ArgumentException when agent name is invalid. - /// - [Theory] - [MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))] - public async Task CreateAIAgentAsync_WithOptions_WithInvalidAgentName_ThrowsArgumentExceptionAsync(string invalidName) - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var options = new ChatClientAgentOptions { Name = invalidName }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - client.CreateAIAgentAsync("test-model", options)); - - Assert.Equal("name", exception.ParamName); - Assert.Contains("Agent name must be 1-63 characters long", exception.Message); - } - - /// - /// Verify that AsAIAgent with AgentReference throws ArgumentException when agent name is invalid. - /// - [Theory] - [MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))] - public void AsAIAgent_WithAgentReference_WithInvalidAgentName_ThrowsArgumentException(string invalidName) - { - // Arrange - var mockClient = new Mock(); - var agentReference = new AgentReference(invalidName, "1"); - - // Act & Assert - var exception = Assert.Throws(() => - mockClient.Object.AsAIAgent(agentReference)); - - Assert.Equal("name", exception.ParamName); - Assert.Contains("Agent name must be 1-63 characters long", exception.Message); - } - - #endregion - - #region AzureAIChatClient Behavior Tests - - /// - /// Verify that the underlying chat client created by extension methods can be wrapped with clientFactory. - /// - [Fact] - public void AsAIAgent_WithClientFactory_WrapsUnderlyingChatClient() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentRecord agentRecord = this.CreateTestAgentRecord(); - int factoryCallCount = 0; - - // Act - var agent = client.AsAIAgent( - agentRecord, - clientFactory: (innerClient) => - { - factoryCallCount++; - return new TestChatClient(innerClient); - }); - - // Assert - Assert.NotNull(agent); - Assert.Equal(1, factoryCallCount); - var wrappedClient = agent.GetService(); - Assert.NotNull(wrappedClient); - } - - /// - /// Verify that clientFactory is called with the correct underlying chat client. - /// - [Fact] - public async Task CreateAIAgentAsync_WithClientFactory_ReceivesCorrectUnderlyingClientAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" }; - IChatClient? receivedClient = null; - - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync( - "test-agent", - options, - clientFactory: (innerClient) => - { - receivedClient = innerClient; - return new TestChatClient(innerClient); - }); - - // Assert - Assert.NotNull(agent); - Assert.NotNull(receivedClient); - var wrappedClient = agent.GetService(); - Assert.NotNull(wrappedClient); - } - - /// - /// Verify that multiple clientFactory calls create independent wrapped clients. - /// - [Fact] - public void AsAIAgent_MultipleCallsWithClientFactory_CreatesIndependentClients() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentRecord agentRecord = this.CreateTestAgentRecord(); - - // Act - var agent1 = client.AsAIAgent( - agentRecord, - clientFactory: (innerClient) => new TestChatClient(innerClient)); - - var agent2 = client.AsAIAgent( - agentRecord, - clientFactory: (innerClient) => new TestChatClient(innerClient)); - - // Assert - Assert.NotNull(agent1); - Assert.NotNull(agent2); - var client1 = agent1.GetService(); - var client2 = agent2.GetService(); - Assert.NotNull(client1); - Assert.NotNull(client2); - Assert.NotSame(client1, client2); - } - - /// - /// Verify that agent created with clientFactory maintains agent properties. - /// - [Fact] - public async Task CreateAIAgentAsync_WithClientFactory_PreservesAgentPropertiesAsync() - { - // Arrange - const string AgentName = "test-agent"; - const string Model = "test-model"; - const string Instructions = "Test instructions"; - using var testClient = CreateTestAgentClientWithHandler(AgentName, Instructions); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync( - AgentName, - Model, - Instructions, - clientFactory: (innerClient) => new TestChatClient(innerClient)); - - // Assert - Assert.NotNull(agent); - Assert.Equal(AgentName, agent.Name); - Assert.Equal(Instructions, agent.GetService()!.Instructions); - var wrappedClient = agent.GetService(); - Assert.NotNull(wrappedClient); - } - - /// - /// Verify that agent created with clientFactory is created successfully. - /// - [Fact] - public async Task CreateAIAgentAsync_WithClientFactory_CreatesAgentSuccessfullyAsync() - { - // Arrange - var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" }; - - var agentDefinitionResponse = GeneratePromptDefinitionResponse(definition, null); - using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: agentDefinitionResponse); - - var options = new AgentVersionCreationOptions(definition); - - // Act - var agent = await testClient.Client.CreateAIAgentAsync( - "test-agent", - options, - clientFactory: (innerClient) => new TestChatClient(innerClient)); - - // Assert - Assert.NotNull(agent); - var wrappedClient = agent.GetService(); - Assert.NotNull(wrappedClient); - var agentVersion = agent.GetService(); - Assert.NotNull(agentVersion); - } - - #endregion - - #region User-Agent Header Tests - - /// - /// Verifies that the MEAI user-agent header is added to CreateAIAgentAsync POST requests - /// via the protocol method's RequestOptions pipeline policy. - /// - [Fact] - public async Task CreateAIAgentAsync_UserAgentHeaderAddedToRequestsAsync() - { - using var httpHandler = new HttpHandlerAssert(request => - { - Assert.Equal("POST", request.Method.Method); - - // Verify MEAI user-agent header is present on CreateAgentVersion POST request - Assert.True(request.Headers.TryGetValues("User-Agent", out var userAgentValues)); - Assert.Contains(userAgentValues, v => v.Contains("MEAI")); - - return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json") }; - }); - -#pragma warning disable CA5399 - using var httpClient = new HttpClient(httpHandler); -#pragma warning restore CA5399 - - // Arrange - var aiProjectClient = new AIProjectClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) }); - - var agentOptions = new ChatClientAgentOptions { Name = "test-agent" }; - - // Act - var agent = await aiProjectClient.CreateAIAgentAsync("test", agentOptions); - - // Assert - Assert.NotNull(agent); - } - - /// - /// Verifies that the user-agent header is added to asynchronous GetAIAgentAsync requests. - /// - [Fact] - public async Task GetAIAgent_UserAgentHeaderAddedToRequestsAsync() - { - using var httpHandler = new HttpHandlerAssert(request => - { - Assert.Equal("GET", request.Method.Method); - Assert.Contains("MEAI", request.Headers.UserAgent.ToString()); - - return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") }; - }); - -#pragma warning disable CA5399 - using var httpClient = new HttpClient(httpHandler); -#pragma warning restore CA5399 - - // Arrange - var aiProjectClient = new AIProjectClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) }); - - // Act - var agent = await aiProjectClient.GetAIAgentAsync("test"); - - // Assert - Assert.NotNull(agent); - } - - #endregion - - #region GetAIAgent(AIProjectClient, AgentReference) Tests - - /// - /// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null. - /// - [Fact] - public void AsAIAgent_WithAgentReference_WithNullClient_ThrowsArgumentNullException() - { - // Arrange - AIProjectClient? client = null; - var agentReference = new AgentReference("test-name", "1"); - - // Act & Assert - var exception = Assert.Throws(() => - client!.AsAIAgent(agentReference)); - - Assert.Equal("aiProjectClient", exception.ParamName); - } - - /// - /// Verify that AsAIAgent throws ArgumentNullException when agentReference is null. - /// - [Fact] - public void AsAIAgent_WithAgentReference_WithNullAgentReference_ThrowsArgumentNullException() - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - var exception = Assert.Throws(() => - mockClient.Object.AsAIAgent((AgentReference)null!)); - - Assert.Equal("agentReference", exception.ParamName); - } - - /// - /// Verify that AsAIAgent with AgentReference creates a valid agent. - /// - [Fact] - public void AsAIAgent_WithAgentReference_CreatesValidAgent() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var agentReference = new AgentReference("test-name", "1"); - - // Act - var agent = client.AsAIAgent(agentReference); - - // Assert - Assert.NotNull(agent); - Assert.Equal("test-name", agent.Name); - Assert.Equal("test-name:1", agent.Id); - } - - /// - /// Verify that AsAIAgent with AgentReference and clientFactory applies the factory. - /// - [Fact] - public void AsAIAgent_WithAgentReference_WithClientFactory_AppliesFactoryCorrectly() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var agentReference = new AgentReference("test-name", "1"); - TestChatClient? testChatClient = null; - - // Act - var agent = client.AsAIAgent( - agentReference, - clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient)); - - // Assert - Assert.NotNull(agent); - var retrievedTestClient = agent.GetService(); - Assert.NotNull(retrievedTestClient); - Assert.Same(testChatClient, retrievedTestClient); - } - - /// - /// Verify that AsAIAgent with AgentReference sets the agent ID correctly. - /// - [Fact] - public void AsAIAgent_WithAgentReference_SetsAgentIdCorrectly() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var agentReference = new AgentReference("test-name", "2"); - - // Act - var agent = client.AsAIAgent(agentReference); - - // Assert - Assert.NotNull(agent); - Assert.Equal("test-name:2", agent.Id); - } - - /// - /// Verify that AsAIAgent with AgentReference and tools includes the tools in ChatOptions. - /// - [Fact] - public void AsAIAgent_WithAgentReference_WithTools_IncludesToolsInChatOptions() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var agentReference = new AgentReference("test-name", "1"); - var tools = new List - { - AIFunctionFactory.Create(() => "test", "test_function", "A test function") - }; - - // Act - var agent = client.AsAIAgent(agentReference, tools: tools); - - // Assert - Assert.NotNull(agent); - var chatOptions = GetAgentChatOptions(agent); - Assert.NotNull(chatOptions); - Assert.NotNull(chatOptions.Tools); - Assert.Single(chatOptions.Tools); - } - - #endregion - - #region GetService Tests - - /// - /// Verify that GetService returns AgentRecord for agents created from AgentRecord. - /// - [Fact] - public void GetService_WithAgentRecord_ReturnsAgentRecord() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentRecord agentRecord = this.CreateTestAgentRecord(); - - // Act - var agent = client.AsAIAgent(agentRecord); - var retrievedRecord = agent.GetService(); - - // Assert - Assert.NotNull(retrievedRecord); - Assert.Equal(agentRecord.Id, retrievedRecord.Id); - } - - /// - /// Verify that GetService returns null for AgentRecord when agent is created from AgentReference. - /// - [Fact] - public void GetService_WithAgentReference_ReturnsNullForAgentRecord() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var agentReference = new AgentReference("test-name", "1"); - - // Act - var agent = client.AsAIAgent(agentReference); - var retrievedRecord = agent.GetService(); - - // Assert - Assert.Null(retrievedRecord); - } - - #endregion - - #region GetService Tests - - /// - /// Verify that GetService returns AgentVersion for agents created from AgentVersion. - /// - [Fact] - public void GetService_WithAgentVersion_ReturnsAgentVersion() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentVersion agentVersion = this.CreateTestAgentVersion(); - - // Act - var agent = client.AsAIAgent(agentVersion); - var retrievedVersion = agent.GetService(); - - // Assert - Assert.NotNull(retrievedVersion); - Assert.Equal(agentVersion.Id, retrievedVersion.Id); - } - - /// - /// Verify that GetService returns null for AgentVersion when agent is created from AgentReference. - /// - [Fact] - public void GetService_WithAgentReference_ReturnsNullForAgentVersion() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var agentReference = new AgentReference("test-name", "1"); - - // Act - var agent = client.AsAIAgent(agentReference); - var retrievedVersion = agent.GetService(); - - // Assert - Assert.Null(retrievedVersion); - } - - #endregion - - #region ChatClientMetadata Tests - - /// - /// Verify that ChatClientMetadata is properly populated for agents created from AgentRecord. - /// - [Fact] - public void ChatClientMetadata_WithAgentRecord_IsPopulatedCorrectly() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentRecord agentRecord = this.CreateTestAgentRecord(); - - // Act - var agent = client.AsAIAgent(agentRecord); - var metadata = agent.GetService(); - - // Assert - Assert.NotNull(metadata); - Assert.NotNull(metadata.DefaultModelId); - } - - /// - /// Verify that ChatClientMetadata.DefaultModelId is set from PromptAgentDefinition model property. - /// - [Fact] - public void ChatClientMetadata_WithPromptAgentDefinition_SetsDefaultModelIdFromModel() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var definition = new PromptAgentDefinition("gpt-4-turbo") - { - Instructions = "Test instructions" - }; - AgentRecord agentRecord = this.CreateTestAgentRecord(definition); - - // Act - var agent = client.AsAIAgent(agentRecord); - var metadata = agent.GetService(); - - // Assert - Assert.NotNull(metadata); - // The metadata should contain the model information from the agent definition - Assert.NotNull(metadata.DefaultModelId); - Assert.Equal("gpt-4-turbo", metadata.DefaultModelId); - } - - /// - /// Verify that ChatClientMetadata is properly populated for agents created from AgentVersion. - /// - [Fact] - public void ChatClientMetadata_WithAgentVersion_IsPopulatedCorrectly() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentVersion agentVersion = this.CreateTestAgentVersion(); - - // Act - var agent = client.AsAIAgent(agentVersion); - var metadata = agent.GetService(); - - // Assert - Assert.NotNull(metadata); - Assert.NotNull(metadata.DefaultModelId); - Assert.Equal((agentVersion.Definition as PromptAgentDefinition)!.Model, metadata.DefaultModelId); - } - - #endregion - - #region AgentReference Availability Tests - - /// - /// Verify that GetService returns AgentReference for agents created from AgentReference. - /// - [Fact] - public void GetService_WithAgentReference_ReturnsAgentReference() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var agentReference = new AgentReference("test-agent", "1.0"); - - // Act - var agent = client.AsAIAgent(agentReference); - var retrievedReference = agent.GetService(); - - // Assert - Assert.NotNull(retrievedReference); - Assert.Equal("test-agent", retrievedReference.Name); - Assert.Equal("1.0", retrievedReference.Version); - } - - /// - /// Verify that GetService returns null for AgentReference when agent is created from AgentRecord. - /// - [Fact] - public void GetService_WithAgentRecord_ReturnsAlsoAgentReference() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentRecord agentRecord = this.CreateTestAgentRecord(); - - // Act - var agent = client.AsAIAgent(agentRecord); - var retrievedReference = agent.GetService(); - - // Assert - Assert.NotNull(retrievedReference); - Assert.Equal(agentRecord.Name, retrievedReference.Name); - } - - /// - /// Verify that GetService returns null for AgentReference when agent is created from AgentVersion. - /// - [Fact] - public void GetService_WithAgentVersion_ReturnsAlsoAgentReference() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - AgentVersion agentVersion = this.CreateTestAgentVersion(); - - // Act - var agent = client.AsAIAgent(agentVersion); - var retrievedReference = agent.GetService(); - - // Assert - Assert.NotNull(retrievedReference); - Assert.Equal(agentVersion.Name, retrievedReference.Name); - } - - /// - /// Verify that GetService returns AgentReference with correct version information. - /// - [Fact] - public void GetService_WithAgentReference_ReturnsCorrectVersionInformation() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var agentReference = new AgentReference("versioned-agent", "3.5"); - - // Act - var agent = client.AsAIAgent(agentReference); - var retrievedReference = agent.GetService(); - - // Assert - Assert.NotNull(retrievedReference); - Assert.Equal("versioned-agent", retrievedReference.Name); - Assert.Equal("3.5", retrievedReference.Version); - } - - #endregion - - #region GetAIAgentAsync - Empty Name Tests - - /// - /// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentException when name is null. - /// - [Fact] - public async Task GetAIAgentAsync_WithOptions_WithNullName_ThrowsArgumentExceptionAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var options = new ChatClientAgentOptions { Name = null }; - - // Act & Assert - ArgumentException exception = await Assert.ThrowsAsync(() => - client.GetAIAgentAsync(options)); - - Assert.Equal("options", exception.ParamName); - Assert.Contains("Agent name must be provided", exception.Message); - } - - /// - /// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentException when name is empty. - /// - [Fact] - public async Task GetAIAgentAsync_WithOptions_WithEmptyName_ThrowsArgumentExceptionAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var options = new ChatClientAgentOptions { Name = string.Empty }; - - // Act & Assert - ArgumentException exception = await Assert.ThrowsAsync(() => - client.GetAIAgentAsync(options)); - - Assert.Equal("options", exception.ParamName); - Assert.Contains("Agent name must be provided", exception.Message); - } - - /// - /// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentException when name is whitespace. - /// - [Fact] - public async Task GetAIAgentAsync_WithOptions_WithWhitespaceName_ThrowsArgumentExceptionAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var options = new ChatClientAgentOptions { Name = " " }; - - // Act & Assert - ArgumentException exception = await Assert.ThrowsAsync(() => - client.GetAIAgentAsync(options)); - - Assert.Equal("options", exception.ParamName); - Assert.Contains("Agent name must be provided", exception.Message); - } - - #endregion - - #region CreateAIAgentAsync - Empty Name Tests - - /// - /// Verify that CreateAIAgentAsync with model and options throws ArgumentException when name is null. - /// - [Fact] - public async Task CreateAIAgentAsync_WithModelAndOptions_WithNullName_ThrowsArgumentExceptionAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var options = new ChatClientAgentOptions - { - Name = null, - ChatOptions = new ChatOptions { Instructions = "Test" } - }; - - // Act & Assert - ArgumentException exception = await Assert.ThrowsAsync(() => - client.CreateAIAgentAsync("test-model", options)); - - Assert.Equal("options", exception.ParamName); - Assert.Contains("Agent name must be provided", exception.Message); - } - - /// - /// Verify that CreateAIAgentAsync with model and options throws ArgumentException when name is empty. - /// - [Fact] - public async Task CreateAIAgentAsync_WithModelAndOptions_WithEmptyName_ThrowsArgumentExceptionAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var options = new ChatClientAgentOptions - { - Name = string.Empty, - ChatOptions = new ChatOptions { Instructions = "Test" } - }; - - // Act & Assert - ArgumentException exception = await Assert.ThrowsAsync(() => - client.CreateAIAgentAsync("test-model", options)); - - Assert.Equal("options", exception.ParamName); - Assert.Contains("Agent name must be provided", exception.Message); - } - - /// - /// Verify that CreateAIAgentAsync with model and options throws ArgumentException when name is whitespace. - /// - [Fact] - public async Task CreateAIAgentAsync_WithModelAndOptions_WithWhitespaceName_ThrowsArgumentExceptionAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var options = new ChatClientAgentOptions - { - Name = " ", - ChatOptions = new ChatOptions { Instructions = "Test" } - }; - - // Act & Assert - ArgumentException exception = await Assert.ThrowsAsync(() => - client.CreateAIAgentAsync("test-model", options)); - - Assert.Equal("options", exception.ParamName); - Assert.Contains("Agent name must be provided", exception.Message); - } - - #endregion - - #region CreateAIAgentAsync - Response Format Tests - - /// - /// Verify that CreateAIAgentAsync with ChatResponseFormatText response format creates agent successfully. - /// - [Fact] - public async Task CreateAIAgentAsync_WithTextResponseFormat_CreatesAgentSuccessfullyAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - ResponseFormat = ChatResponseFormat.Text - } - }; - - // Act - FoundryAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgentAsync with ChatResponseFormatJson response format without schema creates agent successfully. - /// - [Fact] - public async Task CreateAIAgentAsync_WithJsonResponseFormatWithoutSchema_CreatesAgentSuccessfullyAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - ResponseFormat = ChatResponseFormat.Json - } - }; - - // Act - FoundryAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgentAsync with ChatResponseFormatJson with schema creates agent successfully. - /// - [Fact] - public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchema_CreatesAgentSuccessfullyAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - JsonElement schemaElement = AIJsonUtilities.CreateJsonSchema(typeof(TestSchema)); - var jsonFormat = ChatResponseFormat.ForJsonSchema(schemaElement, "test_schema", "A test schema"); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - ResponseFormat = jsonFormat - } - }; - - // Act - FoundryAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgentAsync with ChatResponseFormatJson with schema and strict mode creates agent successfully. - /// - [Fact] - public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchemaAndStrictMode_CreatesAgentSuccessfullyAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - JsonElement schemaElement = AIJsonUtilities.CreateJsonSchema(typeof(TestSchema)); - var jsonFormat = ChatResponseFormat.ForJsonSchema(schemaElement, "test_schema", "A test schema"); - var additionalProps = new AdditionalPropertiesDictionary - { - ["strictJsonSchema"] = true - }; - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - ResponseFormat = jsonFormat, - AdditionalProperties = additionalProps - } - }; - - // Act - FoundryAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgentAsync with ChatResponseFormatJson with schema and strict mode false creates agent successfully. - /// - [Fact] - public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchemaAndStrictModeFalse_CreatesAgentSuccessfullyAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - JsonElement schemaElement = AIJsonUtilities.CreateJsonSchema(typeof(TestSchema)); - var jsonFormat = ChatResponseFormat.ForJsonSchema(schemaElement, "test_schema", "A test schema"); - var additionalProps = new AdditionalPropertiesDictionary - { - ["strictJsonSchema"] = false - }; - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - ResponseFormat = jsonFormat, - AdditionalProperties = additionalProps - } - }; - - // Act - FoundryAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - #endregion - - #region CreateAIAgentAsync - RawRepresentationFactory Tests - - /// - /// Verify that CreateAIAgentAsync with RawRepresentationFactory that returns CreateResponseOptions creates agent successfully. - /// - [Fact] - public async Task CreateAIAgentAsync_WithRawRepresentationFactory_CreatesAgentSuccessfullyAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - RawRepresentationFactory = _ => new CreateResponseOptions() - } - }; - - // Act - FoundryAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgentAsync with RawRepresentationFactory that returns null does not fail. - /// - [Fact] - public async Task CreateAIAgentAsync_WithRawRepresentationFactoryReturningNull_CreatesAgentSuccessfullyAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - RawRepresentationFactory = _ => null - } - }; - - // Act - FoundryAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgentAsync with RawRepresentationFactory that returns non-CreateResponseOptions does not fail. - /// - [Fact] - public async Task CreateAIAgentAsync_WithRawRepresentationFactoryReturningNonCreateResponseOptions_CreatesAgentSuccessfullyAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - RawRepresentationFactory = _ => new object() - } - }; - - // Act - FoundryAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - #endregion - - #region CreateAIAgentAsync - Description Tests - - /// - /// Verify that CreateAIAgentAsync with description sets description on the agent. - /// - [Fact] - public async Task CreateAIAgentAsync_WithDescription_SetsDescriptionAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(description: "Test description"); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - Description = "Test description", - ChatOptions = new ChatOptions { Instructions = "Test" } - }; - - // Act - FoundryAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - Assert.Equal("Test description", agent.Description); - } - - /// - /// Verify that CreateAIAgentAsync without description still creates agent successfully. - /// - [Fact] - public async Task CreateAIAgentAsync_WithoutDescription_CreatesAgentSuccessfullyAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions { Instructions = "Test" } - }; - - // Act - FoundryAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - } - - #endregion - - #region CreateChatClientAgentOptions - Missing Tools Tests - - /// - /// Verify that when invocable tools are required but not provided, an exception is thrown. - /// - [Fact] - public async Task GetAIAgentAsync_WithToolsRequiredButNotProvided_ThrowsArgumentExceptionAsync() - { - // Arrange - PromptAgentDefinition definition = new("test-model") { Instructions = "Test" }; - definition.Tools.Add(ResponseTool.CreateFunctionTool("required_function", BinaryData.FromString("{}"), strictModeEnabled: false)); - - AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions { Instructions = "Test" } - }; - - // Act & Assert - ArgumentException exception = await Assert.ThrowsAsync(() => - client.GetAIAgentAsync(options)); - - Assert.Contains("in-process tools must be provided", exception.Message); - } - - /// - /// Verify that when specific invocable tools are required but wrong ones are provided, InvalidOperationException is thrown. - /// - [Fact] - public async Task GetAIAgentAsync_WithWrongToolsProvided_ThrowsInvalidOperationExceptionAsync() - { - // Arrange - PromptAgentDefinition definition = new("test-model") { Instructions = "Test" }; - definition.Tools.Add(ResponseTool.CreateFunctionTool("required_function", BinaryData.FromString("{}"), strictModeEnabled: false)); - - AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition); - var tools = new List - { - AIFunctionFactory.Create(() => "test", "wrong_function", "Wrong function") - }; - - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - Tools = tools - } - }; - - // Act & Assert - InvalidOperationException exception = await Assert.ThrowsAsync(() => - client.GetAIAgentAsync(options)); - - Assert.Contains("required_function", exception.Message); - Assert.Contains("were not provided", exception.Message); - } - - /// - /// Verify that when tools are provided that match the definition, agent is created successfully. - /// - [Fact] - public async Task GetAIAgentAsync_WithMatchingToolsProvided_CreatesAgentSuccessfullyAsync() - { - // Arrange - PromptAgentDefinition definition = new("test-model") { Instructions = "Test" }; - definition.Tools.Add(ResponseTool.CreateFunctionTool("required_function", BinaryData.FromString("{}"), strictModeEnabled: false)); - - AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition); - var tools = new List - { - AIFunctionFactory.Create(() => "test", "required_function", "Required function") - }; - - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - Tools = tools - } - }; - - // Act - FoundryAgent agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - #endregion - - #region CreateChatClientAgentOptions - Options Preservation Tests - - /// - /// Verify that CreateChatClientAgentOptions preserves AIContextProviders. - /// - [Fact] - public async Task GetAIAgentAsync_WithAIContextProviders_PreservesProviderAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions { Instructions = "Test" }, - AIContextProviders = [new TestAIContextProvider()] - }; - - // Act - FoundryAgent agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - } - - /// - /// Verify that CreateChatClientAgentOptions preserves ChatHistoryProvider. - /// - [Fact] - public async Task GetAIAgentAsync_WithChatHistoryProvider_PreservesProviderAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions { Instructions = "Test" }, - ChatHistoryProvider = new TestChatHistoryProvider() - }; - - // Act - FoundryAgent agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - } - - /// - /// Verify that CreateChatClientAgentOptions preserves UseProvidedChatClientAsIs. - /// - [Fact] - public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_PreservesSettingAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClient(); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions { Instructions = "Test" }, - UseProvidedChatClientAsIs = true - }; - - // Act - FoundryAgent agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - } - - /// - /// Verify that GetAIAgentAsync with UseProvidedChatClientAsIs=true skips tool validation - /// and does not throw even when server-side function tools exist without matching invocable tools. - /// - [Fact] - public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_SkipsToolValidationAsync() - { - // Arrange - PromptAgentDefinition definition = new("test-model") { Instructions = "Test" }; - definition.Tools.Add(ResponseTool.CreateFunctionTool("required_function", BinaryData.FromString("{}"), strictModeEnabled: false)); - - AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions { Instructions = "Test" }, - UseProvidedChatClientAsIs = true - }; - - // Act - should not throw even without tools when UseProvidedChatClientAsIs is true - FoundryAgent agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - } - - /// - /// Verify that GetAIAgentAsync with UseProvidedChatClientAsIs=true still matches provided AIFunction tools - /// to server-side function definitions, instead of falling back to the ResponseToolAITool wrapper. - /// - [Fact] - public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_PreservesProvidedToolsAsync() - { - // Arrange - PromptAgentDefinition definition = new("test-model") { Instructions = "Test" }; - definition.Tools.Add(ResponseTool.CreateFunctionTool("my_function", BinaryData.FromString("{}"), strictModeEnabled: false)); - - AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition); - - var providedTool = AIFunctionFactory.Create(() => "test", "my_function", "A test function"); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - UseProvidedChatClientAsIs = true, - ChatOptions = new ChatOptions - { - Instructions = "Test", - Tools = [providedTool] - }, - }; - - // Act - UseProvidedChatClientAsIs is true, but provided AIFunctions should still be matched and preserved - FoundryAgent agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - - // Verify the provided AIFunction was matched and preserved in ChatOptions.Tools (not replaced by AsAITool wrapper) - var chatOptions = agent.GetService(); - Assert.NotNull(chatOptions); - Assert.NotNull(chatOptions!.Tools); - Assert.Contains(chatOptions.Tools, t => t is AIFunction af && af.Name == "my_function"); - } - - #endregion - - #region Empty Version and ID Handling Tests - - /// - /// Verify that GetAIAgentAsync handles an agent with empty version by using "latest" as fallback. - /// - [Fact] - public async Task GetAIAgentAsync_WithEmptyVersion_CreatesAgentSuccessfullyAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion(); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions { Instructions = "Test" } - }; - - // Act - FoundryAgent agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - // Verify the agent ID is generated from server-returned name ("agent_abc123") and "latest" - Assert.Equal("agent_abc123:latest", agent.Id); - } - - /// - /// Verify that AsAIAgent with AgentRecord handles empty version by using "latest" as fallback. - /// - [Fact] - public void AsAIAgent_WithAgentRecordEmptyVersion_CreatesAgentWithGeneratedId() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion(); - AgentRecord agentRecord = this.CreateTestAgentRecordWithEmptyVersion(); - - // Act - var agent = client.AsAIAgent(agentRecord); - - // Assert - Assert.NotNull(agent); - // Verify the agent ID is generated from agent record name ("agent_abc123") and "latest" - Assert.Equal("agent_abc123:latest", agent.Id); - } - - /// - /// Verify that AsAIAgent with AgentVersion handles empty version by using "latest" as fallback. - /// - [Fact] - public void AsAIAgent_WithAgentVersionEmptyVersion_CreatesAgentWithGeneratedId() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion(); - AgentVersion agentVersion = this.CreateTestAgentVersionWithEmptyVersion(); - - // Act - var agent = client.AsAIAgent(agentVersion); - - // Assert - Assert.NotNull(agent); - // Verify the agent ID is generated from agent version name ("agent_abc123") and "latest" - Assert.Equal("agent_abc123:latest", agent.Id); - } - - /// - /// Verify that GetAIAgentAsync handles an agent with whitespace-only version by using "latest" as fallback. - /// - [Fact] - public async Task GetAIAgentAsync_WithWhitespaceVersion_CreatesAgentSuccessfullyAsync() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion(); - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions { Instructions = "Test" } - }; - - // Act - FoundryAgent agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - // Verify the agent ID is generated from server-returned name ("agent_abc123") and "latest" - Assert.Equal("agent_abc123:latest", agent.Id); - } - - /// - /// Verify that AsAIAgent with AgentRecord handles whitespace-only version by using "latest" as fallback. - /// - [Fact] - public void AsAIAgent_WithAgentRecordWhitespaceVersion_CreatesAgentWithGeneratedId() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion(); - AgentRecord agentRecord = this.CreateTestAgentRecordWithWhitespaceVersion(); - - // Act - var agent = client.AsAIAgent(agentRecord); - - // Assert - Assert.NotNull(agent); - // Verify the agent ID is generated from agent record name ("agent_abc123") and "latest" - Assert.Equal("agent_abc123:latest", agent.Id); - } - - /// - /// Verify that AsAIAgent with AgentVersion handles whitespace-only version by using "latest" as fallback. - /// - [Fact] - public void AsAIAgent_WithAgentVersionWhitespaceVersion_CreatesAgentWithGeneratedId() - { - // Arrange - AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion(); - AgentVersion agentVersion = this.CreateTestAgentVersionWithWhitespaceVersion(); - - // Act - var agent = client.AsAIAgent(agentVersion); - - // Assert - Assert.NotNull(agent); - // Verify the agent ID is generated from agent version name ("agent_abc123") and "latest" - Assert.Equal("agent_abc123:latest", agent.Id); - } - - #endregion - - #region ApplyToolsToAgentDefinition Tests - - /// - /// Verify that CreateAIAgentAsync with non-PromptAgentDefinition and tools throws ArgumentException. - /// - [Fact] - public async Task CreateAIAgentAsync_WithNonPromptAgentDefinitionAndTools_ThrowsArgumentExceptionAsync() - { - // Arrange - var tools = new List - { - AIFunctionFactory.Create(() => "test", "test_function", "A test function") - }; - - using HttpHandlerAssert httpHandler = new(_ => new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json") - }); - -#pragma warning disable CA5399 - using HttpClient httpClient = new(httpHandler); -#pragma warning restore CA5399 - - AIProjectClient client = new(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) }); - - // Create a mock AgentDefinition that is not PromptAgentDefinition - // Since we can't easily create a non-PromptAgentDefinition in the public API, we test this path via the CreateAIAgentAsync that builds a PromptAgentDefinition - // The ApplyToolsToAgentDefinition is only called when tools.Count > 0, and we provide tools - // But PromptAgentDefinition is always created by CreateAIAgentAsync(name, model, instructions, tools) - // So this path is hard to hit without mocking. Let's test the declarative function rejection instead. - var declarativeFunction = AIFunctionFactory.CreateDeclaration("test_function", "A test function", JsonDocument.Parse("{}").RootElement); - - // Act & Assert - InvalidOperationException exception = await Assert.ThrowsAsync(() => - client.CreateAIAgentAsync( - name: "test-agent", - model: "test-model", - instructions: "Test", - tools: [declarativeFunction])); - - Assert.Contains("invokable AIFunctions", exception.Message); - } - - /// - /// Verify that CreateAIAgentAsync with AIFunctionDeclaration tools throws InvalidOperationException. - /// - [Fact] - public async Task CreateAIAgentAsync_WithAIFunctionDeclarationTool_ThrowsInvalidOperationExceptionAsync() - { - // Arrange - using var doc = JsonDocument.Parse("{}"); - var declarativeFunction = AIFunctionFactory.CreateDeclaration("test_function", "A test function", doc.RootElement); - - using HttpHandlerAssert httpHandler = new(_ => new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json") - }); - -#pragma warning disable CA5399 - using HttpClient httpClient = new(httpHandler); -#pragma warning restore CA5399 - - AIProjectClient client = new(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) }); - - // Act & Assert - InvalidOperationException exception = await Assert.ThrowsAsync(() => - client.CreateAIAgentAsync( - name: "test-agent", - model: "test-model", - instructions: "Test", - tools: [declarativeFunction])); - - Assert.Contains("invokable AIFunctions", exception.Message); - } - - /// - /// Verify that CreateAIAgentAsync with ResponseTool converted via AsAITool works. - /// - [Fact] - public async Task CreateAIAgentAsync_WithResponseToolAsAITool_CreatesAgentSuccessfullyAsync() - { - // Arrange - ResponseTool responseTool = ResponseTool.CreateFunctionTool("response_tool", BinaryData.FromString("{}"), strictModeEnabled: false); - AITool convertedTool = responseTool.AsAITool(); - - // Create a definition with the function tool already in it - PromptAgentDefinition definition = new("test-model") { Instructions = "Test" }; - definition.Tools.Add(responseTool); - - AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition); - - // Matching invokable tool must be provided - var invokableTool = AIFunctionFactory.Create(() => "test", "response_tool", "Invokable version of the tool"); - - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - Tools = [invokableTool] - } - }; - - // Act - FoundryAgent agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that CreateAIAgentAsync with hosted tool types works correctly. - /// - [Fact] - public async Task CreateAIAgentAsync_WithHostedToolTypes_CreatesAgentSuccessfullyAsync() - { - // Arrange - using var testClient = CreateTestAgentClientWithHandler(); - var webSearchTool = new HostedWebSearchTool(); - - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - Tools = [webSearchTool] - } - }; - - // Act - FoundryAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that when the server returns tools but matching tools are provided, the agent is created. - /// - [Fact] - public async Task GetAIAgentAsync_WithServerDefinedToolsAndMatchingProvidedTools_CreatesAgentAsync() - { - // Arrange - PromptAgentDefinition definition = new("test-model") { Instructions = "Test" }; - // Add multiple function tools - definition.Tools.Add(ResponseTool.CreateFunctionTool("tool_one", BinaryData.FromString("{}"), strictModeEnabled: false)); - definition.Tools.Add(ResponseTool.CreateFunctionTool("tool_two", BinaryData.FromString("{}"), strictModeEnabled: false)); - - AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition); - - var tools = new List - { - AIFunctionFactory.Create(() => "one", "tool_one", "Tool one"), - AIFunctionFactory.Create(() => "two", "tool_two", "Tool two") - }; - - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - Tools = tools - } - }; - - // Act - FoundryAgent agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that when the server returns mixed tools (function and hosted), the agent handles them correctly. - /// - [Fact] - public async Task GetAIAgentAsync_WithMixedServerTools_MatchesFunctionToolsOnlyAsync() - { - // Arrange - PromptAgentDefinition definition = new("test-model") { Instructions = "Test" }; - // Add a function tool - definition.Tools.Add(ResponseTool.CreateFunctionTool("function_tool", BinaryData.FromString("{}"), strictModeEnabled: false)); - // Add a hosted tool - definition.Tools.Add(new HostedWebSearchTool().GetService() ?? new HostedWebSearchTool().AsOpenAIResponseTool()); - - AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition); - - var tools = new List - { - AIFunctionFactory.Create(() => "result", "function_tool", "The function tool") - }; - - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - Tools = tools - } - }; - - // Act - FoundryAgent agent = await client.GetAIAgentAsync(options); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - /// - /// Verify that when partial tools are provided (some missing), InvalidOperationException is thrown listing missing tools. - /// - [Fact] - public async Task GetAIAgentAsync_WithPartialToolsProvided_ThrowsInvalidOperationWithMissingToolNamesAsync() - { - // Arrange - PromptAgentDefinition definition = new("test-model") { Instructions = "Test" }; - definition.Tools.Add(ResponseTool.CreateFunctionTool("provided_tool", BinaryData.FromString("{}"), strictModeEnabled: false)); - definition.Tools.Add(ResponseTool.CreateFunctionTool("missing_tool", BinaryData.FromString("{}"), strictModeEnabled: false)); - - AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition); - - var tools = new List - { - // Only providing one of two required tools - AIFunctionFactory.Create(() => "result", "provided_tool", "The provided tool") - }; - - var options = new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new ChatOptions - { - Instructions = "Test", - Tools = tools - } - }; - - // Act & Assert - InvalidOperationException exception = await Assert.ThrowsAsync(() => - client.GetAIAgentAsync(options)); - - Assert.Contains("missing_tool", exception.Message); - Assert.DoesNotContain("provided_tool", exception.Message); - } - - /// - /// Verify that when AsAIAgent is called without requireInvocableTools, hosted tools are correctly added. - /// - [Fact] - public void AsAIAgent_WithServerHostedTools_AddsToolsToAgentOptions() - { - // Arrange - PromptAgentDefinition definition = new("test-model") { Instructions = "Test" }; - definition.Tools.Add(new HostedWebSearchTool().GetService() ?? new HostedWebSearchTool().AsOpenAIResponseTool()); - - AIProjectClient client = this.CreateTestAgentClient(); - AgentVersion agentVersion = ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson(agentDefinition: definition)))!; - - // Act - no tools provided, but requireInvocableTools is false when no tools param is passed - FoundryAgent agent = client.AsAIAgent(agentVersion); - - // Assert - Assert.NotNull(agent); - Assert.IsType(agent); - } - - #endregion - - #region Helper Methods - - /// - /// Creates a test AIProjectClient with fake behavior. - /// - private FakeAgentClient CreateTestAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) - { - return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse); - } - - /// - /// Creates a test AIProjectClient backed by an HTTP handler that returns canned responses. - /// Used for tests that exercise the protocol-method code path (CreateAgentVersion). - /// The returned client must be disposed to clean up the underlying HttpClient/handler. - /// - private static DisposableTestClient CreateTestAgentClientWithHandler(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) - { - var responseJson = TestDataUtil.GetAgentVersionResponseJson(agentName, agentDefinitionResponse, instructions, description); - - var httpHandler = new HttpHandlerAssert(_ => - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(responseJson, Encoding.UTF8, "application/json") }); - -#pragma warning disable CA5399 - var httpClient = new HttpClient(httpHandler); -#pragma warning restore CA5399 - - var client = new AIProjectClient( - new Uri("https://test.openai.azure.com/"), - new FakeAuthenticationTokenProvider(), - new() { Transport = new HttpClientPipelineTransport(httpClient) }); - - return new DisposableTestClient(client, httpClient, httpHandler); - } - - /// - /// Wraps an AIProjectClient and its disposable dependencies for deterministic cleanup. - /// - private sealed class DisposableTestClient : IDisposable - { - private readonly HttpClient _httpClient; - private readonly HttpHandlerAssert _httpHandler; - - public DisposableTestClient(AIProjectClient client, HttpClient httpClient, HttpHandlerAssert httpHandler) - { - this.Client = client; - this._httpClient = httpClient; - this._httpHandler = httpHandler; - } - - public AIProjectClient Client { get; } - - public void Dispose() - { - this._httpClient.Dispose(); - this._httpHandler.Dispose(); - } - } - - /// - /// Creates a test AgentRecord for testing. - /// - private AgentRecord CreateTestAgentRecord(AgentDefinition? agentDefinition = null) - { - return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJson(agentDefinition: agentDefinition)))!; - } - - /// - /// Creates a test AIProjectClient with empty version fields for testing hosted MCP agents. - /// - private FakeAgentClient CreateTestAgentClientWithEmptyVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) - { - return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, useEmptyVersion: true); - } - - /// - /// Creates a test AgentRecord with empty version for testing hosted MCP agents. - /// - private AgentRecord CreateTestAgentRecordWithEmptyVersion(AgentDefinition? agentDefinition = null) - { - return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithEmptyVersion(agentDefinition: agentDefinition)))!; - } - - /// - /// Creates a test AgentVersion with empty version for testing hosted MCP agents. - /// - private AgentVersion CreateTestAgentVersionWithEmptyVersion() - { - return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion()))!; - } - - /// - /// Creates a test AIProjectClient with whitespace-only version fields for testing hosted MCP agents. - /// - private FakeAgentClient CreateTestAgentClientWithWhitespaceVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) - { - return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, versionMode: VersionMode.Whitespace); - } - - /// - /// Creates a test AgentRecord with whitespace-only version for testing hosted MCP agents. - /// - private AgentRecord CreateTestAgentRecordWithWhitespaceVersion(AgentDefinition? agentDefinition = null) - { - return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(agentDefinition: agentDefinition)))!; - } - - /// - /// Creates a test AgentVersion with whitespace-only version for testing hosted MCP agents. - /// - private AgentVersion CreateTestAgentVersionWithWhitespaceVersion() - { - return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion()))!; - } - - private const string OpenAPISpec = """ - { - "openapi": "3.0.3", - "info": { "title": "Tiny Test API", "version": "1.0.0" }, - "paths": { - "/ping": { - "get": { - "summary": "Health check", - "operationId": "getPing", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "message": { "type": "string" } }, - "required": ["message"] - }, - "example": { "message": "pong" } - } - } - } - } - } - } - } - } - """; - - /// - /// Creates a test AgentVersion for testing. - /// - private AgentVersion CreateTestAgentVersion() - { - return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))!; - } - - /// - /// Specifies the version mode for test data generation. - /// - private enum VersionMode - { - Normal, - Empty, - Whitespace - } - - /// - /// Fake AIProjectClient for testing. - /// - private sealed class FakeAgentClient : AIProjectClient - { - public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false, VersionMode versionMode = VersionMode.Normal) - { - // Handle backward compatibility with bool parameter - var effectiveVersionMode = useEmptyVersion ? VersionMode.Empty : versionMode; - this.Agents = new FakeAgentsClient(agentName, instructions, description, agentDefinitionResponse, effectiveVersionMode); - } - - public override ClientConnection GetConnection(string connectionId) - { - return new ClientConnection("fake-connection-id", "http://localhost", ClientPipeline.Create(), CredentialKind.None); - } - - public override AgentsClient Agents { get; } - - private sealed class FakeAgentsClient : AgentsClient - { - private readonly string? _agentName; - private readonly string? _instructions; - private readonly string? _description; - private readonly AgentDefinition? _agentDefinition; - private readonly VersionMode _versionMode; - - public FakeAgentsClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, VersionMode versionMode = VersionMode.Normal) - { - this._agentName = agentName; - this._instructions = instructions; - this._description = description; - this._agentDefinition = agentDefinitionResponse; - this._versionMode = versionMode; - } - - private string GetAgentResponseJson() - { - return this._versionMode switch - { - VersionMode.Empty => TestDataUtil.GetAgentResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description), - VersionMode.Whitespace => TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(this._agentName, this._agentDefinition, this._instructions, this._description), - _ => TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description) - }; - } - - private string GetAgentVersionResponseJson() - { - return this._versionMode switch - { - VersionMode.Empty => TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description), - VersionMode.Whitespace => TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion(this._agentName, this._agentDefinition, this._instructions, this._description), - _ => TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description) - }; - } - - public override ClientResult GetAgent(string agentName, RequestOptions options) - { - var responseJson = this.GetAgentResponseJson(); - return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))); - } - - public override ClientResult GetAgent(string agentName, CancellationToken cancellationToken = default) - { - var responseJson = this.GetAgentResponseJson(); - return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)); - } - - public override Task GetAgentAsync(string agentName, RequestOptions options) - { - var responseJson = this.GetAgentResponseJson(); - return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)))); - } - - public override Task> GetAgentAsync(string agentName, CancellationToken cancellationToken = default) - { - var responseJson = this.GetAgentResponseJson(); - return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200))); - } - - public override ClientResult CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default) - { - var responseJson = this.GetAgentVersionResponseJson(); - return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)); - } - - public override Task> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default) - { - var responseJson = this.GetAgentVersionResponseJson(); - return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200))); - } - } - } - - private static PromptAgentDefinition GeneratePromptDefinitionResponse(PromptAgentDefinition inputDefinition, List? tools) - { - var definitionResponse = new PromptAgentDefinition(inputDefinition.Model) { Instructions = inputDefinition.Instructions }; - if (tools is not null) - { - foreach (var tool in tools) - { - definitionResponse.Tools.Add(tool.GetService() ?? tool.AsOpenAIResponseTool()); - } - } - - return definitionResponse; - } - - /// - /// Test custom chat client that can be used to verify clientFactory functionality. - /// - private sealed class TestChatClient : DelegatingChatClient - { - public TestChatClient(IChatClient innerClient) : base(innerClient) - { - } - } - - /// - /// Mock pipeline response for testing ClientResult wrapping. - /// - private sealed class MockPipelineResponse : PipelineResponse - { - private readonly MockPipelineResponseHeaders _headers; - - public MockPipelineResponse(int status, BinaryData? content = null) - { - this.Status = status; - this.Content = content ?? BinaryData.Empty; - this._headers = new MockPipelineResponseHeaders(); - } - - public override int Status { get; } - - public override string ReasonPhrase => "OK"; - - public override Stream? ContentStream - { - get => null; - set { } - } - - public override BinaryData Content { get; } - - protected override PipelineResponseHeaders HeadersCore => this._headers; - - public override BinaryData BufferContent(CancellationToken cancellationToken = default) => - throw new NotSupportedException("Buffering content is not supported for mock responses."); - - public override ValueTask BufferContentAsync(CancellationToken cancellationToken = default) => - throw new NotSupportedException("Buffering content asynchronously is not supported for mock responses."); - - public override void Dispose() - { - } - - private sealed class MockPipelineResponseHeaders : PipelineResponseHeaders - { - private readonly Dictionary _headers = new(StringComparer.OrdinalIgnoreCase) - { - { "Content-Type", "application/json" }, - { "x-ms-request-id", "test-request-id" } - }; - - public override bool TryGetValue(string name, out string? value) - { - return this._headers.TryGetValue(name, out value); - } - - public override bool TryGetValues(string name, out IEnumerable? values) - { - if (this._headers.TryGetValue(name, out var value)) - { - values = [value]; - return true; - } - - values = null; - return false; - } - - public override IEnumerator> GetEnumerator() - { - return this._headers.GetEnumerator(); - } - } - } - - #endregion - - /// - /// Helper method to access internal ChatOptions property via reflection. - /// - private static ChatOptions? GetAgentChatOptions(AIAgent agent) - { - ChatClientAgent? chatClientAgent = agent as ChatClientAgent ?? agent.GetService(); - if (chatClientAgent is null) - { - return null; - } - - var chatOptionsProperty = typeof(ChatClientAgent).GetProperty( - "ChatOptions", - System.Reflection.BindingFlags.Public | - System.Reflection.BindingFlags.NonPublic | - System.Reflection.BindingFlags.Instance); - - return chatOptionsProperty?.GetValue(chatClientAgent) as ChatOptions; - } - - /// - /// Test schema for JSON response format tests. - /// -#pragma warning disable CA1812 // Avoid uninstantiated internal classes - used via reflection by AIJsonUtilities - private sealed class TestSchema - { - public string? Name { get; set; } - public int Value { get; set; } - } -#pragma warning restore CA1812 - - /// - /// Test AIContextProvider for options preservation tests. - /// - private sealed class TestAIContextProvider : AIContextProvider - { - protected override ValueTask InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default) - { - return new ValueTask(context.AIContext); - } - } - - /// - /// Test ChatHistoryProvider for options preservation tests. - /// - private sealed class TestChatHistoryProvider : ChatHistoryProvider - { - protected override ValueTask> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default) - { - return new ValueTask>(context.RequestMessages); - } - - protected override ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default) - { - return default; - } - } -} -#pragma warning restore CS0618 - -/// -/// Provides test data for invalid agent name validation tests. -/// -internal static class InvalidAgentNameTestData -{ - /// - /// Gets a collection of invalid agent names for theory-based testing. - /// - /// Collection of invalid agent name test cases. - public static IEnumerable GetInvalidAgentNames() - { - yield return new object[] { "-agent" }; - yield return new object[] { "agent-" }; - yield return new object[] { "agent_name" }; - yield return new object[] { "agent name" }; - yield return new object[] { "agent@name" }; - yield return new object[] { "agent#name" }; - yield return new object[] { "agent$name" }; - yield return new object[] { "agent%name" }; - yield return new object[] { "agent&name" }; - yield return new object[] { "agent*name" }; - yield return new object[] { "agent.name" }; - yield return new object[] { "agent/name" }; - yield return new object[] { "agent\\name" }; - yield return new object[] { "agent:name" }; - yield return new object[] { "agent;name" }; - yield return new object[] { "agent,name" }; - yield return new object[] { "agentname" }; - yield return new object[] { "agent?name" }; - yield return new object[] { "agent!name" }; - yield return new object[] { "agent~name" }; - yield return new object[] { "agent`name" }; - yield return new object[] { "agent^name" }; - yield return new object[] { "agent|name" }; - yield return new object[] { "agent[name" }; - yield return new object[] { "agent]name" }; - yield return new object[] { "agent{name" }; - yield return new object[] { "agent}name" }; - yield return new object[] { "agent(name" }; - yield return new object[] { "agent)name" }; - yield return new object[] { "agent+name" }; - yield return new object[] { "agent=name" }; - yield return new object[] { "a" + new string('b', 63) }; - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientExtensionsTests.cs new file mode 100644 index 0000000000..c362fb4ec6 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientExtensionsTests.cs @@ -0,0 +1,1814 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.AI.Projects.Agents; +using Microsoft.Extensions.AI; +using Moq; +using OpenAI.Responses; + +namespace Microsoft.Agents.AI.Foundry.UnitTests; + +#pragma warning disable CS0618 +/// +/// Unit tests for the class. +/// +public sealed class AzureAIProjectChatClientExtensionsTests +{ + #region AsAIAgent(AIProjectClient, model, instructions) Tests + + /// + /// Verify that the non-versioned AsAIAgent overload throws ArgumentNullException when AIProjectClient is null. + /// + [Fact] + public void AsAIAgent_WithModelAndInstructions_WithNullClient_ThrowsArgumentNullException() + { + // Arrange + AIProjectClient? client = null; + + // Act & Assert + ArgumentNullException exception = Assert.Throws(() => + client!.AsAIAgent("gpt-4o-mini", "You are helpful.")); + + Assert.Equal("aiProjectClient", exception.ParamName); + } + + /// + /// Verify that the non-versioned AsAIAgent overload creates a valid ChatClientAgent. + /// + [Fact] + public void AsAIAgent_Rapi_WithModelAndInstructions_CreatesChatClientAgent() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + List tools = + [ + AIFunctionFactory.Create(() => "test", "test_function", "A test function") + ]; + + // Act + ChatClientAgent agent = client.AsAIAgent( + "gpt-4o-mini", + "You are helpful.", + name: "test-agent", + description: "A test agent", + tools: tools); + + // Assert + Assert.NotNull(agent); + Assert.Equal("test-agent", agent.Name); + Assert.Equal("A test agent", agent.Description); + Assert.NotNull(agent.GetService()); + Assert.Null(agent.GetService()); + } + + /// + /// Verify that the non-versioned AsAIAgent overload applies the clientFactory. + /// + [Fact] + public void AsAIAgent_WithModelAndInstructions_WithClientFactory_AppliesFactoryCorrectly() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + TestChatClient? testChatClient = null; + + // Act + ChatClientAgent agent = client.AsAIAgent( + "gpt-4o-mini", + "You are helpful.", + clientFactory: innerClient => testChatClient = new TestChatClient(innerClient)); + + // Assert + Assert.NotNull(agent); + TestChatClient? retrievedTestClient = agent.GetService(); + Assert.NotNull(retrievedTestClient); + Assert.Same(testChatClient, retrievedTestClient); + } + + /// + /// Verify that the options-based non-versioned AsAIAgent overload creates a valid ChatClientAgent. + /// + [Fact] + public void AsAIAgent_Rapi_WithOptions_CreatesChatClientAgent() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + ChatClientAgentOptions options = new() + { + Name = "options-agent", + Description = "Agent from options", + ChatOptions = new ChatOptions + { + ModelId = "gpt-4o-mini", + Instructions = "You are helpful.", + }, + }; + + // Act + ChatClientAgent agent = client.AsAIAgent(options); + + // Assert + Assert.NotNull(agent); + Assert.Equal("options-agent", agent.Name); + Assert.Equal("Agent from options", agent.Description); + Assert.Null(agent.GetService()); + } + + /// + /// Verify that the non-versioned AsAIAgent overload adds the MEAI user-agent header to Responses API requests. + /// + [Fact] + public async Task AsAIAgent_Rapi_WithModelAndInstructions_UserAgentHeaderAddedToResponsesRequestsAsync() + { + // Arrange + bool userAgentFound = false; + using HttpHandlerAssert httpHandler = new(request => + { + if (request.Headers.TryGetValues("User-Agent", out IEnumerable? values)) + { + foreach (string value in values) + { + if (value.Contains("MEAI")) + { + userAgentFound = true; + } + } + } + + if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses")) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + TestDataUtil.GetOpenAIDefaultResponseJson(), + Encoding.UTF8, + "application/json") + }; + } + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}", Encoding.UTF8, "application/json") + }; + }); + +#pragma warning disable CA5399 + using HttpClient httpClient = new(httpHandler); +#pragma warning restore CA5399 + + AIProjectClient aiProjectClient = new( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new() { Transport = new HttpClientPipelineTransport(httpClient) }); + + ChatClientAgent agent = aiProjectClient.AsAIAgent( + "gpt-4o-mini", + "You are helpful."); + + // Act + AgentSession session = await agent.CreateSessionAsync(); + await agent.RunAsync("Hello", session); + + // Assert + Assert.True(userAgentFound, "MEAI user-agent header was not found in any request"); + } + + #endregion + + #region AsAIAgent(AIProjectClient, AgentRecord) Tests + + /// + /// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null. + /// + [Fact] + public void AsAIAgent_WithAgentRecord_WithNullClient_ThrowsArgumentNullException() + { + // Arrange + AIProjectClient? client = null; + AgentRecord agentRecord = this.CreateTestAgentRecord(); + + // Act & Assert + var exception = Assert.Throws(() => + client!.AsAIAgent(agentRecord)); + + Assert.Equal("aiProjectClient", exception.ParamName); + } + + /// + /// Verify that AsAIAgent throws ArgumentNullException when agentRecord is null. + /// + [Fact] + public void AsAIAgent_WithAgentRecord_WithNullAgentRecord_ThrowsArgumentNullException() + { + // Arrange + var mockClient = new Mock(); + + // Act & Assert + var exception = Assert.Throws(() => + mockClient.Object.AsAIAgent((AgentRecord)null!)); + + Assert.Equal("agentRecord", exception.ParamName); + } + + /// + /// Verify that AsAIAgent with AgentRecord creates a valid agent. + /// + [Fact] + public void AsAIAgent_WithAgentRecord_CreatesValidAgent() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentRecord agentRecord = this.CreateTestAgentRecord(); + + // Act + var agent = client.AsAIAgent(agentRecord); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + Assert.Equal("agent_abc123", agent.Name); + Assert.Same(client, agent.GetService()); + } + + /// + /// Verify that AsAIAgent with AgentRecord and clientFactory applies the factory. + /// + [Fact] + public void AsAIAgent_WithAgentRecord_WithClientFactory_AppliesFactoryCorrectly() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentRecord agentRecord = this.CreateTestAgentRecord(); + TestChatClient? testChatClient = null; + + // Act + var agent = client.AsAIAgent( + agentRecord, + clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient)); + + // Assert + Assert.NotNull(agent); + var retrievedTestClient = agent.GetService(); + Assert.NotNull(retrievedTestClient); + Assert.Same(testChatClient, retrievedTestClient); + } + + #endregion + + #region AsAIAgent(AIProjectClient, AgentVersion) Tests + + /// + /// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null. + /// + [Fact] + public void AsAIAgent_WithAgentVersion_WithNullClient_ThrowsArgumentNullException() + { + // Arrange + AIProjectClient? client = null; + AgentVersion agentVersion = this.CreateTestAgentVersion(); + + // Act & Assert + var exception = Assert.Throws(() => + client!.AsAIAgent(agentVersion)); + + Assert.Equal("aiProjectClient", exception.ParamName); + } + + /// + /// Verify that AsAIAgent throws ArgumentNullException when agentVersion is null. + /// + [Fact] + public void AsAIAgent_WithAgentVersion_WithNullAgentVersion_ThrowsArgumentNullException() + { + // Arrange + var mockClient = new Mock(); + + // Act & Assert + var exception = Assert.Throws(() => + mockClient.Object.AsAIAgent((AgentVersion)null!)); + + Assert.Equal("agentVersion", exception.ParamName); + } + + /// + /// Verify that AsAIAgent with AgentVersion creates a valid agent. + /// + [Fact] + public void AsAIAgent_WithAgentVersion_CreatesValidAgent() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentVersion agentVersion = this.CreateTestAgentVersion(); + + // Act + var agent = client.AsAIAgent(agentVersion); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + Assert.Equal("agent_abc123", agent.Name); + Assert.Same(client, agent.GetService()); + } + + /// + /// Verify that AsAIAgent with AgentVersion and clientFactory applies the factory. + /// + [Fact] + public void AsAIAgent_WithAgentVersion_WithClientFactory_AppliesFactoryCorrectly() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentVersion agentVersion = this.CreateTestAgentVersion(); + TestChatClient? testChatClient = null; + + // Act + var agent = client.AsAIAgent( + agentVersion, + clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient)); + + // Assert + Assert.NotNull(agent); + var retrievedTestClient = agent.GetService(); + Assert.NotNull(retrievedTestClient); + Assert.Same(testChatClient, retrievedTestClient); + } + + /// + /// Verify that AsAIAgent with requireInvocableTools=true enforces invocable tools. + /// + [Fact] + public void AsAIAgent_WithAgentVersion_WithRequireInvocableToolsTrue_EnforcesInvocableTools() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentVersion agentVersion = this.CreateTestAgentVersion(); + var tools = new List + { + AIFunctionFactory.Create(() => "test", "test_function", "A test function") + }; + + // Act + var agent = client.AsAIAgent(agentVersion, tools: tools); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + /// + /// Verify that AsAIAgent with requireInvocableTools=false allows declarative functions. + /// + [Fact] + public void AsAIAgent_WithAgentVersion_WithRequireInvocableToolsFalse_AllowsDeclarativeFunctions() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentVersion agentVersion = this.CreateTestAgentVersion(); + + // Act - should not throw even without tools when requireInvocableTools is false + var agent = client.AsAIAgent(agentVersion); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + #endregion + + #region AsAIAgent(AIProjectClient, string) Tests + + /// + /// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null. + /// + [Fact] + public void AsAIAgent_ByName_WithNullClient_ThrowsArgumentNullException() + { + // Arrange + AIProjectClient? client = null; + + // Act & Assert + var exception = Assert.Throws(() => + client!.AsAIAgent("test-agent")); + + Assert.Equal("aiProjectClient", exception.ParamName); + } + + /// + /// Verify that AsAIAgent throws ArgumentNullException when name is null. + /// + [Fact] + public void AsAIAgent_ByName_WithNullName_ThrowsArgumentNullException() + { + // Arrange + var mockClient = new Mock(); + + // Act & Assert + var exception = Assert.Throws(() => + mockClient.Object.AsAIAgent((string)null!)); + + Assert.Equal("name", exception.ParamName); + } + + /// + /// Verify that AsAIAgent throws ArgumentException when name is empty. + /// + [Fact] + public void AsAIAgent_ByName_WithEmptyName_ThrowsArgumentException() + { + // Arrange + var mockClient = new Mock(); + + // Act & Assert + var exception = Assert.Throws(() => + mockClient.Object.AsAIAgent(string.Empty)); + + Assert.Equal("name", exception.ParamName); + } + + #endregion + + #region AsAIAgent(AIProjectClient, AgentRecord) with tools Tests + + /// + /// Verify that AsAIAgent with additional tools when the definition has no tools does not throw and results in an agent with no tools. + /// + [Fact] + public void AsAIAgent_WithAgentRecordAndAdditionalTools_WhenDefinitionHasNoTools_ShouldNotThrow() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentRecord agentRecord = this.CreateTestAgentRecord(); + var tools = new List + { + AIFunctionFactory.Create(() => "test", "test_function", "A test function") + }; + + // Act + var agent = client.AsAIAgent(agentRecord, tools: tools); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + var chatClient = agent.GetService(); + Assert.NotNull(chatClient); + var agentVersion = chatClient.GetService(); + Assert.NotNull(agentVersion); + var definition = Assert.IsType(agentVersion.Definition); + Assert.Empty(definition.Tools); + } + + /// + /// Verify that AsAIAgent with null tools works correctly. + /// + [Fact] + public void AsAIAgent_WithAgentRecordAndNullTools_WorksCorrectly() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentRecord agentRecord = this.CreateTestAgentRecord(); + + // Act + var agent = client.AsAIAgent(agentRecord, tools: null); + + // Assert + Assert.NotNull(agent); + Assert.Equal("agent_abc123", agent.Name); + } + + #endregion + + #region Tool Validation Tests + + /// + /// Verify that when providing AITools with AsAIAgent, any additional tool that doesn't match the tools in agent definition are ignored. + /// + [Fact] + public void AsAIAgent_AdditionalAITools_WhenNotInTheDefinitionAreIgnored() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var agentVersion = this.CreateTestAgentVersion(); + + // Manually add tools to the definition to simulate inline tools + if (agentVersion.Definition is PromptAgentDefinition promptDef) + { + promptDef.Tools.Add(ResponseTool.CreateFunctionTool("inline_tool", BinaryData.FromString("{}"), strictModeEnabled: false)); + } + + var invocableInlineAITool = AIFunctionFactory.Create(() => "test", "inline_tool", "An invocable AIFunction for the inline function"); + var shouldBeIgnoredTool = AIFunctionFactory.Create(() => "test", "additional_tool", "An additional test function that should be ignored"); + + // Act & Assert + var agent = client.AsAIAgent(agentVersion, tools: [invocableInlineAITool, shouldBeIgnoredTool]); + Assert.NotNull(agent); + var version = agent.GetService(); + Assert.NotNull(version); + var definition = Assert.IsType(version.Definition); + Assert.NotEmpty(definition.Tools); + Assert.NotNull(GetAgentChatOptions(agent)); + Assert.NotNull(GetAgentChatOptions(agent)!.Tools); + Assert.Single(GetAgentChatOptions(agent)!.Tools!); + Assert.Equal("inline_tool", (definition.Tools.First() as FunctionTool)?.FunctionName); + } + + #endregion + + #region Inline Tools vs Parameter Tools Tests + + /// + /// Verify that tools passed as parameters are accepted by AsAIAgent. + /// + [Fact] + public void AsAIAgent_WithParameterTools_AcceptsTools() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentRecord agentRecord = this.CreateTestAgentRecord(); + var tools = new List + { + AIFunctionFactory.Create(() => "tool1", "param_tool_1", "First parameter tool"), + AIFunctionFactory.Create(() => "tool2", "param_tool_2", "Second parameter tool") + }; + + // Act + var agent = client.AsAIAgent(agentRecord, tools: tools); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + var chatClient = agent.GetService(); + Assert.NotNull(chatClient); + var agentVersion = chatClient.GetService(); + Assert.NotNull(agentVersion); + } + + #endregion + + #region Declarative Function Handling Tests + + /// + /// Verifies that CreateAIAgent uses tools from definition when they are ResponseTool instances, resulting in successful agent creation. + /// + [Fact] + public async Task CreateAIAgentAsync_WithResponseToolsInDefinition_CreatesAgentSuccessfullyAsync() + { + // Arrange + var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" }; + + var fabricToolOptions = new FabricDataAgentToolOptions(); + fabricToolOptions.ProjectConnections.Add(new ToolProjectConnection("connection-id")); + + var sharepointOptions = new SharePointGroundingToolOptions(); + sharepointOptions.ProjectConnections.Add(new ToolProjectConnection("connection-id")); + + var structuredOutputs = new StructuredOutputDefinition("name", "description", new Dictionary { ["schema"] = BinaryData.FromString(AIJsonUtilities.CreateJsonSchema(new { id = "test" }.GetType()).ToString()) }, false); + + // Add tools to the definition + definition.Tools.Add(ResponseTool.CreateFunctionTool("create_tool", BinaryData.FromString("{}"), strictModeEnabled: false)); + definition.Tools.Add((ResponseTool)AgentTool.CreateBingCustomSearchTool(new BingCustomSearchToolOptions([new BingCustomSearchConfiguration("connection-id", "instance-name")]))); + definition.Tools.Add((ResponseTool)AgentTool.CreateBrowserAutomationTool(new BrowserAutomationToolOptions(new BrowserAutomationToolConnectionParameters("id")))); + definition.Tools.Add(AgentTool.CreateA2ATool(new Uri("https://test-uri.microsoft.com"))); + definition.Tools.Add((ResponseTool)AgentTool.CreateBingGroundingTool(new BingGroundingSearchToolOptions([new BingGroundingSearchConfiguration("connection-id")]))); + definition.Tools.Add((ResponseTool)AgentTool.CreateMicrosoftFabricTool(fabricToolOptions)); + definition.Tools.Add((ResponseTool)AgentTool.CreateOpenApiTool(new OpenApiFunctionDefinition("name", BinaryData.FromString(OpenAPISpec), new OpenAPIAnonymousAuthenticationDetails()))); + definition.Tools.Add((ResponseTool)AgentTool.CreateSharepointTool(sharepointOptions)); + definition.Tools.Add((ResponseTool)AgentTool.CreateStructuredOutputsTool(structuredOutputs)); + definition.Tools.Add((ResponseTool)AgentTool.CreateAzureAISearchTool(new AzureAISearchToolOptions([new AzureAISearchToolIndex() { IndexName = "name" }]))); + + // Generate agent definition response with the tools + var definitionResponse = GeneratePromptDefinitionResponse(definition, definition.Tools.Select(t => t.AsAITool()).ToList()); + + using var testClient = CreateTestAgentClientWithHandler(agentDefinitionResponse: definitionResponse); + + var options = new AgentVersionCreationOptions(definition); + + // Act + var agentVersion = (await testClient.Client.Agents.CreateAgentVersionAsync("test-agent", options)).Value; + var agent = testClient.Client.AsAIAgent(agentVersion); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + var agentVersion2 = agent.GetService()!; + Assert.NotNull(agentVersion); + if (agentVersion2.Definition is PromptAgentDefinition promptDef) + { + Assert.NotEmpty(promptDef.Tools); + Assert.Equal(10, promptDef.Tools.Count); + } + } + + /// + /// Verify that AsAIAgentAsync accepts FunctionTools from definition. + /// + [Fact] + public async Task AsAIAgent_WithFunctionToolsInDefinition_AcceptsDeclarativeFunctionAsync() + { + // Arrange + var functionTool = ResponseTool.CreateFunctionTool( + functionName: "get_user_name", + functionParameters: BinaryData.FromString("{}"), + strictModeEnabled: false, + functionDescription: "Gets the user's name, as used for friendly address." + ); + + var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" }; + definition.Tools.Add(functionTool); + + // Generate response with the declarative function + var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test" }; + definitionResponse.Tools.Add(functionTool); + + using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse); + + var options = new AgentVersionCreationOptions(definition); + + // Act + var agentVersion = (await testClient.Client.Agents.CreateAgentVersionAsync("test-agent", options)).Value; + var agent = testClient.Client.AsAIAgent(agentVersion); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + /// + /// Verify that AsAIAgentAsync accepts declarative functions from definition. + /// + [Fact] + public async Task AsAIAgent_WithDeclarativeFunctionFromDefinition_AcceptsDeclarativeFunctionAsync() + { + // Arrange + using var testClient = CreateTestAgentClientWithHandler(); + var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" }; + + // Create a declarative function (not invocable) using AIFunctionFactory.CreateDeclaration + using var doc = JsonDocument.Parse("{}"); + var declarativeFunction = AIFunctionFactory.CreateDeclaration("test_function", "A test function", doc.RootElement); + + // Add to definition + definition.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException()); + + var options = new AgentVersionCreationOptions(definition); + + // Act + var agentVersion = (await testClient.Client.Agents.CreateAgentVersionAsync("test-agent", options)).Value; + var agent = testClient.Client.AsAIAgent(agentVersion); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + /// + /// Verify that AsAIAgentAsync accepts declarative functions from definition. + /// + [Fact] + public async Task AsAIAgent_WithDeclarativeFunctionInDefinition_AcceptsDeclarativeFunctionAsync() + { + // Arrange + var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" }; + + // Create a declarative function (not invocable) using AIFunctionFactory.CreateDeclaration + using var doc = JsonDocument.Parse("{}"); + var declarativeFunction = AIFunctionFactory.CreateDeclaration("test_function", "A test function", doc.RootElement); + + // Add to definition + definition.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException()); + + // Generate response with the declarative function + var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test" }; + definitionResponse.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException()); + + using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse); + + var options = new AgentVersionCreationOptions(definition); + + // Act + var agentVersion = (await testClient.Client.Agents.CreateAgentVersionAsync("test-agent", options)).Value; + var agent = testClient.Client.AsAIAgent(agentVersion); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + #endregion + + #region AgentName Validation Tests + + /// + /// Verify that AsAIAgent throws ArgumentException when agent name is invalid. + /// + [Theory] + [MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))] + public void AsAIAgent_ByName_WithInvalidAgentName_ThrowsArgumentException(string invalidName) + { + // Arrange + var mockClient = new Mock(); + + // Act & Assert + var exception = Assert.Throws(() => + mockClient.Object.AsAIAgent(invalidName)); + + Assert.Equal("name", exception.ParamName); + Assert.Contains("Agent name must be 1-63 characters long", exception.Message); + } + + /// + /// Verify that AsAIAgent with AgentReference throws ArgumentException when agent name is invalid. + /// + [Theory] + [MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))] + public void AsAIAgent_WithAgentReference_WithInvalidAgentName_ThrowsArgumentException(string invalidName) + { + // Arrange + var mockClient = new Mock(); + var agentReference = new AgentReference(invalidName, "1"); + + // Act & Assert + var exception = Assert.Throws(() => + mockClient.Object.AsAIAgent(agentReference)); + + Assert.Equal("name", exception.ParamName); + Assert.Contains("Agent name must be 1-63 characters long", exception.Message); + } + + #endregion + + #region AzureAIChatClient Behavior Tests + + /// + /// Verify that the underlying chat client created by extension methods can be wrapped with clientFactory. + /// + [Fact] + public void AsAIAgent_WithClientFactory_WrapsUnderlyingChatClient() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentRecord agentRecord = this.CreateTestAgentRecord(); + int factoryCallCount = 0; + + // Act + var agent = client.AsAIAgent( + agentRecord, + clientFactory: (innerClient) => + { + factoryCallCount++; + return new TestChatClient(innerClient); + }); + + // Assert + Assert.NotNull(agent); + Assert.Equal(1, factoryCallCount); + var wrappedClient = agent.GetService(); + Assert.NotNull(wrappedClient); + } + + /// + /// Verify that multiple clientFactory calls create independent wrapped clients. + /// + [Fact] + public void AsAIAgent_MultipleCallsWithClientFactory_CreatesIndependentClients() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentRecord agentRecord = this.CreateTestAgentRecord(); + + // Act + var agent1 = client.AsAIAgent( + agentRecord, + clientFactory: (innerClient) => new TestChatClient(innerClient)); + + var agent2 = client.AsAIAgent( + agentRecord, + clientFactory: (innerClient) => new TestChatClient(innerClient)); + + // Assert + Assert.NotNull(agent1); + Assert.NotNull(agent2); + var client1 = agent1.GetService(); + var client2 = agent2.GetService(); + Assert.NotNull(client1); + Assert.NotNull(client2); + Assert.NotSame(client1, client2); + } + + #endregion + + #region User-Agent Header Tests + + /// + /// Verifies that the MEAI user-agent header is added to Responses API POST requests + /// via the protocol method's RequestOptions pipeline policy. + /// + [Fact] + public async Task AsAIAgent_Rapi_UserAgentHeaderAddedToRequestsAsync() + { + bool userAgentFound = false; + using var httpHandler = new HttpHandlerAssert(request => + { + if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses")) + { + // Verify MEAI user-agent header is present on Responses API POST request + if (request.Headers.TryGetValues("User-Agent", out var userAgentValues) + && userAgentValues.Any(v => v.Contains("MEAI"))) + { + userAgentFound = true; + } + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + TestDataUtil.GetOpenAIDefaultResponseJson(), + Encoding.UTF8, + "application/json") + }; + } + + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}", Encoding.UTF8, "application/json") }; + }); + +#pragma warning disable CA5399 + using var httpClient = new HttpClient(httpHandler); +#pragma warning restore CA5399 + + // Arrange + var aiProjectClient = new AIProjectClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) }); + + var agentOptions = new ChatClientAgentOptions + { + Name = "test-agent", + ChatOptions = new ChatOptions { ModelId = "gpt-4o-mini" } + }; + + // Act + var agent = aiProjectClient.AsAIAgent(agentOptions); + + var response = await agent.RunAsync("Hello"); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(response); + Assert.True(userAgentFound, "MEAI user-agent header was not found in any Responses API request"); + } + + /// + /// Verifies that the MEAI user-agent header is added to Responses API POST requests + /// when using a versioned agent created via CreateAgentVersionAsync. + /// + [Fact] + public async Task AsAIAgent_Versioned_UserAgentHeaderAddedToRequestsAsync() + { + bool userAgentFound = false; + using var httpHandler = new HttpHandlerAssert(request => + { + Assert.Equal("POST", request.Method.Method); + + if (request.RequestUri!.PathAndQuery.Contains("/responses")) + { + // Verify MEAI user-agent header is present on Responses API POST request + Assert.True(request.Headers.TryGetValues("User-Agent", out var userAgentValues)); + Assert.Contains(userAgentValues, v => v.Contains("MEAI")); + userAgentFound = true; + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + TestDataUtil.GetOpenAIDefaultResponseJson(), + Encoding.UTF8, + "application/json") + }; + } + + // CreateAgentVersion POST — return agent version response + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json") }; + }); + +#pragma warning disable CA5399 + using var httpClient = new HttpClient(httpHandler); +#pragma warning restore CA5399 + + // Arrange + var aiProjectClient = new AIProjectClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) }); + + var agentVersion = (await aiProjectClient.Agents.CreateAgentVersionAsync("test-agent", new AgentVersionCreationOptions(new PromptAgentDefinition("test-model") { Instructions = "Test instructions" }))).Value; + + // Act + var agent = aiProjectClient.AsAIAgent(agentVersion); + + var response = await agent.RunAsync("Hello"); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(response); + Assert.True(userAgentFound, "MEAI user-agent header was not found in any Responses API request"); + } + + #endregion + + #region GetAIAgent(AIProjectClient, AgentReference) Tests + + /// + /// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null. + /// + [Fact] + public void AsAIAgent_WithAgentReference_WithNullClient_ThrowsArgumentNullException() + { + // Arrange + AIProjectClient? client = null; + var agentReference = new AgentReference("test-name", "1"); + + // Act & Assert + var exception = Assert.Throws(() => + client!.AsAIAgent(agentReference)); + + Assert.Equal("aiProjectClient", exception.ParamName); + } + + /// + /// Verify that AsAIAgent throws ArgumentNullException when agentReference is null. + /// + [Fact] + public void AsAIAgent_WithAgentReference_WithNullAgentReference_ThrowsArgumentNullException() + { + // Arrange + var mockClient = new Mock(); + + // Act & Assert + var exception = Assert.Throws(() => + mockClient.Object.AsAIAgent((AgentReference)null!)); + + Assert.Equal("agentReference", exception.ParamName); + } + + /// + /// Verify that AsAIAgent with AgentReference creates a valid agent. + /// + [Fact] + public void AsAIAgent_WithAgentReference_CreatesValidAgent() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var agentReference = new AgentReference("test-name", "1"); + + // Act + var agent = client.AsAIAgent(agentReference); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + Assert.Equal("test-name", agent.Name); + Assert.Equal("test-name:1", agent.Id); + Assert.Same(client, agent.GetService()); + } + + /// + /// Verify that AsAIAgent with AgentReference and clientFactory applies the factory. + /// + [Fact] + public void AsAIAgent_WithAgentReference_WithClientFactory_AppliesFactoryCorrectly() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var agentReference = new AgentReference("test-name", "1"); + TestChatClient? testChatClient = null; + + // Act + var agent = client.AsAIAgent( + agentReference, + clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient)); + + // Assert + Assert.NotNull(agent); + var retrievedTestClient = agent.GetService(); + Assert.NotNull(retrievedTestClient); + Assert.Same(testChatClient, retrievedTestClient); + } + + /// + /// Verify that AsAIAgent with AgentReference sets the agent ID correctly. + /// + [Fact] + public void AsAIAgent_WithAgentReference_SetsAgentIdCorrectly() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var agentReference = new AgentReference("test-name", "2"); + + // Act + var agent = client.AsAIAgent(agentReference); + + // Assert + Assert.NotNull(agent); + Assert.Equal("test-name:2", agent.Id); + } + + /// + /// Verify that AsAIAgent with AgentReference and tools includes the tools in ChatOptions. + /// + [Fact] + public void AsAIAgent_WithAgentReference_WithTools_IncludesToolsInChatOptions() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var agentReference = new AgentReference("test-name", "1"); + var tools = new List + { + AIFunctionFactory.Create(() => "test", "test_function", "A test function") + }; + + // Act + var agent = client.AsAIAgent(agentReference, tools: tools); + + // Assert + Assert.NotNull(agent); + var chatOptions = GetAgentChatOptions(agent); + Assert.NotNull(chatOptions); + Assert.NotNull(chatOptions.Tools); + Assert.Single(chatOptions.Tools); + } + + #endregion + + #region GetService Tests + + /// + /// Verify that GetService returns AgentRecord for agents created from AgentRecord. + /// + [Fact] + public void GetService_WithAgentRecord_ReturnsAgentRecord() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentRecord agentRecord = this.CreateTestAgentRecord(); + + // Act + var agent = client.AsAIAgent(agentRecord); + var retrievedRecord = agent.GetService(); + + // Assert + Assert.NotNull(retrievedRecord); + Assert.Equal(agentRecord.Id, retrievedRecord.Id); + } + + /// + /// Verify that GetService returns null for AgentRecord when agent is created from AgentReference. + /// + [Fact] + public void GetService_WithAgentReference_ReturnsNullForAgentRecord() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var agentReference = new AgentReference("test-name", "1"); + + // Act + var agent = client.AsAIAgent(agentReference); + var retrievedRecord = agent.GetService(); + + // Assert + Assert.Null(retrievedRecord); + } + + #endregion + + #region GetService Tests + + /// + /// Verify that GetService returns AgentVersion for agents created from AgentVersion. + /// + [Fact] + public void GetService_WithAgentVersion_ReturnsAgentVersion() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentVersion agentVersion = this.CreateTestAgentVersion(); + + // Act + var agent = client.AsAIAgent(agentVersion); + var retrievedVersion = agent.GetService(); + + // Assert + Assert.NotNull(retrievedVersion); + Assert.Equal(agentVersion.Id, retrievedVersion.Id); + } + + /// + /// Verify that GetService returns null for AgentVersion when agent is created from AgentReference. + /// + [Fact] + public void GetService_WithAgentReference_ReturnsNullForAgentVersion() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var agentReference = new AgentReference("test-name", "1"); + + // Act + var agent = client.AsAIAgent(agentReference); + var retrievedVersion = agent.GetService(); + + // Assert + Assert.Null(retrievedVersion); + } + + #endregion + + #region ChatClientMetadata Tests + + /// + /// Verify that ChatClientMetadata is properly populated for agents created from AgentRecord. + /// + [Fact] + public void ChatClientMetadata_WithAgentRecord_IsPopulatedCorrectly() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentRecord agentRecord = this.CreateTestAgentRecord(); + + // Act + var agent = client.AsAIAgent(agentRecord); + var metadata = agent.GetService(); + + // Assert + Assert.NotNull(metadata); + Assert.NotNull(metadata.DefaultModelId); + } + + /// + /// Verify that ChatClientMetadata.DefaultModelId is set from PromptAgentDefinition model property. + /// + [Fact] + public void ChatClientMetadata_WithPromptAgentDefinition_SetsDefaultModelIdFromModel() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var definition = new PromptAgentDefinition("gpt-4-turbo") + { + Instructions = "Test instructions" + }; + AgentRecord agentRecord = this.CreateTestAgentRecord(definition); + + // Act + var agent = client.AsAIAgent(agentRecord); + var metadata = agent.GetService(); + + // Assert + Assert.NotNull(metadata); + // The metadata should contain the model information from the agent definition + Assert.NotNull(metadata.DefaultModelId); + Assert.Equal("gpt-4-turbo", metadata.DefaultModelId); + } + + /// + /// Verify that ChatClientMetadata is properly populated for agents created from AgentVersion. + /// + [Fact] + public void ChatClientMetadata_WithAgentVersion_IsPopulatedCorrectly() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentVersion agentVersion = this.CreateTestAgentVersion(); + + // Act + var agent = client.AsAIAgent(agentVersion); + var metadata = agent.GetService(); + + // Assert + Assert.NotNull(metadata); + Assert.NotNull(metadata.DefaultModelId); + Assert.Equal((agentVersion.Definition as PromptAgentDefinition)!.Model, metadata.DefaultModelId); + } + + #endregion + + #region AgentReference Availability Tests + + /// + /// Verify that GetService returns AgentReference for agents created from AgentReference. + /// + [Fact] + public void GetService_WithAgentReference_ReturnsAgentReference() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var agentReference = new AgentReference("test-agent", "1.0"); + + // Act + var agent = client.AsAIAgent(agentReference); + var retrievedReference = agent.GetService(); + + // Assert + Assert.NotNull(retrievedReference); + Assert.Equal("test-agent", retrievedReference.Name); + Assert.Equal("1.0", retrievedReference.Version); + } + + /// + /// Verify that GetService returns null for AgentReference when agent is created from AgentRecord. + /// + [Fact] + public void GetService_WithAgentRecord_ReturnsAlsoAgentReference() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentRecord agentRecord = this.CreateTestAgentRecord(); + + // Act + var agent = client.AsAIAgent(agentRecord); + var retrievedReference = agent.GetService(); + + // Assert + Assert.NotNull(retrievedReference); + Assert.Equal(agentRecord.Name, retrievedReference.Name); + } + + /// + /// Verify that GetService returns null for AgentReference when agent is created from AgentVersion. + /// + [Fact] + public void GetService_WithAgentVersion_ReturnsAlsoAgentReference() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + AgentVersion agentVersion = this.CreateTestAgentVersion(); + + // Act + var agent = client.AsAIAgent(agentVersion); + var retrievedReference = agent.GetService(); + + // Assert + Assert.NotNull(retrievedReference); + Assert.Equal(agentVersion.Name, retrievedReference.Name); + } + + /// + /// Verify that GetService returns AgentReference with correct version information. + /// + [Fact] + public void GetService_WithAgentReference_ReturnsCorrectVersionInformation() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var agentReference = new AgentReference("versioned-agent", "3.5"); + + // Act + var agent = client.AsAIAgent(agentReference); + var retrievedReference = agent.GetService(); + + // Assert + Assert.NotNull(retrievedReference); + Assert.Equal("versioned-agent", retrievedReference.Name); + Assert.Equal("3.5", retrievedReference.Version); + } + + #endregion + + #region Empty Version and ID Handling Tests + + /// + /// Verify that AsAIAgent with AgentRecord handles empty version by using "latest" as fallback. + /// + [Fact] + public void AsAIAgent_WithAgentRecordEmptyVersion_CreatesAgentWithGeneratedId() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion(); + AgentRecord agentRecord = this.CreateTestAgentRecordWithEmptyVersion(); + + // Act + var agent = client.AsAIAgent(agentRecord); + + // Assert + Assert.NotNull(agent); + // Verify the agent ID is generated from agent record name ("agent_abc123") and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + /// + /// Verify that AsAIAgent with AgentVersion handles empty version by using "latest" as fallback. + /// + [Fact] + public void AsAIAgent_WithAgentVersionEmptyVersion_CreatesAgentWithGeneratedId() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion(); + AgentVersion agentVersion = this.CreateTestAgentVersionWithEmptyVersion(); + + // Act + var agent = client.AsAIAgent(agentVersion); + + // Assert + Assert.NotNull(agent); + // Verify the agent ID is generated from agent version name ("agent_abc123") and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + /// + /// Verify that AsAIAgent with AgentRecord handles whitespace-only version by using "latest" as fallback. + /// + [Fact] + public void AsAIAgent_WithAgentRecordWhitespaceVersion_CreatesAgentWithGeneratedId() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion(); + AgentRecord agentRecord = this.CreateTestAgentRecordWithWhitespaceVersion(); + + // Act + var agent = client.AsAIAgent(agentRecord); + + // Assert + Assert.NotNull(agent); + // Verify the agent ID is generated from agent record name ("agent_abc123") and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + /// + /// Verify that AsAIAgent with AgentVersion handles whitespace-only version by using "latest" as fallback. + /// + [Fact] + public void AsAIAgent_WithAgentVersionWhitespaceVersion_CreatesAgentWithGeneratedId() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion(); + AgentVersion agentVersion = this.CreateTestAgentVersionWithWhitespaceVersion(); + + // Act + var agent = client.AsAIAgent(agentVersion); + + // Assert + Assert.NotNull(agent); + // Verify the agent ID is generated from agent version name ("agent_abc123") and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + #endregion + + #region ApplyToolsToAgentDefinition Tests + + /// + /// Verify that when AsAIAgent is called without requireInvocableTools, hosted tools are correctly added. + /// + [Fact] + public void AsAIAgent_WithServerHostedTools_AddsToolsToAgentOptions() + { + // Arrange + PromptAgentDefinition definition = new("test-model") { Instructions = "Test" }; + definition.Tools.Add(new HostedWebSearchTool().GetService() ?? new HostedWebSearchTool().AsOpenAIResponseTool()); + + AIProjectClient client = this.CreateTestAgentClient(); + AgentVersion agentVersion = ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson(agentDefinition: definition)))!; + + // Act - no tools provided, but requireInvocableTools is false when no tools param is passed + FoundryAgent agent = client.AsAIAgent(agentVersion); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + #endregion + + #region Helper Methods + + /// + /// Creates a test AIProjectClient with fake behavior. + /// + private FakeAgentClient CreateTestAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) + { + return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse); + } + + /// + /// Creates a test AIProjectClient backed by an HTTP handler that returns canned responses. + /// Used for tests that exercise the protocol-method code path (CreateAgentVersion). + /// The returned client must be disposed to clean up the underlying HttpClient/handler. + /// + private static DisposableTestClient CreateTestAgentClientWithHandler(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) + { + var responseJson = TestDataUtil.GetAgentVersionResponseJson(agentName, agentDefinitionResponse, instructions, description); + + var httpHandler = new HttpHandlerAssert(_ => + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(responseJson, Encoding.UTF8, "application/json") }); + +#pragma warning disable CA5399 + var httpClient = new HttpClient(httpHandler); +#pragma warning restore CA5399 + + var client = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new() { Transport = new HttpClientPipelineTransport(httpClient) }); + + return new DisposableTestClient(client, httpClient, httpHandler); + } + + /// + /// Wraps an AIProjectClient and its disposable dependencies for deterministic cleanup. + /// + private sealed class DisposableTestClient : IDisposable + { + private readonly HttpClient _httpClient; + private readonly HttpHandlerAssert _httpHandler; + + public DisposableTestClient(AIProjectClient client, HttpClient httpClient, HttpHandlerAssert httpHandler) + { + this.Client = client; + this._httpClient = httpClient; + this._httpHandler = httpHandler; + } + + public AIProjectClient Client { get; } + + public void Dispose() + { + this._httpClient.Dispose(); + this._httpHandler.Dispose(); + } + } + + /// + /// Creates a test AgentRecord for testing. + /// + private AgentRecord CreateTestAgentRecord(AgentDefinition? agentDefinition = null) + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJson(agentDefinition: agentDefinition)))!; + } + + /// + /// Creates a test AIProjectClient with empty version fields for testing hosted MCP agents. + /// + private FakeAgentClient CreateTestAgentClientWithEmptyVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) + { + return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, useEmptyVersion: true); + } + + /// + /// Creates a test AgentRecord with empty version for testing hosted MCP agents. + /// + private AgentRecord CreateTestAgentRecordWithEmptyVersion(AgentDefinition? agentDefinition = null) + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithEmptyVersion(agentDefinition: agentDefinition)))!; + } + + /// + /// Creates a test AgentVersion with empty version for testing hosted MCP agents. + /// + private AgentVersion CreateTestAgentVersionWithEmptyVersion() + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion()))!; + } + + /// + /// Creates a test AIProjectClient with whitespace-only version fields for testing hosted MCP agents. + /// + private FakeAgentClient CreateTestAgentClientWithWhitespaceVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) + { + return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, versionMode: VersionMode.Whitespace); + } + + /// + /// Creates a test AgentRecord with whitespace-only version for testing hosted MCP agents. + /// + private AgentRecord CreateTestAgentRecordWithWhitespaceVersion(AgentDefinition? agentDefinition = null) + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(agentDefinition: agentDefinition)))!; + } + + /// + /// Creates a test AgentVersion with whitespace-only version for testing hosted MCP agents. + /// + private AgentVersion CreateTestAgentVersionWithWhitespaceVersion() + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion()))!; + } + + private const string OpenAPISpec = """ + { + "openapi": "3.0.3", + "info": { "title": "Tiny Test API", "version": "1.0.0" }, + "paths": { + "/ping": { + "get": { + "summary": "Health check", + "operationId": "getPing", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "message": { "type": "string" } }, + "required": ["message"] + }, + "example": { "message": "pong" } + } + } + } + } + } + } + } + } + """; + + /// + /// Creates a test AgentVersion for testing. + /// + private AgentVersion CreateTestAgentVersion() + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))!; + } + + /// + /// Specifies the version mode for test data generation. + /// + private enum VersionMode + { + Normal, + Empty, + Whitespace + } + + /// + /// Fake AIProjectClient for testing. + /// + private sealed class FakeAgentClient : AIProjectClient + { + public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false, VersionMode versionMode = VersionMode.Normal) + { + // Handle backward compatibility with bool parameter + var effectiveVersionMode = useEmptyVersion ? VersionMode.Empty : versionMode; + this.Agents = new FakeAgentsClient(agentName, instructions, description, agentDefinitionResponse, effectiveVersionMode); + } + + public override ClientConnection GetConnection(string connectionId) + { + return new ClientConnection("fake-connection-id", "http://localhost", ClientPipeline.Create(), CredentialKind.None); + } + + public override AgentsClient Agents { get; } + + private sealed class FakeAgentsClient : AgentsClient + { + private readonly string? _agentName; + private readonly string? _instructions; + private readonly string? _description; + private readonly AgentDefinition? _agentDefinition; + private readonly VersionMode _versionMode; + + public FakeAgentsClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, VersionMode versionMode = VersionMode.Normal) + { + this._agentName = agentName; + this._instructions = instructions; + this._description = description; + this._agentDefinition = agentDefinitionResponse; + this._versionMode = versionMode; + } + + private string GetAgentResponseJson() + { + return this._versionMode switch + { + VersionMode.Empty => TestDataUtil.GetAgentResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description), + VersionMode.Whitespace => TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(this._agentName, this._agentDefinition, this._instructions, this._description), + _ => TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description) + }; + } + + private string GetAgentVersionResponseJson() + { + return this._versionMode switch + { + VersionMode.Empty => TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description), + VersionMode.Whitespace => TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion(this._agentName, this._agentDefinition, this._instructions, this._description), + _ => TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description) + }; + } + + public override ClientResult GetAgent(string agentName, RequestOptions options) + { + var responseJson = this.GetAgentResponseJson(); + return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))); + } + + public override ClientResult GetAgent(string agentName, CancellationToken cancellationToken = default) + { + var responseJson = this.GetAgentResponseJson(); + return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)); + } + + public override Task GetAgentAsync(string agentName, RequestOptions options) + { + var responseJson = this.GetAgentResponseJson(); + return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)))); + } + + public override Task> GetAgentAsync(string agentName, CancellationToken cancellationToken = default) + { + var responseJson = this.GetAgentResponseJson(); + return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200))); + } + + public override ClientResult CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default) + { + var responseJson = this.GetAgentVersionResponseJson(); + return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)); + } + + public override Task> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default) + { + var responseJson = this.GetAgentVersionResponseJson(); + return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200))); + } + } + } + + private static PromptAgentDefinition GeneratePromptDefinitionResponse(PromptAgentDefinition inputDefinition, List? tools) + { + var definitionResponse = new PromptAgentDefinition(inputDefinition.Model) { Instructions = inputDefinition.Instructions }; + if (tools is not null) + { + foreach (var tool in tools) + { + definitionResponse.Tools.Add(tool.GetService() ?? tool.AsOpenAIResponseTool()); + } + } + + return definitionResponse; + } + + /// + /// Test custom chat client that can be used to verify clientFactory functionality. + /// + private sealed class TestChatClient : DelegatingChatClient + { + public TestChatClient(IChatClient innerClient) : base(innerClient) + { + } + } + + /// + /// Mock pipeline response for testing ClientResult wrapping. + /// + private sealed class MockPipelineResponse : PipelineResponse + { + private readonly MockPipelineResponseHeaders _headers; + + public MockPipelineResponse(int status, BinaryData? content = null) + { + this.Status = status; + this.Content = content ?? BinaryData.Empty; + this._headers = new MockPipelineResponseHeaders(); + } + + public override int Status { get; } + + public override string ReasonPhrase => "OK"; + + public override Stream? ContentStream + { + get => null; + set { } + } + + public override BinaryData Content { get; } + + protected override PipelineResponseHeaders HeadersCore => this._headers; + + public override BinaryData BufferContent(CancellationToken cancellationToken = default) => + throw new NotSupportedException("Buffering content is not supported for mock responses."); + + public override ValueTask BufferContentAsync(CancellationToken cancellationToken = default) => + throw new NotSupportedException("Buffering content asynchronously is not supported for mock responses."); + + public override void Dispose() + { + } + + private sealed class MockPipelineResponseHeaders : PipelineResponseHeaders + { + private readonly Dictionary _headers = new(StringComparer.OrdinalIgnoreCase) + { + { "Content-Type", "application/json" }, + { "x-ms-request-id", "test-request-id" } + }; + + public override bool TryGetValue(string name, out string? value) + { + return this._headers.TryGetValue(name, out value); + } + + public override bool TryGetValues(string name, out IEnumerable? values) + { + if (this._headers.TryGetValue(name, out var value)) + { + values = [value]; + return true; + } + + values = null; + return false; + } + + public override IEnumerator> GetEnumerator() + { + return this._headers.GetEnumerator(); + } + } + } + + #endregion + + /// + /// Helper method to access internal ChatOptions property via reflection. + /// + private static ChatOptions? GetAgentChatOptions(AIAgent agent) + { + ChatClientAgent? chatClientAgent = agent as ChatClientAgent ?? agent.GetService(); + if (chatClientAgent is null) + { + return null; + } + + var chatOptionsProperty = typeof(ChatClientAgent).GetProperty( + "ChatOptions", + System.Reflection.BindingFlags.Public | + System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Instance); + + return chatOptionsProperty?.GetValue(chatClientAgent) as ChatOptions; + } + + /// + /// Test schema for JSON response format tests. + /// +#pragma warning disable CA1812 // Avoid uninstantiated internal classes - used via reflection by AIJsonUtilities + private sealed class TestSchema + { + public string? Name { get; set; } + public int Value { get; set; } + } +#pragma warning restore CA1812 +#pragma warning restore CS0618 + +} + +/// +/// Provides test data for invalid agent name validation tests. +/// +internal static class InvalidAgentNameTestData +{ + /// + /// Gets a collection of invalid agent names for theory-based testing. + /// + /// Collection of invalid agent name test cases. + public static IEnumerable GetInvalidAgentNames() + { + yield return new object[] { "-agent" }; + yield return new object[] { "agent-" }; + yield return new object[] { "agent_name" }; + yield return new object[] { "agent name" }; + yield return new object[] { "agent@name" }; + yield return new object[] { "agent#name" }; + yield return new object[] { "agent$name" }; + yield return new object[] { "agent%name" }; + yield return new object[] { "agent&name" }; + yield return new object[] { "agent*name" }; + yield return new object[] { "agent.name" }; + yield return new object[] { "agent/name" }; + yield return new object[] { "agent\\name" }; + yield return new object[] { "agent:name" }; + yield return new object[] { "agent;name" }; + yield return new object[] { "agent,name" }; + yield return new object[] { "agentname" }; + yield return new object[] { "agent?name" }; + yield return new object[] { "agent!name" }; + yield return new object[] { "agent~name" }; + yield return new object[] { "agent`name" }; + yield return new object[] { "agent^name" }; + yield return new object[] { "agent|name" }; + yield return new object[] { "agent[name" }; + yield return new object[] { "agent]name" }; + yield return new object[] { "agent{name" }; + yield return new object[] { "agent}name" }; + yield return new object[] { "agent(name" }; + yield return new object[] { "agent)name" }; + yield return new object[] { "agent+name" }; + yield return new object[] { "agent=name" }; + yield return new object[] { "a" + new string('b', 63) }; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientTests.cs similarity index 82% rename from dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientTests.cs index 8582ccc2b6..e3461d8191 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientTests.cs @@ -6,33 +6,35 @@ using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using Azure.AI.Extensions.OpenAI; using Azure.AI.Projects; -namespace Microsoft.Agents.AI.AzureAI.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests; #pragma warning disable CS0618 -[Obsolete("Uses obsolete AIProjectClient.GetAIAgentAsync compatibility extensions while validating chat-client behavior.")] public class AzureAIProjectChatClientTests { /// - /// Verify that when the ChatOptions has a "conv_" prefixed conversation ID, the chat client uses conversation in the http requests via the chat client + /// Verify that after the first RunAsync, the session's ConversationId is set from the + /// response, and subsequent requests include that conversation ID automatically. /// [Fact] public async Task ChatClient_UsesDefaultConversationIdAsync() { // Arrange - var requestTriggered = false; + var responsesRequestCount = 0; using var httpHandler = new HttpHandlerAssert(async (request) => { if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses")) { - requestTriggered = true; + responsesRequestCount++; - // Assert - if (request.Content is not null) + // Assert: On the second Responses API call, verify the conversation ID + // from the first response is automatically included in the request body. + if (responsesRequestCount == 2 && request.Content is not null) { var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false); - Assert.Contains("conv_12345", requestBody); + Assert.Contains("resp_0888a", requestBody); } return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") }; @@ -50,20 +52,17 @@ public class AzureAIProjectChatClientTests new FakeAuthenticationTokenProvider(), new AIProjectClientOptions() { Transport = new HttpClientPipelineTransport(httpClient) }); - var agent = await projectClient.GetAIAgentAsync( - new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new() { Instructions = "Test instructions", ConversationId = "conv_12345" } - }); + var agent = projectClient.AsAIAgent(new AgentReference("agent-name")); // Act var session = await agent.CreateSessionAsync(); await agent.RunAsync("Hello", session); + await agent.RunAsync("Follow up", session); - Assert.True(requestTriggered); + // Assert + Assert.Equal(2, responsesRequestCount); var chatClientSession = Assert.IsType(session); - Assert.Equal("conv_12345", chatClientSession.ConversationId); + Assert.Equal("resp_0888a46cbf2b1ff3006914596e05d08195a77c3f5187b769a7", chatClientSession.ConversationId); } /// @@ -102,12 +101,7 @@ public class AzureAIProjectChatClientTests new FakeAuthenticationTokenProvider(), new AIProjectClientOptions() { Transport = new HttpClientPipelineTransport(httpClient) }); - var agent = await projectClient.GetAIAgentAsync( - new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new() { Instructions = "Test instructions" }, - }); + var agent = projectClient.AsAIAgent(new AgentReference("agent-name")); // Act var session = await agent.CreateSessionAsync(); @@ -154,12 +148,7 @@ public class AzureAIProjectChatClientTests new FakeAuthenticationTokenProvider(), new AIProjectClientOptions() { Transport = new HttpClientPipelineTransport(httpClient) }); - var agent = await projectClient.GetAIAgentAsync( - new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new() { Instructions = "Test instructions", ConversationId = "conv_should_not_use_default" } - }); + var agent = projectClient.AsAIAgent(new AgentReference("agent-name")); // Act var session = await agent.CreateSessionAsync(); @@ -206,12 +195,7 @@ public class AzureAIProjectChatClientTests new FakeAuthenticationTokenProvider(), new AIProjectClientOptions() { Transport = new HttpClientPipelineTransport(httpClient) }); - var agent = await projectClient.GetAIAgentAsync( - new ChatClientAgentOptions - { - Name = "test-agent", - ChatOptions = new() { Instructions = "Test instructions" }, - }); + var agent = projectClient.AsAIAgent(new AgentReference("agent-name")); // Act var session = await agent.CreateSessionAsync(); diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/FakeAuthenticationTokenProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FakeAuthenticationTokenProvider.cs similarity index 95% rename from dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/FakeAuthenticationTokenProvider.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FakeAuthenticationTokenProvider.cs index d37ed881ff..594ed85af3 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/FakeAuthenticationTokenProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FakeAuthenticationTokenProvider.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.Agents.AI.AzureAI.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests; internal sealed class FakeAuthenticationTokenProvider : AuthenticationTokenProvider { diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/FoundryAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/FoundryAgentTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentTests.cs index 1b77e8f57e..1ddc8c7c82 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/FoundryAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentTests.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Azure.AI.Projects; using Microsoft.Extensions.AI; -namespace Microsoft.Agents.AI.AzureAI.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests; /// /// Unit tests for the class. diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/HttpHandlerAssert.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/HttpHandlerAssert.cs similarity index 96% rename from dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/HttpHandlerAssert.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/HttpHandlerAssert.cs index 3b8025ed9e..0febf216b4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/HttpHandlerAssert.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/HttpHandlerAssert.cs @@ -5,7 +5,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.Agents.AI.AzureAI.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests; internal sealed class HttpHandlerAssert : HttpClientHandler { diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Memory/FoundryMemoryProviderTests.cs similarity index 98% rename from dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Memory/FoundryMemoryProviderTests.cs index 226596a374..b1696d3162 100644 --- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Memory/FoundryMemoryProviderTests.cs @@ -2,7 +2,7 @@ using System; -namespace Microsoft.Agents.AI.FoundryMemory.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests.Memory; /// /// Tests for constructor validation. diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Memory/TestableAIProjectClient.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Memory/TestableAIProjectClient.cs index 25c041f754..f1c4c75718 100644 --- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Memory/TestableAIProjectClient.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using Azure.AI.Projects; using Azure.Core; -namespace Microsoft.Agents.AI.FoundryMemory.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests.Memory; /// /// Creates a testable AIProjectClient with a mock HTTP handler. diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj similarity index 65% rename from dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj index 193a7d47da..7b85de0384 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj @@ -1,7 +1,12 @@ - + + + + + + diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/ProjectResponsesClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/ProjectResponsesClientExtensionsTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/ProjectResponsesClientExtensionsTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/ProjectResponsesClientExtensionsTests.cs index d10ef861c9..fda9962e12 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/ProjectResponsesClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/ProjectResponsesClientExtensionsTests.cs @@ -6,7 +6,7 @@ using Azure.AI.Extensions.OpenAI; using Microsoft.Extensions.AI; using OpenAI.Responses; -namespace Microsoft.Agents.AI.AzureAI.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests; /// /// Unit tests for the class. diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestData/AgentResponse.json b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/AgentResponse.json similarity index 100% rename from dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestData/AgentResponse.json rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/AgentResponse.json diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestData/AgentVersionResponse.json b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/AgentVersionResponse.json similarity index 100% rename from dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestData/AgentVersionResponse.json rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/AgentVersionResponse.json diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestData/OpenAIDefaultResponse.json b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/OpenAIDefaultResponse.json similarity index 100% rename from dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestData/OpenAIDefaultResponse.json rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/OpenAIDefaultResponse.json diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestDataUtil.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestDataUtil.cs index 9cd2ecea46..0a541f5562 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestDataUtil.cs @@ -4,7 +4,7 @@ using System.ClientModel.Primitives; using System.IO; using Azure.AI.Projects.Agents; -namespace Microsoft.Agents.AI.AzureAI.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests; /// /// Utility class for loading and processing test data files. diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj deleted file mode 100644 index af184142ca..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - True - True - - - - - - - - - - - - - - - - diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj deleted file mode 100644 index 1fe8dc57bd..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - false - - - - - - - - - - - - diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj index 37c0fa98cf..bc503de675 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj @@ -10,7 +10,7 @@ - +