mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Declarative Agents (#2476)
* Update the declarative agent samples * Add the Microsoft.Agents.AI.Declarative project * Make the package non packable * Use the RecalcEngine when creating the ChatOptions * Ignore VSTHRD200 * Add geting started samples * Address code review feedback
This commit is contained in:
committed by
GitHub
Unverified
parent
a3909b8b5b
commit
b26108975d
@@ -1,9 +1,9 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
|
||||
model:
|
||||
id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME
|
||||
id: gpt-4o-mini
|
||||
provider: AzureOpenAI
|
||||
apiType: Assistants
|
||||
options:
|
||||
@@ -12,14 +12,14 @@ model:
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
|
||||
model:
|
||||
id: gpt-4o-mini
|
||||
provider: AzureOpenAI
|
||||
apiType: Chat
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -1,28 +1,25 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
|
||||
model:
|
||||
id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME
|
||||
id: gpt-4o-mini
|
||||
provider: AzureOpenAI
|
||||
apiType: Responses
|
||||
options:
|
||||
text:
|
||||
verbosity: medium
|
||||
connection:
|
||||
kind: remote
|
||||
endpoint: =Env.AZURE_OPENAI_ENDPOINT
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format.
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
|
||||
model:
|
||||
options:
|
||||
temperature: 0.9
|
||||
@@ -9,10 +9,10 @@ model:
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
|
||||
@@ -4,6 +4,8 @@ description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions using the tools provided.
|
||||
model:
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
allowMultipleToolCalls: true
|
||||
chatToolMode: auto
|
||||
tools:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
|
||||
model:
|
||||
id: gpt-4.1-mini
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: Remote
|
||||
endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
@@ -1,30 +1,28 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
|
||||
model:
|
||||
id: =Env.OPENAI_MODEL
|
||||
id: gpt-4.1-mini
|
||||
provider: OpenAI
|
||||
apiType: Assistants
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: key
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_APIKEY
|
||||
outputSchema:
|
||||
name: AssistantResponse
|
||||
description: The response from the assistant.
|
||||
properties:
|
||||
language:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
|
||||
model:
|
||||
id: gpt-4.1-mini
|
||||
provider: OpenAI
|
||||
apiType: Chat
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_APIKEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -1,28 +1,28 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
|
||||
model:
|
||||
id: =Env.OPENAI_MODEL
|
||||
id: gpt-4.1-mini
|
||||
provider: OpenAI
|
||||
apiType: Responses
|
||||
options:
|
||||
text:
|
||||
verbosity: medium
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: key
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_APIKEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
kind: string
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
|
||||
@@ -78,6 +78,10 @@
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Agent_Step16_ChatReduction.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Agent_Step17_BackgroundResponses.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Agent_Step18_DeepResearch.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step19_Declarative/Agent_Step19_Declarative.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/DeclarativeAgents/">
|
||||
<Project Path="samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/DevUI/">
|
||||
<File Path="samples/GettingStarted/DevUI/README.md" />
|
||||
@@ -343,12 +347,13 @@
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
@@ -384,11 +389,12 @@
|
||||
<Folder Name="/Tests/UnitTests/">
|
||||
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Declarative\Microsoft.Agents.AI.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create an agent from a YAML based declarative representation.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
// Create the chat client
|
||||
IChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsIChatClient();
|
||||
|
||||
// Define the agent using a YAML definition.
|
||||
var text =
|
||||
"""
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
|
||||
model:
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
""";
|
||||
|
||||
// Create the agent from the YAML definition.
|
||||
var agentFactory = new ChatClientPromptAgentFactory(chatClient);
|
||||
var agent = await agentFactory.CreateFromYamlAsync(text);
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent!.RunAsync("Tell me a joke about a pirate in English."));
|
||||
|
||||
// Invoke the agent with streaming support.
|
||||
await foreach (var update in agent!.RunStreamingAsync("Tell me a joke about a pirate in French."))
|
||||
{
|
||||
Console.WriteLine(update);
|
||||
}
|
||||
@@ -45,6 +45,7 @@ Before you begin, ensure you have the following prerequisites:
|
||||
|[Reducing chat history size](./Agent_Step16_ChatReduction/)|This sample demonstrates how to reduce the chat history to constrain its size, where chat history is maintained locally|
|
||||
|[Background responses](./Agent_Step17_BackgroundResponses/)|This sample demonstrates how to use background responses for long-running operations with polling and resumption support|
|
||||
|[Deep research with an agent](./Agent_Step18_DeepResearch/)|This sample demonstrates how to use the Deep Research Tool to perform comprehensive research on complex topics|
|
||||
|[Declarative agent](./Agent_Step19_Declarative/)|This sample demonstrates how to declaratively define an agent.|
|
||||
|
||||
## Running the samples from the console
|
||||
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Declarative\Microsoft.Agents.AI.Declarative.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to load an AI agent from a YAML file and process a prompt using Azure OpenAI as the backend.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
// Create the chat client
|
||||
IChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsIChatClient();
|
||||
|
||||
// Read command-line arguments
|
||||
if (args.Length < 2)
|
||||
{
|
||||
Console.WriteLine("Usage: DeclarativeAgents <yaml-file-path> <prompt>");
|
||||
Console.WriteLine(" <yaml-file-path>: The path to the YAML file containing the agent definition");
|
||||
Console.WriteLine(" <prompt>: The prompt to send to the agent");
|
||||
return;
|
||||
}
|
||||
|
||||
var yamlFilePath = args[0];
|
||||
var prompt = args[1];
|
||||
|
||||
// Verify the YAML file exists
|
||||
if (!File.Exists(yamlFilePath))
|
||||
{
|
||||
Console.WriteLine($"Error: File not found: {yamlFilePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the YAML content from the file
|
||||
var text = await File.ReadAllTextAsync(yamlFilePath);
|
||||
|
||||
// Example function tool that can be used by the agent.
|
||||
[Description("Get the weather for a given location.")]
|
||||
static string GetWeather(
|
||||
[Description("The city and state, e.g. San Francisco, CA")] string location,
|
||||
[Description("The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.")] string unit)
|
||||
=> $"The weather in {location} is cloudy with a high of {(unit.Equals("celsius", StringComparison.Ordinal) ? "15°C" : "59°F")}.";
|
||||
|
||||
// Create the agent from the YAML definition.
|
||||
var agentFactory = new ChatClientPromptAgentFactory(chatClient, [AIFunctionFactory.Create(GetWeather, "GetWeather")]);
|
||||
var agent = await agentFactory.CreateFromYamlAsync(text);
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent!.RunAsync(prompt));
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"GetWeather": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\chatclient\\GetWeather.yaml \"What is the weather in Cambridge, MA in °C?\""
|
||||
},
|
||||
"Assistant": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\chatclient\\Assistant.yaml \"Tell me a joke about a pirate in Italian.\""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.Bot.ObjectModel.Yaml;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for creating <see cref="BotElement"/> from YAML.
|
||||
/// </summary>
|
||||
internal static class AgentBotElementYaml
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert the given YAML text to a <see cref="GptComponentMetadata"/> model.
|
||||
/// </summary>
|
||||
/// <param name="text">YAML representation of the <see cref="BotElement"/> to use to create the prompt function.</param>
|
||||
/// <param name="configuration">Optional <see cref="IConfiguration"/> instance which provides environment variables to the template.</param>
|
||||
[RequiresDynamicCode("Calls YamlDotNet.Serialization.DeserializerBuilder.DeserializerBuilder()")]
|
||||
public static GptComponentMetadata FromYaml(string text, IConfiguration? configuration = null)
|
||||
{
|
||||
Throw.IfNullOrEmpty(text);
|
||||
|
||||
using var yamlReader = new StringReader(text);
|
||||
BotElement rootElement = YamlSerializer.Deserialize<BotElement>(yamlReader) ?? throw new InvalidDataException("Text does not contain a valid agent definition.");
|
||||
|
||||
if (rootElement is not GptComponentMetadata promptAgent)
|
||||
{
|
||||
throw new InvalidDataException($"Unsupported root element: {rootElement.GetType().Name}. Expected an {nameof(GptComponentMetadata)}.");
|
||||
}
|
||||
|
||||
var botDefinition = WrapPromptAgentWithBot(promptAgent, configuration);
|
||||
|
||||
return botDefinition.Descendants().OfType<GptComponentMetadata>().First();
|
||||
}
|
||||
|
||||
#region private
|
||||
private sealed class AgentFeatureConfiguration : IFeatureConfiguration
|
||||
{
|
||||
public long GetInt64Value(string settingName, long defaultValue) => defaultValue;
|
||||
|
||||
public string GetStringValue(string settingName, string defaultValue) => defaultValue;
|
||||
|
||||
public bool IsEnvironmentFeatureEnabled(string featureName, bool defaultValue) => true;
|
||||
|
||||
public bool IsTenantFeatureEnabled(string featureName, bool defaultValue) => defaultValue;
|
||||
}
|
||||
|
||||
public static BotDefinition WrapPromptAgentWithBot(this GptComponentMetadata element, IConfiguration? configuration = null)
|
||||
{
|
||||
var botBuilder =
|
||||
new BotDefinition.Builder
|
||||
{
|
||||
Components =
|
||||
{
|
||||
new GptComponent.Builder
|
||||
{
|
||||
SchemaName = "default-schema",
|
||||
Metadata = element.ToBuilder(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (configuration is not null)
|
||||
{
|
||||
foreach (var kvp in configuration.AsEnumerable().Where(kvp => kvp.Value is not null))
|
||||
{
|
||||
botBuilder.EnvironmentVariables.Add(new EnvironmentVariableDefinition.Builder()
|
||||
{
|
||||
SchemaName = kvp.Key,
|
||||
Id = Guid.NewGuid(),
|
||||
DisplayName = kvp.Key,
|
||||
ValueComponent = new EnvironmentVariableValue.Builder()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Value = kvp.Value!,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return botBuilder.Build();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a <see cref="PromptAgentFactory"/> which aggregates multiple agent factories.
|
||||
/// </summary>
|
||||
public sealed class AggregatorPromptAgentFactory : PromptAgentFactory
|
||||
{
|
||||
private readonly PromptAgentFactory[] _agentFactories;
|
||||
|
||||
/// <summary>Initializes the instance.</summary>
|
||||
/// <param name="agentFactories">Ordered <see cref="PromptAgentFactory"/> instances to aggregate.</param>
|
||||
/// <remarks>
|
||||
/// Where multiple <see cref="PromptAgentFactory"/> instances are provided, the first factory that supports the <see cref="GptComponentMetadata"/> will be used.
|
||||
/// </remarks>
|
||||
public AggregatorPromptAgentFactory(params PromptAgentFactory[] agentFactories)
|
||||
{
|
||||
Throw.IfNullOrEmpty(agentFactories);
|
||||
|
||||
foreach (PromptAgentFactory agentFactory in agentFactories)
|
||||
{
|
||||
Throw.IfNull(agentFactory, nameof(agentFactories));
|
||||
}
|
||||
|
||||
this._agentFactories = agentFactories;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AIAgent?> TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(promptAgent);
|
||||
|
||||
foreach (var agentFactory in this._agentFactories)
|
||||
{
|
||||
var agent = await agentFactory.TryCreateAsync(promptAgent, cancellationToken).ConfigureAwait(false);
|
||||
if (agent is not null)
|
||||
{
|
||||
return agent;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an <see cref="PromptAgentFactory"/> which creates instances of <see cref="ChatClientAgent"/>.
|
||||
/// </summary>
|
||||
public sealed class ChatClientPromptAgentFactory : PromptAgentFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="ChatClientPromptAgentFactory"/> class.
|
||||
/// </summary>
|
||||
public ChatClientPromptAgentFactory(IChatClient chatClient, IList<AIFunction>? functions = null, RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(engine, configuration)
|
||||
{
|
||||
Throw.IfNull(chatClient);
|
||||
|
||||
this._chatClient = chatClient;
|
||||
this._functions = functions;
|
||||
this._loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task<AIAgent?> TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(promptAgent);
|
||||
|
||||
var options = new ChatClientAgentOptions()
|
||||
{
|
||||
Name = promptAgent.Name,
|
||||
Description = promptAgent.Description,
|
||||
Instructions = promptAgent.Instructions?.ToTemplateString(),
|
||||
ChatOptions = promptAgent.GetChatOptions(this.Engine, this._functions),
|
||||
};
|
||||
|
||||
var agent = new ChatClientAgent(this._chatClient, options, this._loggerFactory);
|
||||
|
||||
return Task.FromResult<AIAgent?>(agent);
|
||||
}
|
||||
|
||||
#region private
|
||||
private readonly IChatClient _chatClient;
|
||||
private readonly IList<AIFunction>? _functions;
|
||||
private readonly ILoggerFactory? _loggerFactory;
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="BoolExpression"/>.
|
||||
/// </summary>
|
||||
internal static class BoolExpressionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates the given <see cref="BoolExpression"/> using the provided <see cref="RecalcEngine"/>.
|
||||
/// </summary>
|
||||
/// <param name="expression">Expression to evaluate.</param>
|
||||
/// <param name="engine">Recalc engine to use for evaluation.</param>
|
||||
/// <returns>The evaluated boolean value, or null if the expression is null or cannot be evaluated.</returns>
|
||||
internal static bool? Eval(this BoolExpression? expression, RecalcEngine? engine)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expression.IsLiteral)
|
||||
{
|
||||
return expression.LiteralValue;
|
||||
}
|
||||
|
||||
if (engine is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expression.IsExpression)
|
||||
{
|
||||
return engine.Eval(expression.ExpressionText!).AsBoolean();
|
||||
}
|
||||
else if (expression.IsVariableReference)
|
||||
{
|
||||
var formulaValue = engine.Eval(expression.VariableReference!.VariableName);
|
||||
if (formulaValue is BooleanValue booleanValue)
|
||||
{
|
||||
return booleanValue.Value;
|
||||
}
|
||||
|
||||
if (formulaValue is StringValue stringValue && bool.TryParse(stringValue.Value, out bool result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="CodeInterpreterTool"/>.
|
||||
/// </summary>
|
||||
internal static class CodeInterpreterToolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HostedCodeInterpreterTool"/> from a <see cref="CodeInterpreterTool"/>.
|
||||
/// </summary>
|
||||
/// <param name="tool">Instance of <see cref="CodeInterpreterTool"/></param>
|
||||
internal static HostedCodeInterpreterTool AsCodeInterpreterTool(this CodeInterpreterTool tool)
|
||||
{
|
||||
Throw.IfNull(tool);
|
||||
|
||||
return new HostedCodeInterpreterTool();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="FileSearchTool"/>.
|
||||
/// </summary>
|
||||
internal static class FileSearchToolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a <see cref="HostedFileSearchTool"/> from a <see cref="FileSearchTool"/>.
|
||||
/// </summary>
|
||||
/// <param name="tool">Instance of <see cref="FileSearchTool"/></param>
|
||||
internal static HostedFileSearchTool CreateFileSearchTool(this FileSearchTool tool)
|
||||
{
|
||||
Throw.IfNull(tool);
|
||||
|
||||
return new HostedFileSearchTool()
|
||||
{
|
||||
MaximumResultCount = (int?)tool.MaximumResultCount?.LiteralValue,
|
||||
Inputs = tool.VectorStoreIds?.LiteralValue.Select(id => (AIContent)new HostedVectorStoreContent(id)).ToList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="InvokeClientTaskAction"/>.
|
||||
/// </summary>
|
||||
internal static class FunctionToolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="AIFunctionDeclaration"/> from a <see cref="InvokeClientTaskAction"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a matching function already exists in the provided list, it will be returned.
|
||||
/// Otherwise, a new function declaration will be created.
|
||||
/// </remarks>
|
||||
/// <param name="tool">Instance of <see cref="InvokeClientTaskAction"/></param>
|
||||
/// <param name="functions">Instance of <see cref="IList{AIFunction}"/></param>
|
||||
internal static AITool CreateOrGetAITool(this InvokeClientTaskAction tool, IList<AIFunction>? functions)
|
||||
{
|
||||
Throw.IfNull(tool);
|
||||
Throw.IfNull(tool.Name);
|
||||
|
||||
// use the tool from the provided list if it exists
|
||||
if (functions is not null)
|
||||
{
|
||||
var function = functions.FirstOrDefault(f => tool.Matches(f));
|
||||
|
||||
if (function is not null)
|
||||
{
|
||||
return function;
|
||||
}
|
||||
}
|
||||
|
||||
return AIFunctionFactory.CreateDeclaration(
|
||||
name: tool.Name,
|
||||
description: tool.Description,
|
||||
jsonSchema: tool.ClientActionInputSchema?.GetSchema() ?? s_defaultSchema);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a <see cref="InvokeClientTaskAction"/> matches an <see cref="AITool"/>.
|
||||
/// </summary>
|
||||
/// <param name="tool">Instance of <see cref="InvokeClientTaskAction"/></param>
|
||||
/// <param name="aiFunc">Instance of <see cref="AIFunction"/></param>
|
||||
internal static bool Matches(this InvokeClientTaskAction tool, AIFunction aiFunc)
|
||||
{
|
||||
Throw.IfNull(tool);
|
||||
Throw.IfNull(aiFunc);
|
||||
|
||||
return tool.Name == aiFunc.Name;
|
||||
}
|
||||
|
||||
private static readonly JsonElement s_defaultSchema = JsonDocument.Parse("{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}").RootElement;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Globalization;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IntExpression"/>.
|
||||
/// </summary>
|
||||
internal static class IntExpressionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates the given <see cref="IntExpression"/> using the provided <see cref="RecalcEngine"/>.
|
||||
/// </summary>
|
||||
/// <param name="expression">Expression to evaluate.</param>
|
||||
/// <param name="engine">Recalc engine to use for evaluation.</param>
|
||||
/// <returns>The evaluated integer value, or null if the expression is null or cannot be evaluated.</returns>
|
||||
internal static long? Eval(this IntExpression? expression, RecalcEngine? engine)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expression.IsLiteral)
|
||||
{
|
||||
return expression.LiteralValue;
|
||||
}
|
||||
|
||||
if (engine is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expression.IsExpression)
|
||||
{
|
||||
return (long)engine.Eval(expression.ExpressionText!).AsDouble();
|
||||
}
|
||||
else if (expression.IsVariableReference)
|
||||
{
|
||||
var formulaValue = engine.Eval(expression.VariableReference!.VariableName);
|
||||
if (formulaValue is NumberValue numberValue)
|
||||
{
|
||||
return (long)numberValue.Value;
|
||||
}
|
||||
|
||||
if (formulaValue is StringValue stringValue && int.TryParse(stringValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="McpServerToolApprovalMode"/>.
|
||||
/// </summary>
|
||||
internal static class McpServerToolApprovalModeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="McpServerToolApprovalMode"/> to a <see cref="HostedMcpServerToolApprovalMode"/>.
|
||||
/// </summary>
|
||||
/// <param name="mode">Instance of <see cref="McpServerToolApprovalMode"/></param>
|
||||
internal static HostedMcpServerToolApprovalMode AsHostedMcpServerToolApprovalMode(this McpServerToolApprovalMode mode)
|
||||
{
|
||||
return mode switch
|
||||
{
|
||||
McpServerToolNeverRequireApprovalMode => HostedMcpServerToolApprovalMode.NeverRequire,
|
||||
McpServerToolAlwaysRequireApprovalMode => HostedMcpServerToolApprovalMode.AlwaysRequire,
|
||||
McpServerToolRequireSpecificApprovalMode specificMode =>
|
||||
HostedMcpServerToolApprovalMode.RequireSpecific(
|
||||
specificMode?.AlwaysRequireApprovalToolNames?.LiteralValue ?? [],
|
||||
specificMode?.NeverRequireApprovalToolNames?.LiteralValue ?? []
|
||||
),
|
||||
_ => HostedMcpServerToolApprovalMode.AlwaysRequire,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="McpServerTool"/>.
|
||||
/// </summary>
|
||||
internal static class McpServerToolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HostedMcpServerTool"/> from a <see cref="McpServerTool"/>.
|
||||
/// </summary>
|
||||
/// <param name="tool">Instance of <see cref="McpServerTool"/></param>
|
||||
internal static HostedMcpServerTool CreateHostedMcpTool(this McpServerTool tool)
|
||||
{
|
||||
Throw.IfNull(tool);
|
||||
Throw.IfNull(tool.ServerName?.LiteralValue);
|
||||
Throw.IfNull(tool.Connection);
|
||||
|
||||
var connection = tool.Connection as AnonymousConnection ?? throw new ArgumentException("Only AnonymousConnection is supported for MCP Server Tool connections.", nameof(tool));
|
||||
var serverUrl = connection.Endpoint?.LiteralValue;
|
||||
Throw.IfNullOrEmpty(serverUrl, nameof(connection.Endpoint));
|
||||
|
||||
return new HostedMcpServerTool(tool.ServerName.LiteralValue, serverUrl)
|
||||
{
|
||||
ServerDescription = tool.ServerDescription?.LiteralValue,
|
||||
AllowedTools = tool.AllowedTools?.LiteralValue,
|
||||
ApprovalMode = tool.ApprovalMode?.AsHostedMcpServerToolApprovalMode(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="ModelOptions"/>.
|
||||
/// </summary>
|
||||
internal static class ModelOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the 'chatToolMode' property from a <see cref="ModelOptions"/> to a <see cref="ChatToolMode"/>.
|
||||
/// </summary>
|
||||
/// <param name="modelOptions">Instance of <see cref="ModelOptions"/></param>
|
||||
internal static ChatToolMode? AsChatToolMode(this ModelOptions modelOptions)
|
||||
{
|
||||
Throw.IfNull(modelOptions);
|
||||
|
||||
var mode = modelOptions.ExtensionData?.GetPropertyOrNull<StringDataValue>(InitializablePropertyPath.Create("chatToolMode"))?.Value;
|
||||
if (mode is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return mode switch
|
||||
{
|
||||
"auto" => ChatToolMode.Auto,
|
||||
"none" => ChatToolMode.None,
|
||||
"require_any" => ChatToolMode.RequireAny,
|
||||
_ => ChatToolMode.RequireSpecific(mode),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the 'additional_properties' property from a <see cref="ModelOptions"/>.
|
||||
/// </summary>
|
||||
/// <param name="modelOptions">Instance of <see cref="ModelOptions"/></param>
|
||||
/// <param name="excludedProperties">List of properties which should not be included in additional properties.</param>
|
||||
internal static AdditionalPropertiesDictionary? GetAdditionalProperties(this ModelOptions modelOptions, string[] excludedProperties)
|
||||
{
|
||||
Throw.IfNull(modelOptions);
|
||||
|
||||
var options = modelOptions.ExtensionData;
|
||||
if (options is null || options.Properties.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var additionalProperties = options.Properties
|
||||
.Where(kvp => !excludedProperties.Contains(kvp.Key))
|
||||
.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value?.ToObject());
|
||||
|
||||
if (additionalProperties is null || additionalProperties.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AdditionalPropertiesDictionary(additionalProperties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Globalization;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="NumberExpression"/>.
|
||||
/// </summary>
|
||||
internal static class NumberExpressionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates the given <see cref="NumberExpression"/> using the provided <see cref="RecalcEngine"/>.
|
||||
/// </summary>
|
||||
/// <param name="expression">Expression to evaluate.</param>
|
||||
/// <param name="engine">Recalc engine to use for evaluation.</param>
|
||||
/// <returns>The evaluated number value, or null if the expression is null or cannot be evaluated.</returns>
|
||||
internal static double? Eval(this NumberExpression? expression, RecalcEngine? engine)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expression.IsLiteral)
|
||||
{
|
||||
return expression.LiteralValue;
|
||||
}
|
||||
|
||||
if (engine is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expression.IsExpression)
|
||||
{
|
||||
return engine.Eval(expression.ExpressionText!).AsDouble();
|
||||
}
|
||||
else if (expression.IsVariableReference)
|
||||
{
|
||||
var formulaValue = engine.Eval(expression.VariableReference!.VariableName);
|
||||
if (formulaValue is NumberValue numberValue)
|
||||
{
|
||||
return numberValue.Value;
|
||||
}
|
||||
|
||||
if (formulaValue is StringValue stringValue && double.TryParse(stringValue.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="GptComponentMetadata"/>.
|
||||
/// </summary>
|
||||
public static class PromptAgentExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the 'options' property from a <see cref="GptComponentMetadata"/> as a <see cref="ChatOptions"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="promptAgent">Instance of <see cref="GptComponentMetadata"/></param>
|
||||
/// <param name="engine">Instance of <see cref="RecalcEngine"/></param>
|
||||
/// <param name="functions">Instance of <see cref="IList{AIFunction}"/></param>
|
||||
public static ChatOptions? GetChatOptions(this GptComponentMetadata promptAgent, RecalcEngine? engine, IList<AIFunction>? functions)
|
||||
{
|
||||
Throw.IfNull(promptAgent);
|
||||
|
||||
var outputSchema = promptAgent.OutputType;
|
||||
var modelOptions = promptAgent.Model?.Options;
|
||||
|
||||
var tools = promptAgent.GetAITools(functions);
|
||||
|
||||
if (modelOptions is null && tools is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ChatOptions()
|
||||
{
|
||||
Instructions = promptAgent.ResponseInstructions?.ToTemplateString(),
|
||||
Temperature = (float?)modelOptions?.Temperature?.Eval(engine),
|
||||
MaxOutputTokens = (int?)modelOptions?.MaxOutputTokens?.Eval(engine),
|
||||
TopP = (float?)modelOptions?.TopP?.Eval(engine),
|
||||
TopK = (int?)modelOptions?.TopK?.Eval(engine),
|
||||
FrequencyPenalty = (float?)modelOptions?.FrequencyPenalty?.Eval(engine),
|
||||
PresencePenalty = (float?)modelOptions?.PresencePenalty?.Eval(engine),
|
||||
Seed = modelOptions?.Seed?.Eval(engine),
|
||||
ResponseFormat = outputSchema?.AsChatResponseFormat(),
|
||||
ModelId = promptAgent.Model?.ModelNameHint,
|
||||
StopSequences = modelOptions?.StopSequences,
|
||||
AllowMultipleToolCalls = modelOptions?.AllowMultipleToolCalls?.Eval(engine),
|
||||
ToolMode = modelOptions?.AsChatToolMode(),
|
||||
Tools = tools,
|
||||
AdditionalProperties = modelOptions?.GetAdditionalProperties(s_chatOptionProperties),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the 'tools' property from a <see cref="GptComponentMetadata"/>.
|
||||
/// </summary>
|
||||
/// <param name="promptAgent">Instance of <see cref="GptComponentMetadata"/></param>
|
||||
/// <param name="functions">Instance of <see cref="IList{AIFunction}"/></param>
|
||||
internal static List<AITool>? GetAITools(this GptComponentMetadata promptAgent, IList<AIFunction>? functions)
|
||||
{
|
||||
return promptAgent.Tools.Select(tool =>
|
||||
{
|
||||
return tool switch
|
||||
{
|
||||
CodeInterpreterTool => ((CodeInterpreterTool)tool).AsCodeInterpreterTool(),
|
||||
InvokeClientTaskAction => ((InvokeClientTaskAction)tool).CreateOrGetAITool(functions),
|
||||
McpServerTool => ((McpServerTool)tool).CreateHostedMcpTool(),
|
||||
FileSearchTool => ((FileSearchTool)tool).CreateFileSearchTool(),
|
||||
WebSearchTool => ((WebSearchTool)tool).CreateWebSearchTool(),
|
||||
_ => throw new NotSupportedException($"Unable to create tool definition because of unsupported tool type: {tool.Kind}, supported tool types are: {string.Join(",", s_validToolKinds)}"),
|
||||
};
|
||||
}).ToList() ?? [];
|
||||
}
|
||||
|
||||
#region private
|
||||
private const string CodeInterpreterKind = "codeInterpreter";
|
||||
private const string FileSearchKind = "fileSearch";
|
||||
private const string FunctionKind = "function";
|
||||
private const string WebSearchKind = "webSearch";
|
||||
private const string McpKind = "mcp";
|
||||
|
||||
private static readonly string[] s_validToolKinds =
|
||||
[
|
||||
CodeInterpreterKind,
|
||||
FileSearchKind,
|
||||
FunctionKind,
|
||||
WebSearchKind,
|
||||
McpKind
|
||||
];
|
||||
|
||||
private static readonly string[] s_chatOptionProperties =
|
||||
[
|
||||
"allowMultipleToolCalls",
|
||||
"conversationId",
|
||||
"chatToolMode",
|
||||
"frequencyPenalty",
|
||||
"additionalInstructions",
|
||||
"maxOutputTokens",
|
||||
"modelId",
|
||||
"presencePenalty",
|
||||
"responseFormat",
|
||||
"seed",
|
||||
"stopSequences",
|
||||
"temperature",
|
||||
"topK",
|
||||
"topP",
|
||||
"toolMode",
|
||||
"tools",
|
||||
];
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="PropertyInfo"/>.
|
||||
/// </summary>
|
||||
public static class PropertyInfoExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Dictionary{TKey, TValue}"/> of <see cref="string"/> and <see cref="object"/>
|
||||
/// from an <see cref="IReadOnlyDictionary{TKey, TValue}"/> of <see cref="string"/> and <see cref="PropertyInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="properties">A read-only dictionary of property names and their corresponding <see cref="PropertyInfo"/> objects.</param>
|
||||
public static Dictionary<string, object> AsObjectDictionary(this IReadOnlyDictionary<string, PropertyInfo> properties)
|
||||
{
|
||||
var result = new Dictionary<string, object>();
|
||||
|
||||
foreach (var property in properties)
|
||||
{
|
||||
result[property.Key] = BuildPropertySchema(property.Value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#region private
|
||||
private static Dictionary<string, object> BuildPropertySchema(PropertyInfo propertyInfo)
|
||||
{
|
||||
var propertySchema = new Dictionary<string, object>();
|
||||
|
||||
// Map the DataType to JSON schema type and add type-specific properties
|
||||
switch (propertyInfo.Type)
|
||||
{
|
||||
case StringDataType:
|
||||
propertySchema["type"] = "string";
|
||||
break;
|
||||
case NumberDataType:
|
||||
propertySchema["type"] = "number";
|
||||
break;
|
||||
case BooleanDataType:
|
||||
propertySchema["type"] = "boolean";
|
||||
break;
|
||||
case DateTimeDataType:
|
||||
propertySchema["type"] = "string";
|
||||
propertySchema["format"] = "date-time";
|
||||
break;
|
||||
case DateDataType:
|
||||
propertySchema["type"] = "string";
|
||||
propertySchema["format"] = "date";
|
||||
break;
|
||||
case TimeDataType:
|
||||
propertySchema["type"] = "string";
|
||||
propertySchema["format"] = "time";
|
||||
break;
|
||||
case RecordDataType nestedRecordType:
|
||||
#pragma warning disable IL2026, IL3050
|
||||
// For nested records, recursively build the schema
|
||||
var nestedSchema = nestedRecordType.GetSchema();
|
||||
var nestedJson = JsonSerializer.Serialize(nestedSchema, ElementSerializer.CreateOptions());
|
||||
var nestedDict = JsonSerializer.Deserialize<Dictionary<string, object>>(nestedJson, ElementSerializer.CreateOptions());
|
||||
#pragma warning restore IL2026, IL3050
|
||||
if (nestedDict != null)
|
||||
{
|
||||
return nestedDict;
|
||||
}
|
||||
propertySchema["type"] = "object";
|
||||
break;
|
||||
case TableDataType tableType:
|
||||
propertySchema["type"] = "array";
|
||||
// TableDataType has Properties like RecordDataType
|
||||
propertySchema["items"] = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "object",
|
||||
["properties"] = AsObjectDictionary(tableType.Properties),
|
||||
["additionalProperties"] = false
|
||||
};
|
||||
break;
|
||||
default:
|
||||
propertySchema["type"] = "string";
|
||||
break;
|
||||
}
|
||||
|
||||
// Add description if available
|
||||
if (!string.IsNullOrEmpty(propertyInfo.Description))
|
||||
{
|
||||
propertySchema["description"] = propertyInfo.Description;
|
||||
}
|
||||
|
||||
return propertySchema;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="RecordDataType"/>.
|
||||
/// </summary>
|
||||
public static class RecordDataTypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ChatResponseFormat"/> from a <see cref="RecordDataType"/>.
|
||||
/// </summary>
|
||||
/// <param name="recordDataType">Instance of <see cref="RecordDataType"/></param>
|
||||
internal static ChatResponseFormat? AsChatResponseFormat(this RecordDataType recordDataType)
|
||||
{
|
||||
Throw.IfNull(recordDataType);
|
||||
|
||||
if (recordDataType.Properties.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Consider adding schemaName and schemaDescription parameters to this method.
|
||||
return ChatResponseFormat.ForJsonSchema(
|
||||
schema: recordDataType.GetSchema(),
|
||||
schemaName: recordDataType.GetSchemaName(),
|
||||
schemaDescription: recordDataType.GetSchemaDescription());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="RecordDataType"/> to a <see cref="JsonElement"/>.
|
||||
/// </summary>
|
||||
/// <param name="recordDataType">Instance of <see cref="RecordDataType"/></param>
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
public static JsonElement GetSchema(this RecordDataType recordDataType)
|
||||
{
|
||||
Throw.IfNull(recordDataType);
|
||||
|
||||
var schemaObject = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "object",
|
||||
["properties"] = recordDataType.Properties.AsObjectDictionary(),
|
||||
["additionalProperties"] = false
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(schemaObject, ElementSerializer.CreateOptions());
|
||||
return JsonSerializer.Deserialize<JsonElement>(json);
|
||||
}
|
||||
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the 'schemaName' property from a <see cref="RecordDataType"/>.
|
||||
/// </summary>
|
||||
private static string? GetSchemaName(this RecordDataType recordDataType)
|
||||
{
|
||||
Throw.IfNull(recordDataType);
|
||||
|
||||
return recordDataType.ExtensionData?.GetPropertyOrNull<StringDataValue>(InitializablePropertyPath.Create("schemaName"))?.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the 'schemaDescription' property from a <see cref="RecordDataType"/>.
|
||||
/// </summary>
|
||||
private static string? GetSchemaDescription(this RecordDataType recordDataType)
|
||||
{
|
||||
Throw.IfNull(recordDataType);
|
||||
|
||||
return recordDataType.ExtensionData?.GetPropertyOrNull<StringDataValue>(InitializablePropertyPath.Create("schemaDescription"))?.Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="RecordDataValue"/>.
|
||||
/// </summary>
|
||||
public static class RecordDataValueExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a 'number' property from a <see cref="RecordDataValue"/>
|
||||
/// </summary>
|
||||
/// <param name="recordData">Instance of <see cref="RecordDataValue"/></param>
|
||||
/// <param name="propertyPath">Path of the property to retrieve</param>
|
||||
public static decimal? GetNumber(this RecordDataValue recordData, string propertyPath)
|
||||
{
|
||||
Throw.IfNull(recordData);
|
||||
|
||||
var numberValue = recordData.GetPropertyOrNull<NumberDataValue>(InitializablePropertyPath.Create(propertyPath));
|
||||
return numberValue?.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a nullable boolean value from the specified property path within the given record data.
|
||||
/// </summary>
|
||||
/// <param name="recordData">Instance of <see cref="RecordDataValue"/></param>
|
||||
/// <param name="propertyPath">Path of the property to retrieve</param>
|
||||
public static bool? GetBoolean(this RecordDataValue recordData, string propertyPath)
|
||||
{
|
||||
Throw.IfNull(recordData);
|
||||
|
||||
var booleanValue = recordData.GetPropertyOrNull<BooleanDataValue>(InitializablePropertyPath.Create(propertyPath));
|
||||
return booleanValue?.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="RecordDataValue"/> to a <see cref="IReadOnlyDictionary{TKey, TValue}"/>.
|
||||
/// </summary>
|
||||
/// <param name="recordData">Instance of <see cref="RecordDataValue"/></param>
|
||||
public static IReadOnlyDictionary<string, string> ToDictionary(this RecordDataValue recordData)
|
||||
{
|
||||
Throw.IfNull(recordData);
|
||||
|
||||
return recordData.Properties.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value?.ToString() ?? string.Empty
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the 'schema' property from a <see cref="RecordDataValue"/>.
|
||||
/// </summary>
|
||||
/// <param name="recordData">Instance of <see cref="RecordDataValue"/></param>
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
public static JsonElement? GetSchema(this RecordDataValue recordData)
|
||||
{
|
||||
Throw.IfNull(recordData);
|
||||
|
||||
try
|
||||
{
|
||||
var schemaStr = recordData.GetPropertyOrNull<StringDataValue>(InitializablePropertyPath.Create("json_schema.schema"));
|
||||
if (schemaStr?.Value is not null)
|
||||
{
|
||||
return JsonSerializer.Deserialize<JsonElement>(schemaStr.Value);
|
||||
}
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
// Ignore and try next
|
||||
}
|
||||
|
||||
var responseFormRec = recordData.GetPropertyOrNull<RecordDataValue>(InitializablePropertyPath.Create("json_schema.schema"));
|
||||
if (responseFormRec is not null)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(responseFormRec, ElementSerializer.CreateOptions());
|
||||
return JsonSerializer.Deserialize<JsonElement>(json);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
|
||||
internal static object? ToObject(this DataValue? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return value switch
|
||||
{
|
||||
StringDataValue s => s.Value,
|
||||
NumberDataValue n => n.Value,
|
||||
BooleanDataValue b => b.Value,
|
||||
TableDataValue t => t.Values.Select(v => v.ToObject()).ToList(),
|
||||
RecordDataValue r => r.Properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToObject()),
|
||||
_ => throw new NotSupportedException($"Unsupported DataValue type: {value.GetType().FullName}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="StringExpression"/>.
|
||||
/// </summary>
|
||||
public static class StringExpressionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates the given <see cref="StringExpression"/> using the provided <see cref="RecalcEngine"/>.
|
||||
/// </summary>
|
||||
/// <param name="expression">Expression to evaluate.</param>
|
||||
/// <param name="engine">Recalc engine to use for evaluation.</param>
|
||||
/// <returns>The evaluated string value, or null if the expression is null or cannot be evaluated.</returns>
|
||||
public static string? Eval(this StringExpression? expression, RecalcEngine? engine)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expression.IsLiteral)
|
||||
{
|
||||
return expression.LiteralValue?.ToString();
|
||||
}
|
||||
|
||||
if (engine is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expression.IsExpression)
|
||||
{
|
||||
return engine.Eval(expression.ExpressionText!).ToString();
|
||||
}
|
||||
else if (expression.IsVariableReference)
|
||||
{
|
||||
var stringValue = engine.Eval(expression.VariableReference!.VariableName) as StringValue;
|
||||
return stringValue?.Value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Bot.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="WebSearchTool"/>.
|
||||
/// </summary>
|
||||
internal static class WebSearchToolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a <see cref="HostedWebSearchTool"/> from a <see cref="WebSearchTool"/>.
|
||||
/// </summary>
|
||||
/// <param name="tool">Instance of <see cref="WebSearchTool"/></param>
|
||||
internal static HostedWebSearchTool CreateWebSearchTool(this WebSearchTool tool)
|
||||
{
|
||||
Throw.IfNull(tool);
|
||||
|
||||
return new HostedWebSearchTool();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="PromptAgentFactory"/> to support YAML based agent definitions.
|
||||
/// </summary>
|
||||
public static class YamlAgentFactoryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a <see cref="AIAgent"/> from the given agent YAML.
|
||||
/// </summary>
|
||||
/// <param name="agentFactory"><see cref="PromptAgentFactory"/> which will be used to create the agent.</param>
|
||||
/// <param name="agentYaml">Text string containing the YAML representation of an <see cref="AIAgent" />.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token</param>
|
||||
[RequiresDynamicCode("Calls YamlDotNet.Serialization.DeserializerBuilder.DeserializerBuilder()")]
|
||||
public static Task<AIAgent> CreateFromYamlAsync(this PromptAgentFactory agentFactory, string agentYaml, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(agentFactory);
|
||||
Throw.IfNullOrEmpty(agentYaml);
|
||||
|
||||
var agentDefinition = AgentBotElementYaml.FromYaml(agentYaml);
|
||||
|
||||
return agentFactory.CreateAsync(
|
||||
agentDefinition,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<NoWarn>$(NoWarn);MEAI001</NoWarn>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Microsoft Agent Framework Declarative Agents</Title>
|
||||
<Description>Provides Microsoft Agent Framework support for declarative agents.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
|
||||
<PackageReference Include="Microsoft.PowerFx.Interpreter" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.Declarative.UnitTests" />
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.Declarative.IntegrationTests" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a factory for creating <see cref="AIAgent"/> instances.
|
||||
/// </summary>
|
||||
public abstract class PromptAgentFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PromptAgentFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="engine">Optional <see cref="RecalcEngine"/>, if none is provided a default instance will be created.</param>
|
||||
/// <param name="configuration">Optional configuration to be added as variables to the <see cref="RecalcEngine"/>.</param>
|
||||
protected PromptAgentFactory(RecalcEngine? engine = null, IConfiguration? configuration = null)
|
||||
{
|
||||
this.Engine = engine ?? new RecalcEngine();
|
||||
|
||||
if (configuration is not null)
|
||||
{
|
||||
foreach (var kvp in configuration.AsEnumerable())
|
||||
{
|
||||
this.Engine.UpdateVariable(kvp.Key, kvp.Value ?? string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Power Fx recalculation engine used to evaluate expressions in agent definitions.
|
||||
/// This engine is configured with variables from the <see cref="IConfiguration"/> provided during construction.
|
||||
/// </summary>
|
||||
protected RecalcEngine Engine { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="AIAgent"/> from the specified <see cref="GptComponentMetadata"/>.
|
||||
/// </summary>
|
||||
/// <param name="promptAgent">Definition of the agent to create.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token.</param>
|
||||
/// <return>The created <see cref="AIAgent"/>, if null the agent type is not supported.</return>
|
||||
public async Task<AIAgent> CreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(promptAgent);
|
||||
|
||||
var agent = await this.TryCreateAsync(promptAgent, cancellationToken).ConfigureAwait(false);
|
||||
return agent ?? throw new NotSupportedException($"Agent type {promptAgent.Kind} is not supported.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to create a <see cref="AIAgent"/> from the specified <see cref="GptComponentMetadata"/>.
|
||||
/// </summary>
|
||||
/// <param name="promptAgent">Definition of the agent to create.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token.</param>
|
||||
/// <return>The created <see cref="AIAgent"/>, if null the agent type is not supported.</return>
|
||||
public abstract Task<AIAgent?> TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -32,6 +32,7 @@
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.UnitTests" />
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.Declarative.UnitTests" />
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.Hosting.UnitTests"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.PowerFx;
|
||||
|
||||
namespace Microsoft.Agents.AI.Declarative.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="AgentBotElementYaml"/>
|
||||
/// </summary>
|
||||
public sealed class AgentBotElementYamlTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(PromptAgents.AgentWithEverything)]
|
||||
[InlineData(PromptAgents.AgentWithApiKeyConnection)]
|
||||
[InlineData(PromptAgents.AgentWithVariableReferences)]
|
||||
[InlineData(PromptAgents.AgentWithOutputSchema)]
|
||||
[InlineData(PromptAgents.OpenAIChatAgent)]
|
||||
[InlineData(PromptAgents.AgentWithCurrentModels)]
|
||||
[InlineData(PromptAgents.AgentWithRemoteConnection)]
|
||||
public void FromYaml_DoesNotThrow(string text)
|
||||
{
|
||||
// Arrange & Act
|
||||
var agent = AgentBotElementYaml.FromYaml(text);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromYaml_NotPromptAgent_Throws()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
Assert.Throws<InvalidDataException>(() => AgentBotElementYaml.FromYaml(PromptAgents.Workflow));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromYaml_Properties()
|
||||
{
|
||||
// Arrange & Act
|
||||
var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.Equal("AgentName", agent.Name);
|
||||
Assert.Equal("Agent description", agent.Description);
|
||||
Assert.Equal("You are a helpful assistant.", agent.Instructions?.ToTemplateString());
|
||||
Assert.NotNull(agent.Model);
|
||||
Assert.True(agent.Tools.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromYaml_CurrentModels()
|
||||
{
|
||||
// Arrange & Act
|
||||
var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithCurrentModels);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.NotNull(agent.Model);
|
||||
Assert.Equal("gpt-4o", agent.Model.ModelNameHint);
|
||||
Assert.NotNull(agent.Model.Options);
|
||||
Assert.Equal(0.7f, (float?)agent.Model.Options?.Temperature?.LiteralValue);
|
||||
Assert.Equal(0.9f, (float?)agent.Model.Options?.TopP?.LiteralValue);
|
||||
|
||||
// Assert contents using extension methods
|
||||
Assert.Equal(1024, agent.Model.Options?.MaxOutputTokens?.LiteralValue);
|
||||
Assert.Equal(50, agent.Model.Options?.TopK?.LiteralValue);
|
||||
Assert.Equal(0.7f, (float?)agent.Model.Options?.FrequencyPenalty?.LiteralValue);
|
||||
Assert.Equal(0.7f, (float?)agent.Model.Options?.PresencePenalty?.LiteralValue);
|
||||
Assert.Equal(42, agent.Model.Options?.Seed?.LiteralValue);
|
||||
Assert.Equal(PromptAgents.s_stopSequences, agent.Model.Options?.StopSequences);
|
||||
Assert.True(agent.Model.Options?.AllowMultipleToolCalls?.LiteralValue);
|
||||
Assert.Equal(ChatToolMode.Auto, agent.Model.Options?.AsChatToolMode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromYaml_OutputSchema()
|
||||
{
|
||||
// Arrange & Act
|
||||
var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithOutputSchema);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.NotNull(agent.OutputType);
|
||||
ChatResponseFormatJson responseFormat = (agent.OutputType.AsChatResponseFormat() as ChatResponseFormatJson)!;
|
||||
Assert.NotNull(responseFormat);
|
||||
Assert.NotNull(responseFormat.Schema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromYaml_CodeInterpreter()
|
||||
{
|
||||
// Arrange & Act
|
||||
var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
var tools = agent.Tools;
|
||||
var codeInterpreterTools = tools.Where(t => t is CodeInterpreterTool).ToArray();
|
||||
Assert.Single(codeInterpreterTools);
|
||||
CodeInterpreterTool codeInterpreterTool = (codeInterpreterTools[0] as CodeInterpreterTool)!;
|
||||
Assert.NotNull(codeInterpreterTool);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromYaml_FunctionTool()
|
||||
{
|
||||
// Arrange & Act
|
||||
var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
var tools = agent.Tools;
|
||||
var functionTools = tools.Where(t => t is InvokeClientTaskAction).ToArray();
|
||||
Assert.Single(functionTools);
|
||||
InvokeClientTaskAction functionTool = (functionTools[0] as InvokeClientTaskAction)!;
|
||||
Assert.NotNull(functionTool);
|
||||
Assert.Equal("GetWeather", functionTool.Name);
|
||||
Assert.Equal("Get the weather for a given location.", functionTool.Description);
|
||||
// TODO check schema
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromYaml_MCP()
|
||||
{
|
||||
// Arrange & Act
|
||||
var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
var tools = agent.Tools;
|
||||
var mcpTools = tools.Where(t => t is McpServerTool).ToArray();
|
||||
Assert.Single(mcpTools);
|
||||
McpServerTool mcpTool = (mcpTools[0] as McpServerTool)!;
|
||||
Assert.NotNull(mcpTool);
|
||||
Assert.Equal("PersonInfoTool", mcpTool.ServerName?.LiteralValue);
|
||||
AnonymousConnection connection = (mcpTool.Connection as AnonymousConnection)!;
|
||||
Assert.NotNull(connection);
|
||||
Assert.Equal("https://my-mcp-endpoint.com/api", connection.Endpoint?.LiteralValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromYaml_WebSearchTool()
|
||||
{
|
||||
// Arrange & Act
|
||||
var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
var tools = agent.Tools;
|
||||
var webSearchTools = tools.Where(t => t is WebSearchTool).ToArray();
|
||||
Assert.Single(webSearchTools);
|
||||
Assert.NotNull(webSearchTools[0] as WebSearchTool);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromYaml_FileSearchTool()
|
||||
{
|
||||
// Arrange & Act
|
||||
var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
var tools = agent.Tools;
|
||||
var fileSearchTools = tools.Where(t => t is FileSearchTool).ToArray();
|
||||
Assert.Single(fileSearchTools);
|
||||
FileSearchTool fileSearchTool = (fileSearchTools[0] as FileSearchTool)!;
|
||||
Assert.NotNull(fileSearchTool);
|
||||
|
||||
// Verify vector store content property exists and has correct values
|
||||
Assert.NotNull(fileSearchTool.VectorStoreIds);
|
||||
Assert.Equal(3, fileSearchTool.VectorStoreIds.LiteralValue.Length);
|
||||
Assert.Equal("1", fileSearchTool.VectorStoreIds.LiteralValue[0]);
|
||||
Assert.Equal("2", fileSearchTool.VectorStoreIds.LiteralValue[1]);
|
||||
Assert.Equal("3", fileSearchTool.VectorStoreIds.LiteralValue[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromYaml_ApiKeyConnection()
|
||||
{
|
||||
// Arrange & Act
|
||||
var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithApiKeyConnection);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.NotNull(agent.Model);
|
||||
CurrentModels model = (agent.Model as CurrentModels)!;
|
||||
Assert.NotNull(model);
|
||||
Assert.NotNull(model.Connection);
|
||||
Assert.IsType<ApiKeyConnection>(model.Connection);
|
||||
ApiKeyConnection connection = (model.Connection as ApiKeyConnection)!;
|
||||
Assert.NotNull(connection);
|
||||
Assert.Equal("https://my-azure-openai-endpoint.openai.azure.com/", connection.Endpoint?.LiteralValue);
|
||||
Assert.Equal("my-api-key", connection.Key?.LiteralValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromYaml_RemoteConnection()
|
||||
{
|
||||
// Arrange & Act
|
||||
var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithRemoteConnection);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.NotNull(agent.Model);
|
||||
CurrentModels model = (agent.Model as CurrentModels)!;
|
||||
Assert.NotNull(model);
|
||||
Assert.NotNull(model.Connection);
|
||||
Assert.IsType<RemoteConnection>(model.Connection);
|
||||
RemoteConnection connection = (model.Connection as RemoteConnection)!;
|
||||
Assert.NotNull(connection);
|
||||
Assert.Equal("https://my-azure-openai-endpoint.openai.azure.com/", connection.Endpoint?.LiteralValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromYaml_WithVariableReferences()
|
||||
{
|
||||
// Arrange
|
||||
IConfiguration configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["OpenAIEndpoint"] = "endpoint",
|
||||
["OpenAIApiKey"] = "apiKey",
|
||||
["Temperature"] = "0.9",
|
||||
["TopP"] = "0.8"
|
||||
})
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithVariableReferences, configuration);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.NotNull(agent.Model);
|
||||
CurrentModels model = (agent.Model as CurrentModels)!;
|
||||
Assert.NotNull(model);
|
||||
Assert.NotNull(model.Options);
|
||||
Assert.Equal(0.9, Eval(model.Options?.Temperature, configuration));
|
||||
Assert.Equal(0.8, Eval(model.Options?.TopP, configuration));
|
||||
Assert.NotNull(model.Connection);
|
||||
Assert.IsType<ApiKeyConnection>(model.Connection);
|
||||
ApiKeyConnection connection = (model.Connection as ApiKeyConnection)!;
|
||||
Assert.NotNull(connection);
|
||||
Assert.NotNull(connection.Endpoint);
|
||||
Assert.NotNull(connection.Key);
|
||||
Assert.Equal("endpoint", Eval(connection.Endpoint, configuration));
|
||||
Assert.Equal("apiKey", Eval(connection.Key, configuration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents information about a person, including their name, age, and occupation, matched to the JSON schema used in the agent.
|
||||
/// </summary>
|
||||
[Description("Information about a person including their name, age, and occupation")]
|
||||
public sealed class PersonInfo
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("age")]
|
||||
public int? Age { get; set; }
|
||||
|
||||
[JsonPropertyName("occupation")]
|
||||
public string? Occupation { get; set; }
|
||||
}
|
||||
|
||||
private static string? Eval(StringExpression? expression, IConfiguration? configuration = null)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
RecalcEngine engine = new();
|
||||
if (configuration is not null)
|
||||
{
|
||||
foreach (var kvp in configuration.AsEnumerable())
|
||||
{
|
||||
engine.UpdateVariable(kvp.Key, kvp.Value ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
return expression.Eval(engine);
|
||||
}
|
||||
|
||||
private static double? Eval(NumberExpression? expression, IConfiguration? configuration = null)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
RecalcEngine engine = new();
|
||||
if (configuration != null)
|
||||
{
|
||||
foreach (var kvp in configuration.AsEnumerable())
|
||||
{
|
||||
engine.UpdateVariable(kvp.Key, kvp.Value ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
return expression.Eval(engine);
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Declarative.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="AggregatorPromptAgentFactory"/>
|
||||
/// </summary>
|
||||
public sealed class AggregatorPromptAgentFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void AggregatorAgentFactory_ThrowsForEmptyArray()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
Assert.Throws<ArgumentException>(() => new AggregatorPromptAgentFactory([]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AggregatorAgentFactory_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var factory = new AggregatorPromptAgentFactory([new TestAgentFactory(null)]);
|
||||
|
||||
// Act
|
||||
var agent = await factory.TryCreateAsync(new GptComponentMetadata("test"));
|
||||
|
||||
// Assert
|
||||
Assert.Null(agent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AggregatorAgentFactory_ReturnsAgent()
|
||||
{
|
||||
// Arrange
|
||||
var agentToReturn = new TestAgent();
|
||||
var factory = new AggregatorPromptAgentFactory([new TestAgentFactory(null), new TestAgentFactory(agentToReturn)]);
|
||||
|
||||
// Act
|
||||
var agent = await factory.TryCreateAsync(new GptComponentMetadata("test"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(agentToReturn, agent);
|
||||
}
|
||||
|
||||
private sealed class TestAgentFactory : PromptAgentFactory
|
||||
{
|
||||
private readonly AIAgent? _agentToReturn;
|
||||
|
||||
public TestAgentFactory(AIAgent? agentToReturn = null)
|
||||
{
|
||||
this._agentToReturn = agentToReturn;
|
||||
}
|
||||
|
||||
public override Task<AIAgent?> TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(this._agentToReturn);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestAgent : AIAgent
|
||||
{
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override AgentThread GetNewThread()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Declarative.UnitTests.ChatClient;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="ChatClientPromptAgentFactory"/>.
|
||||
/// </summary>
|
||||
public sealed class ChatClientAgentFactoryTests
|
||||
{
|
||||
private readonly Mock<IChatClient> _mockChatClient;
|
||||
|
||||
public ChatClientAgentFactoryTests()
|
||||
{
|
||||
this._mockChatClient = new();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryCreateAsync_WithChatClientInConstructor_CreatesAgentAsync()
|
||||
{
|
||||
// Arrange
|
||||
var promptAgent = PromptAgents.CreateTestPromptAgent();
|
||||
ChatClientPromptAgentFactory factory = new(this._mockChatClient.Object);
|
||||
|
||||
// Act
|
||||
AIAgent? agent = await factory.TryCreateAsync(promptAgent);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.IsType<ChatClientAgent>(agent);
|
||||
Assert.Equal("Test Agent", agent.Name);
|
||||
Assert.Equal("Test Description", agent.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryCreateAsync_Creates_ChatClientAgentAsync()
|
||||
{
|
||||
// Arrange
|
||||
var promptAgent = PromptAgents.CreateTestPromptAgent();
|
||||
ChatClientPromptAgentFactory factory = new(this._mockChatClient.Object);
|
||||
|
||||
// Act
|
||||
AIAgent? agent = await factory.TryCreateAsync(promptAgent);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.IsType<ChatClientAgent>(agent);
|
||||
var chatClientAgent = agent as ChatClientAgent;
|
||||
Assert.NotNull(chatClientAgent);
|
||||
Assert.Equal("You are a helpful assistant.", chatClientAgent.Instructions);
|
||||
Assert.NotNull(chatClientAgent.ChatClient);
|
||||
Assert.NotNull(chatClientAgent.ChatOptions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryCreateAsync_Creates_ChatOptionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var promptAgent = PromptAgents.CreateTestPromptAgent();
|
||||
ChatClientPromptAgentFactory factory = new(this._mockChatClient.Object);
|
||||
|
||||
// Act
|
||||
AIAgent? agent = await factory.TryCreateAsync(promptAgent);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.IsType<ChatClientAgent>(agent);
|
||||
var chatClientAgent = agent as ChatClientAgent;
|
||||
Assert.NotNull(chatClientAgent?.ChatOptions);
|
||||
Assert.Equal("Provide detailed and accurate responses.", chatClientAgent?.ChatOptions?.Instructions);
|
||||
Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.Temperature);
|
||||
Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.FrequencyPenalty);
|
||||
Assert.Equal(1024, chatClientAgent?.ChatOptions?.MaxOutputTokens);
|
||||
Assert.Equal(0.9F, chatClientAgent?.ChatOptions?.TopP);
|
||||
Assert.Equal(50, chatClientAgent?.ChatOptions?.TopK);
|
||||
Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.PresencePenalty);
|
||||
Assert.Equal(42L, chatClientAgent?.ChatOptions?.Seed);
|
||||
Assert.NotNull(chatClientAgent?.ChatOptions?.ResponseFormat);
|
||||
Assert.Equal("gpt-4o", chatClientAgent?.ChatOptions?.ModelId);
|
||||
Assert.Equal(["###", "END", "STOP"], chatClientAgent?.ChatOptions?.StopSequences);
|
||||
Assert.True(chatClientAgent?.ChatOptions?.AllowMultipleToolCalls);
|
||||
Assert.Equal(ChatToolMode.Auto, chatClientAgent?.ChatOptions?.ToolMode);
|
||||
Assert.Equal("customValue", chatClientAgent?.ChatOptions?.AdditionalProperties?["customProperty"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryCreateAsync_Creates_ToolsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var promptAgent = PromptAgents.CreateTestPromptAgent();
|
||||
ChatClientPromptAgentFactory factory = new(this._mockChatClient.Object);
|
||||
|
||||
// Act
|
||||
AIAgent? agent = await factory.TryCreateAsync(promptAgent);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.IsType<ChatClientAgent>(agent);
|
||||
var chatClientAgent = agent as ChatClientAgent;
|
||||
Assert.NotNull(chatClientAgent?.ChatOptions?.Tools);
|
||||
var tools = chatClientAgent?.ChatOptions?.Tools;
|
||||
Assert.Equal(5, tools?.Count);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);IDE1006;VSTHRD200</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Declarative\Microsoft.Agents.AI.Declarative.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,386 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Declarative.UnitTests;
|
||||
|
||||
internal static class PromptAgents
|
||||
{
|
||||
internal const string AgentWithEverything =
|
||||
"""
|
||||
kind: Prompt
|
||||
name: AgentName
|
||||
description: Agent description
|
||||
instructions: You are a helpful assistant.
|
||||
model:
|
||||
id: gpt-4o
|
||||
options:
|
||||
temperature: 0.7
|
||||
maxOutputTokens: 1024
|
||||
topP: 0.9
|
||||
topK: 50
|
||||
frequencyPenalty: 0.0
|
||||
presencePenalty: 0.0
|
||||
seed: 42
|
||||
responseFormat: text
|
||||
stopSequences:
|
||||
- "###"
|
||||
- "END"
|
||||
- "STOP"
|
||||
allowMultipleToolCalls: true
|
||||
tools:
|
||||
- kind: codeInterpreter
|
||||
inputs:
|
||||
- kind: HostedFileContent
|
||||
FileId: fileId123
|
||||
- kind: function
|
||||
name: GetWeather
|
||||
description: Get the weather for a given location.
|
||||
parameters:
|
||||
- name: location
|
||||
type: string
|
||||
description: The city and state, e.g. San Francisco, CA
|
||||
required: true
|
||||
- name: unit
|
||||
type: string
|
||||
description: The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.
|
||||
required: false
|
||||
enum:
|
||||
- celsius
|
||||
- fahrenheit
|
||||
- kind: mcp
|
||||
serverName: PersonInfoTool
|
||||
serverDescription: Get information about a person.
|
||||
connection:
|
||||
kind: AnonymousConnection
|
||||
endpoint: https://my-mcp-endpoint.com/api
|
||||
allowedTools:
|
||||
- "GetPersonInfo"
|
||||
- "UpdatePersonInfo"
|
||||
- "DeletePersonInfo"
|
||||
approvalMode:
|
||||
kind: HostedMcpServerToolRequireSpecificApprovalMode
|
||||
AlwaysRequireApprovalToolNames:
|
||||
- "UpdatePersonInfo"
|
||||
- "DeletePersonInfo"
|
||||
NeverRequireApprovalToolNames:
|
||||
- "GetPersonInfo"
|
||||
- kind: webSearch
|
||||
name: WebSearchTool
|
||||
description: Search the web for information.
|
||||
- kind: fileSearch
|
||||
name: FileSearchTool
|
||||
description: Search files for information.
|
||||
ranker: default
|
||||
scoreThreshold: 0.5
|
||||
maxResults: 5
|
||||
maxContentLength: 2000
|
||||
vectorStoreIds:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
""";
|
||||
|
||||
internal const string AgentWithOutputSchema =
|
||||
"""
|
||||
kind: Prompt
|
||||
name: Translation Assistant
|
||||
description: A helpful assistant that translates text to a specified language.
|
||||
model:
|
||||
id: gpt-4o
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
instructions: You are a helpful assistant. You answer questions in {language}. You return your answers in a JSON format.
|
||||
additionalInstructions: You must always respond in the specified language.
|
||||
tools:
|
||||
- kind: codeInterpreter
|
||||
template:
|
||||
format: PowerFx # Mustache is the other option
|
||||
parser: None # Prompty and XML are the other options
|
||||
inputSchema:
|
||||
properties:
|
||||
language: string
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
""";
|
||||
|
||||
internal const string AgentWithApiKeyConnection =
|
||||
"""
|
||||
kind: Prompt
|
||||
name: AgentName
|
||||
description: Agent description
|
||||
instructions: You are a helpful assistant.
|
||||
model:
|
||||
id: gpt-4o
|
||||
connection:
|
||||
kind: ApiKey
|
||||
endpoint: https://my-azure-openai-endpoint.openai.azure.com/
|
||||
key: my-api-key
|
||||
""";
|
||||
|
||||
internal const string AgentWithRemoteConnection =
|
||||
"""
|
||||
kind: Prompt
|
||||
name: AgentName
|
||||
description: Agent description
|
||||
instructions: You are a helpful assistant.
|
||||
model:
|
||||
id: gpt-4o
|
||||
connection:
|
||||
kind: Remote
|
||||
endpoint: https://my-azure-openai-endpoint.openai.azure.com/
|
||||
""";
|
||||
|
||||
internal const string AgentWithVariableReferences =
|
||||
"""
|
||||
kind: Prompt
|
||||
name: AgentName
|
||||
description: Agent description
|
||||
instructions: You are a helpful assistant.
|
||||
model:
|
||||
id: gpt-4o
|
||||
options:
|
||||
temperature: =Env.Temperature
|
||||
topP: =Env.TopP
|
||||
connection:
|
||||
kind: apiKey
|
||||
endpoint: =Env.OpenAIEndpoint
|
||||
key: =Env.OpenAIApiKey
|
||||
""";
|
||||
|
||||
internal const string OpenAIChatAgent =
|
||||
"""
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
|
||||
model:
|
||||
id: =Env.OPENAI_MODEL
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: apiKey
|
||||
key: =Env.OPENAI_APIKEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
""";
|
||||
|
||||
internal const string AgentWithCurrentModels =
|
||||
"""
|
||||
kind: Prompt
|
||||
name: AgentName
|
||||
description: Agent description
|
||||
instructions: You are a helpful assistant.
|
||||
model:
|
||||
id: gpt-4o
|
||||
options:
|
||||
temperature: 0.7
|
||||
maxOutputTokens: 1024
|
||||
topP: 0.9
|
||||
topK: 50
|
||||
frequencyPenalty: 0.7
|
||||
presencePenalty: 0.7
|
||||
seed: 42
|
||||
responseFormat: text
|
||||
stopSequences:
|
||||
- "###"
|
||||
- "END"
|
||||
- "STOP"
|
||||
allowMultipleToolCalls: true
|
||||
chatToolMode: auto
|
||||
""";
|
||||
|
||||
internal const string AgentWithCurrentModelsSnakeCase =
|
||||
"""
|
||||
kind: Prompt
|
||||
name: AgentName
|
||||
description: Agent description
|
||||
instructions: You are a helpful assistant.
|
||||
model:
|
||||
id: gpt-4o
|
||||
options:
|
||||
temperature: 0.7
|
||||
max_output_tokens: 1024
|
||||
top_p: 0.9
|
||||
top_k: 50
|
||||
frequency_penalty: 0.7
|
||||
presence_penalty: 0.7
|
||||
seed: 42
|
||||
response_format: text
|
||||
stop_sequences:
|
||||
- "###"
|
||||
- "END"
|
||||
- "STOP"
|
||||
allow_multiple_tool_calls: true
|
||||
chat_tool_mode: auto
|
||||
""";
|
||||
|
||||
internal const string Workflow =
|
||||
"""
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_student
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: StudentAgent
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_teacher
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: TeacherAgent
|
||||
output:
|
||||
messages: Local.TeacherResponse
|
||||
|
||||
- kind: SetVariable
|
||||
id: set_count_increment
|
||||
variable: Local.TurnCount
|
||||
value: =Local.TurnCount + 1
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: check_completion
|
||||
conditions:
|
||||
|
||||
- condition: =!IsBlank(Find("CONGRATULATIONS", Upper(MessageText(Local.TeacherResponse))))
|
||||
id: check_turn_done
|
||||
actions:
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_done
|
||||
activity: GOLD STAR!
|
||||
|
||||
- condition: =Local.TurnCount < 4
|
||||
id: check_turn_count
|
||||
actions:
|
||||
|
||||
- kind: GotoAction
|
||||
id: goto_student_agent
|
||||
actionId: question_student
|
||||
|
||||
elseActions:
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_tired
|
||||
activity: Let's try again later...
|
||||
|
||||
""";
|
||||
|
||||
internal static readonly string[] s_stopSequences = ["###", "END", "STOP"];
|
||||
|
||||
internal static GptComponentMetadata CreateTestPromptAgent(string? publisher = "OpenAI", string? apiType = "Chat")
|
||||
{
|
||||
string agentYaml =
|
||||
$"""
|
||||
kind: Prompt
|
||||
name: Test Agent
|
||||
description: Test Description
|
||||
instructions: You are a helpful assistant.
|
||||
additionalInstructions: Provide detailed and accurate responses.
|
||||
model:
|
||||
id: gpt-4o
|
||||
publisher: {publisher}
|
||||
apiType: {apiType}
|
||||
options:
|
||||
modelId: gpt-4o
|
||||
temperature: 0.7
|
||||
maxOutputTokens: 1024
|
||||
topP: 0.9
|
||||
topK: 50
|
||||
frequencyPenalty: 0.7
|
||||
presencePenalty: 0.7
|
||||
seed: 42
|
||||
responseFormat: text
|
||||
stopSequences:
|
||||
- "###"
|
||||
- "END"
|
||||
- "STOP"
|
||||
allowMultipleToolCalls: true
|
||||
chatToolMode: auto
|
||||
customProperty: customValue
|
||||
connection:
|
||||
kind: apiKey
|
||||
endpoint: https://my-azure-openai-endpoint.openai.azure.com/
|
||||
key: my-api-key
|
||||
tools:
|
||||
- kind: codeInterpreter
|
||||
- kind: function
|
||||
name: GetWeather
|
||||
description: Get the weather for a given location.
|
||||
parameters:
|
||||
- name: location
|
||||
type: string
|
||||
description: The city and state, e.g. San Francisco, CA
|
||||
required: true
|
||||
- name: unit
|
||||
type: string
|
||||
description: The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.
|
||||
required: false
|
||||
enum:
|
||||
- celsius
|
||||
- fahrenheit
|
||||
- kind: mcp
|
||||
serverName: PersonInfoTool
|
||||
serverDescription: Get information about a person.
|
||||
allowedTools:
|
||||
- "GetPersonInfo"
|
||||
- "UpdatePersonInfo"
|
||||
- "DeletePersonInfo"
|
||||
approvalMode:
|
||||
kind: HostedMcpServerToolRequireSpecificApprovalMode
|
||||
AlwaysRequireApprovalToolNames:
|
||||
- "UpdatePersonInfo"
|
||||
- "DeletePersonInfo"
|
||||
NeverRequireApprovalToolNames:
|
||||
- "GetPersonInfo"
|
||||
connection:
|
||||
kind: AnonymousConnection
|
||||
endpoint: https://my-mcp-endpoint.com/api
|
||||
- kind: webSearch
|
||||
name: WebSearchTool
|
||||
description: Search the web for information.
|
||||
- kind: fileSearch
|
||||
name: FileSearchTool
|
||||
description: Search files for information.
|
||||
vectorStoreIds:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
""";
|
||||
|
||||
return AgentBotElementYaml.FromYaml(agentYaml);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user