Skip to content

Axsuarez/turn context send activities #433

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 5 commits into from
Nov 14, 2019
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ disable=print-statement,
too-many-function-args,
too-many-return-statements,
import-error,
no-name-in-module
no-name-in-module,
too-many-branches

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
Expand Down
6 changes: 5 additions & 1 deletion libraries/botbuilder-ai/tests/luis/null_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ class NullAdapter(BotAdapter):
This is a BotAdapter that does nothing on the Send operation, equivalent to piping to /dev/null.
"""

async def send_activities(self, context: TurnContext, activities: List[Activity]):
# pylint: disable=unused-argument

async def send_activities(
self, context: TurnContext, activities: List[Activity]
) -> List[ResourceResponse]:
return [ResourceResponse()]

async def update_activity(self, context: TurnContext, activity: Activity):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ async def process_activity(
activity.timestamp = activity.timestamp or datetime.utcnow()
await self.run_pipeline(TurnContext(self, activity), logic)

async def send_activities(self, context, activities: List[Activity]):
async def send_activities(
self, context, activities: List[Activity]
) -> List[ResourceResponse]:
"""
INTERNAL: called by the logic under test to send a set of activities. These will be buffered
to the current `TestFlow` instance for comparison against the expected results.
Expand Down
6 changes: 4 additions & 2 deletions libraries/botbuilder-core/botbuilder/core/bot_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from abc import ABC, abstractmethod
from typing import List, Callable, Awaitable
from botbuilder.schema import Activity, ConversationReference
from botbuilder.schema import Activity, ConversationReference, ResourceResponse

from . import conversation_reference_extension
from .bot_assert import BotAssert
Expand All @@ -19,7 +19,9 @@ def __init__(
self.on_turn_error = on_turn_error

@abstractmethod
async def send_activities(self, context: TurnContext, activities: List[Activity]):
async def send_activities(
self, context: TurnContext, activities: List[Activity]
) -> List[ResourceResponse]:
"""
Sends a set of activities to the user. An array of responses from the server will be returned.
:param context:
Expand Down
46 changes: 36 additions & 10 deletions libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ConversationAccount,
ConversationParameters,
ConversationReference,
ResourceResponse,
TokenResponse,
)
from botframework.connector import Channels, EmulatorApiClient
Expand Down Expand Up @@ -330,9 +331,13 @@ async def delete_activity(
except Exception as error:
raise error

async def send_activities(self, context: TurnContext, activities: List[Activity]):
async def send_activities(
self, context: TurnContext, activities: List[Activity]
) -> List[ResourceResponse]:
try:
responses: List[ResourceResponse] = []
for activity in activities:
response: ResourceResponse = None
if activity.type == "delay":
try:
delay_in_ms = float(activity.value) / 1000
Expand All @@ -345,17 +350,38 @@ async def send_activities(self, context: TurnContext, activities: List[Activity]
else:
await asyncio.sleep(delay_in_ms)
elif activity.type == "invokeResponse":
context.turn_state.add(self._INVOKE_RESPONSE_KEY)
elif activity.reply_to_id:
client = self.create_connector_client(activity.service_url)
await client.conversations.reply_to_activity(
activity.conversation.id, activity.reply_to_id, activity
)
context.turn_state[self._INVOKE_RESPONSE_KEY] = activity
else:
if not getattr(activity, "service_url", None):
raise TypeError(
"BotFrameworkAdapter.send_activity(): service_url can not be None."
)
if (
not hasattr(activity, "conversation")
or not activity.conversation
or not getattr(activity.conversation, "id", None)
):
raise TypeError(
"BotFrameworkAdapter.send_activity(): conversation.id can not be None."
)

client = self.create_connector_client(activity.service_url)
await client.conversations.send_to_conversation(
activity.conversation.id, activity
)
if activity.type == "trace" and activity.channel_id != "emulator":
pass
elif activity.reply_to_id:
response = await client.conversations.reply_to_activity(
activity.conversation.id, activity.reply_to_id, activity
)
else:
response = await client.conversations.send_to_conversation(
activity.conversation.id, activity
)

if not response:
response = ResourceResponse(activity.id or "")

responses.append(response)
return responses
except Exception as error:
raise error

Expand Down
25 changes: 23 additions & 2 deletions libraries/botbuilder-core/botbuilder/core/transcript_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import datetime
import copy
import random
import string
from queue import Queue
from abc import ABC, abstractmethod
from typing import Awaitable, Callable, List
Expand Down Expand Up @@ -57,8 +59,27 @@ async def send_activities_handler(
):
# Run full pipeline
responses = await next_send()
for activity in activities:
self.log_activity(transcript, copy.copy(activity))
for index, activity in enumerate(activities):
cloned_activity = copy.copy(activity)
if index < len(responses):
cloned_activity.id = responses[index].id

# For certain channels, a ResourceResponse with an id is not always sent to the bot.
# This fix uses the timestamp on the activity to populate its id for logging the transcript
# If there is no outgoing timestamp, the current time for the bot is used for the activity.id
if not cloned_activity.id:
alphanumeric = string.ascii_lowercase + string.digits
prefix = "g_" + "".join(
random.choice(alphanumeric) for i in range(5)
)
epoch = datetime.datetime.utcfromtimestamp(0)
if cloned_activity.timestamp:
reference = cloned_activity.timestamp
else:
reference = datetime.datetime.today()
delta = (reference - epoch).total_seconds() * 1000
cloned_activity.id = f"{prefix}{delta}"
self.log_activity(transcript, cloned_activity)
return responses

context.on_send_activities(send_activities_handler)
Expand Down
57 changes: 40 additions & 17 deletions libraries/botbuilder-core/botbuilder/core/turn_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
# Licensed under the MIT License.

import re
from copy import copy
from copy import copy, deepcopy
from datetime import datetime
from typing import List, Callable, Union, Dict
from botbuilder.schema import (
Activity,
ActivityTypes,
ConversationReference,
InputHints,
Mention,
ResourceResponse,
)
Expand Down Expand Up @@ -144,35 +145,57 @@ def set(self, key: str, value: object) -> None:
self._services[key] = value

async def send_activity(
self, *activity_or_text: Union[Activity, str]
self,
activity_or_text: Union[Activity, str],
speak: str = None,
input_hint: str = None,
) -> ResourceResponse:
"""
Sends a single activity or message to the user.
:param activity_or_text:
:return:
"""
reference = TurnContext.get_conversation_reference(self.activity)
if isinstance(activity_or_text, str):
activity_or_text = Activity(
text=activity_or_text,
input_hint=input_hint or InputHints.accepting_input,
speak=speak,
)

result = await self.send_activities([activity_or_text])
return result[0] if result else None

async def send_activities(
self, activities: List[Activity]
) -> List[ResourceResponse]:
sent_non_trace_activity = False
ref = TurnContext.get_conversation_reference(self.activity)

def activity_validator(activity: Activity) -> Activity:
if not getattr(activity, "type", None):
activity.type = ActivityTypes.message
if activity.type != ActivityTypes.trace:
nonlocal sent_non_trace_activity
sent_non_trace_activity = True
if not activity.input_hint:
activity.input_hint = "acceptingInput"
activity.id = None
return activity

output = [
TurnContext.apply_conversation_reference(
Activity(text=a, type="message") if isinstance(a, str) else a, reference
activity_validator(
TurnContext.apply_conversation_reference(deepcopy(act), ref)
)
for a in activity_or_text
for act in activities
]
for activity in output:
if not activity.input_hint:
activity.input_hint = "acceptingInput"

async def callback(context: "TurnContext", output):
responses = await context.adapter.send_activities(context, output)
context._responded = True # pylint: disable=protected-access
async def logic():
responses = await self.adapter.send_activities(self, output)
if sent_non_trace_activity:
self.responded = True
return responses

result = await self._emit(
self._on_send_activities, output, callback(self, output)
)

return result[0] if result else ResourceResponse()
return await self._emit(self._on_send_activities, output, logic())

async def update_activity(self, activity: Activity):
"""
Expand Down
6 changes: 5 additions & 1 deletion libraries/botbuilder-core/tests/simple_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@


class SimpleAdapter(BotAdapter):
# pylint: disable=unused-argument

def __init__(self, call_on_send=None, call_on_update=None, call_on_delete=None):
super(SimpleAdapter, self).__init__()
self.test_aux = unittest.TestCase("__init__")
Expand All @@ -24,7 +26,9 @@ async def delete_activity(
if self._call_on_delete is not None:
self._call_on_delete(reference)

async def send_activities(self, context: TurnContext, activities: List[Activity]):
async def send_activities(
self, context: TurnContext, activities: List[Activity]
) -> List[ResourceResponse]:
self.test_aux.assertIsNotNone(
activities, "SimpleAdapter.delete_activity: missing reference"
)
Expand Down
5 changes: 4 additions & 1 deletion libraries/botbuilder-core/tests/test_activity_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ChannelAccount,
ConversationReference,
MessageReaction,
ResourceResponse,
)


Expand Down Expand Up @@ -66,7 +67,9 @@ async def delete_activity(
):
raise NotImplementedError()

async def send_activities(self, context: TurnContext, activities: List[Activity]):
async def send_activities(
self, context: TurnContext, activities: List[Activity]
) -> List[ResourceResponse]:
raise NotImplementedError()

async def update_activity(self, context: TurnContext, activity: Activity):
Expand Down
2 changes: 1 addition & 1 deletion libraries/botbuilder-core/tests/test_bot_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def validate_responses( # pylint: disable=unused-argument

resource_response = await context.send_activity(activity)
self.assertTrue(
resource_response.id == activity_id, "Incorrect response Id returned"
resource_response.id != activity_id, "Incorrect response Id returned"
)

async def test_continue_conversation_direct_msg(self):
Expand Down
4 changes: 2 additions & 2 deletions libraries/botbuilder-core/tests/test_turn_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@


class SimpleAdapter(BotAdapter):
async def send_activities(self, context, activities):
async def send_activities(self, context, activities) -> List[ResourceResponse]:
responses = []
assert context is not None
assert activities is not None
Expand Down Expand Up @@ -205,7 +205,7 @@ async def send_handler(context, activities, next_handler_coroutine):
called = True
assert activities is not None
assert context is not None
assert activities[0].id == "1234"
assert not activities[0].id
await next_handler_coroutine()

context.on_send_activities(send_handler)
Expand Down
2 changes: 1 addition & 1 deletion samples/01.console-echo/adapter/console_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ async def process_activity(self, logic: Callable):
context = TurnContext(self, activity)
await self.run_pipeline(context, logic)

async def send_activities(self, context: TurnContext, activities: List[Activity]):
async def send_activities(self, context: TurnContext, activities: List[Activity]) -> List[ResourceResponse]:
"""
Logs a series of activities to the console.
:param context:
Expand Down