Repository navigation
DecisionFactories
Note
DecisionFactories are available for OpenAI, Jev, and Microsoft-Decision-1 through the Azure OpenAI package, starting with version 1.24.1.
Runtime-defined questions through the non-generic CreateDecisionAsync overload are available in version 1.24.2.
The samples below use an OpenAIDecisionFactory. All three factories implement IDecisionFactory and use the same requests, question attributes, and results from AgentFrameworkToolkit.Decisions.
DecisionFactories evaluate choices, probabilities, and ordered scores. Use a single-question method, supply runtime-defined questions, or define several questions on a typed result class.
| Provider | Factory | Package | Authentication |
|---|---|---|---|
| OpenAI | OpenAIDecisionFactory |
AgentFrameworkToolkit.OpenAI |
API key |
| Jev | JevDecisionFactory |
AgentFrameworkToolkit.Jev |
API key |
| Azure OpenAI (Microsoft-Decision-1) | AzureOpenAIDecisionFactory |
AgentFrameworkToolkit.AzureOpenAI |
API key or RBAC |
Install the provider package you need. The shared contracts are included through the core package dependency.
You can use simplified connection information or a Connection instance. The model or Azure deployment name must be supplied explicitly.
Option 1. Create a new instance manually
using AgentFrameworkToolkit.Decisions;
using AgentFrameworkToolkit.OpenAI;
IDecisionFactory decisionFactory = new OpenAIDecisionFactory(
"<api-key>", OpenAIChatModels.Gpt6Luna);
// Or use a connection for advanced options
IDecisionFactory configuredFactory = new OpenAIDecisionFactory(
new OpenAIConnection
{
ApiKey = "<api-key>",
NetworkTimeout = TimeSpan.FromSeconds(60)
},
OpenAIChatModels.Gpt6Luna);Option 2. Use Dependency Injection
builder.Services.AddOpenAIDecisionFactory("<api-key>", OpenAIChatModels.Gpt6Luna);
// Or use a connection
builder.Services.AddOpenAIDecisionFactory(new OpenAIConnection
{
ApiKey = "<api-key>",
NetworkTimeout = TimeSpan.FromSeconds(60)
}, OpenAIChatModels.Gpt6Luna);Each registration supplies the concrete factory and IDecisionFactory as the same singleton. Inject IDecisionFactory into your application. If you register several decision providers, resolve their concrete factories; the last registration supplies IDecisionFactory.
using AgentFrameworkToolkit.Jev;
IDecisionFactory decisionFactory = new JevDecisionFactory("<api-key>", "<decision-model>");
builder.Services.AddJevDecisionFactory("<api-key>", "<decision-model>");Use JevConnection to configure the endpoint, network timeout, or a caller-owned HTTP client.
using AgentFrameworkToolkit.AzureOpenAI;
using Azure.Identity;
IDecisionFactory withApiKey = new AzureOpenAIDecisionFactory(
"https://<resource>.services.ai.azure.com", "<api-key>", "<deployment-name>");
IDecisionFactory withRbac = new AzureOpenAIDecisionFactory(
"https://<resource>.services.ai.azure.com", new AzureCliCredential(), "<deployment-name>");
builder.Services.AddAzureOpenAIDecisionFactory(
"https://<resource>.services.ai.azure.com", "<api-key>", "<deployment-name>");This factory calls Microsoft's dedicated decision API for Microsoft-Decision-1. Supply your Foundry resource endpoint and deployed model's name. It reuses AzureOpenAIConnection for endpoint correction, authentication, timeout, and transport configuration.
These methods work through any IDecisionFactory.
string input = "The production website is unavailable to every customer.";
Department department = await decisionFactory.ChooseAsync<Department>(new ChoiceRequest
{
Input = input,
Question = "Which department should handle this?"
});
bool urgent = await decisionFactory.IsTrueAsync(new ProbabilityRequest
{
Input = input,
Question = "Does this need urgent attention?",
Threshold = 0.7
});
Probability probability = await decisionFactory.ProbabilityAsync(new ProbabilityRequest
{
Input = input,
Question = "Does this need urgent attention?",
Threshold = 0.7
});
Score<Severity> severity = await decisionFactory.ScoreAsync<Severity>(new ScoreRequest
{
Input = input,
Question = "How severe is the impact?"
});| Method | Result |
|---|---|
ChooseAsync<TEnum> |
The selected enum value |
IsTrueAsync |
Whether the probability meets the inclusive threshold |
ProbabilityAsync |
Probability with Value, Threshold, and IsTrue
|
ScoreAsync<TEnum> |
Score<TEnum> with the weighted level index, confidence, probabilities, and level descriptions |
CreateDecisionAsync<T> |
Multiple typed answers and token usage |
CreateDecisionAsync |
Runtime-defined answers retrieved by question ID, plus token usage (1.24.2+) |
Threshold defaults to 0.5. Read Probability.Value for the raw probability or Probability.IsTrue for the threshold comparison. All operations accept a cancellation token.
Use DynamicDecisionRequest when questions or choices come from configuration, a
database, or application code without a result class or enums. Its Questions
collection accepts the built-in IQuestion implementations: ChoiceQuestion,
ScoreQuestion, and ProbabilityQuestion.
DynamicDecisionRequest request = new()
{
Input = "My card was charged twice. Please refund the duplicate.",
Questions =
[
new ChoiceQuestion
{
Id = "department",
Question = "Which team should handle this?",
Choices =
[
new() { Id = "billing", Description = "Charges, payments, and refunds" },
new() { Id = "technical", Description = "Software defects and outages" }
]
},
new ScoreQuestion
{
Id = "urgency",
Question = "How urgently should support respond?",
Levels = ["Routine", "Soon", "Immediately"]
},
new ProbabilityQuestion
{
Id = "refund_requested",
Question = "Does the customer request a refund?",
Threshold = 0.8
}
]
};
DecisionResponse response = await decisionFactory.CreateDecisionAsync(request);
ChoiceAnswer department = response.GetChoice("department");
Console.WriteLine(department.Value.Id);
Console.WriteLine(department.Value.Description);
Console.WriteLine(department.Value.Probability);
Console.WriteLine(department.Confidence);
foreach (ChoiceOption option in department.Options)
{
Console.WriteLine($"{option.Id}: {option.Description} — {option.Probability:P0}");
}
ScoreAnswer urgency = response.GetScore("URGENCY");
Console.WriteLine(urgency.Value);
Console.WriteLine(urgency.Confidence);
foreach (ScoreLevel level in urgency.Levels)
{
Console.WriteLine($"{level.Index}: {level.Description} — {level.Probability:P0}");
}
ProbabilityAnswer refund = response.GetProbability("refund_requested");
Console.WriteLine(refund.Value);
Console.WriteLine(refund.Threshold);
Console.WriteLine(refund.IsTrue);
Console.WriteLine(response.Model);
Console.WriteLine(response.InputTokenCount);
Console.WriteLine(response.OutputTokenCount);
Console.WriteLine(response.TotalTokenCount);The same request works with any of the three factories shown above.
The response exposes answer accessors directly, without a .Result or .Answers
property. There are no generic dynamic questions or answer accessors.
| Accessor | Answer details |
|---|---|
GetChoice(id) |
ChoiceAnswer.Value is the selected ChoiceOption, with its string Id, Description, and Probability. Confidence and the immutable Options collection provide full details. |
GetScore(id) |
ScoreAnswer.Value is the weighted zero-based level index. Confidence and the immutable, ordered Levels collection retain every level's Index, Description, and Probability. |
GetProbability(id) |
ProbabilityAnswer.Value is the numeric probability. Threshold defaults to 0.5, and IsTrue checks Value >= Threshold. |
Question IDs and lookups are case-insensitive: GetScore("URGENCY") retrieves
the question declared as "urgency". Duplicate question IDs, including IDs that
differ only by case, are rejected before sending a provider request. IDs and
instructions must be nonempty. Choices require at least two options with nonempty
IDs and descriptions; option IDs must be unique ignoring case. Scores require
at least two nonempty level descriptions; request order defines indices starting
at zero. Jev's existing maximum of ten score levels still applies.
Unknown question IDs throw KeyNotFoundException. Using the wrong accessor, such
as GetScore("department"), throws InvalidOperationException. Custom
implementations of IQuestion are rejected with NotSupportedException.
Thresholds must be finite and between zero and one. Refused, incomplete, and
invalid provider answers fail the entire operation.
OpenAI also supports DynamicDecisionImageRequest, with required Images and
Questions, optional text Input, and ImageDetail:
using Microsoft.Extensions.AI;
DataContent image = new(await File.ReadAllBytesAsync("customer-photo.png"), "image/png");
DecisionResponse imageResponse = await decisionFactory.CreateDecisionAsync(
new DynamicDecisionImageRequest
{
Images = [image],
Input = "Customer photo", // Optional.
ImageDetail = ImageDetail.High,
Questions =
[
new ProbabilityQuestion
{
Id = "damaged",
Question = "Is the product visibly damaged?",
Threshold = 0.7
}
]
});
Console.WriteLine(imageResponse.GetProbability("damaged").IsTrue);Use an OpenAIDecisionFactory for this sample. At most 128 nonempty inline images
with image MIME types are supported. Jev and Azure OpenAI / Microsoft-Decision-1
continue to throw NotSupportedException for image requests.
Define a result class with question attributes and public setters. Use Choice<TEnum> to retain confidence and the full choice distribution.
DecisionResponse<SupportDecision> response =
await decisionFactory.CreateDecisionAsync<SupportDecision>(new DecisionRequest
{
Input = "The production website is unavailable to every customer."
});
Console.WriteLine(response.Result.Department!.Value);
Console.WriteLine(response.Result.Department.Confidence);
Console.WriteLine(response.Result.Severity!.Value);
Console.WriteLine(response.TotalTokenCount);using System.ComponentModel;
public class SupportDecision
{
[ProbabilityQuestion("Does this need urgent attention?", threshold: 0.7)]
public bool Urgent { get; set; }
[ChoiceQuestion<Department>("Which department should handle this?")]
public Choice<Department>? Department { get; set; }
[ScoreQuestion<Severity>("How severe is the impact?")]
public Score<Severity>? Severity { get; set; }
}
public enum Department
{
[Description("Invoices, payments, and subscriptions")]
Billing,
[Description("Outages, errors, and product troubleshooting")]
Technical
}
public enum Severity
{
[Description("Appearance only; functionality is unaffected")]
Cosmetic,
[Description("Usable with limitations")]
Degraded,
[Description("Unusable")]
Blocking
}Predicate properties support bool, double, decimal, their nullable forms, or Probability. Numeric probabilities return the raw value. Choice properties support an enum, its nullable form, or Choice<TEnum>. Score properties support double, decimal, their nullable forms, or Score<TEnum>.
Enum numeric order defines zero-based score levels; the score is a weighted level index, not the enum's underlying numeric value. Enums need at least two distinct values without aliases. DescriptionAttribute supplies option descriptions.
DecisionResponse<T> includes Result, Model, InputTokenCount, OutputTokenCount, and TotalTokenCount.
| Capability | OpenAI | Jev | Azure OpenAI / Microsoft-Decision-1 |
|---|---|---|---|
| Text decisions | Yes | Yes | Yes |
| Inline images | Yes | Unsupported | Unsupported |
SafetyIdentifier |
Supported | Unsupported | Unsupported |
| Score levels | At least two | 2–10 | At least two; toolkit imposes no additional maximum |
Use DynamicDecisionImageRequest, DecisionImageRequest, ChoiceImageRequest, ProbabilityImageRequest, or ScoreImageRequest for OpenAI image evidence. These requests accept Images, optional text Input, and ImageDetail. Jev and Microsoft-Decision-1 throw NotSupportedException for image requests or a supplied SafetyIdentifier.
Refusals throw DecisionRefusalException, whose QuestionName identifies the refused question. Missing or invalid answers throw; partial results are never returned for either typed or dynamic decisions.
Set RawHttpCallDetails on a request to inspect its URL and request/response bodies:
Probability probability = await decisionFactory.ProbabilityAsync(new ProbabilityRequest
{
Input = "The production website is unavailable.",
Question = "Is this an outage?",
RawHttpCallDetails = details =>
{
Console.WriteLine(details.RequestUrl);
Console.WriteLine(details.RequestData);
Console.WriteLine(details.ResponseData);
}
});Connection settings are retained when the factory is constructed; later changes do not reconfigure it.
Replace AgentFrameworkToolkit.OpenAI.Decisions with AgentFrameworkToolkit.Decisions, and rename OpenAIDecisionResponse<T> to DecisionResponse<T>. Shared request and attributed result classes can then be used with all three providers.
Custom classes implementing IDecisionFactory directly must implement the two
new non-generic CreateDecisionAsync overloads for DynamicDecisionRequest and
DynamicDecisionImageRequest. The three built-in factories implement them.
Existing typed requests, result types, and calls to built-in factories are unchanged.