diff --git a/dotnet/samples/04.e.twentyQuestions/GameBotHandlers.cs b/dotnet/samples/04.e.twentyQuestions/GameBotHandlers.cs index 6435eb38d..db05b12c4 100644 --- a/dotnet/samples/04.e.twentyQuestions/GameBotHandlers.cs +++ b/dotnet/samples/04.e.twentyQuestions/GameBotHandlers.cs @@ -1,15 +1,17 @@ using Microsoft.Bot.Builder; using Microsoft.Teams.AI; +using Microsoft.Teams.AI.AI.Planners; +using Microsoft.Teams.AI.AI.Prompts; namespace TwentyQuestions { public class GameBotHandlers { - private readonly Application _app; + private readonly ActionPlanner _actionPlanner; - public GameBotHandlers(Application app) + public GameBotHandlers(ActionPlanner actionPlanner) { - this._app = app; + this._actionPlanner = actionPlanner; } public async Task OnMessageActivityAsync(ITurnContext turnContext, GameState turnState, CancellationToken cancellationToken) @@ -19,7 +21,7 @@ public async Task OnMessageActivityAsync(ITurnContext turnContext, GameState tur if (string.Equals("/quit", input, StringComparison.OrdinalIgnoreCase)) { // Quit Game - turnState.ConversationStateEntry?.Delete(); + turnState.DeleteConversationState(); await turnContext.SendActivityAsync(ResponseBuilder.QuitGame(secretWord), cancellationToken: cancellationToken); } else if (string.IsNullOrEmpty(secretWord)) @@ -82,13 +84,17 @@ private async Task GetHint(ITurnContext turnContext, GameState turnState // Set input for prompt turnState.Temp!.Input = turnContext.Activity.Text; + PromptTemplate template = _actionPlanner.Options.Prompts.GetPrompt("hint"); // Set prompt variables - _app.AI.Prompts.Variables.Add("guessCount", turnState.Conversation!.GuessCount.ToString()); - _app.AI.Prompts.Variables.Add("remainingGuesses", turnState.Conversation!.RemainingGuesses.ToString()); - _app.AI.Prompts.Variables.Add("secretWord", turnState.Conversation!.SecretWord!); + PromptResponse response = await _actionPlanner.CompletePromptAsync(turnContext, turnState, template, null, cancellationToken); - string hint = await _app.AI.CompletePromptAsync(turnContext, turnState, "Hint", null, cancellationToken); - return hint ?? throw new Exception("The request to OpenAI was rate limited. Please try again later."); + if (response.Status == PromptResponseStatus.Success && response.Message != null) + { + // Prompt completed successfully + return response.Message.Content!; + } + + throw new Exception($"An error occured when trying to make a call to the AI service: {response.Error}"); } } } diff --git a/dotnet/samples/04.e.twentyQuestions/GameState.cs b/dotnet/samples/04.e.twentyQuestions/GameState.cs index 8f837be9a..d64452be8 100644 --- a/dotnet/samples/04.e.twentyQuestions/GameState.cs +++ b/dotnet/samples/04.e.twentyQuestions/GameState.cs @@ -5,18 +5,53 @@ namespace TwentyQuestions /// /// Extend the turn state by configuring custom strongly typed state classes. /// - public class GameState : TurnState + public class GameState : TurnState { + public GameState() + { + ScopeDefaults[CONVERSATION_SCOPE] = new ConversationState(); + } + + /// + /// Stores all the conversation-related state. + /// + public new ConversationState Conversation + { + get + { + TurnStateEntry? scope = GetScope(CONVERSATION_SCOPE); + + if (scope == null) + { + throw new ArgumentException("TurnState hasn't been loaded. Call LoadStateAsync() first."); + } + + return (ConversationState)scope.Value!; + } + set + { + TurnStateEntry? scope = GetScope(CONVERSATION_SCOPE); + + if (scope == null) + { + throw new ArgumentException("TurnState hasn't been loaded. Call LoadStateAsync() first."); + } + + scope.Replace(value!); + } + } } /// /// This class adds custom properties to the turn state which will be accessible in the activity handler methods. /// - public class ConversationState : StateBase + public class ConversationState : Record { - private const string _secretWordKey = "secretWordKey"; - private const string _guessCountKey = "guessCountKey"; - private const string _remainingGuessesKey = "remainingGuessesKey"; + // The keys can be referenced in the prompt using the ${{conversation.}} syntax. + // For example, ${{conversation.secretWord}} + private const string _secretWordKey = "secretWord"; + private const string _guessCountKey = "guessCount"; + private const string _remainingGuessesKey = "remainingGuesses"; public string? SecretWord { diff --git a/dotnet/samples/04.e.twentyQuestions/GameStateManager.cs b/dotnet/samples/04.e.twentyQuestions/GameStateManager.cs deleted file mode 100644 index d4947d310..000000000 --- a/dotnet/samples/04.e.twentyQuestions/GameStateManager.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.Teams.AI.State; - -namespace TwentyQuestions -{ - /// - /// Shorter the class name since turn state is strongly typed. - /// - public class GameStateManager : TurnStateManager { } -} diff --git a/dotnet/samples/04.e.twentyQuestions/Program.cs b/dotnet/samples/04.e.twentyQuestions/Program.cs index a92b7badf..b328579a5 100644 --- a/dotnet/samples/04.e.twentyQuestions/Program.cs +++ b/dotnet/samples/04.e.twentyQuestions/Program.cs @@ -3,10 +3,9 @@ using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Schema; using Microsoft.Teams.AI; -using Microsoft.Teams.AI.AI; -using Microsoft.Teams.AI.AI.Moderator; -using Microsoft.Teams.AI.AI.Planner; -using Microsoft.Teams.AI.AI.Prompt; +using Microsoft.Teams.AI.AI.Models; +using Microsoft.Teams.AI.AI.Planners; +using Microsoft.Teams.AI.AI.Prompts; using TwentyQuestions; var builder = WebApplication.CreateBuilder(args); @@ -34,76 +33,25 @@ // Create singleton instances for bot application builder.Services.AddSingleton(); -#region Use Azure OpenAI and Azure Content Safety -// Following code is for using Azure OpenAI and Azure Content Safety -if (config.Azure == null - || string.IsNullOrEmpty(config.Azure.OpenAIApiKey) - || string.IsNullOrEmpty(config.Azure.OpenAIEndpoint) - || string.IsNullOrEmpty(config.Azure.ContentSafetyApiKey) - || string.IsNullOrEmpty(config.Azure.ContentSafetyEndpoint)) +OpenAIModel? model = null; + +if (!string.IsNullOrEmpty(config.OpenAI?.ApiKey)) { - throw new Exception("Missing Azure configuration."); + model = new(new OpenAIModelOptions(config.OpenAI.ApiKey, "gpt-3.5-turbo")); } -builder.Services.AddSingleton(_ => new(config.Azure.OpenAIApiKey, "text-davinci-003", config.Azure.OpenAIEndpoint)); -builder.Services.AddSingleton(_ => new(config.Azure.ContentSafetyApiKey, config.Azure.ContentSafetyEndpoint, ModerationType.Both)); - -// Create the bot as a transient. In this case the ASP Controller is expecting an IBot. -builder.Services.AddTransient(sp => +else if (!string.IsNullOrEmpty(config.Azure?.OpenAIApiKey) && !string.IsNullOrEmpty(config.Azure.OpenAIEndpoint)) { - // Create loggers - ILoggerFactory loggerFactory = sp.GetService()!; - - // Create AzureOpenAIPlanner - IPlanner planner = new AzureOpenAIPlanner( - sp.GetService()!, - loggerFactory); - - // Create AzureContentSafetyModerator - IModerator moderator = new AzureContentSafetyModerator(sp.GetService()!); - - // Setup Application - AIHistoryOptions aiHistoryOptions = new() - { - AssistantHistoryType = AssistantHistoryType.Text - }; - AIOptions aiOptions = new( - planner: planner, - promptManager: new PromptManager("./Prompts"), - moderator: moderator, - prompt: "Chat", - history: aiHistoryOptions); - var applicationBuilder = new ApplicationBuilder() - .WithAIOptions(aiOptions) - .WithLoggerFactory(loggerFactory) - .WithTurnStateManager(new GameStateManager()); - - // Set storage options - IStorage? storage = sp.GetService(); - if (storage != null) - { - applicationBuilder.WithStorage(storage); - } - - // Create Application - Application app = applicationBuilder.Build(); - - GameBotHandlers handlers = new(app); - - // register turn and activity handlers - app.OnActivity(ActivityTypes.Message, handlers.OnMessageActivityAsync); - - return app; -}); -#endregion + model = new(new AzureOpenAIModelOptions( + config.Azure.OpenAIApiKey, + "gpt-35-turbo", + config.Azure.OpenAIEndpoint + )); +} -#region Use OpenAI -/** // Use OpenAI -if (config.OpenAI == null || string.IsNullOrEmpty(config.OpenAI.ApiKey)) +if (model == null) { - throw new Exception("Missing OpenAI configuration."); + throw new Exception("please configure settings for either OpenAI or Azure"); } -builder.Services.AddSingleton(_ => new(config.OpenAI.ApiKey, "text-davinci-003")); -builder.Services.AddSingleton(_ => new(config.OpenAI.ApiKey, ModerationType.Both)); // Create the bot as a transient. In this case the ASP Controller is expecting an IBot. builder.Services.AddTransient(sp => @@ -111,35 +59,31 @@ // Create loggers ILoggerFactory loggerFactory = sp.GetService()!; - // Get HttpClient - HttpClient moderatorHttpClient = sp.GetService()!.CreateClient("WebClient"); - - // Create OpenAIPlanner - IPlanner planner = new OpenAIPlanner( - sp.GetService()!, - loggerFactory); - - // Create OpenAIModerator - IModerator moderator = new OpenAIModerator( - sp.GetService()!, - loggerFactory, - moderatorHttpClient); - - // Setup Application - AIHistoryOptions aiHistoryOptions = new() + // Create Prompt Manager + PromptManager prompts = new(new() { - AssistantHistoryType = AssistantHistoryType.Text - }; - AIOptions aiOptions = new( - planner: planner, - promptManager: new PromptManager("./Prompts"), - moderator: moderator, - prompt: "Chat", - history: aiHistoryOptions); - var applicationBuilder = new ApplicationBuilder() - .WithAIOptions(aiOptions) + PromptFolder = "./Prompts" + }); + + // Create ActionPlanner + ActionPlanner planner = new( + options: new( + model: model, + prompts: prompts, + defaultPrompt: async (context, state, planner) => + { + PromptTemplate template = prompts.GetPrompt("sequence"); + return await Task.FromResult(template); + } + ) + { LogRepairs = true }, + loggerFactory: loggerFactory + ); + + var applicationBuilder = new ApplicationBuilder() + .WithAIOptions(new(planner)) .WithLoggerFactory(loggerFactory) - .WithTurnStateManager(new GameStateManager()); + .WithTurnStateFactory(() => new GameState()); // Set storage options IStorage? storage = sp.GetService(); @@ -149,17 +93,15 @@ } // Create Application - Application app = applicationBuilder.Build(); + Application app = applicationBuilder.Build(); - GameBotHandlers handlers = new(app); + GameBotHandlers handlers = new(planner); // register turn and activity handlers app.OnActivity(ActivityTypes.Message, handlers.OnMessageActivityAsync); return app; }); -**/ -#endregion var app = builder.Build(); diff --git a/dotnet/samples/04.e.twentyQuestions/Prompts/Hint/config.json b/dotnet/samples/04.e.twentyQuestions/Prompts/Hint/config.json index 7c2bbdcaf..5bb3c811b 100644 --- a/dotnet/samples/04.e.twentyQuestions/Prompts/Hint/config.json +++ b/dotnet/samples/04.e.twentyQuestions/Prompts/Hint/config.json @@ -1,15 +1,16 @@ { - "schema": 1, - "description": "A bot that plays a game of 20 questions", - "type": "completion", - "completion": { - "max_tokens": 256, - "temperature": 0.7, - "top_p": 0.0, - "presence_penalty": 0.6, - "frequency_penalty": 0.0 - }, - "default_backends": [ - "text-davinci-003" - ] + "schema": 1.1, + "description": "A bot that plays a game of 20 questions", + "type": "completion", + "completion": { + "completion_type": "chat", + "include_history": false, + "include_input": true, + "max_input_tokens": 2000, + "max_tokens": 256, + "temperature": 0.7, + "top_p": 0.0, + "presence_penalty": 0.6, + "frequency_penalty": 0.0 + } } \ No newline at end of file diff --git a/dotnet/samples/04.e.twentyQuestions/Prompts/Hint/skprompt.txt b/dotnet/samples/04.e.twentyQuestions/Prompts/Hint/skprompt.txt index f6130ff9a..bf52dcca8 100644 --- a/dotnet/samples/04.e.twentyQuestions/Prompts/Hint/skprompt.txt +++ b/dotnet/samples/04.e.twentyQuestions/Prompts/Hint/skprompt.txt @@ -1,13 +1,10 @@ -You are the AI in a game of 20 questions. -The goal of the game is for the Human to guess a secret within 20 questions. +You are the AI in a game of 20 questions. +The goal of the game is for the Human to guess a secret within 20 questions. The AI should answer questions about the secret. The AI should assume that every message from the Human is a question about the secret. -GuessCount: {{$guessCount}} -RemainingGuesses: {{$remainingGuesses}} -Secret: {{$secretWord}} +GuessCount: {{$conversation.guessCount}} +RemainingGuesses: {{$conversation.remainingGuesses}} +Secret: {{$conversation.secretWord}} -Answer the humans question but do not mention the secret word. - -Human: {{$input}} -AI: \ No newline at end of file +Answer the humans question but do not mention the secret word. \ No newline at end of file diff --git a/dotnet/samples/04.e.twentyQuestions/README.md b/dotnet/samples/04.e.twentyQuestions/README.md index aa6075e5a..78c1dc273 100644 --- a/dotnet/samples/04.e.twentyQuestions/README.md +++ b/dotnet/samples/04.e.twentyQuestions/README.md @@ -1,39 +1,7 @@ # AI in Microsoft Teams: Twenty Qestions Welcome to the 20 Questions Bot: The Ultimate Guessing Game! This developer sample application showcases the incredible capabilities of language models and the concept of user intent. Challenge your skills as the human player and try to guess a secret within 20 questions, while the AI-powered bot answers your queries about the secret. Experience firsthand how language models interpret user input and provide informative responses, creating an engaging and interactive gaming experience. Get ready to dive into the world of language models and explore the fascinating realm of user interaction and intent. -It shows following SDK capabilities: - -
-

Bot scaffolding

- Throughout the 'Program.cs' and 'GameBot.cs' files you'll see the scaffolding created to run a bot with AI features. -
-
-

Prompt engineering

-The 'Prompts/Hint/skprompt.txt' file has descriptive prompt engineering that, in plain language, instructs GPT how the bot should conduct itself at submit time. For example, in 'skprompt.txt': - -#### skprompt.txt - -```text -You are the AI in a game of 20 questions. -The goal of the game is for the Human to guess a secret within 20 questions. -The AI should answer questions about the secret. -The AI should assume that every message from the Human is a question about the secret. - -GuessCount: {{$guessCount}} -RemainingGuesses: {{$remainingGuesses}} -Secret: {{$secretWord}} - -Answer the humans question but do not mention the secret word. - -Human: {{$input}} -AI: -``` - -- The major section ("*You are the AI ... the secret word.*") defines the basic direction, to tell how AI should behave on human's input. -- Variables "*{{$guessCount}}*", "*{{$remainingGuesses}}*" and "*{{$secretWord}}*" are set via `AI.Prompt.Variables` in `GameBot.cs`. -- You can also add function call via `AI.Prompt.AddFunction`, then reference it as "*{{function}}*" in prompt. -- The final section ("*Human: ... AI: ...*") defines the input of current turn. In addition, you can also add "*{{$history}}*" here to let AI to know the context about previous turns. -- "*{{input}}*", "*{{output}}*" and "*{{history}}*" are automatically resolved from `TurnState.Temp`. +It shows following SDK capabilities.
@@ -90,11 +58,6 @@ The `SECRET_` prefix is a convention used by Teams Toolkit to mask the value in Above steps use Azure OpenAI as AI service, optionally, you can also use OpenAI as AI service. -**As prerequisites** - -1. Prepare your own OpenAI service. -1. Modify source code `Program.cs`, comment out the "*#Use Azure OpenAI and Azure Content Safety*" part, and uncomment the "*#Use OpenAI*" part. - **For Local Debug (F5) with Teams Toolkit for Visual Studio** 1. Set your [OpenAI API Key](https://openai.com/api/) to *appsettings.Development.json*.