Skip to content
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

Axsuarez/skills layer #492

Merged
merged 10 commits into from
Dec 11, 2019
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ extension-pkg-whitelist=

# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS,build,botbuilder-schema,samples,django_tests,Generator,operations,operations_async
ignore=CVS,build,botbuilder-schema,samples,django_tests,Generator,operations,operations_async,schema

# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
Expand Down
2 changes: 2 additions & 0 deletions libraries/botbuilder-core/botbuilder/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .about import __version__
from .activity_handler import ActivityHandler
from .auto_save_state_middleware import AutoSaveStateMiddleware
from .bot import Bot
from .bot_assert import BotAssert
from .bot_adapter import BotAdapter
from .bot_framework_adapter import BotFrameworkAdapter, BotFrameworkAdapterSettings
Expand Down Expand Up @@ -42,6 +43,7 @@
"ActivityHandler",
"AnonymousReceiveMiddleware",
"AutoSaveStateMiddleware",
"Bot",
"BotAdapter",
"BotAssert",
"BotFrameworkAdapter",
Expand Down
11 changes: 9 additions & 2 deletions libraries/botbuilder-core/botbuilder/core/activity_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ async def on_turn(self, turn_context: TurnContext):
await self.on_message_reaction_activity(turn_context)
elif turn_context.activity.type == ActivityTypes.event:
await self.on_event_activity(turn_context)
elif turn_context.activity.type == ActivityTypes.end_of_conversation:
await self.on_end_of_conversation(turn_context)
else:
await self.on_unrecognized_activity_type(turn_context)

Expand All @@ -58,12 +60,12 @@ async def on_conversation_update_activity(self, turn_context: TurnContext):
return

async def on_members_added_activity(
self, members_added: ChannelAccount, turn_context: TurnContext
self, members_added: List[ChannelAccount], turn_context: TurnContext
): # pylint: disable=unused-argument
return

async def on_members_removed_activity(
self, members_removed: ChannelAccount, turn_context: TurnContext
self, members_removed: List[ChannelAccount], turn_context: TurnContext
): # pylint: disable=unused-argument
return

Expand Down Expand Up @@ -104,6 +106,11 @@ async def on_event( # pylint: disable=unused-argument
):
return

async def on_end_of_conversation( # pylint: disable=unused-argument
self, turn_context: TurnContext
):
return

async def on_unrecognized_activity_type( # pylint: disable=unused-argument
self, turn_context: TurnContext
):
Expand Down
14 changes: 11 additions & 3 deletions libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ResourceResponse,
TokenResponse,
)
from botframework.connector.auth import ClaimsIdentity
from ..bot_adapter import BotAdapter
from ..turn_context import TurnContext
from ..user_token_provider import UserTokenProvider
Expand Down Expand Up @@ -157,16 +158,23 @@ async def update_activity(self, context, activity: Activity):
self.updated_activities.append(activity)

async def continue_conversation(
self, bot_id: str, reference: ConversationReference, callback: Callable
self,
reference: ConversationReference,
callback: Callable,
bot_id: str = None,
claims_identity: ClaimsIdentity = None, # pylint: disable=unused-argument
):
"""
The `TestAdapter` just calls parent implementation.
:param bot_id
:param reference:
:param callback:
:param bot_id:
:param claims_identity:
:return:
"""
await super().continue_conversation(bot_id, reference, callback)
await super().continue_conversation(
reference, callback, bot_id, claims_identity
)

async def receive_activity(self, activity):
"""
Expand Down
21 changes: 21 additions & 0 deletions libraries/botbuilder-core/botbuilder/core/bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from abc import ABC, abstractmethod

from .turn_context import TurnContext


class Bot(ABC):
"""
Represents a bot that can operate on incoming activities.
"""

@abstractmethod
async def on_turn(self, context: TurnContext):
"""
When implemented in a bot, handles an incoming activity.
:param context: The context object for this turn.
:return:
"""
raise NotImplementedError()
10 changes: 8 additions & 2 deletions libraries/botbuilder-core/botbuilder/core/bot_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from abc import ABC, abstractmethod
from typing import List, Callable, Awaitable
from botbuilder.schema import Activity, ConversationReference, ResourceResponse
from botframework.connector.auth import ClaimsIdentity

from . import conversation_reference_extension
from .bot_assert import BotAssert
Expand Down Expand Up @@ -62,8 +63,12 @@ def use(self, middleware):
return self

async def continue_conversation(
self, bot_id: str, reference: ConversationReference, callback: Callable
): # pylint: disable=unused-argument
self,
reference: ConversationReference,
callback: Callable,
bot_id: str = None, # pylint: disable=unused-argument
claims_identity: ClaimsIdentity = None, # pylint: disable=unused-argument
):
"""
Sends a proactive message to a conversation. Call this method to proactively send a message to a conversation.
Most _channels require a user to initiate a conversation with a bot before the bot can send activities
Expand All @@ -73,6 +78,7 @@ async def continue_conversation(
which is multi-tenant aware. </param>
:param reference: A reference to the conversation to continue.</param>
:param callback: The method to call for the resulting bot turn.</param>
:param claims_identity:
"""
context = TurnContext(
self, conversation_reference_extension.get_continuation_activity(reference)
Expand Down
43 changes: 33 additions & 10 deletions libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from .bot_adapter import BotAdapter
from .turn_context import TurnContext
from .user_token_provider import UserTokenProvider
from .conversation_reference_extension import get_continuation_activity

USER_AGENT = f"Microsoft-BotFramework/3.1 (BotBuilder Python/{__version__})"
OAUTH_ENDPOINT = "https://api.botframework.com"
Expand Down Expand Up @@ -128,8 +129,14 @@ def __init__(self, settings: BotFrameworkAdapterSettings):
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
)

self._connector_client_cache: Dict[str, ConnectorClient] = {}

async def continue_conversation(
self, bot_id: str, reference: ConversationReference, callback: Callable
self,
reference: ConversationReference,
callback: Callable,
bot_id: str = None,
claims_identity: ClaimsIdentity = None, # pylint: disable=unused-argument
):
"""
Continues a conversation with a user. This is often referred to as the bots "Proactive Messaging"
Expand All @@ -139,18 +146,26 @@ async def continue_conversation(
:param bot_id:
:param reference:
:param callback:
:param claims_identity:
:return:
"""

# TODO: proactive messages
if not claims_identity:
if not bot_id:
raise TypeError("Expected bot_id: str but got None instead")

claims_identity = ClaimsIdentity(
claims={
AuthenticationConstants.AUDIENCE_CLAIM: bot_id,
AuthenticationConstants.APP_ID_CLAIM: bot_id,
},
is_authenticated=True,
)

if not bot_id:
raise TypeError("Expected bot_id: str but got None instead")

request = TurnContext.apply_conversation_reference(
Activity(), reference, is_incoming=True
)
context = self.create_context(request)
context = TurnContext(self, get_continuation_activity(reference))
context.turn_state[BOT_IDENTITY_KEY] = claims_identity
context.turn_state["BotCallbackHandler"] = callback
return await self.run_pipeline(context, callback)

async def create_conversation(
Expand Down Expand Up @@ -660,8 +675,16 @@ async def create_connector_client(
else:
credentials = self._credentials

client = ConnectorClient(credentials, base_url=service_url)
client.config.add_user_agent(USER_AGENT)
client_key = (
f"{service_url}{credentials.microsoft_app_id if credentials else ''}"
)
client = self._connector_client_cache.get(client_key)

if not client:
client = ConnectorClient(credentials, base_url=service_url)
client.config.add_user_agent(USER_AGENT)
self._connector_client_cache[client_key] = client

return client

def create_token_api_client(self, service_url: str) -> TokenApiClient:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@

from .aiohttp_channel_service import aiohttp_channel_service_routes
from .bot_framework_http_client import BotFrameworkHttpClient
from .channel_service_handler import ChannelServiceHandler
from .channel_service_handler import BotActionNotImplementedError, ChannelServiceHandler
from .aiohttp_channel_service_exception_middleware import aiohttp_error_middleware

__all__ = [
"aiohttp_channel_service_routes",
"BotFrameworkHttpClient",
"BotActionNotImplementedError",
"ChannelServiceHandler",
"aiohttp_error_middleware",
]
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from aiohttp.web import RouteTableDef, Request, Response
from msrest.serialization import Model

from botbuilder.schema import (
Activity,
AttachmentData,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from aiohttp.web import (
middleware,
HTTPNotImplemented,
HTTPUnauthorized,
HTTPNotFound,
HTTPInternalServerError,
)

from .channel_service_handler import BotActionNotImplementedError


@middleware
async def aiohttp_error_middleware(request, handler):
try:
response = await handler(request)
return response
except BotActionNotImplementedError:
raise HTTPNotImplemented()
except PermissionError:
raise HTTPUnauthorized()
except KeyError:
raise HTTPNotFound()
except Exception:
raise HTTPInternalServerError()
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async def post_activity(
app_credentials = await self._get_app_credentials(from_bot_id, to_bot_id)

if not app_credentials:
raise RuntimeError("Unable to get appCredentials to connect to the skill")
raise KeyError("Unable to get appCredentials to connect to the skill")

# Get token for the skill call
token = (
Expand All @@ -66,10 +66,12 @@ async def post_activity(
# TODO: DO we need to set the activity ID? (events that are created manually don't have it).
original_conversation_id = activity.conversation.id
original_service_url = activity.service_url
original_caller_id = activity.caller_id

try:
activity.conversation.id = conversation_id
activity.service_url = service_url
activity.caller_id = from_bot_id

headers_dict = {
"Content-type": "application/json; charset=utf-8",
Expand All @@ -94,6 +96,7 @@ async def post_activity(
# Restore activity properties.
activity.conversation.id = original_conversation_id
activity.service_url = original_service_url
activity.caller_id = original_caller_id

async def _get_app_credentials(
self, app_id: str, oauth_scope: str
Expand Down