Skip to content

Only show "NO MATCH" response button if active learning is active #1579

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 25, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -67,13 +67,6 @@ def get_qna_prompts_card(result: QueryResult, card_no_match_text: str) -> Activi
for prompt in result.context.prompts
]

# Add No match text
Copy link
Contributor Author

@Zerryth Zerryth Mar 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • get_qna_prompts_card gets called for multi-turn qna dialog scenarios
  • It's not an active learning scenario
  • remove active learning NO MATCH response appending from get_qna_prompts_card
  • the NO MATCH response is still present within get_suggestions_card, which is what is called for active learning prompt scenarios

button_list.append(
CardAction(
value=card_no_match_text, type="imBack", title=card_no_match_text,
)
)

attachment = CardFactory.hero_card(HeroCard(buttons=button_list))

return Activity(
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"answers": [
{
"questions": [
"Esper seeks"
],
"answer": "Esper seeks. She's a curious little explorer. Young toddlers seek out new adventures, expanding their knowledge base. It's their job to test limits, to learn about them. It's the adult's job to enforce the limits, while also allowing room for exploration",
"score": 79.65,
"id": 35,
"source": "Editorial",
"isDocumentText": false,
"metadata": [],
"context": {
"isContextOnly": false,
"prompts": []
}
},
{
"questions": [
"Esper sups"
],
"answer": "Esper sups. She eats just about anything. She loves her broccoli. Anything that she sees her parents eating, she wants to part take in herself.\n\nCaution though. If she spots you eating dessert, you best be prepared to share with her. Best to wait until she goes down for bed and then sneak your favorite snack in, without her prying eyes.",
"score": 79.65,
"id": 36,
"source": "Editorial",
"isDocumentText": false,
"metadata": [],
"context": {
"isContextOnly": false,
"prompts": []
}
},
{
"questions": [
"Esper screams"
],
"answer": "Esper screams. The currently 1-year old toddler has a brain that's rapidly developing, expanding to new abilities at an alarming rate. With it may come fright or possibly frustration as they understand what could be done, however they need to master how to do a task themselves",
"score": 66.89,
"id": 34,
"source": "Editorial",
"isDocumentText": false,
"metadata": [],
"context": {
"isContextOnly": false,
"prompts": []
}
},
{
"questions": [
"Esper sleeps"
],
"answer": "Esper sleeps. Esper sleeps on her floor bed. She never had a crib, as her parents placed her directly on the floor bed since birth. With this comes the benefit of not having to have an awkward transition period from crib to bed, when she gets old enough.\n\nThe idea of using the bed is that it offers the child more freedom to move about--more autonomy. Downside is, they will definitely wander off the bed, when they don't want to sleep",
"score": 65.71,
"id": 33,
"source": "Editorial",
"isDocumentText": false,
"metadata": [],
"context": {
"isContextOnly": false,
"prompts": []
}
}
],
"activeLearningEnabled": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"answers": [
{
"questions": [
"Tell me about birds",
"What do you know about birds"
],
"answer": "Choose one of the following birds to get more info",
"score": 100.0,
"id": 37,
"source": "Editorial",
"isDocumentText": false,
"metadata": [],
"context": {
"isContextOnly": false,
"prompts": [
{
"displayOrder": 1,
"qnaId": 38,
"displayText": "Bald Eagle"
},
{
"displayOrder": 2,
"qnaId": 39,
"displayText": "Hummingbird"
}
]
}
}
],
"activeLearningEnabled": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"answers": [
{
"questions": [
"Bald Eagle"
],
"answer": "Apparently these guys aren't actually bald!",
"score": 100.0,
"id": 38,
"source": "Editorial",
"isDocumentText": false,
"metadata": [],
"context": {
"isContextOnly": true,
"prompts": []
}
}
],
"activeLearningEnabled": true
}
165 changes: 165 additions & 0 deletions libraries/botbuilder-ai/tests/qna/test_qna_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import json
from os import path
from unittest.mock import patch
import aiounittest

# from botbuilder.ai.qna import QnAMakerEndpoint, QnAMaker, QnAMakerOptions
from botbuilder.ai.qna.dialogs import QnAMakerDialog
from botbuilder.schema import Activity, ActivityTypes
from botbuilder.core import ConversationState, MemoryStorage, TurnContext
from botbuilder.core.adapters import TestAdapter, TestFlow
from botbuilder.dialogs import DialogSet, DialogTurnStatus


class QnaMakerDialogTest(aiounittest.AsyncTestCase):
# Note this is NOT a real QnA Maker application ID nor a real QnA Maker subscription-key
# theses are GUIDs edited to look right to the parsing and validation code.

_knowledge_base_id: str = "f028d9k3-7g9z-11d3-d300-2b8x98227q8w"
_endpoint_key: str = "1k997n7w-207z-36p3-j2u1-09tas20ci6011"
_host: str = "https://dummyqnahost.azurewebsites.net/qnamaker"

_tell_me_about_birds: str = "Tell me about birds"
_choose_bird: str = "Choose one of the following birds to get more info"
_bald_eagle: str = "Bald Eagle"
_esper: str = "Esper"

DEFAULT_ACTIVE_LEARNING_TITLE: str = "Did you mean:"
DEFAULT_NO_MATCH_TEXT: str = "None of the above."
DEFAULT_CARD_NO_MATCH_RESPONSE: str = "Thanks for the feedback."

async def test_multiturn_dialog(self):
# Set Up QnAMakerDialog
convo_state = ConversationState(MemoryStorage())
dialog_state = convo_state.create_property("dialogState")
dialogs = DialogSet(dialog_state)

qna_dialog = QnAMakerDialog(
self._knowledge_base_id, self._endpoint_key, self._host
)
dialogs.add(qna_dialog)

# Callback that runs the dialog
async def execute_qna_dialog(turn_context: TurnContext) -> None:
if turn_context.activity.type != ActivityTypes.message:
raise TypeError(
"Failed to execute QnA dialog. Should have received a message activity."
)

response_json = self._get_json_res(turn_context.activity.text)
dialog_context = await dialogs.create_context(turn_context)
with patch(
"aiohttp.ClientSession.post",
return_value=aiounittest.futurized(response_json),
):
results = await dialog_context.continue_dialog()

if results.status == DialogTurnStatus.Empty:
await dialog_context.begin_dialog("QnAMakerDialog")

await convo_state.save_changes(turn_context)

# Send and receive messages from QnA dialog
test_adapter = TestAdapter(execute_qna_dialog)
test_flow = TestFlow(None, test_adapter)
tf2 = await test_flow.send(self._tell_me_about_birds)
dialog_reply: Activity = tf2.adapter.activity_buffer[0]
self._assert_has_valid_hero_card_buttons(dialog_reply, button_count=2)
tf3 = await tf2.assert_reply(self._choose_bird)
tf4 = await tf3.send(self._bald_eagle)
await tf4.assert_reply("Apparently these guys aren't actually bald!")

async def test_active_learning(self):
# Set Up QnAMakerDialog
convo_state = ConversationState(MemoryStorage())
dialog_state = convo_state.create_property("dialogState")
dialogs = DialogSet(dialog_state)

qna_dialog = QnAMakerDialog(
self._knowledge_base_id, self._endpoint_key, self._host
)
dialogs.add(qna_dialog)

# Callback that runs the dialog
async def execute_qna_dialog(turn_context: TurnContext) -> None:
if turn_context.activity.type != ActivityTypes.message:
raise TypeError(
"Failed to execute QnA dialog. Should have received a message activity."
)

response_json = self._get_json_res(turn_context.activity.text)
dialog_context = await dialogs.create_context(turn_context)
with patch(
"aiohttp.ClientSession.post",
return_value=aiounittest.futurized(response_json),
):
results = await dialog_context.continue_dialog()

if results.status == DialogTurnStatus.Empty:
await dialog_context.begin_dialog("QnAMakerDialog")

await convo_state.save_changes(turn_context)

# Send and receive messages from QnA dialog
test_adapter = TestAdapter(execute_qna_dialog)
test_flow = TestFlow(None, test_adapter)
tf2 = await test_flow.send(self._esper)
dialog_reply: Activity = tf2.adapter.activity_buffer[0]
self._assert_has_valid_hero_card_buttons(dialog_reply, button_count=3)
tf3 = await tf2.assert_reply(self.DEFAULT_ACTIVE_LEARNING_TITLE)
tf4 = await tf3.send(self.DEFAULT_NO_MATCH_TEXT)
await tf4.assert_reply(self.DEFAULT_CARD_NO_MATCH_RESPONSE)

print(tf2)

def _assert_has_valid_hero_card_buttons(
self, activity: Activity, button_count: int
):
self.assertIsInstance(activity, Activity)
attachments = activity.attachments
self.assertTrue(attachments)
self.assertEqual(len(attachments), 1)
buttons = attachments[0].content.buttons
button_count_err = (
f"Should have only received {button_count} buttons in multi-turn prompt"
)

if activity.text == self._choose_bird:
self.assertEqual(len(buttons), button_count, button_count_err)
self.assertEqual(buttons[0].value, self._bald_eagle)
self.assertEqual(buttons[1].value, "Hummingbird")

if activity.text == self.DEFAULT_ACTIVE_LEARNING_TITLE:
self.assertEqual(len(buttons), button_count, button_count_err)
self.assertEqual(buttons[0].value, "Esper seeks")
self.assertEqual(buttons[1].value, "Esper sups")
self.assertEqual(buttons[2].value, self.DEFAULT_NO_MATCH_TEXT)

def _get_json_res(self, text: str) -> object:
if text == self._tell_me_about_birds:
return QnaMakerDialogTest._get_json_for_file(
"QnAMakerDialog_MultiTurn_Answer1.json"
)

if text == self._bald_eagle:
return QnaMakerDialogTest._get_json_for_file(
"QnAMakerDialog_MultiTurn_Answer2.json"
)

if text == self._esper:
return QnaMakerDialogTest._get_json_for_file(
"QnAMakerDialog_ActiveLearning.json"
)

return None

@staticmethod
def _get_json_for_file(response_file: str) -> object:
curr_dir = path.dirname(path.abspath(__file__))
response_path = path.join(curr_dir, "test_data", response_file)

with open(response_path, "r", encoding="utf-8-sig") as file:
response_str = file.read()
response_json = json.loads(response_str)

return response_json