Skip to content

Commit 1d48723

Browse files
Eric Dahlvangaxelsrz
Eric Dahlvang
authored andcommitted
[Teams] Draft action based fetch task scenario (#512)
* add crude action based fetch task scenario * add ExampleData and some cleanup of preview extension * black cleanup
1 parent 3465df2 commit 1d48723

File tree

9 files changed

+430
-0
lines changed

9 files changed

+430
-0
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import json
5+
import sys
6+
from datetime import datetime
7+
8+
from aiohttp import web
9+
from aiohttp.web import Request, Response, json_response
10+
from botbuilder.core import (
11+
BotFrameworkAdapterSettings,
12+
TurnContext,
13+
BotFrameworkAdapter,
14+
)
15+
from botbuilder.schema import Activity, ActivityTypes
16+
from bots import ActionBasedMessagingExtensionFetchTaskBot
17+
from config import DefaultConfig
18+
19+
CONFIG = DefaultConfig()
20+
21+
# Create adapter.
22+
# See https://aka.ms/about-bot-adapter to learn more about how bots work.
23+
SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
24+
ADAPTER = BotFrameworkAdapter(SETTINGS)
25+
26+
27+
# Catch-all for errors.
28+
async def on_error(context: TurnContext, error: Exception):
29+
# This check writes out errors to console log .vs. app insights.
30+
# NOTE: In production environment, you should consider logging this to Azure
31+
# application insights.
32+
print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
33+
34+
# Send a message to the user
35+
await context.send_activity("The bot encountered an error or bug.")
36+
await context.send_activity(
37+
"To continue to run this bot, please fix the bot source code."
38+
)
39+
# Send a trace activity if we're talking to the Bot Framework Emulator
40+
if context.activity.channel_id == "emulator":
41+
# Create a trace activity that contains the error object
42+
trace_activity = Activity(
43+
label="TurnError",
44+
name="on_turn_error Trace",
45+
timestamp=datetime.utcnow(),
46+
type=ActivityTypes.trace,
47+
value=f"{error}",
48+
value_type="https://www.botframework.com/schemas/error",
49+
)
50+
# Send a trace activity, which will be displayed in Bot Framework Emulator
51+
await context.send_activity(trace_activity)
52+
53+
54+
ADAPTER.on_turn_error = on_error
55+
56+
# Create the Bot
57+
BOT = ActionBasedMessagingExtensionFetchTaskBot()
58+
59+
60+
# Listen for incoming requests on /api/messages
61+
async def messages(req: Request) -> Response:
62+
# Main bot message handler.
63+
if "application/json" in req.headers["Content-Type"]:
64+
body = await req.json()
65+
else:
66+
return Response(status=415)
67+
68+
activity = Activity().deserialize(body)
69+
auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
70+
71+
try:
72+
invoke_response = await ADAPTER.process_activity(
73+
activity, auth_header, BOT.on_turn
74+
)
75+
if invoke_response:
76+
return json_response(
77+
data=invoke_response.body, status=invoke_response.status
78+
)
79+
return Response(status=201)
80+
except PermissionError:
81+
return Response(status=401)
82+
except Exception:
83+
return Response(status=500)
84+
85+
86+
APP = web.Application()
87+
APP.router.add_post("/api/messages", messages)
88+
89+
if __name__ == "__main__":
90+
try:
91+
web.run_app(APP, host="localhost", port=CONFIG.PORT)
92+
except Exception as error:
93+
raise error
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from .action_based_messaging_extension_fetch_task_bot import (
5+
ActionBasedMessagingExtensionFetchTaskBot,
6+
)
7+
8+
__all__ = ["ActionBasedMessagingExtensionFetchTaskBot"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# Copyright (c) Microsoft Corp. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from typing import List
5+
import random
6+
from botbuilder.core import (
7+
CardFactory,
8+
MessageFactory,
9+
TurnContext,
10+
)
11+
from botbuilder.schema import Attachment
12+
from botbuilder.schema.teams import (
13+
MessagingExtensionAction,
14+
MessagingExtensionActionResponse,
15+
TaskModuleContinueResponse,
16+
MessagingExtensionResult,
17+
TaskModuleTaskInfo,
18+
)
19+
from botbuilder.core.teams import TeamsActivityHandler
20+
from example_data import ExampleData
21+
22+
23+
class ActionBasedMessagingExtensionFetchTaskBot(TeamsActivityHandler):
24+
async def on_message_activity(self, turn_context: TurnContext):
25+
value = turn_context.activity.value
26+
if value is not None:
27+
# This was a message from the card.
28+
answer = value["Answer"]
29+
choices = value["Choices"]
30+
reply = MessageFactory.text(
31+
f"{turn_context.activity.from_property.name} answered '{answer}' and chose '{choices}'."
32+
)
33+
await turn_context.send_activity(reply)
34+
else:
35+
# This is a regular text message.
36+
reply = MessageFactory.text(
37+
"Hello from ActionBasedMessagingExtensionFetchTaskBot."
38+
)
39+
await turn_context.send_activity(reply)
40+
41+
async def on_teams_messaging_extension_fetch_task(
42+
self, turn_context: TurnContext, action: MessagingExtensionAction
43+
) -> MessagingExtensionActionResponse:
44+
card = self._create_adaptive_card_editor()
45+
task_info = TaskModuleTaskInfo(
46+
card=card, height=450, title="Task Module Fetch Example", width=500
47+
)
48+
continue_response = TaskModuleContinueResponse(type="continue", value=task_info)
49+
return MessagingExtensionActionResponse(task=continue_response)
50+
51+
async def on_teams_messaging_extension_submit_action( # pylint: disable=unused-argument
52+
self, turn_context: TurnContext, action: MessagingExtensionAction
53+
) -> MessagingExtensionActionResponse:
54+
question = action.data["Question"]
55+
multi_select = action.data["MultiSelect"]
56+
option1 = action.data["Option1"]
57+
option2 = action.data["Option2"]
58+
option3 = action.data["Option3"]
59+
preview_card = self._create_adaptive_card_preview(
60+
user_text=question,
61+
is_multi_select=multi_select,
62+
option1=option1,
63+
option2=option2,
64+
option3=option3,
65+
)
66+
67+
extension_result = MessagingExtensionResult(
68+
type="botMessagePreview",
69+
activity_preview=MessageFactory.attachment(preview_card),
70+
)
71+
return MessagingExtensionActionResponse(compose_extension=extension_result)
72+
73+
async def on_teams_messaging_extension_bot_message_preview_edit( # pylint: disable=unused-argument
74+
self, turn_context: TurnContext, action: MessagingExtensionAction
75+
) -> MessagingExtensionActionResponse:
76+
activity_preview = action.bot_activity_preview[0]
77+
content = activity_preview.attachments[0].content
78+
data = self._get_example_data(content)
79+
card = self._create_adaptive_card_editor(
80+
data.question,
81+
data.is_multi_select,
82+
data.option1,
83+
data.option2,
84+
data.option3,
85+
)
86+
task_info = TaskModuleTaskInfo(
87+
card=card, height=450, title="Task Module Fetch Example", width=500
88+
)
89+
continue_response = TaskModuleContinueResponse(type="continue", value=task_info)
90+
return MessagingExtensionActionResponse(task=continue_response)
91+
92+
async def on_teams_messaging_extension_bot_message_preview_send( # pylint: disable=unused-argument
93+
self, turn_context: TurnContext, action: MessagingExtensionAction
94+
) -> MessagingExtensionActionResponse:
95+
activity_preview = action.bot_activity_preview[0]
96+
content = activity_preview.attachments[0].content
97+
data = self._get_example_data(content)
98+
card = self._create_adaptive_card_preview(
99+
data.question,
100+
data.is_multi_select,
101+
data.option1,
102+
data.option2,
103+
data.option3,
104+
)
105+
message = MessageFactory.attachment(card)
106+
await turn_context.send_activity(message)
107+
108+
def _get_example_data(self, content: dict) -> ExampleData:
109+
body = content["body"]
110+
question = body[1]["text"]
111+
choice_set = body[3]
112+
multi_select = "isMultiSelect" in choice_set
113+
option1 = choice_set["choices"][0]["value"]
114+
option2 = choice_set["choices"][1]["value"]
115+
option3 = choice_set["choices"][2]["value"]
116+
return ExampleData(question, multi_select, option1, option2, option3)
117+
118+
def _create_adaptive_card_editor(
119+
self,
120+
user_text: str = None,
121+
is_multi_select: bool = False,
122+
option1: str = None,
123+
option2: str = None,
124+
option3: str = None,
125+
) -> Attachment:
126+
return CardFactory.adaptive_card(
127+
{
128+
"actions": [
129+
{
130+
"data": {"submitLocation": "messagingExtensionFetchTask"},
131+
"title": "Submit",
132+
"type": "Action.Submit",
133+
}
134+
],
135+
"body": [
136+
{
137+
"text": "This is an Adaptive Card within a Task Module",
138+
"type": "TextBlock",
139+
"weight": "bolder",
140+
},
141+
{"type": "TextBlock", "text": "Enter text for Question:"},
142+
{
143+
"id": "Question",
144+
"placeholder": "Question text here",
145+
"type": "Input.Text",
146+
"value": user_text,
147+
},
148+
{"type": "TextBlock", "text": "Options for Question:"},
149+
{"type": "TextBlock", "text": "Is Multi-Select:"},
150+
{
151+
"choices": [
152+
{"title": "True", "value": "true"},
153+
{"title": "False", "value": "false"},
154+
],
155+
"id": "MultiSelect",
156+
"isMultiSelect": "false",
157+
"style": "expanded",
158+
"type": "Input.ChoiceSet",
159+
"value": "true" if is_multi_select else "false",
160+
},
161+
{
162+
"id": "Option1",
163+
"placeholder": "Option 1 here",
164+
"type": "Input.Text",
165+
"value": option1,
166+
},
167+
{
168+
"id": "Option2",
169+
"placeholder": "Option 2 here",
170+
"type": "Input.Text",
171+
"value": option2,
172+
},
173+
{
174+
"id": "Option3",
175+
"placeholder": "Option 3 here",
176+
"type": "Input.Text",
177+
"value": option3,
178+
},
179+
],
180+
"type": "AdaptiveCard",
181+
"version": "1.0",
182+
}
183+
)
184+
185+
def _create_adaptive_card_preview(
186+
self,
187+
user_text: str = None,
188+
is_multi_select: bool = False,
189+
option1: str = None,
190+
option2: str = None,
191+
option3: str = None,
192+
) -> Attachment:
193+
return CardFactory.adaptive_card(
194+
{
195+
"actions": [
196+
{
197+
"type": "Action.Submit",
198+
"title": "Submit",
199+
"data": {"submitLocation": "messagingExtensionSubmit"},
200+
}
201+
],
202+
"body": [
203+
{
204+
"text": "Adaptive Card from Task Module",
205+
"type": "TextBlock",
206+
"weight": "bolder",
207+
},
208+
{"text": user_text, "type": "TextBlock", "id": "Question"},
209+
{
210+
"id": "Answer",
211+
"placeholder": "Answer here...",
212+
"type": "Input.Text",
213+
},
214+
{
215+
"choices": [
216+
{"title": option1, "value": option1},
217+
{"title": option2, "value": option2},
218+
{"title": option3, "value": option3},
219+
],
220+
"id": "Choices",
221+
"isMultiSelect": is_multi_select,
222+
"style": "expanded",
223+
"type": "Input.ChoiceSet",
224+
},
225+
],
226+
"type": "AdaptiveCard",
227+
"version": "1.0",
228+
}
229+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License.
4+
5+
import os
6+
7+
8+
class DefaultConfig:
9+
""" Bot Configuration """
10+
11+
PORT = 3978
12+
APP_ID = os.environ.get("MicrosoftAppId", "")
13+
APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
5+
class ExampleData(object):
6+
def __init__(
7+
self,
8+
question: str = None,
9+
is_multi_select: bool = False,
10+
option1: str = None,
11+
option2: str = None,
12+
option3: str = None,
13+
):
14+
self.question = question
15+
self.is_multi_select = is_multi_select
16+
self.option1 = option1
17+
self.option2 = option2
18+
self.option3 = option3
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
botbuilder-core>=4.4.0b1
2+
flask>=1.0.3
Loading
Loading

0 commit comments

Comments
 (0)