Skip to content

DecisionFactories

Rasmus Wulff Jensen edited this page Oct 10, 2026 · 2 revisions

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.

Providers and packages

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.

Create a DecisionFactory

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.

Jev

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.

Azure OpenAI / Microsoft-Decision-1

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.

Single-question decisions

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.

Runtime-defined questions

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.

Runtime-defined questions with images

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.

Multiple questions and typed results

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.

Images and provider limits

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.

Errors and HTTP details

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.

Migration in 1.24.1

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 implementations in 1.24.2

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.

Clone this wiki locally