Repository navigation
Add experimental provider-neutral decision abstractions (Layer 1) #7795
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
252989a
Add experimental decision abstractions
luisquintanilla 6aa33bf
Fix decision JSON and rounding validation
luisquintanilla 7848b09
Document business decision example
luisquintanilla 157f9e0
Clarify decision example snippet
luisquintanilla c080234
Add typed decision ergonomics
luisquintanilla d617343
Fix typed decision response correlation
luisquintanilla ac49720
Simplify decision usage examples
luisquintanilla a067c99
Tidy decision usage sample spacing
luisquintanilla d5eafd8
Tighten decision provider proof semantics
luisquintanilla 230213b
Harden decision proof validation
luisquintanilla 02e54f0
Expand decision regression coverage
luisquintanilla File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
404 changes: 404 additions & 0 deletions
404
src/Libraries/Microsoft.Extensions.AI.Abstractions/Decisions/DecisionAnswers.cs
Large diffs are not rendered by default.
Oops, something went wrong.
191 changes: 191 additions & 0 deletions
191
src/Libraries/Microsoft.Extensions.AI.Abstractions/Decisions/DecisionBinding.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| #pragma warning disable SA1402 // File may only contain a single type — binding proof types are co-located as one API surface. | ||
| #pragma warning disable SA1649 // File name should match first type name — binding proof types are co-located as one API surface. | ||
| #pragma warning disable SA1204 // Static extension type is intentionally grouped with the binding types. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Collections.ObjectModel; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Linq; | ||
| using System.Text.Json.Serialization.Metadata; | ||
| using Microsoft.Shared.DiagnosticIds; | ||
| using Microsoft.Shared.Diagnostics; | ||
|
|
||
| namespace Microsoft.Extensions.AI; | ||
|
|
||
| /// <summary>Provides an explicit mapping from a choice candidate ID to an enum value.</summary> | ||
| /// <typeparam name="TEnum">The application-owned enum type.</typeparam> | ||
| [Experimental(DiagnosticIds.Experiments.AIDecisions, UrlFormat = DiagnosticIds.UrlFormat)] | ||
| public sealed class DecisionEnumDefinition<TEnum> | ||
| where TEnum : struct, Enum | ||
| { | ||
| private readonly ReadOnlyDictionary<string, TEnum> _values; | ||
|
|
||
| /// <summary>Initializes a new instance of the <see cref="DecisionEnumDefinition{TEnum}"/> class.</summary> | ||
| public DecisionEnumDefinition(IEnumerable<DecisionEnumValue<TEnum>> values) | ||
| { | ||
| _ = Throw.IfNull(values); | ||
|
|
||
| DecisionEnumValue<TEnum>[] copy = values.ToArray(); | ||
| if (copy.Length == 0) | ||
| { | ||
| Throw.ArgumentException(nameof(values), "An enum definition requires at least one value."); | ||
| } | ||
|
|
||
| Dictionary<string, TEnum> byId = new(StringComparer.Ordinal); | ||
| HashSet<TEnum> enumValues = new(); | ||
| DecisionCandidate[] candidates = new DecisionCandidate[copy.Length]; | ||
|
|
||
| for (int i = 0; i < copy.Length; i++) | ||
| { | ||
| DecisionEnumValue<TEnum> value = copy[i]; | ||
| if (!enumValues.Add(value.Value)) | ||
| { | ||
| Throw.ArgumentException(nameof(values), "Enum aliases or duplicate enum values are not allowed."); | ||
| } | ||
|
|
||
| if (byId.ContainsKey(value.Id)) | ||
| { | ||
| Throw.ArgumentException( | ||
| nameof(values), | ||
| "Enum candidate IDs must be unique using ordinal comparison."); | ||
| } | ||
|
|
||
| byId.Add(value.Id, value.Value); | ||
| candidates[i] = new DecisionCandidate(value.Id, value.Description); | ||
| } | ||
|
|
||
| _values = new ReadOnlyDictionary<string, TEnum>(byId); | ||
| Candidates = Array.AsReadOnly(candidates); | ||
| } | ||
|
|
||
| /// <summary>Gets the ordered candidate snapshot for a choice question.</summary> | ||
| public IReadOnlyList<DecisionCandidate> Candidates { get; } | ||
|
|
||
| /// <summary>Creates a choice question using this exact enum mapping.</summary> | ||
| /// <returns>The choice question with the exact mapped candidate IDs.</returns> | ||
| public ChoiceDecisionQuestion CreateQuestion(string id, string? instructions) => | ||
| new(id, instructions, Candidates); | ||
|
|
||
| /// <summary>Maps an exact selected candidate ID to its enum value.</summary> | ||
| /// <returns>The mapped enum value.</returns> | ||
| public TEnum Parse(string candidateId) | ||
| { | ||
| _ = Throw.IfNullOrWhitespace(candidateId); | ||
| if (_values.TryGetValue(candidateId, out TEnum value)) | ||
| { | ||
| return value; | ||
| } | ||
|
|
||
| throw new DecisionProtocolException($"Unknown candidate ID '{candidateId}'."); | ||
| } | ||
| } | ||
|
|
||
| /// <summary>Describes one enum mapping entry.</summary> | ||
| /// <typeparam name="TEnum">The application-owned enum type.</typeparam> | ||
| [Experimental(DiagnosticIds.Experiments.AIDecisions, UrlFormat = DiagnosticIds.UrlFormat)] | ||
| public sealed class DecisionEnumValue<TEnum> | ||
| where TEnum : struct, Enum | ||
| { | ||
| /// <summary>Initializes a new instance of the <see cref="DecisionEnumValue{TEnum}"/> class.</summary> | ||
| public DecisionEnumValue(TEnum value, string id, string description) | ||
| { | ||
| Value = value; | ||
| Id = Throw.IfNullOrWhitespace(id); | ||
| Description = Throw.IfNullOrWhitespace(description); | ||
| } | ||
|
|
||
| /// <summary>Gets the application enum value.</summary> | ||
| public TEnum Value { get; } | ||
|
|
||
| /// <summary>Gets the exact candidate ID.</summary> | ||
| public string Id { get; } | ||
|
|
||
| /// <summary>Gets the candidate description.</summary> | ||
| public string Description { get; } | ||
| } | ||
|
|
||
| /// <summary>Provides typed access to a complete decision response.</summary> | ||
| [Experimental(DiagnosticIds.Experiments.AIDecisions, UrlFormat = DiagnosticIds.UrlFormat)] | ||
| public sealed class DecisionAnswerSet | ||
| { | ||
| internal DecisionAnswerSet(DecisionResponse response) | ||
| { | ||
| Response = response; | ||
| } | ||
|
|
||
| /// <summary>Gets the complete unmodified response, including probability evidence.</summary> | ||
| public DecisionResponse Response { get; } | ||
|
|
||
| /// <summary>Gets a binary answer by exact question ID.</summary> | ||
| /// <returns>The complete binary answer.</returns> | ||
| public BinaryDecisionAnswer GetBinary(string questionId) => | ||
| Response.GetAnswer(questionId) as BinaryDecisionAnswer ?? | ||
| throw new DecisionProtocolException($"Question '{questionId}' is not binary."); | ||
|
|
||
| /// <summary>Gets a choice answer by exact question ID.</summary> | ||
| /// <returns>The complete choice answer.</returns> | ||
| public ChoiceDecisionAnswer GetChoice(string questionId) => | ||
| Response.GetAnswer(questionId) as ChoiceDecisionAnswer ?? | ||
| throw new DecisionProtocolException($"Question '{questionId}' is not a choice."); | ||
|
|
||
| /// <summary>Gets a choice answer and maps its exact selected ID to an enum.</summary> | ||
| /// <typeparam name="TEnum">The application-owned enum type.</typeparam> | ||
| /// <returns>The mapped enum value.</returns> | ||
| public TEnum GetChoice<TEnum>(string questionId, DecisionEnumDefinition<TEnum> definition) | ||
| where TEnum : struct, Enum | ||
| { | ||
| _ = Throw.IfNull(definition); | ||
| return definition.Parse(GetChoice(questionId).SelectedCandidateId); | ||
| } | ||
|
|
||
| /// <summary>Gets a score answer by exact question ID.</summary> | ||
| /// <returns>The complete score answer.</returns> | ||
| public ScoreDecisionAnswer GetScore(string questionId) => | ||
| Response.GetAnswer(questionId) as ScoreDecisionAnswer ?? | ||
| throw new DecisionProtocolException($"Question '{questionId}' is not a score."); | ||
| } | ||
|
|
||
| /// <summary>Maps one complete response to an application-owned result without inference or reflection.</summary> | ||
| /// <typeparam name="TResult">The application-owned result type.</typeparam> | ||
| [Experimental(DiagnosticIds.Experiments.AIDecisions, UrlFormat = DiagnosticIds.UrlFormat)] | ||
| public sealed class DecisionResultBinding<TResult> | ||
| { | ||
| private readonly Func<DecisionAnswerSet, TResult> _map; | ||
|
|
||
| /// <summary>Initializes a new instance of the <see cref="DecisionResultBinding{TResult}"/> class.</summary> | ||
| /// <param name="resultTypeInfo">The source-generated JSON contract for the application result.</param> | ||
| /// <param name="map">The application-owned mapping delegate.</param> | ||
| public DecisionResultBinding( | ||
| JsonTypeInfo<TResult> resultTypeInfo, | ||
| Func<DecisionAnswerSet, TResult> map) | ||
| { | ||
| ResultTypeInfo = Throw.IfNull(resultTypeInfo); | ||
| _map = Throw.IfNull(map); | ||
| } | ||
|
|
||
| /// <summary>Gets the explicit source-generated result contract.</summary> | ||
| public JsonTypeInfo<TResult> ResultTypeInfo { get; } | ||
|
|
||
| internal TResult Map(DecisionResponse response) => _map(new DecisionAnswerSet(response)); | ||
| } | ||
|
|
||
| /// <summary>Provides response binding extensions.</summary> | ||
| [Experimental(DiagnosticIds.Experiments.AIDecisions, UrlFormat = DiagnosticIds.UrlFormat)] | ||
| public static class DecisionBindingExtensions | ||
| { | ||
| /// <summary>Maps a complete response locally using the supplied explicit binding.</summary> | ||
| /// <typeparam name="TResult">The application-owned result type.</typeparam> | ||
| /// <returns>The application-owned result.</returns> | ||
| public static TResult Bind<TResult>( | ||
| this DecisionResponse response, | ||
| DecisionResultBinding<TResult> binding) | ||
| { | ||
| _ = Throw.IfNull(response); | ||
| _ = Throw.IfNull(binding); | ||
| return binding.Map(response); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The README calls DecisionResultBinding the “older” API, but both it and the typed-definition API are introduced by this PR. Is the delegate-binding surface retained from an earlier prototype, or does it serve a distinct scenario that should remain public?