ai newsSo., 25. Okt.
EilmeldungAnthropic Sandbox Runtime: OS-Level Agent Sandboxing Without Containers

Decisions API in Azure OpenAI: A C# Walkthrough

Decisions API in Azure OpenAI: A C# Walkthrough Recently I wrote about Microsoft-Decision-1 , a model that scores a fixed set of options instead of writing text. That post used a Microsoft provider route. This week the Azure OpenAI Decisions API went generally available in Microsoft Foundry, and it

dev.toSonntag 20:02 · 25.10.8 Min. Lesezeit
Bild: dev.to

Veröffentlicht am 25. Oktober • Zuerst erschienen auf taswar.zeytinsoft.com

Kürzlich habe ich über Microsoft-Decision-1 geschrieben, ein Modell, das eine feste Menge von Optionen bewertet, statt Text zu schreiben. Dieser Beitrag nutzte einen Microsoft-Provider-Route. Diese Woche ist die Azure OpenAI Decisions API in Microsoft Foundry allgemein verfügbar geworden, und sie löst dasselbe Problem über die Azure OpenAI Ressource, die Sie möglicherweise bereits haben.

Jeder Agent trifft kleine Urteile: Welches Team ist für dieses Ticket zuständig, möchte der Kunde eine Erstattung, wie schwerwiegend ist dieses Problem, soll der Workflow fortgesetzt oder gestoppt werden. Diese Schritte werden weiterhin an ein Chat-Modell geschickt, was bedeutet, dass Sie nach JSON fragen, Text parsen, der fast wie JSON aussieht, und einer Konfidenzzahl vertrauen, die das Modell nie wirklich zurückgeliefert hat.

Die Decisions API ist genau für diese Urteile gebaut. Sie bewertet Text oder Bilder und liefert eine typisierte Antwort: eine Wahrscheinlichkeit, eine Auswahl aus Ihrer Liste oder eine Bewertung anhand Ihrer Rubrik.

Die Ankündigung beschreibt drei Ergebnisarten, eine pro Fragetyp:

Sie senden eine gemeinsame Eingabe und ein questions-Array. Jede Frage hat einen eindeutigen Namen, sodass Sie jede Antwort ihrer Frage zuordnen können. Zurück bekommen Sie ein answers-Array in derselben Reihenfolge, zusätzlich zur Token-Nutzung.

POST https://YOUR-RESOURCE-NAME.openai.azure.com/openai/v1/decisions Enter fullscreen mode Exit fullscreen mode Das Feld model nimmt Ihren Deployment-Namen. Die Dokumentation verwendet gpt-6-luna, das derzeit das einzige unterstützte Modell ist. Es funktioniert entweder mit einem API-Schlüssel im api-key-Header oder mit einem Microsoft Entra ID Bearer-Token.

Beide Wege liefern typisierte Antworten. Das Beispiel in diesem Beitrag verwendet den Azure OpenAI Route, daher entsprechen der Code und das JSON der offiziellen Dokumentation.

Die offizielle Dokumentation zeigt kein C# SDK für diesen Endpunkt, daher ruft das Beispiel ihn mit HttpClient auf. Es verwendet dasselbe Muster wie die Decision-1-Demo: Azure.Identity für Entra ID, mit einem API-Schlüssel als Abkürzung, wenn einer gesetzt ist.

Das vollständige Projekt liegt auf GitHub unter taswar/OpenAI-DecisionApi-CSharp-Sample. Klonen Sie es, wenn Sie den Code ausführen möchten, ohne ihn aus diesem Beitrag zu kopieren.

Erstellen Sie das Projekt auf.NET 10 und fügen Sie dann die Pakete hinzu. Die Pakete ermöglichen der App, Einstellungen aus User Secrets zu lesen, sodass der Endpunkt und der Schlüssel nicht in Ihrer Shell und nicht im Code landen:

dotnet new console -n OpenAI-DecisionApi-CSharp-Sample cd OpenAI-DecisionApi-CSharp-Sample dotnet add package Azure.Identity dotnet add package Microsoft.Extensions.Configuration.UserSecrets dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables dotnet user-secrets init Enter fullscreen mode Exit fullscreen mode dotnet user-secrets init fügt eine UserSecretsId zur Projektdatei hinzu. Lassen Sie sie unverändert.

Speichern Sie Ihre Einstellungen mit User Secrets aus dem Projektordner. Verwenden Sie nur die Basis-URL, ohne Pfad:

dotnet user-secrets set "AZURE_OPENAI_ENDPOINT" "https://YOUR-RESOURCE-NAME.openai.azure.com" dotnet user-secrets set "AZURE_OPENAI_DEPLOYMENT" "gpt-6-luna" dotnet user-secrets set "AZURE_OPENAI_API_KEY" "your-key-here" Enter fullscreen mode Exit fullscreen mode Der Deployment-Name ist optional, und die App verwendet standardmäßig gpt-6-luna. Wenn Sie AZURE_OPENAI_API_KEY weglassen, nutzt die App stattdessen DefaultAzureCredential, sodass az login funktioniert.

dotnet user-secrets list Enter fullscreen mode Exit fullscreen mode Die Projektdatei ist OpenAI-DecisionApi-CSharp-Sample.csproj. Der UserSecretsId-Wert ist derjenige, den dotnet user-secrets init in Ihre Kopie schreibt:

<Project Sdk= "Microsoft.NET.Sdk" > <PropertyGroup> <OutputType> Exe </OutputType> <TargetFramework> net10.0 </TargetFramework> <ImplicitUsings> enable </ImplicitUsings> <Nullable> enable </Nullable> <UserSecretsId> 91102972-0612-452c-90b4-a2dfa5578168 </UserSecretsId> </PropertyGroup> <ItemGroup> <PackageReference Include= "Azure.Identity" Version= "1.21.0" /> <PackageReference Include= "Microsoft.Extensions.Configuration.EnvironmentVariables" Version= "10.0.12" /> <PackageReference Include= "Microsoft.Extensions.Configuration.UserSecrets" Version= "10.0.12" /> </ItemGroup> </Project> Enter fullscreen mode Exit fullscreen mode Dann Program.cs. Das Ticket und die drei Fragen sind dieselben wie im offiziellen Python-Beispiel, sodass die Ausgabe direkt verglichen werden kann:

using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using Azure.Core; using Azure.Identity; using Microsoft.Extensions.Configuration; var config = new ConfigurationBuilder (). AddUserSecrets < Program >(). AddEnvironmentVariables ().

Build (); var endpoint = ( config [ "AZURE_OPENAI_ENDPOINT" ]?? throw new InvalidOperationException ( "Set AZURE_OPENAI_ENDPOINT with dotnet user-secrets, for example https://YOUR-RESOURCE-NAME.openai.azure.com" )). TrimEnd ( '/' ); var deployment = config [ "AZURE_OPENAI_DEPLOYMENT" ]?? "gpt-6-luna"; var apiKey = config [ "AZURE_OPENAI_API_KEY" ]; var ticket = "I was charged twice for my order.

Please return the extra charge."; var body = new JsonObject { [ "model" ] = deployment, [ "input" ] = ticket, [ "questions" ] = new JsonArray { new JsonObject { [ "type" ] = "predicate", [ "name" ] = "refund_requested", [ "instructions" ] = "Does the customer ask for a refund?" }, new JsonObject { [ "type" ] = "choice", [ "name" ] = "department", [ "instructions" ] = "Which department should handle this complaint?", [ "choices" ] = new JsonArray { new JsonObject { [ "value" ] = "billing", [ "description" ] = "Payments and refunds." }, new JsonObject { [ "value" ] = "technical", [ "description" ] = "Product problems." }, new JsonObject { [ "value" ] = "shipping", [ "description" ] = "Delivery and tracking." }, new JsonObject { [ "value" ] = "other", [ "description" ] = "All other requests." } } }, new JsonObject { [ "type" ] = "score", [ "name" ] = "severity", [ "instructions" ] = "How severe is this issue?", [ "levels" ] = new JsonArray { new JsonObject { [ "label" ] = "Cosmetic", [ "description" ] = "No lost functionality." }, new JsonObject { [ "label" ] = "Workaround available", [ "description" ] = "A task fails, but another way works." }, new JsonObject { [ "label" ] = "Fully blocked", [ "description" ] = "A task fails with no workaround." } } } } }; using var decision = await PostDecisionAsync ( endpoint, apiKey, body ); foreach ( var answer in decision. RootElement. GetProperty ( "answers" ).

EnumerateArray ()) { PrintAnswer ( answer ); } static async Task < JsonDocument > PostDecisionAsync ( string endpoint, string? apiKey, JsonObject body ) { using var http = new HttpClient { Timeout = TimeSpan. FromSeconds ( 60 ) }; using var request = new HttpRequestMessage ( HttpMethod. Post, $" { endpoint } /openai/v1/decisions" ); if (! string.

IsNullOrWhiteSpace ( apiKey )) { request. Headers. Add ( "api-key", apiKey ); } else { var credential = new DefaultAzureCredential (); var token = await credential.

GetTokenAsync ( new TokenRequestContext ([ "https://cognitiveservices.azure.com/.default" ]), CancellationToken. None ); request. Headers.

Authorization = new AuthenticationHeaderValue ( "Bearer", token. Token ); } // StringContent setzt eine feste Content-Length; JsonContent verwendet Chunked Encoding. request. Content = new StringContent ( body.

ToJsonString (), Encoding. UTF8, "application/json" ); using var response = await http. SendAsync ( request ); var text = await response.

Content. ReadAsStringAsync (); if (! response. IsSuccessStatusCode ) { throw new InvalidOperationException ( $"Decisions API returned {( int ) response.

StatusCode }: { text } " ); } return JsonDocument. Parse ( text ); } static void PrintAnswer ( JsonElement answer ) { var type = answer. GetProperty ( "type" ).

GetString (); var name = answer. TryGetProperty ( "name", out var nameValue )? nameValue. GetString (): null; switch ( type ) { case "predicate": var probability = answer.

GetProperty ( "probability" ). GetDouble (); Console. WriteLine ( $" { name }: { probability: P0 } true" ); break; case "choice": var choice = answer.

GetProperty ( "choice" ). GetString (); var choiceConfidence = answer. GetProperty ( "confidence" ).

GetDouble (); Console. WriteLine ( $" { name }: { choice } (confidence { choiceConfidence: P0 } )" ); break; case "score": var score = answer. GetProperty ( "score" ).

GetDouble (); var scoreConfidence = answer. GetProperty ( "confidence" ). GetDouble (); Console.

WriteLine ( $" { name }: { score: 0.00 } (confidence { scoreConfidence: P0 } )" ); break; case "refusal": // Eine Verweigerung ist keine niedrige Wahrscheinlichkeit. Diesen Fall an eine Person weiterleiten. Console.

WriteLine ( $" { name?? "(unnamed)" }: refused, send to a person" ); break; default: Console. WriteLine ( $"Unexpected answer: { answer.

GetRawText ()} " ); break; } } Enter fullscreen mode Exit fullscreen mode Ausführen:

dotnet run Enter fullscreen mode Exit fullscreen mode Die Ausgabe sollte wie folgt aussehen. Ihre Zahlen werden abweichen:

refund_requested: 100% wahr, Abteilung: billing (Konfidenz 100%), Schweregrad: 0,98 (Konfidenz 13%) Vollbildmodus aktivieren Vollbildmodus beenden Was sich im Code geändert hat: Zunächst fällt auf, dass alle drei Fragen in einer einzigen Anfrage herausgehen. Das Ticket wird nur einmal ausgewertet, und die Antworten kommen gemeinsam zurück. Das ist wichtig, wenn ein Workflow viele kleine Entscheidungen trifft.

Die Score-Antwort ist ein gewichteter Durchschnitt der Level-Positionen. Ein Score von 0,98 liegt nahe an "Workaround verfügbar" (Position 1), die Konfidenz beträgt jedoch nur 13%. Diese niedrige Konfidenz ist das Signal, auf das man reagieren sollte. Leiten Sie dieses Ticket an eine Person weiter, statt dem Score zu vertrauen. Verwenden Sie den Score für Schwellenwerte und Sortierungen, nicht als exakte Kennzeichnung.

Der Verweigerungs-Zweig ist absichtlich vorhanden. Die Dokumentation stellt klar, dass eine Verweigerung etwas anderes ist als eine Prädikat-Antwort mit niedriger Wahrscheinlichkeit, weshalb der Code dafür einen eigenen Fall vorsieht.

Ich habe den Code in einer einzigen Datei belassen, damit er sich von oben nach unten lesen lässt. In einem echten Dienst würden Sie die Anfrage in eine eigene Klasse auslagern und eine Retry-Strategie ergänzen.

Setzen Sie es ein, wenn der nächste Schritt eine geschlossene Auswahl ist:

Behalten Sie ein Chat-Modell für alles bei, was Worte erfordert, etwa Zusammenfassungen, Umformulierungen oder Erklärungen. Das Entscheidungsmodell entscheidet, und das Chat-Modell schreibt.

Legen Sie Ihre eigenen Schwellenwerte mit einem gelabelten Satz von Tickets aus Ihrer Anwendung fest. Sowohl die Ankündigung als auch die Dokumentation beschreiben diese Wahrscheinlichkeiten als Schätzungen, also betrachten Sie 0,95 nicht als Garantie.

Die Decisions API verfolgt dieselbe Idee wie Microsoft-Decision-1, läuft aber auf der Azure OpenAI Ressource mit gpt-6-luna und den drei Fragetypen aus der Dokumentation. Wenn Ihr Team bereits über eine Azure OpenAI Ressource verfügt, ist dies der einfachere Einstiegspunkt.

Testen Sie es an Ihren eigenen Tickets, bevor Sie ein Routing anpassen. Wenn Ihr Team bereits einen Klassifikator hat, stellen Sie diesen daneben und vergleichen Sie die Antworten anhand derselben Daten. 🙂

Sie bauen KI-Funktionen in C#? Ich schreibe über praktisches, werbefreies Prompt Engineering und Azure AI Muster für.NET Entwickler. Schauen Sie sich Prompt Engineering for.NET Developers an, kostenlos und ohne Python. Abonnieren Sie außerdem meinen Newsletter, um die neuesten Beiträge, Tipps und Tricks zu erhalten.

Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.

Original (Englisch) anzeigen

Posted on Oct 25 • Originally published at taswar.zeytinsoft.com

Recently I wrote about Microsoft-Decision-1, a model that scores a fixed set of options instead of writing text. That post used a Microsoft provider route. This week the Azure OpenAI Decisions API went generally available in Microsoft Foundry, and it solves the same problem through the Azure OpenAI resource you may already have.

Every agent makes small judgments: which team owns this ticket, does the customer want a refund, how severe is this issue, should the workflow continue or stop. Those steps still get sent to a chat model, which means you ask for JSON, parse text that almost looks like JSON, and trust a confidence number the model never really returned.

The Decisions API is built for those judgments. It evaluates text or images and returns a typed answer: a probability, a choice from your list, or a score against your rubric.

The announcement describes three outcomes, one per question type:

You send one shared input and a questions array. Each question has a unique name, so you can match each answer back to its question. You get back an answers array in the same order, plus token usage.

POST https://YOUR-RESOURCE-NAME.openai.azure.com/openai/v1/decisions Enter fullscreen mode Exit fullscreen mode The model field takes your deployment name. The docs use gpt-6-luna, which is currently the only supported model. It works with either an API key in the api-key header or a Microsoft Entra ID bearer token.

Both return typed answers. The sample in this post uses the Azure OpenAI route, so the code and the JSON match the official docs.

The official docs don't show a C# SDK for this endpoint, so the sample calls it with HttpClient. It uses the same pattern as the Decision-1 demo: Azure.Identity for Entra ID, with an API key as a shortcut when one is set.

The complete project is on GitHub at taswar/OpenAI-DecisionApi-CSharp-Sample. Clone it if you want to run the code without copying it from this post.

Create the project on.NET 10, then add the packages. The packages let the app read settings from user secrets, so the endpoint and key stay out of your shell and out of the code:

dotnet new console -n OpenAI-DecisionApi-CSharp-Sample cd OpenAI-DecisionApi-CSharp-Sample dotnet add package Azure.Identity dotnet add package Microsoft.Extensions.Configuration.UserSecrets dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables dotnet user-secrets init Enter fullscreen mode Exit fullscreen mode dotnet user-secrets init adds a UserSecretsId to the project file. Keep it as it is.

Store your settings with user secrets from the project folder. Use the base URL only, with no path:

dotnet user-secrets set "AZURE_OPENAI_ENDPOINT" "https://YOUR-RESOURCE-NAME.openai.azure.com" dotnet user-secrets set "AZURE_OPENAI_DEPLOYMENT" "gpt-6-luna" dotnet user-secrets set "AZURE_OPENAI_API_KEY" "your-key-here" Enter fullscreen mode Exit fullscreen mode The deployment name is optional, and the app defaults to gpt-6-luna. If you leave out AZURE_OPENAI_API_KEY, the app uses DefaultAzureCredential instead, so az login works.

dotnet user-secrets list Enter fullscreen mode Exit fullscreen mode The project file is OpenAI-DecisionApi-CSharp-Sample.csproj. The UserSecretsId value is the one dotnet user-secrets init writes in your copy:

<Project Sdk= "Microsoft.NET.Sdk" > <PropertyGroup> <OutputType> Exe </OutputType> <TargetFramework> net10.0 </TargetFramework> <ImplicitUsings> enable </ImplicitUsings> <Nullable> enable </Nullable> <UserSecretsId> 91102972-0612-452c-90b4-a2dfa5578168 </UserSecretsId> </PropertyGroup> <ItemGroup> <PackageReference Include= "Azure.Identity" Version= "1.21.0" /> <PackageReference Include= "Microsoft.Extensions.Configuration.EnvironmentVariables" Version= "10.0.12" /> <PackageReference Include= "Microsoft.Extensions.Configuration.UserSecrets" Version= "10.0.12" /> </ItemGroup> </Project> Enter fullscreen mode Exit fullscreen mode Then Program.cs. The ticket and the three questions are the same ones the official Python example uses, so the output can be compared directly:

using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using Azure.Core; using Azure.Identity; using Microsoft.Extensions.Configuration; var config = new ConfigurationBuilder (). AddUserSecrets < Program >(). AddEnvironmentVariables ().

Build (); var endpoint = ( config [ "AZURE_OPENAI_ENDPOINT" ]?? throw new InvalidOperationException ( "Set AZURE_OPENAI_ENDPOINT with dotnet user-secrets, for example https://YOUR-RESOURCE-NAME.openai.azure.com" )). TrimEnd ( '/' ); var deployment = config [ "AZURE_OPENAI_DEPLOYMENT" ]?? "gpt-6-luna"; var apiKey = config [ "AZURE_OPENAI_API_KEY" ]; var ticket = "I was charged twice for my order.

Please return the extra charge."; var body = new JsonObject { [ "model" ] = deployment, [ "input" ] = ticket, [ "questions" ] = new JsonArray { new JsonObject { [ "type" ] = "predicate", [ "name" ] = "refund_requested", [ "instructions" ] = "Does the customer ask for a refund?" }, new JsonObject { [ "type" ] = "choice", [ "name" ] = "department", [ "instructions" ] = "Which department should handle this complaint?", [ "choices" ] = new JsonArray { new JsonObject { [ "value" ] = "billing", [ "description" ] = "Payments and refunds." }, new JsonObject { [ "value" ] = "technical", [ "description" ] = "Product problems." }, new JsonObject { [ "value" ] = "shipping", [ "description" ] = "Delivery and tracking." }, new JsonObject { [ "value" ] = "other", [ "description" ] = "All other requests." } } }, new JsonObject { [ "type" ] = "score", [ "name" ] = "severity", [ "instructions" ] = "How severe is this issue?", [ "levels" ] = new JsonArray { new JsonObject { [ "label" ] = "Cosmetic", [ "description" ] = "No lost functionality." }, new JsonObject { [ "label" ] = "Workaround available", [ "description" ] = "A task fails, but another way works." }, new JsonObject { [ "label" ] = "Fully blocked", [ "description" ] = "A task fails with no workaround." } } } } }; using var decision = await PostDecisionAsync ( endpoint, apiKey, body ); foreach ( var answer in decision. RootElement. GetProperty ( "answers" ).

EnumerateArray ()) { PrintAnswer ( answer ); } static async Task < JsonDocument > PostDecisionAsync ( string endpoint, string? apiKey, JsonObject body ) { using var http = new HttpClient { Timeout = TimeSpan. FromSeconds ( 60 ) }; using var request = new HttpRequestMessage ( HttpMethod. Post, $" { endpoint } /openai/v1/decisions" ); if (! string.

IsNullOrWhiteSpace ( apiKey )) { request. Headers. Add ( "api-key", apiKey ); } else { var credential = new DefaultAzureCredential (); var token = await credential.

GetTokenAsync ( new TokenRequestContext ([ "https://cognitiveservices.azure.com/.default" ]), CancellationToken. None ); request. Headers.

Authorization = new AuthenticationHeaderValue ( "Bearer", token. Token ); } // StringContent sets a fixed Content-Length; JsonContent uses chunked encoding. request. Content = new StringContent ( body.

ToJsonString (), Encoding. UTF8, "application/json" ); using var response = await http. SendAsync ( request ); var text = await response.

Content. ReadAsStringAsync (); if (! response. IsSuccessStatusCode ) { throw new InvalidOperationException ( $"Decisions API returned {( int ) response.

StatusCode }: { text } " ); } return JsonDocument. Parse ( text ); } static void PrintAnswer ( JsonElement answer ) { var type = answer. GetProperty ( "type" ).

GetString (); var name = answer. TryGetProperty ( "name", out var nameValue )? nameValue. GetString (): null; switch ( type ) { case "predicate": var probability = answer.

GetProperty ( "probability" ). GetDouble (); Console. WriteLine ( $" { name }: { probability: P0 } true" ); break; case "choice": var choice = answer.

GetProperty ( "choice" ). GetString (); var choiceConfidence = answer. GetProperty ( "confidence" ).

GetDouble (); Console. WriteLine ( $" { name }: { choice } (confidence { choiceConfidence: P0 } )" ); break; case "score": var score = answer. GetProperty ( "score" ).

GetDouble (); var scoreConfidence = answer. GetProperty ( "confidence" ). GetDouble (); Console.

WriteLine ( $" { name }: { score: 0.00 } (confidence { scoreConfidence: P0 } )" ); break; case "refusal": // A refusal is not a low probability. Send this one to a person. Console.

WriteLine ( $" { name?? "(unnamed)" }: refused, send to a person" ); break; default: Console. WriteLine ( $"Unexpected answer: { answer.

GetRawText ()} " ); break; } } Enter fullscreen mode Exit fullscreen mode Run it:

dotnet run Enter fullscreen mode Exit fullscreen mode The output should look like this. Your numbers will differ:

refund_requested: 100% true department: billing (confidence 100%) severity: 0.98 (confidence 13%) Enter fullscreen mode Exit fullscreen mode What changed in the code The first thing to notice is that all three questions go out in one request. The ticket is evaluated once, and the answers come back together. That matters when a workflow makes many small decisions.

The score answer is a weighted average of the level positions. A score of 0.98 sits close to "Workaround available" (position 1), but the confidence is only 13%. That low confidence is the signal to act on. Send this ticket to a person rather than trust the score. Use the score for thresholds and ordering, not as an exact label.

The refusal branch is there on purpose. The docs are clear that a refusal is different from a predicate answer with a low probability, so the code gives it its own case.

I kept the code as one file so it reads top to bottom. In a real service you would move the request into its own class and add a retry policy.

Use it when the next step is a closed choice:

Keep a chat model for anything that needs words, such as summaries, rewrites, or explanations. The decision model decides, and the chat model writes.

Set your own thresholds with a labeled set of tickets from your application. The announcement and the docs both describe these probabilities as estimates, so do not treat 0.95 as a guarantee.

The Decisions API is the same idea as Microsoft-Decision-1, but it sits on the Azure OpenAI resource with gpt-6-luna and the three question types from the docs. If your team already has an Azure OpenAI resource, this is the easier starting point.

Try it against your own tickets before you change any routing. If your team already has a classifier, put this next to it and compare the answers on the same data. 🙂

Building AI features in C#? I write about practical, no-hype prompt engineering and Azure AI patterns for.NET developers. Check out Prompt Engineering for.NET Developers — free, no Python required. Also subscribe to my mailing list for the latest blogs, tips and tricks I share.

Quelle: dev.to (dev.to). Original öffnen