diff --git a/libraries/bot-dialogs/pom.xml b/libraries/bot-dialogs/pom.xml index 94560c4bf..1357cbf29 100644 --- a/libraries/bot-dialogs/pom.xml +++ b/libraries/bot-dialogs/pom.xml @@ -70,7 +70,7 @@ com.microsoft.bot bot-builder - 4.6.0-preview8 + ${project.version} com.microsoft.bot diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Dialog.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Dialog.java index 6aced56e8..59974a94a 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Dialog.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Dialog.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.microsoft.bot.builder.BotTelemetryClient; import com.microsoft.bot.builder.NullBotTelemetryClient; +import com.microsoft.bot.builder.StatePropertyAccessor; import com.microsoft.bot.builder.TurnContext; import java.util.concurrent.CompletableFuture; import org.apache.commons.lang3.StringUtils; @@ -15,11 +16,13 @@ * Base class for all dialogs. */ public abstract class Dialog { + /** - * A {@link DialogTurnResult} that indicates that the current dialog is - * still active and waiting for input from the user next turn. + * A {@link DialogTurnResult} that indicates that the current dialog is still active and waiting + * for input from the user next turn. */ - public static final DialogTurnResult END_OF_TURN = new DialogTurnResult(DialogTurnStatus.WAITING); + public static final DialogTurnResult END_OF_TURN = new DialogTurnResult( + DialogTurnStatus.WAITING); @JsonIgnore private BotTelemetryClient telemetryClient; @@ -29,6 +32,7 @@ public abstract class Dialog { /** * Initializes a new instance of the Dialog class. + * * @param dialogId The ID to assign to this dialog. */ public Dialog(String dialogId) { @@ -38,6 +42,7 @@ public Dialog(String dialogId) { /** * Gets id for the dialog. + * * @return Id for the dialog. */ public String getId() { @@ -49,6 +54,7 @@ public String getId() { /** * Sets id for the dialog. + * * @param withId Id for the dialog. */ public void setId(String withId) { @@ -57,6 +63,7 @@ public void setId(String withId) { /** * Gets the {@link BotTelemetryClient} to use for logging. + * * @return The BotTelemetryClient to use for logging. */ public BotTelemetryClient getTelemetryClient() { @@ -65,6 +72,7 @@ public BotTelemetryClient getTelemetryClient() { /** * Sets the {@link BotTelemetryClient} to use for logging. + * * @param withTelemetryClient The BotTelemetryClient to use for logging. */ public void setTelemetryClient(BotTelemetryClient withTelemetryClient) { @@ -74,10 +82,9 @@ public void setTelemetryClient(BotTelemetryClient withTelemetryClient) { /** * Called when the dialog is started and pushed onto the dialog stack. * - * @param dc The {@link DialogContext} for the current turn of - * conversation. - * @return If the task is successful, the result indicates whether the dialog is still - * active after the turn has been processed by the dialog. + * @param dc The {@link DialogContext} for the current turn of conversation. + * @return If the task is successful, the result indicates whether the dialog is still active + * after the turn has been processed by the dialog. */ public CompletableFuture beginDialog(DialogContext dc) { return beginDialog(dc, null); @@ -86,25 +93,25 @@ public CompletableFuture beginDialog(DialogContext dc) { /** * Called when the dialog is started and pushed onto the dialog stack. * - * @param dc The {@link DialogContext} for the current turn of - * conversation. + * @param dc The {@link DialogContext} for the current turn of conversation. * @param options Initial information to pass to the dialog. - * @return If the task is successful, the result indicates whether the dialog is still - * active after the turn has been processed by the dialog. + * @return If the task is successful, the result indicates whether the dialog is still active + * after the turn has been processed by the dialog. */ - public abstract CompletableFuture beginDialog(DialogContext dc, Object options); + public abstract CompletableFuture beginDialog( + DialogContext dc, Object options + ); /** - * Called when the dialog is _continued_, where it is the active dialog and the - * user replies with a new activity. + * Called when the dialog is _continued_, where it is the active dialog and the user replies + * with a new activity. * - *

If this method is *not* overridden, the dialog automatically ends when the user replies.

+ *

If this method is *not* overridden, the dialog automatically ends when the user + * replies.

* - * @param dc The {@link DialogContext} for the current turn of - * conversation. - * @return If the task is successful, the result indicates whether the dialog is still - * active after the turn has been processed by the dialog. The result may also contain a - * return value. + * @param dc The {@link DialogContext} for the current turn of conversation. + * @return If the task is successful, the result indicates whether the dialog is still active + * after the turn has been processed by the dialog. The result may also contain a return value. */ public CompletableFuture continueDialog(DialogContext dc) { // By default just end the current dialog. @@ -115,16 +122,17 @@ public CompletableFuture continueDialog(DialogContext dc) { * Called when a child dialog completed this turn, returning control to this dialog. * *

Generally, the child dialog was started with a call to - * {@link #beginDialog(DialogContext, Object)} However, if the - * {@link DialogContext#replaceDialog(String)} method - * is called, the logical child dialog may be different than the original.

+ * {@link #beginDialog(DialogContext, Object)} However, if the {@link + * DialogContext#replaceDialog(String)} method is called, the logical child dialog may be + * different than the original.

* - *

If this method is *not* overridden, the dialog automatically ends when the user replies.

+ *

If this method is *not* overridden, the dialog automatically ends when the user + * replies.

* - * @param dc The dialog context for the current turn of the conversation. + * @param dc The dialog context for the current turn of the conversation. * @param reason Reason why the dialog resumed. - * @return If the task is successful, the result indicates whether this dialog is still - * active after this dialog turn has been processed. + * @return If the task is successful, the result indicates whether this dialog is still active + * after this dialog turn has been processed. */ public CompletableFuture resumeDialog(DialogContext dc, DialogReason reason) { return resumeDialog(dc, reason, null); @@ -134,43 +142,53 @@ public CompletableFuture resumeDialog(DialogContext dc, Dialog * Called when a child dialog completed this turn, returning control to this dialog. * *

Generally, the child dialog was started with a call to - * {@link #beginDialog(DialogContext, Object)} However, if the - * {@link DialogContext#replaceDialog(String, Object)} method - * is called, the logical child dialog may be different than the original.

+ * {@link #beginDialog(DialogContext, Object)} However, if the {@link + * DialogContext#replaceDialog(String, Object)} method is called, the logical child dialog may + * be different than the original.

* - *

If this method is *not* overridden, the dialog automatically ends when the user replies.

+ *

If this method is *not* overridden, the dialog automatically ends when the user + * replies.

* - * @param dc The dialog context for the current turn of the conversation. + * @param dc The dialog context for the current turn of the conversation. * @param reason Reason why the dialog resumed. - * @param result Optional, value returned from the dialog that was called. The type of the - * value returned is dependent on the child dialog. - * @return If the task is successful, the result indicates whether this dialog is still - * active after this dialog turn has been processed. + * @param result Optional, value returned from the dialog that was called. The type of the value + * returned is dependent on the child dialog. + * @return If the task is successful, the result indicates whether this dialog is still active + * after this dialog turn has been processed. */ - public CompletableFuture resumeDialog(DialogContext dc, DialogReason reason, Object result) { + public CompletableFuture resumeDialog( + DialogContext dc, DialogReason reason, Object result + ) { // By default just end the current dialog and return result to parent. return dc.endDialog(result); } /** * Called when the dialog should re-prompt the user for input. + * * @param turnContext The context object for this turn. - * @param instance State information for this dialog. + * @param instance State information for this dialog. * @return A CompletableFuture representing the asynchronous operation. */ - public CompletableFuture repromptDialog(TurnContext turnContext, DialogInstance instance) { + public CompletableFuture repromptDialog( + TurnContext turnContext, DialogInstance instance + ) { // No-op by default return CompletableFuture.completedFuture(null); } /** * Called when the dialog is ending. + * * @param turnContext The context object for this turn. - * @param instance State information associated with the instance of this dialog on the dialog stack. - * @param reason Reason why the dialog ended. + * @param instance State information associated with the instance of this dialog on the + * dialog stack. + * @param reason Reason why the dialog ended. * @return A CompletableFuture representing the asynchronous operation. */ - public CompletableFuture endDialog(TurnContext turnContext, DialogInstance instance, DialogReason reason) { + public CompletableFuture endDialog( + TurnContext turnContext, DialogInstance instance, DialogReason reason + ) { // No-op by default return CompletableFuture.completedFuture(null); } @@ -178,7 +196,9 @@ public CompletableFuture endDialog(TurnContext turnContext, DialogInstance /** * Gets a unique String which represents the version of this dialog. If the version changes * between turns the dialog system will emit a DialogChanged event. - * @return Unique String which should only change when dialog has changed in a way that should restart the dialog. + * + * @return Unique String which should only change when dialog has changed in a way that should + * restart the dialog. */ @JsonIgnore public String getVersion() { @@ -188,8 +208,9 @@ public String getVersion() { /** * Called when an event has been raised, using `DialogContext.emitEvent()`, by either the * current dialog or a dialog that the current dialog started. + * * @param dc The dialog context for the current turn of conversation. - * @param e The event being raised. + * @param e The event being raised. * @return True if the event is handled by the current dialog and bubbling should stop. */ public CompletableFuture onDialogEvent(DialogContext dc, DialogEvent e) { @@ -222,8 +243,9 @@ public CompletableFuture onDialogEvent(DialogContext dc, DialogEvent e) * dialogs from performing their default processing.

* * @param dc The dialog context for the current turn of conversation. - * @param e The event being raised. - * @return Whether the event is handled by the current dialog and further processing should stop. + * @param e The event being raised. + * @return Whether the event is handled by the current dialog and further processing should + * stop. */ protected CompletableFuture onPreBubbleEvent(DialogContext dc, DialogEvent e) { return CompletableFuture.completedFuture(false); @@ -232,12 +254,14 @@ protected CompletableFuture onPreBubbleEvent(DialogContext dc, DialogEv /** * Called after an event was bubbled to all parents and wasn't handled. * - *

This is a good place to perform default processing logic for an event. Returning `true` will + *

This is a good place to perform default processing logic for an event. Returning `true` + * will * prevent any processing of the event by child dialogs.

* * @param dc The dialog context for the current turn of conversation. - * @param e The event being raised. - * @return Whether the event is handled by the current dialog and further processing should stop. + * @param e The event being raised. + * @return Whether the event is handled by the current dialog and further processing should + * stop. */ protected CompletableFuture onPostBubbleEvent(DialogContext dc, DialogEvent e) { return CompletableFuture.completedFuture(false); @@ -245,9 +269,46 @@ protected CompletableFuture onPostBubbleEvent(DialogContext dc, DialogE /** * Computes an id for the Dialog. + * * @return The id. */ protected String onComputeId() { return this.getClass().getName(); } + + /** + * Creates a dialog stack and starts a dialog, pushing it onto the stack. + * + * @param dialog The dialog to start. + * @param turnContext The context for the current turn of the conversation. + * @param accessor The StatePropertyAccessor accessor with which to manage the state of the + * dialog stack. + * @return A Task representing the asynchronous operation. + */ + public static CompletableFuture run( + Dialog dialog, + TurnContext turnContext, + StatePropertyAccessor accessor + ) { + DialogSet dialogSet = new DialogSet(accessor); + dialogSet.add(dialog); + dialogSet.setTelemetryClient(dialog.getTelemetryClient()); + + return dialogSet.createContext(turnContext) + .thenCompose(dialogContext -> continueOrStart(dialogContext, dialog)) + .thenApply(result -> null); + } + + private static CompletableFuture continueOrStart( + DialogContext dialogContext, Dialog dialog + ) { + return dialogContext.continueDialog() + .thenCompose(result -> { + if (result.getStatus() == DialogTurnStatus.EMPTY) { + return dialogContext.beginDialog(dialog.getId(), null); + } + + return CompletableFuture.completedFuture(result); + }); + } } diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContext.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContext.java index 3a0a51306..7416e814b 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContext.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContext.java @@ -200,7 +200,7 @@ public CompletableFuture beginDialog(String dialogId, Object o // Look up dialog Dialog dialog = findDialog(dialogId); if (dialog == null) { - Async.completeExceptionally(new Exception(String.format( + return Async.completeExceptionally(new Exception(String.format( "DialogContext.beginDialog(): A dialog with an id of '%s' wasn't found." + " The dialog must be included in the current or parent DialogSet." + " For example, if subclassing a ComponentDialog you can call AddDialog()" diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallDialog.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallDialog.java index aedde17ba..1ac77aef5 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallDialog.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallDialog.java @@ -4,7 +4,6 @@ package com.microsoft.bot.dialogs; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,9 +37,9 @@ public class WaterfallDialog extends Dialog { * @param dialogId The dialog ID. * @param actions Optional actions to be defined by the caller. */ - public WaterfallDialog(String dialogId, Collection actions) { + public WaterfallDialog(String dialogId, List actions) { super(dialogId); - steps = actions != null ? new ArrayList(actions) : new ArrayList(); + steps = actions != null ? actions : new ArrayList(); } /** @@ -126,7 +125,7 @@ public CompletableFuture continueDialog(DialogContext dc) { } // Don't do anything for non-message activities. - if (dc.getContext().getActivity().getType() != ActivityTypes.MESSAGE) { + if (!dc.getContext().getActivity().isType(ActivityTypes.MESSAGE)) { return CompletableFuture.completedFuture(END_OF_TURN); } diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceFactory.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceFactory.java index 7e484021e..1971e3d96 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceFactory.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceFactory.java @@ -11,6 +11,7 @@ import com.microsoft.bot.schema.HeroCard; import com.microsoft.bot.schema.InputHints; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -376,6 +377,16 @@ public static List toChoices(List choices) { return choices == null ? new ArrayList<>() : choices.stream().map(Choice::new).collect(Collectors.toList()); } + /** + * Returns a list of strings as a list of Choices. + * + * @param choices The strings to convert. + * @return A List of Choices. + */ + public static List toChoices(String... choices) { + return toChoices(Arrays.asList(choices)); + } + private static List extractActions(List choices) { if (choices == null) { choices = new ArrayList<>(); diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/AttachmentPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/AttachmentPrompt.java index 13fc79dbb..d7903050f 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/AttachmentPrompt.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/AttachmentPrompt.java @@ -105,7 +105,7 @@ protected CompletableFuture>> onRecogniz } PromptRecognizerResult> result = new PromptRecognizerResult>(); - if (turnContext.getActivity().getType() == ActivityTypes.MESSAGE) { + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { Activity message = turnContext.getActivity(); if (message.getAttachments() != null && message.getAttachments().size() > 0) { result.setSucceeded(true); diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ChoicePrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ChoicePrompt.java index 533ce5f58..9b8bb126f 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ChoicePrompt.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ChoicePrompt.java @@ -39,6 +39,15 @@ public class ChoicePrompt extends Prompt { private FindChoicesOptions recognizerOptions; private ChoiceFactoryOptions choiceOptions; + /** + * Initializes a new instance of the {@link ChoicePrompt} class. + * + * @param dialogId The ID to assign to this prompt. + */ + public ChoicePrompt(String dialogId) { + this(dialogId, null, null); + } + /** * Initializes a new instance of the {@link ChoicePrompt} class. * @@ -267,7 +276,7 @@ protected CompletableFuture> onRecognize(Tur List choices = options.getChoices() != null ? options.getChoices() : new ArrayList(); PromptRecognizerResult result = new PromptRecognizerResult(); - if (turnContext.getActivity().getType() == ActivityTypes.MESSAGE) { + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { Activity activity = turnContext.getActivity(); String utterance = activity.getText(); if (StringUtils.isEmpty(utterance)) { diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ConfirmPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ConfirmPrompt.java index 2a317d17a..bb7afe798 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ConfirmPrompt.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ConfirmPrompt.java @@ -283,7 +283,7 @@ protected CompletableFuture> onRecognize(TurnCon } PromptRecognizerResult result = new PromptRecognizerResult(); - if (turnContext.getActivity().getType() == ActivityTypes.MESSAGE) { + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { // Recognize utterance String utterance = turnContext.getActivity().getText(); if (StringUtils.isBlank(utterance)) { diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/DateTimePrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/DateTimePrompt.java index 9e971c486..83cd57306 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/DateTimePrompt.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/DateTimePrompt.java @@ -136,7 +136,7 @@ protected CompletableFuture onPrompt(TurnContext turnContext, Map> result = new PromptRecognizerResult>(); - if (turnContext.getActivity().getType() == ActivityTypes.MESSAGE) { + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { String utterance = turnContext.getActivity().getText(); if (StringUtils.isEmpty(utterance)) { return CompletableFuture.completedFuture(result); diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/NumberPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/NumberPrompt.java index 2446328f7..d95dd2be4 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/NumberPrompt.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/NumberPrompt.java @@ -7,8 +7,6 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; -import javax.activation.UnsupportedDataTypeException; - import com.microsoft.bot.builder.TurnContext; import com.microsoft.bot.connector.Async; import com.microsoft.bot.schema.ActivityTypes; @@ -37,13 +35,29 @@ public class NumberPrompt extends Prompt { * {@link DialogSet} or {@link ComponentDialog} . * @param classOfNumber Type of used to determine within the class what type was created for. This is required * due to type erasure in Java not allowing checking the type of during runtime. - * @throws UnsupportedDataTypeException thrown if a type other than int, long, float, or double are used for . + * @throws IllegalArgumentException thrown if a type other than int, long, float, or double are used for . */ public NumberPrompt(String dialogId, Class classOfNumber) - throws UnsupportedDataTypeException { + throws IllegalArgumentException { this(dialogId, null, null, classOfNumber); } + /** + * Initializes a new instance of the {@link NumberPrompt{T}} class. + * + * @param dialogId Unique ID of the dialog within its parent + * {@link DialogSet} or {@link ComponentDialog} . + * @param validator Validator that will be called each time the user + * responds to the prompt. + * @param classOfNumber Type of used to determine within the class what type was created for. This is required + * due to type erasure in Java not allowing checking the type of during runtime. + * @throws IllegalArgumentException thrown if a type other than int, long, float, or double are used for . + */ + public NumberPrompt(String dialogId, PromptValidator validator, Class classOfNumber) + throws IllegalArgumentException { + this(dialogId, validator, null, classOfNumber); + } + /** * Initializes a new instance of the {@link NumberPrompt{T}} class. * @@ -54,18 +68,17 @@ public NumberPrompt(String dialogId, Class classOfNumber) * @param defaultLocale Locale to use. * @param classOfNumber Type of used to determine within the class what type was created for. This is required * due to type erasure in Java not allowing checking the type of during runtime. - * @throws UnsupportedDataTypeException thrown if a type other than int, long, float, or double are used for . + * @throws IllegalArgumentException thrown if a type other than int, long, float, or double are used for . */ public NumberPrompt(String dialogId, PromptValidator validator, String defaultLocale, Class classOfNumber) - throws UnsupportedDataTypeException { - + throws IllegalArgumentException { super(dialogId, validator); this.defaultLocale = defaultLocale; this.classOfNumber = classOfNumber; if (!(classOfNumber.getSimpleName().equals("Long") || classOfNumber.getSimpleName().equals("Integer") || classOfNumber.getSimpleName().equals("Float") || classOfNumber.getSimpleName().equals("Double"))) { - throw new UnsupportedDataTypeException(String.format("NumberPrompt: Type argument %s is not supported", + throw new IllegalArgumentException(String.format("NumberPrompt: Type argument %s is not supported", classOfNumber.getSimpleName())); } } @@ -160,7 +173,7 @@ protected CompletableFuture> onRecognize(TurnContext t } PromptRecognizerResult result = new PromptRecognizerResult(); - if (turnContext.getActivity().getType() == ActivityTypes.MESSAGE) { + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { String utterance = turnContext.getActivity().getText(); if (StringUtils.isEmpty(utterance)) { return CompletableFuture.completedFuture(result); diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/Prompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/Prompt.java index b4130d560..aced3a4ab 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/Prompt.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/Prompt.java @@ -151,7 +151,7 @@ public CompletableFuture continueDialog(DialogContext dc) { } // Don't do anything for non-message activities - if (dc.getContext().getActivity().getType() != ActivityTypes.MESSAGE) { + if (!dc.getContext().getActivity().isType(ActivityTypes.MESSAGE)) { return CompletableFuture.completedFuture(Dialog.END_OF_TURN); } @@ -248,7 +248,7 @@ public CompletableFuture repromptDialog(TurnContext turnContext, DialogIns @Override protected CompletableFuture onPreBubbleEvent(DialogContext dc, DialogEvent e) { if (e.getName() == DialogEvents.ACTIVITY_RECEIVED - && dc.getContext().getActivity().getType() == ActivityTypes.MESSAGE) { + && dc.getContext().getActivity().isType(ActivityTypes.MESSAGE)) { // Perform base recognition Map state = dc.getActiveDialog().getState(); PromptRecognizerResult recognized = onRecognize(dc.getContext(), diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/TextPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/TextPrompt.java index a26c9b2e5..20869b2a7 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/TextPrompt.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/TextPrompt.java @@ -104,7 +104,7 @@ protected CompletableFuture> onRecognize(TurnCont } PromptRecognizerResult result = new PromptRecognizerResult(); - if (turnContext.getActivity().getType() == ActivityTypes.MESSAGE) { + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { Activity message = turnContext.getActivity(); if (message.getText() != null) { result.setSucceeded(true); diff --git a/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/ComponentDialogTests.java b/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/ComponentDialogTests.java index 2b125f4f3..2a0bb2a98 100644 --- a/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/ComponentDialogTests.java +++ b/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/ComponentDialogTests.java @@ -114,8 +114,8 @@ public void BasicWaterfallTest() throws UnsupportedDataTypeException { dialogs.add(createWaterfall()); try { dialogs.add(new NumberPrompt("number", Integer.class)); - } catch (UnsupportedDataTypeException e) { - e.printStackTrace(); + } catch (Throwable t) { + t.printStackTrace(); } DialogContext dc = dialogs.createContext(turnContext).join(); diff --git a/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/WaterfallTests.java b/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/WaterfallTests.java index 188f1a2cb..c9944e5fc 100644 --- a/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/WaterfallTests.java +++ b/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/WaterfallTests.java @@ -220,9 +220,9 @@ public void WaterfallPrompt() throws UnsupportedDataTypeException{ try { numberPrompt = new NumberPrompt("number", null, PromptCultureModels.ENGLISH_CULTURE, Integer.class); - } catch (UnsupportedDataTypeException e) { + } catch (Throwable t) { // TODO Auto-generated catch block - e.printStackTrace(); + t.printStackTrace(); } dialogs.add(numberPrompt); diff --git a/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/prompts/NumberPromptTests.java b/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/prompts/NumberPromptTests.java index 3ea7723a0..a8939f2a5 100644 --- a/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/prompts/NumberPromptTests.java +++ b/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/prompts/NumberPromptTests.java @@ -45,7 +45,7 @@ public void NumberPromptWithNullIdShouldFail() { @Test public void NumberPromptWithUnsupportedTypeShouldFail() { - Assert.assertThrows(UnsupportedDataTypeException.class, () -> new NumberPrompt("prompt", Short.class)); + Assert.assertThrows(IllegalArgumentException.class, () -> new NumberPrompt("prompt", Short.class)); } @Test diff --git a/samples/05.multi-turn-prompt/LICENSE b/samples/05.multi-turn-prompt/LICENSE new file mode 100644 index 000000000..21071075c --- /dev/null +++ b/samples/05.multi-turn-prompt/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/samples/05.multi-turn-prompt/README.md b/samples/05.multi-turn-prompt/README.md new file mode 100644 index 000000000..e4375c425 --- /dev/null +++ b/samples/05.multi-turn-prompt/README.md @@ -0,0 +1,90 @@ +# Multi-turn prompt + +Bot Framework v4 multi-turn prompt bot sample + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to use the prompts classes included in `botbuilder-dialogs`. This bot will ask for the user's name and age, then store the responses. It demonstrates a multi-turn dialog flow using a text prompt, a number prompt, and state accessors to store and retrieve values. + +## Prerequisites + +- Java 1.8+ +- Install [Maven](https://maven.apache.org/) +- An account on [Azure](https://azure.microsoft.com) if you want to deploy to Azure. + +## To try this sample locally +- From the root of this project folder: + - Build the sample using `mvn package` + - Run it by using `java -jar .\target\bot-multiturnprompt-sample.jar` + +- Test the bot using Bot Framework Emulator + + [Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + + - Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + + - Connect to the bot using Bot Framework Emulator + + - Launch Bot Framework Emulator + - File -> Open Bot + - Enter a Bot URL of `http://localhost:3978/api/messages` + +## Deploy the bot to Azure + +As described on [Deploy your bot](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-deploy-az-cli), you will perform the first 4 steps to setup the Azure app, then deploy the code using the azure-webapp Maven plugin. + +### 1. Login to Azure +From a command (or PowerShell) prompt in the root of the bot folder, execute: +`az login` + +### 2. Set the subscription +`az account set --subscription ""` + +If you aren't sure which subscription to use for deploying the bot, you can view the list of subscriptions for your account by using `az account list` command. + +### 3. Create an App registration +`az ad app create --display-name "" --password "" --available-to-other-tenants` + +Replace `` and `` with your own values. + +`` is the unique name of your bot. +`` is a minimum 16 character password for your bot. + +Record the `appid` from the returned JSON + +### 4. Create the Azure resources +Replace the values for ``, ``, ``, and `` in the following commands: + +#### To a new Resource Group +`az deployment create --name "stateBotDeploy" --location "westus" --template-file ".\deploymentTemplates\template-with-new-rg.json" --parameters groupName="" botId="" appId="" appSecret=""` + +#### To an existing Resource Group +`az group deployment create --name "stateBotDeploy" --resource-group "" --template-file ".\deploymentTemplates\template-with-preexisting-rg.json" --parameters botId="" appId="" appSecret=""` + +### 5. Update app id and password +In src/main/resources/application.properties update + - `MicrosoftAppPassword` with the botsecret value + - `MicrosoftAppId` with the appid from the first step + +### 6. Deploy the code +- Execute `mvn clean package` +- Execute `mvn azure-webapp:deploy -Dgroupname="" -Dbotname=""` + +If the deployment is successful, you will be able to test it via "Test in Web Chat" from the Azure Portal using the "Bot Channel Registration" for the bot. + +After the bot is deployed, you only need to execute #6 if you make changes to the bot. + + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) +- [Gathering Input Using Prompts](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) +- [Azure Portal](https://portal.azure.com) +- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) +- [Maven Plugin for Azure App Service](https://docs.microsoft.com/en-us/java/api/overview/azure/maven/azure-webapp-maven-plugin/readme?view=azure-java-stable) +- [Spring Boot](https://spring.io/projects/spring-boot) +- [Azure for Java cloud developers](https://docs.microsoft.com/en-us/azure/java/?view=azure-java-stable) diff --git a/samples/05.multi-turn-prompt/deploymentTemplates/new-rg-parameters.json b/samples/05.multi-turn-prompt/deploymentTemplates/new-rg-parameters.json new file mode 100644 index 000000000..ead339093 --- /dev/null +++ b/samples/05.multi-turn-prompt/deploymentTemplates/new-rg-parameters.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "value": "" + }, + "groupName": { + "value": "" + }, + "appId": { + "value": "" + }, + "appSecret": { + "value": "" + }, + "botId": { + "value": "" + }, + "botSku": { + "value": "" + }, + "newAppServicePlanName": { + "value": "" + }, + "newAppServicePlanSku": { + "value": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + } + }, + "newAppServicePlanLocation": { + "value": "" + }, + "newWebAppName": { + "value": "" + } + } +} \ No newline at end of file diff --git a/samples/05.multi-turn-prompt/deploymentTemplates/preexisting-rg-parameters.json b/samples/05.multi-turn-prompt/deploymentTemplates/preexisting-rg-parameters.json new file mode 100644 index 000000000..b6f5114fc --- /dev/null +++ b/samples/05.multi-turn-prompt/deploymentTemplates/preexisting-rg-parameters.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "value": "" + }, + "appSecret": { + "value": "" + }, + "botId": { + "value": "" + }, + "botSku": { + "value": "" + }, + "newAppServicePlanName": { + "value": "" + }, + "newAppServicePlanSku": { + "value": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + } + }, + "appServicePlanLocation": { + "value": "" + }, + "existingAppServicePlan": { + "value": "" + }, + "newWebAppName": { + "value": "" + } + } +} \ No newline at end of file diff --git a/samples/05.multi-turn-prompt/deploymentTemplates/template-with-new-rg.json b/samples/05.multi-turn-prompt/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 000000000..dcd6260a5 --- /dev/null +++ b/samples/05.multi-turn-prompt/deploymentTemplates/template-with-new-rg.json @@ -0,0 +1,191 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "F0", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "P1v2", + "tier": "PremiumV2", + "size": "P1v2", + "family": "Pv2", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "resourcesLocation": "[deployment().location]", + "effectiveGroupLocation": "[if(empty(parameters('groupLocation')), variables('resourcesLocation'), parameters('groupLocation'))]", + "effectivePlanLocation": "[if(empty(parameters('newAppServicePlanLocation')), variables('resourcesLocation'), parameters('newAppServicePlanLocation'))]", + "appServicePlanName": "[if(empty(parameters('newAppServicePlanName')), concat(parameters('botId'), 'ServicePlan'), parameters('newAppServicePlanName'))]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[variables('effectiveGroupLocation')]", + "properties": { + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new App Service Plan", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('effectivePlanLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "name": "[variables('appServicePlanName')]", + "reserved":true + } + }, + { + "comments": "Create a Web App using the new App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "serverFarmId": "[variables('appServicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "JAVA_OPTS", + "value": "-Dserver.port=80" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} \ No newline at end of file diff --git a/samples/05.multi-turn-prompt/deploymentTemplates/template-with-preexisting-rg.json b/samples/05.multi-turn-prompt/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..b790d2bdc --- /dev/null +++ b/samples/05.multi-turn-prompt/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,158 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Defaults to \"\"." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "S1", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "P1v2", + "tier": "PremiumV2", + "size": "P1v2", + "family": "Pv2", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), if(empty(parameters('newAppServicePlanName')),concat(parameters('botId'), 'ServicePlan'),parameters('newAppServicePlanName')))]", + "resourcesLocation": "[if(empty(parameters('appServicePlanLocation')), resourceGroup().location, parameters('appServicePlanLocation'))]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingAppServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "name": "[variables('servicePlanName')]", + "reserved":true + } + }, + { + "comments": "Create a Web App using an App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2016-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "serverFarmId": "[variables('servicePlanName')]", + "siteConfig": { + "linuxFxVersion": "JAVA|8-jre8", + "appSettings": [ + { + "name": "JAVA_OPTS", + "value": "-Dserver.port=80" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/samples/05.multi-turn-prompt/pom.xml b/samples/05.multi-turn-prompt/pom.xml new file mode 100644 index 000000000..7dfbebe83 --- /dev/null +++ b/samples/05.multi-turn-prompt/pom.xml @@ -0,0 +1,244 @@ + + + + 4.0.0 + + com.microsoft.bot.sample + bot-multiturnprompt + sample + jar + + ${project.groupId}:${project.artifactId} + This package contains the Multi-turn Prompt sample using Spring Boot. + http://maven.apache.org + + + org.springframework.boot + spring-boot-starter-parent + 2.4.0 + + + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + Bot Framework Development + + Microsoft + https://dev.botframework.com/ + + + + + 1.8 + 1.8 + 1.8 + com.microsoft.bot.sample.multiturnprompt.Application + + + + + org.springframework.boot + spring-boot-starter-test + 2.4.0 + test + + + junit + junit + 4.13.1 + test + + + org.junit.vintage + junit-vintage-engine + test + + + + org.slf4j + slf4j-api + + + org.apache.logging.log4j + log4j-api + 2.11.0 + + + org.apache.logging.log4j + log4j-core + 2.13.2 + + + + com.microsoft.bot + bot-integration-spring + 4.6.0-preview8 + compile + + + com.microsoft.bot + bot-dialogs + 4.6.0-preview8 + compile + + + + + + build + + true + + + + + src/main/resources + false + + + + + maven-compiler-plugin + 3.8.1 + + + maven-war-plugin + 3.2.3 + + src/main/webapp + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + com.microsoft.bot.sample.multiturnprompt.Application + + + + + + com.microsoft.azure + azure-webapp-maven-plugin + 1.7.0 + + V2 + ${groupname} + ${botname} + + + JAVA_OPTS + -Dserver.port=80 + + + + linux + jre8 + jre8 + + + + + ${project.basedir}/target + + *.jar + + + + + + + + + + + + publish + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + maven-war-plugin + 3.2.3 + + src/main/webapp + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + true + ossrh + https://oss.sonatype.org/ + true + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + 8 + false + + + + attach-javadocs + + jar + + + + + + + + + diff --git a/samples/05.multi-turn-prompt/src/main/java/com/microsoft/bot/sample/multiturnprompt/Application.java b/samples/05.multi-turn-prompt/src/main/java/com/microsoft/bot/sample/multiturnprompt/Application.java new file mode 100644 index 000000000..1324a2348 --- /dev/null +++ b/samples/05.multi-turn-prompt/src/main/java/com/microsoft/bot/sample/multiturnprompt/Application.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multiturnprompt; + +import com.microsoft.bot.integration.AdapterWithErrorHandler; +import com.microsoft.bot.integration.BotFrameworkHttpAdapter; +import com.microsoft.bot.integration.Configuration; +import com.microsoft.bot.integration.spring.BotController; +import com.microsoft.bot.integration.spring.BotDependencyConfiguration; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; + +/** + * This is the starting point of the Sprint Boot Bot application. + * + * This class also provides overrides for dependency injections. A class that + * extends the {@link com.microsoft.bot.builder.Bot} interface should be + * annotated with @Component. + * + * @see DialogBot + */ +@SpringBootApplication + +// Use the default BotController to receive incoming Channel messages. A custom +// controller could be used by eliminating this import and creating a new +// RestController. +// The default controller is created by the Spring Boot container using +// dependency injection. The default route is /api/messages. +@Import({BotController.class}) + +public class Application extends BotDependencyConfiguration { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + /** + * Returns a custom Adapter that provides error handling. + * + * @param configuration The Configuration object to use. + * @return An error handling BotFrameworkHttpAdapter. + */ + @Override + public BotFrameworkHttpAdapter getBotFrameworkHttpAdaptor(Configuration configuration) { + return new AdapterWithErrorHandler(configuration); + } +} diff --git a/samples/05.multi-turn-prompt/src/main/java/com/microsoft/bot/sample/multiturnprompt/DialogBot.java b/samples/05.multi-turn-prompt/src/main/java/com/microsoft/bot/sample/multiturnprompt/DialogBot.java new file mode 100644 index 000000000..d71d53ed6 --- /dev/null +++ b/samples/05.multi-turn-prompt/src/main/java/com/microsoft/bot/sample/multiturnprompt/DialogBot.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multiturnprompt; + +import com.microsoft.bot.builder.ActivityHandler; +import com.microsoft.bot.builder.BotState; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.dialogs.Dialog; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.UserState; +import java.util.concurrent.CompletableFuture; +import org.springframework.stereotype.Component; + +/** + * This IBot implementation can run any type of Dialog. The use of type parameterization is to + * allows multiple different bots to be run at different endpoints within the same project. This + * can be achieved by defining distinct Controller types each with dependency on distinct IBot + * types, this way ASP Dependency Injection can glue everything together without ambiguity. The + * ConversationState is used by the Dialog system. The UserState isn't, however, it might have + * been used in a Dialog implementation, and the requirement is that all BotState objects are + * saved at the end of a turn. + */ +@Component +public class DialogBot extends ActivityHandler { + protected Dialog dialog; + protected BotState conversationState; + protected BotState userState; + + public DialogBot( + ConversationState withConversationState, + UserState withUserState, + Dialog withDialog + ) { + dialog = withDialog; + conversationState = withConversationState; + userState = withUserState; + } + + @Override + public CompletableFuture onTurn( + TurnContext turnContext + ) { + return super.onTurn(turnContext) + .thenCompose(result -> conversationState.saveChanges(turnContext)) + .thenCompose(result -> userState.saveChanges(turnContext)); + } + + @Override + protected CompletableFuture onMessageActivity( + TurnContext turnContext + ) { + return Dialog.run(dialog, turnContext, conversationState.createProperty("DialogState")); + } +} diff --git a/samples/05.multi-turn-prompt/src/main/java/com/microsoft/bot/sample/multiturnprompt/UserProfile.java b/samples/05.multi-turn-prompt/src/main/java/com/microsoft/bot/sample/multiturnprompt/UserProfile.java new file mode 100644 index 000000000..a04f2f72b --- /dev/null +++ b/samples/05.multi-turn-prompt/src/main/java/com/microsoft/bot/sample/multiturnprompt/UserProfile.java @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multiturnprompt; + +import com.microsoft.bot.schema.Attachment; + +public class UserProfile { + public String transport; + public String name; + public Integer age; + public Attachment picture; +} diff --git a/samples/05.multi-turn-prompt/src/main/java/com/microsoft/bot/sample/multiturnprompt/UserProfileDialog.java b/samples/05.multi-turn-prompt/src/main/java/com/microsoft/bot/sample/multiturnprompt/UserProfileDialog.java new file mode 100644 index 000000000..302fe5d3a --- /dev/null +++ b/samples/05.multi-turn-prompt/src/main/java/com/microsoft/bot/sample/multiturnprompt/UserProfileDialog.java @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multiturnprompt; + +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.StatePropertyAccessor; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.dialogs.ComponentDialog; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.dialogs.WaterfallDialog; +import com.microsoft.bot.dialogs.WaterfallStep; +import com.microsoft.bot.dialogs.WaterfallStepContext; +import com.microsoft.bot.dialogs.choices.ChoiceFactory; +import com.microsoft.bot.dialogs.choices.FoundChoice; +import com.microsoft.bot.dialogs.prompts.AttachmentPrompt; +import com.microsoft.bot.dialogs.prompts.ChoicePrompt; +import com.microsoft.bot.dialogs.prompts.ConfirmPrompt; +import com.microsoft.bot.dialogs.prompts.NumberPrompt; +import com.microsoft.bot.dialogs.prompts.PromptOptions; +import com.microsoft.bot.dialogs.prompts.PromptValidatorContext; +import com.microsoft.bot.dialogs.prompts.TextPrompt; +import com.microsoft.bot.schema.Attachment; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +@Component +public class UserProfileDialog extends ComponentDialog { + private StatePropertyAccessor userProfileAccessor; + + public UserProfileDialog(UserState withUserState) { + super("UserProfileDialog"); + + userProfileAccessor = withUserState.createProperty("UserProfile"); + + WaterfallStep[] waterfallSteps = { + UserProfileDialog::transportStep, + UserProfileDialog::nameStep, + UserProfileDialog::nameConfirmStep, + UserProfileDialog::ageStep, + UserProfileDialog::pictureStep, + UserProfileDialog::confirmStep, + this::summaryStep + }; + + // Add named dialogs to the DialogSet. These names are saved in the dialog state. + addDialog(new WaterfallDialog("WaterfallDialog", Arrays.asList(waterfallSteps))); + addDialog(new TextPrompt("TextPrompt")); + addDialog(new NumberPrompt("NumberPrompt", UserProfileDialog::agePromptValidator, Integer.class)); + addDialog(new ChoicePrompt("ChoicePrompt")); + addDialog(new ConfirmPrompt("ConfirmPrompt")); + addDialog(new AttachmentPrompt("AttachmentPrompt", UserProfileDialog::picturePromptValidator)); + + // The initial child Dialog to run. + setInitialDialogId("WaterfallDialog"); + } + + private static CompletableFuture transportStep(WaterfallStepContext stepContext) { + // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. + // Running a prompt here means the next WaterfallStep will be run when the user's response is received. + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(MessageFactory.text("Please enter your mode of transport.")); + promptOptions.setChoices(ChoiceFactory.toChoices("Car", "Bus", "Bicycle")); + + return stepContext.prompt("ChoicePrompt", promptOptions); + } + + private static CompletableFuture nameStep(WaterfallStepContext stepContext) { + stepContext.getValues().put("transport", ((FoundChoice) stepContext.getResult()).getValue()); + + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(MessageFactory.text("Please enter your name.")); + return stepContext.prompt("TextPrompt", promptOptions); + } + + private static CompletableFuture nameConfirmStep(WaterfallStepContext stepContext) { + stepContext.getValues().put("name", stepContext.getResult()); + + // We can send messages to the user at any point in the WaterfallStep. + return stepContext.getContext().sendActivity(MessageFactory.text(String.format("Thanks %s", stepContext.getResult()))) + .thenCompose(resourceResponse -> { + // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(MessageFactory.text("Would you like to give your age?")); + return stepContext.prompt("ConfirmPrompt", promptOptions); + }); + } + + private static CompletableFuture ageStep(WaterfallStepContext stepContext) { + if ((Boolean)stepContext.getResult()) { + // User said "yes" so we will be prompting for the age. + // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(MessageFactory.text("Please enter your age.")); + promptOptions.setRetryPrompt(MessageFactory.text("The value entered must be greater than 0 and less than 150.")); + + return stepContext.prompt("NumberPrompt", promptOptions); + } + + // User said "no" so we will skip the next step. Give -1 as the age. + return stepContext.next(-1); + } + + private static CompletableFuture pictureStep(WaterfallStepContext stepContext) { + stepContext.getValues().put("age", (Integer) stepContext.getResult()); + + String msg = (Integer)stepContext.getValues().get("age") == -1 + ? "No age given." + : String.format("I have your age as %d.", (Integer)stepContext.getValues().get("age")); + + // We can send messages to the user at any point in the WaterfallStep. + return stepContext.getContext().sendActivity(MessageFactory.text(msg)) + .thenCompose(resourceResponse -> { + if (StringUtils.equals(stepContext.getContext().getActivity().getChannelId(), Channels.MSTEAMS)) { + // This attachment prompt example is not designed to work for Teams attachments, so skip it in this case + return stepContext.getContext().sendActivity(MessageFactory.text("Skipping attachment prompt in Teams channel...")) + .thenCompose(resourceResponse1 -> stepContext.next(null)); + } + + // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(MessageFactory.text("Please attach a profile picture (or type any message to skip).")); + promptOptions.setRetryPrompt(MessageFactory.text("The attachment must be a jpeg/png image file.")); + + return stepContext.prompt("AttachmentPrompt", promptOptions); + }); + } + + private static CompletableFuture confirmStep(WaterfallStepContext stepContext) { + List attachments = (List)stepContext.getResult(); + stepContext.getValues().put("picture", attachments == null ? null : attachments.get(0)); + + // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(MessageFactory.text("Is this ok?")); + return stepContext.prompt("ConfirmPrompt", promptOptions); + } + + private CompletableFuture summaryStep(WaterfallStepContext stepContext) { + if ((Boolean)stepContext.getResult()) { + // Get the current profile object from user state. + return userProfileAccessor.get(stepContext.getContext(), () -> new UserProfile()) + .thenCompose(userProfile -> { + userProfile.transport = (String) stepContext.getValues().get("transport"); + userProfile.name = (String) stepContext.getValues().get("name"); + userProfile.age = (Integer) stepContext.getValues().get("age"); + userProfile.picture = (Attachment) stepContext.getValues().get("picture"); + + String msg = String.format( + "I have your mode of transport as %s and your name as %s", + userProfile.transport, userProfile.name + ); + + if (userProfile.age != -1) { + msg += String.format(" and your age as %s", userProfile.age); + } + + msg += "."; + + return stepContext.getContext().sendActivity(MessageFactory.text(msg)) + .thenApply(resourceResponse -> userProfile); + }) + .thenCompose(userProfile -> { + if (userProfile.picture != null) { + return stepContext.getContext().sendActivity( + MessageFactory.attachment(userProfile.picture, + "This is your profile picture." + )); + } + + return stepContext.getContext().sendActivity( + MessageFactory.text("A profile picture wasn't attached.") + ); + }) + .thenCompose(resourceResponse -> stepContext.endDialog()); + } + + // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is the end. + return stepContext.getContext().sendActivity(MessageFactory.text("Thanks. Your profile will not be kept.")) + .thenCompose(resourceResponse -> stepContext.endDialog()); + } + + private static CompletableFuture picturePromptValidator( + PromptValidatorContext> promptContext + ) { + if (promptContext.getRecognized().getSucceeded()) { + List attachments = promptContext.getRecognized().getValue(); + List validImages = new ArrayList<>(); + + for (Attachment attachment : attachments) { + if (StringUtils.equals( + attachment.getContentType(), "image/jpeg") || StringUtils.equals(attachment.getContentType(), "image/png") + ) { + validImages.add(attachment); + } + } + + promptContext.getRecognized().setValue(validImages); + + // If none of the attachments are valid images, the retry prompt should be sent. + return CompletableFuture.completedFuture(!validImages.isEmpty()); + } + else { + // We can return true from a validator function even if Recognized.Succeeded is false. + return promptContext.getContext().sendActivity("No attachments received. Proceeding without a profile picture...") + .thenApply(resourceResponse -> true); + } + } + + private static CompletableFuture agePromptValidator( + PromptValidatorContext promptContext + ) { + // This condition is our validation rule. You can also change the value at this point. + return CompletableFuture.completedFuture( + promptContext.getRecognized().getSucceeded() + && promptContext.getRecognized().getValue() > 0 + && promptContext.getRecognized().getValue() < 150); + } +} diff --git a/samples/05.multi-turn-prompt/src/main/resources/application.properties b/samples/05.multi-turn-prompt/src/main/resources/application.properties new file mode 100644 index 000000000..d7d0ee864 --- /dev/null +++ b/samples/05.multi-turn-prompt/src/main/resources/application.properties @@ -0,0 +1,3 @@ +MicrosoftAppId= +MicrosoftAppPassword= +server.port=3978 diff --git a/samples/05.multi-turn-prompt/src/main/resources/log4j2.json b/samples/05.multi-turn-prompt/src/main/resources/log4j2.json new file mode 100644 index 000000000..67c0ad530 --- /dev/null +++ b/samples/05.multi-turn-prompt/src/main/resources/log4j2.json @@ -0,0 +1,18 @@ +{ + "configuration": { + "name": "Default", + "appenders": { + "Console": { + "name": "Console-Appender", + "target": "SYSTEM_OUT", + "PatternLayout": {"pattern": "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"} + } + }, + "loggers": { + "root": { + "level": "debug", + "appender-ref": {"ref": "Console-Appender","level": "debug"} + } + } + } +} diff --git a/samples/05.multi-turn-prompt/src/main/webapp/META-INF/MANIFEST.MF b/samples/05.multi-turn-prompt/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 000000000..254272e1c --- /dev/null +++ b/samples/05.multi-turn-prompt/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/samples/05.multi-turn-prompt/src/main/webapp/WEB-INF/web.xml b/samples/05.multi-turn-prompt/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..383c19004 --- /dev/null +++ b/samples/05.multi-turn-prompt/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,12 @@ + + + dispatcher + + org.springframework.web.servlet.DispatcherServlet + + + contextConfigLocation + /WEB-INF/spring/dispatcher-config.xml + + 1 + \ No newline at end of file diff --git a/samples/05.multi-turn-prompt/src/main/webapp/index.html b/samples/05.multi-turn-prompt/src/main/webapp/index.html new file mode 100644 index 000000000..d5ba5158e --- /dev/null +++ b/samples/05.multi-turn-prompt/src/main/webapp/index.html @@ -0,0 +1,418 @@ + + + + + + + EchoBot + + + + + +
+
+
+
Spring Boot Bot
+
+
+
+
+
Your bot is ready!
+
You can test your bot in the Bot Framework Emulator
+ by connecting to http://localhost:3978/api/messages.
+ +
Visit Azure + Bot Service to register your bot and add it to
+ various channels. The bot's endpoint URL typically looks + like this:
+
https://your_bots_hostname/api/messages
+
+
+
+
+ +
+ + + diff --git a/samples/05.multi-turn-prompt/src/test/java/com/microsoft/bot/sample/multiturnprompt/ApplicationTest.java b/samples/05.multi-turn-prompt/src/test/java/com/microsoft/bot/sample/multiturnprompt/ApplicationTest.java new file mode 100644 index 000000000..ced035bd3 --- /dev/null +++ b/samples/05.multi-turn-prompt/src/test/java/com/microsoft/bot/sample/multiturnprompt/ApplicationTest.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multiturnprompt; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class ApplicationTest { + + @Test + public void contextLoads() { + } + +} diff --git a/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/TranslationMiddleware.java b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/TranslationMiddleware.java index b36653cdb..d8455fcad 100644 --- a/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/TranslationMiddleware.java +++ b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/TranslationMiddleware.java @@ -55,7 +55,7 @@ public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next return this.shouldTranslate(turnContext).thenCompose(translate -> { if (translate) { - if (turnContext.getActivity().getType() == ActivityTypes.MESSAGE) { + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { return this.translator.translate( turnContext.getActivity().getText(), TranslationSettings.DEFAULT_LANGUAGE)