Chapter 3C
Decisions with Reusable Prompts and Native .NET Functions
Static reading edition generated from the Decisions with Reusable Prompts and Native .NET Functions notebook.
Workshop (AI Extensions) - Decisions with Reusable Prompts and Native .NET Functions
Decision Intelligence applied in this module:
- Listing various decision-making frameworks with their descriptions
- Listing types of decision-making frameworks dynamically based on the decision type
- Reusing AI Extensions prompt templates with dynamic inputs
- Selecting a decision-making framework dynamically using native .NET code
- Decision Scenario: Using a Monte Carlo Simulation to estimate decision uncertainty
AI Extensions prompt templates are reusable instructions that help guide GenAI models toward a specific decision workflow. A prompt template usually has one responsibility: list useful decision frameworks, recommend a reasoning approach for a high-stakes decision, summarize a tradeoff, or personalize a recommendation from structured context.
What makes reusable prompt functions useful? GenAI models can already respond to basic prompts, but decision workflows usually need repeatable instructions, consistent formatting, and dynamic inputs. By combining Microsoft.Extensions.AI with simple C# helper methods, a prompt becomes a reusable part of an AI orchestration flow instead of a one-off string.
For example, imagine you want to prepare a great Thanksgiving dinner and you ask a GenAI cooking application to create a recipe. It may produce a delicious plan, but it may also use ingredients or cooking appliances you do not own. You could keep prompting until it narrows the recipe, but it is better to provide available ingredients, time availability, kitchen appliances, and allergy preferences as dynamic inputs. The same pattern applies to Decision Intelligence: reusable prompt templates let the model consider specific business context, user constraints, and decision criteria each time the prompt runs.
In this notebook, reusable prompt functions are implemented with:
IChatClientfrom Microsoft.Extensions.AIChatMessageroles for system and user instructionsChatOptionsfor model settings such as temperature, top-p, and maximum output tokens- C# raw string interpolation for dynamic prompt inputs
Below is an example of a reusable prompt template that can be used for facilitating decision-making. Note the {decisionToMake} input where the parameter can be dynamically passed in to provide additional specificity.
var decisionPrompt = $"""
[DECISION FRAMEWORKS TO USE]
Price's Law
Pareto Principle
Laplace Rule of Succession
Eisenhower Matrix
Median Rule of Five
Second Order Thinking
[DECISION GUIDANCE]
Try to use the best fitting framework.
Prefer using quantitative decision frameworks rather than qualitative ones.
Use calculations or code-based validation when quantitative reasoning is required.
Use the decision frameworks above to help the user make the following decision:
{decisionToMake}
""";
The core value is that the prompt can be reused with different inputs while still flowing through the same AI Extensions IChatClient API.
Step 1 - Initialize Configuration Builder & Build the AI Extensions Responses API Orchestration¶
Execute the next two cells to:
- Use the Configuration Builder to load the API secrets.
- Use the API configuration to build a Responses API-backed AI Extensions
IChatClientorchestration.
#r "nuget: Microsoft.Extensions.Configuration, 10.0.8"
#r "nuget: Microsoft.Extensions.Configuration.Json, 10.0.8"
#r "nuget: System.Text.Json, 10.0.8"
using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.Configuration;
using System.IO;
using System;
var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
.AddJsonFile("secrets.settings.json", optional: true, reloadOnChange: true);
var config = configurationBuilder.Build();
// IMPORTANT: You ONLY NEED either Azure OpenAI or OpenAI connection info, not both.
// Azure OpenAI Connection Info
var azureOpenAIEndpoint = config["AzureOpenAI:Endpoint"];
var azureOpenAIAPIKey = config["AzureOpenAI:APIKey"];
var azureOpenAIModelDeploymentName = config["AzureOpenAI:ModelDeploymentName"];
// OpenAI Connection Info
var openAIAPIKey = config["OpenAI:APIKey"];
var openAIModelId = config["OpenAI:ModelId"];
// Import the Microsoft Extensions AI NuGet Packages
#r "nuget: Microsoft.Extensions.AI, 10.6.0"
#r "nuget: Microsoft.Extensions.AI.Abstractions, 10.6.0"
#r "nuget: Microsoft.Extensions.AI.OpenAI, 10.6.0"
// Import Azure & OpenAI NuGet Packages
#r "nuget: Azure.AI.OpenAI, 2.9.0-beta.1"
#r "nuget: Azure.Identity, 1.21.0"
#r "nuget: OpenAI, 2.10.0"
using Azure.AI.OpenAI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Responses;
using System.ClientModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Text.Json;
// Set the flag to use Azure OpenAI or OpenAI. False to use OpenAI, True to use Azure OpenAI
var useAzureOpenAI = true;
// Create the IChatClient based on the selected service
IChatClient chatClient;
#pragma warning disable OPENAI001
if (useAzureOpenAI)
{
Console.WriteLine("Using Azure OpenAI Service");
var apiKeyCredential = new ApiKeyCredential(azureOpenAIAPIKey);
var azureOpenAIClient = new AzureOpenAIClient(new Uri(azureOpenAIEndpoint), apiKeyCredential);
// Get the ResponsesClient from AzureOpenAIClient and adapt it to Microsoft.Extensions.AI.
var responsesClient = azureOpenAIClient.GetResponsesClient();
chatClient = responsesClient.AsIChatClient(defaultModelId: azureOpenAIModelDeploymentName);
}
else
{
Console.WriteLine("Using OpenAI Service");
var apiKeyCredential = new ApiKeyCredential(openAIAPIKey);
var openAIClient = new OpenAIClient(apiKeyCredential);
// Get the ResponsesClient from OpenAIClient and adapt it to Microsoft.Extensions.AI.
var responsesClient = openAIClient.GetResponsesClient();
chatClient = responsesClient.AsIChatClient(defaultModelId: openAIModelId);
}
#pragma warning restore OPENAI001
var decisionIntelligenceSystemPrompt = """
You are a Decision Intelligence assistant.
Help the user explore options, evaluate tradeoffs, reason through uncertainty, solve problems,
and apply systems thinking to personal, professional, strategic, and operational decisions.
Provide responses that are structured, logical, and thorough.
Aim to improve the user's judgment rather than make choices for them.
Be balanced, analytical, and pragmatic.
Adapt depth and complexity to the user's context.
""";
async Task<string> RunPromptAsync(string prompt, ChatOptions? options = null)
{
ChatResponse response = await chatClient.GetResponseAsync(prompt, options);
return response.Text ?? string.Empty;
}
async Task<string> RunDecisionPromptAsync(string userPrompt, ChatOptions? options = null)
{
var chatMessages = new List<ChatMessage>
{
new(ChatRole.System, decisionIntelligenceSystemPrompt),
new(ChatRole.User, userPrompt)
};
ChatResponse response = await chatClient.GetResponseAsync(chatMessages, options);
return response.Text ?? string.Empty;
}
static string GetFunctionResultText(object? result)
{
if (result is null)
{
return string.Empty;
}
if (result is JsonElement jsonElement)
{
return jsonElement.ValueKind == JsonValueKind.String
? jsonElement.GetString() ?? string.Empty
: jsonElement.ToString();
}
return result.ToString() ?? string.Empty;
}
Step 2 - Create an AI Extensions Reusable Prompt Function¶
📜 "Decision frameworks are like maps. Use them to navigate complex decision-making terrain.
-- Unknown
Reusable prompt functions in this notebook are simple C# helper methods that submit prompt templates through IChatClient. This keeps the prompt reusable while using the same AI Extensions API surface shown in the previous workshop notebooks.
// Simple prompt to list some decision frameworks this GenAI LLM is familiar with
var decisionFrameworksPromptTemplate = """
Identify and list various decision-making frameworks that can enhance the quality of decisions.
Briefly describe how each framework supports better analysis and reasoning in different decision-making scenarios.
Output Format Instructions:
When generating Markdown, do not use any headings higher than ###.
Avoid # and ## headers. Use only ###, ####, or lower-level headings if necessary.
All top-level section headers should start at ### or lower.
Never use ---, ***, or ___ for horizontal lines. There should be no horizontal lines in the output.
For separation, use extra extra spacing. Do not any render horizontal lines.
Format the response using only a Markdown table. Only return a Markdown table.
Do not enclose the table in triple backticks.
""";
// Treat the prompt template as a reusable local function that invokes the AI Extensions IChatClient.
async Task<string> RunDecisionFrameworksPromptAsync(ChatOptions? options = null)
{
return await RunDecisionPromptAsync(decisionFrameworksPromptTemplate, options);
}
var decisionFrameworksResponseText = await RunDecisionFrameworksPromptAsync();
decisionFrameworksResponseText.DisplayAs("text/markdown");
Step 3 - AI Extensions Dynamic Decision Intelligence¶
📜 "If the only tool you have is a hammer, you tend to see every problem as a nail."
-- Abraham Maslow (Renowned American psychologist)
Prompt templates can be dynamically composed using C# variables and raw string interpolation. This allows the ease of passing in parameters and execution settings into reusable prompt functions. This is not groundbreaking, but it does allow you to dynamically compose AI prompts (context-engineering, prompt-engineering dynamically).
Execute the cell below to view how the prompt can dynamically instruct the LLM to retrieve different types of decision-making frameworks.
// Takes input variables and creates a dynamic prompt that can be used to invoke the GenAI model.
string CreateDecisionFrameworkPrompt(int numberOfFrameworks, string decisionType)
{
return $"""
Identify and list {numberOfFrameworks} decision-making frameworks that can enhance the quality of decisions.
Briefly describe how each framework supports better analysis and reasoning in {decisionType} decision-making scenarios.
Output Format Instructions:
When generating Markdown, do not use any headings higher than ###.
Avoid # and ## headers. Use only ###, ####, or lower-level headings if necessary.
All top-level section headers should start at ### or lower.
Never use ---, ***, or ___ for horizontal lines. There should be no horizontal lines in the output.
For separation, use extra extra spacing. Do not any render horizontal lines.
Format the response using only a Markdown table. Only return a Markdown table.
Do not enclose the table in triple backticks.
""";
}
// Create the chat options.
//Try changing the settings to see how they affect the quality of generated text.
#pragma warning disable OPENAI001
var decisionFrameworkChatOptions = new ChatOptions
{
RawRepresentationFactory = _ => new OpenAI.Responses.CreateResponseOptions
{
Model = useAzureOpenAI ? azureOpenAIModelDeploymentName : openAIModelId,
ReasoningOptions = new OpenAI.Responses.ResponseReasoningOptions
{
ReasoningEffortLevel = OpenAI.Responses.ResponseReasoningEffortLevel.Medium,
ReasoningSummaryVerbosity = OpenAI.Responses.ResponseReasoningSummaryVerbosity.Detailed
},
StoredOutputEnabled = false
}
};
#pragma warning restore OPENAI001
// Dynamically set the number of frameworks and decision type -> strategic
var strategicDecisionFrameworkPrompt = CreateDecisionFrameworkPrompt(
numberOfFrameworks: 3,
decisionType: "Long-Term strategic with probabilistic outcomes");
var strategicDecisionFrameworkResponseText = await RunDecisionPromptAsync(
strategicDecisionFrameworkPrompt,
decisionFrameworkChatOptions);
strategicDecisionFrameworkResponseText.DisplayAs("text/markdown");
In the example below, the number of frameworks to return has been changed and the type has been changed to Quick to Implement for rapid Decision-Making.
// Dynamically set the number of frameworks and decision type -> requiring fast action
var rapidDecisionFrameworkPrompt = CreateDecisionFrameworkPrompt(
numberOfFrameworks: 5,
decisionType: "Quick to Implement for rapid Decision-Making");
// Note: The invocation has NOT changed, only the dynamic prompt inputs have changed.
var rapidDecisionFrameworkResponseText = await RunDecisionPromptAsync(
rapidDecisionFrameworkPrompt,
decisionFrameworkChatOptions);
rapidDecisionFrameworkResponseText.DisplayAs("text/markdown");
Step 4 - Decision Scenario with Dynamic Decision Intelligence¶
In the below code a decision-analysis scenario is introduced that uses dynamic inputs to personalize the decision recommendation dynamically. The more specific information that provides contextual information to the Generative AI model can greatly improve the specific recommendations.
Decision Scenario: Michael is a 35-year-old professional chef who is considering opening his own restaurant. This is a significant life decision that could greatly impact his career and personal life. Michael is looking for a recommendation for an approach (decision) for this potentially life-changing decision.
Three factors will be considered for this decision scenario that the user will be prompted for:
- Michael's Total Net Worth in dollars (enter number)
- Level of competition with other restaurants (low, medium, high)
- Level of support from Michael's friends and family (low, medium, high)
Various Decision Inputs: You can simulate different What-If scenarios by varying the input of net worth, restaurant competition and level of family support. Note the different decision recommendations based on this provided by Artificial Intelligence. The recommendations in this example are quite simple, but even on the extreme inputs Generative AI has a concept of understanding the quality of inputs.
// Import the Microsoft.DotNet.Interactive namespace for user input
using Microsoft.DotNet.Interactive;
var totalNetWorth = await Microsoft.DotNet.Interactive.Kernel.GetInputAsync("Michael's total net worth in dollars: ");
var levelOfCompetition = await Microsoft.DotNet.Interactive.Kernel.GetInputAsync("Level of competition with other restaurants (Low, Medium, High): ");
var levelOfFamilySupport = await Microsoft.DotNet.Interactive.Kernel.GetInputAsync("Level of support from Michael's friends and family (Low, Medium, High): ");
Console.WriteLine($"Michael's Net Worth: {totalNetWorth}");
Console.WriteLine($"Level of Restaurant Competition: {levelOfCompetition}");
Console.WriteLine($"Michael's level of Family Support: {levelOfFamilySupport}");
Console.WriteLine();
// Takes dynamic inputs and creates a personalized prompt that can be used to invoke the model.
var restaurantDecisionRecommendationPrompt = $"""
Michael is a 35-year-old professional chef who is considering opening his own restaurant.
This is a significant life decision that could greatly impact his career and personal life.
Michael is looking for a recommendation on how to approach this.
Some key information about Michael:
- Michael's total net worth is (in US dollars) ${totalNetWorth}.
- The level of competition with other restaurants is {levelOfCompetition}.
- The level of support from Michael's friends and family is {levelOfFamilySupport}.
Based on this information, what recommendation would you give to Michael regarding opening his own restaurant?
Output Format Instructions:
When generating Markdown, do not use any headings higher than ###.
Avoid # and ## headers. Use only ###, ####, or lower-level headings if necessary.
All top-level section headers should start at ### or lower.
Never use ---, ***, or ___ for horizontal lines. There should be no horizontal lines in the output.
For separation, use extra extra spacing. Do not any render horizontal lines.
Format the response using only a Markdown table. Only return a Markdown table.
Do not enclose the table in triple backticks.
Provide a small Decision Summary of the recommendation below the table.
""";
#pragma warning disable OPENAI001
// Create the chat options. Try changing the settings to see how they affect the quality of generated text.
var restaurantDecisionChatOptions = new ChatOptions
{
RawRepresentationFactory = _ => new OpenAI.Responses.CreateResponseOptions
{
Model = useAzureOpenAI ? azureOpenAIModelDeploymentName : openAIModelId,
ReasoningOptions = new OpenAI.Responses.ResponseReasoningOptions
{
ReasoningEffortLevel = OpenAI.Responses.ResponseReasoningEffortLevel.Medium,
ReasoningSummaryVerbosity = OpenAI.Responses.ResponseReasoningSummaryVerbosity.Detailed
},
StoredOutputEnabled = false
}
};
var restaurantDecisionRecommendationResponseText = await RunDecisionPromptAsync(
restaurantDecisionRecommendationPrompt,
restaurantDecisionChatOptions);
restaurantDecisionRecommendationResponseText.DisplayAs("text/markdown");
#pragma warning restore OPENAI001
Native .NET Functions with Microsoft.Extensions.AI¶
The first part of this notebook used reusable prompt templates. The next section adds native .NET functions with AIFunctionFactory, which lets AI workflows use deterministic C# logic for business rules, calculations, simulations, API calls, and other capabilities that should not be left to prompting alone.
Step 5 - Create a Simple Native .NET Function with AI Extensions¶
Execute the cell below to create a very simple native .NET function using a C# inline method. Notice the function takes no parameters. It retrieves the name of a productivity decision framework to use. In this case, that return name is hard-coded to "Price's Law".
The function returns instantly because it is not calling a GenAI service. The point is to show how Microsoft.Extensions.AI can wrap deterministic .NET logic as a first-class AIFunction.
// Create an AIFunction from an inline C# method.
var nameOfProductivityFramework = AIFunctionFactory.Create(
(Func<string>)(() => "Price's Law"),
new AIFunctionFactoryOptions
{
Name = "GetNameOfProductivityFramework",
Description = "Retrieves the name of the Productivity Framework to use."
});
// Invoke the function directly with AI Extensions.
var response = await nameOfProductivityFramework.InvokeAsync(new AIFunctionArguments());
Console.WriteLine(GetFunctionResultText(response));
Step 6 - Create a Native .NET Function with Dynamic Parameters¶
Execute the cell below to create a native .NET function that takes a parameter as input. This shows that C# native functions can have different execution paths. The execution paths can obviously be quite complex. Basically, any C# logic flow will work.
// Create an AIFunction from an inline C# method with parameters.
var nameOfProductivityFrameworkByType = AIFunctionFactory.Create(
(Func<string, string>)(typeOfProductivity => typeOfProductivity == "Sales" ? "Price's Law" : "Pareto Principle"),
new AIFunctionFactoryOptions
{
Name = "GetNameOfProductivityFramework",
Description = "Retrieves the name of the Productivity Framework to use."
});
// Pass the "Sales" parameter to the function.
var salesResponse = await nameOfProductivityFrameworkByType.InvokeAsync(new AIFunctionArguments
{
["typeOfProductivity"] = "Sales"
});
Console.WriteLine(GetFunctionResultText(salesResponse));
// Pass the "Other" parameter to the function.
var otherResponse = await nameOfProductivityFrameworkByType.InvokeAsync(new AIFunctionArguments
{
["typeOfProductivity"] = "Other"
});
Console.WriteLine(GetFunctionResultText(otherResponse));
Step 7 - Use Native .NET Functions To Simulate the Uncertainty of a Decision¶
📜 "The best way to predict the future is to simulate it. And the best way to simulate it is with Monte Carlo."
-- Nassim Nicholas Taleb (Lebanese-American essayist, scholar, best known for his work on probability)
For more complex decisions, native .NET functions can use statistics, advanced probabilistic algorithms, analytics, machine learning, AI, and other approaches that have been relied on in software for decades. One such method is Monte Carlo Simulation. These powerful Monte Carlo simulation techniques are used everywhere: risk management, sports gambling, medical decision-making, game theory, energy market forecasting, and more. In simple terms, a Monte Carlo simulation is a series of many runs testing different plausible parameters.
A simple use case for a Monte Carlo simulation is to visualize the distribution of possible outcomes for an average probability. Imagine you want to illustrate the uncertainty of a decision that you have calculated to be 70% successful. On "average" the probability of success can be interpreted as 70%. However, if that 70% decision model is run 100 times, the actual number of successful outcomes will vary. A Monte Carlo simulation can show that variation as a distribution.
Run the cell below to create a native .NET function that takes the confidence parameter and returns a Markdown distribution. The output uses a simple horizontal bar chart so the likely outcomes can be scanned quickly, similar to a compact sales distribution chart.
string RetrieveMonteCarloDistributionMarkdown(
[Description("Claimed Probability Percentage")] int probability)
{
if (probability < 0 || probability > 100)
{
throw new ArgumentOutOfRangeException(nameof(probability), "Probability must be between 0 and 100.");
}
const int NUMBEROFSIMULATIONS = 100000; // 100,000 simulations, make this smaller for faster results
const int NUMBEROFDECISIONS = 100;
const int MAXIMUMBARWIDTH = 24;
var random = new Random(42); // Fixed seed keeps notebook output reproducible
var successfulOutcomesPerSimulation = new List<int>();
var outcomeDistribution = new Dictionary<int, int>();
for (int i = 0; i != NUMBEROFSIMULATIONS; i++) // Bootstrap Simulations (bootstrap estimates)
{
var successfulDecisions = 0;
for (int j = 0; j != NUMBEROFDECISIONS; j++)
{
var randomIndex = random.Next(0, 100);
if (randomIndex < probability)
{
successfulDecisions++;
}
}
successfulOutcomesPerSimulation.Add(successfulDecisions);
if (!outcomeDistribution.ContainsKey(successfulDecisions))
{
outcomeDistribution[successfulDecisions] = 0;
}
outcomeDistribution[successfulDecisions]++;
}
// Sort the success counts to calculate the central 95% simulated range.
var successfulOutcomesSorted = successfulOutcomesPerSimulation.OrderBy(a => a).ToList();
var lowerPercentileIndex = Convert.ToInt32(0.025 * NUMBEROFSIMULATIONS);
var topPercentileIndex = Convert.ToInt32(0.975 * NUMBEROFSIMULATIONS);
var lowerPercentile = successfulOutcomesSorted[lowerPercentileIndex];
var upperPercentile = successfulOutcomesSorted[topPercentileIndex];
var visibleOutcomes = Enumerable.Range(lowerPercentile, upperPercentile - lowerPercentile + 1).ToList();
var maximumOutcomeCount = visibleOutcomes.Max(outcome =>
outcomeDistribution.ContainsKey(outcome) ? outcomeDistribution[outcome] : 0);
var distributionMarkdown = new System.Text.StringBuilder();
distributionMarkdown.AppendLine($"### Monte Carlo Distribution ({probability}% Success Model)");
distributionMarkdown.AppendLine();
distributionMarkdown.AppendLine("```text");
distributionMarkdown.AppendLine($"{NUMBEROFSIMULATIONS:n0} simulations, {NUMBEROFDECISIONS} decisions per simulation");
distributionMarkdown.AppendLine($"Showing central 95% simulated range: {lowerPercentile} to {upperPercentile} successful outcomes");
distributionMarkdown.AppendLine();
foreach (var outcome in visibleOutcomes)
{
var simulationsAtOutcome = outcomeDistribution.ContainsKey(outcome) ? outcomeDistribution[outcome] : 0;
var barWidth = simulationsAtOutcome == 0
? 0
: Math.Max(1, Convert.ToInt32(Math.Round((double)simulationsAtOutcome / maximumOutcomeCount * MAXIMUMBARWIDTH)));
var bar = new string('█', barWidth).PadRight(MAXIMUMBARWIDTH);
distributionMarkdown.AppendLine($"{outcome,3} successes | {bar} {simulationsAtOutcome:n0}");
}
distributionMarkdown.AppendLine("```");
return distributionMarkdown.ToString();
}
Run the cell below to wrap the native .NET method as an AIFunction and invoke it directly. This will run a Monte Carlo Simulation with 100,000 simulations of a decision with a confidence (probability) of 70% being run 100 times. The result is displayed as a Markdown distribution so the uncertainty pattern is easier to scan than a single interval sentence.
var retrieveMonteCarloDistribution = AIFunctionFactory.Create(
(Func<int, string>)RetrieveMonteCarloDistributionMarkdown,
new AIFunctionFactoryOptions
{
Name = "RetrieveMonteCarloDistributionMarkdown",
Description = "Generates a Markdown distribution from a Monte Carlo simulation."
});
// Change the probability to another integer and invoke the function to see other outcome distributions.
var monteCarloDistribution70 = await retrieveMonteCarloDistribution.InvokeAsync(new AIFunctionArguments
{
["probability"] = 70
});
var monteCarloDistributionMarkdown70 = GetFunctionResultText(monteCarloDistribution70);
monteCarloDistributionMarkdown70.DisplayAs("text/markdown");