Skip to content

Support originating audience for skills conversations #821

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 4, 2020
Merged
Show file tree
Hide file tree
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
6 changes: 4 additions & 2 deletions libraries/botbuilder-core/botbuilder/core/skills/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

from .bot_framework_skill import BotFrameworkSkill
from .conversation_id_factory import ConversationIdFactoryBase
from .skill_conversation_id_factory import SkillConversationIdFactory
from .skill_handler import SkillHandler
from .skill_conversation_id_factory_options import SkillConversationIdFactoryOptions
from .skill_conversation_reference import SkillConversationReference

__all__ = [
"BotFrameworkSkill",
"ConversationIdFactoryBase",
"SkillConversationIdFactory",
"SkillConversationIdFactoryOptions",
"SkillConversationReference",
"SkillHandler",
]
Original file line number Diff line number Diff line change
@@ -1,22 +1,66 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from abc import ABC, abstractmethod
from typing import Union
from botbuilder.schema import ConversationReference
from .skill_conversation_id_factory_options import SkillConversationIdFactoryOptions
from .skill_conversation_reference import SkillConversationReference


class ConversationIdFactoryBase(ABC):
"""
Handles creating conversation ids for skill and should be subclassed.

.. remarks::
Derive from this class to handle creation of conversation ids, retrieval of
SkillConversationReferences and deletion.
"""

@abstractmethod
async def create_skill_conversation_id(
self, conversation_reference: ConversationReference
self,
options_or_conversation_reference: Union[
SkillConversationIdFactoryOptions, ConversationReference
],
) -> str:
"""
Using the options passed in, creates a conversation id and
SkillConversationReference, storing them for future use.

:param options_or_conversation_reference: The options contain properties useful
for generating a SkillConversationReference and conversation id.
:type options_or_conversation_reference: :class:
`Union[SkillConversationIdFactoryOptions, ConversationReference]`

:returns: A skill conversation id.

.. note::
SkillConversationIdFactoryOptions is the preferred parameter type, while ConversationReference
type is provided for backwards compatability.
"""
raise NotImplementedError()

@abstractmethod
async def get_conversation_reference(
self, skill_conversation_id: str
) -> ConversationReference:
) -> Union[SkillConversationReference, ConversationReference]:
"""
Retrieves a SkillConversationReference using a conversation id passed in.

:param skill_conversation_id: The conversation id for which to retrieve
the SkillConversationReference.
:type skill_conversation_id: str

.. note::
SkillConversationReference is the preferred return type, while ConversationReference
type is provided for backwards compatability.
"""
raise NotImplementedError()

@abstractmethod
async def delete_conversation_reference(self, skill_conversation_id: str):
"""
Removes any reference to objects keyed on the conversation id passed in.
"""
raise NotImplementedError()

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from botbuilder.schema import Activity
from .bot_framework_skill import BotFrameworkSkill


class SkillConversationIdFactoryOptions:
def __init__(
self,
from_bot_oauth_scope: str,
from_bot_id: str,
activity: Activity,
bot_framework_skill: BotFrameworkSkill,
):
if from_bot_oauth_scope is None:
raise TypeError(
"SkillConversationIdFactoryOptions(): from_bot_oauth_scope cannot be None."
)

if from_bot_id is None:
raise TypeError(
"SkillConversationIdFactoryOptions(): from_bot_id cannot be None."
)

if activity is None:
raise TypeError(
"SkillConversationIdFactoryOptions(): activity cannot be None."
)

if bot_framework_skill is None:
raise TypeError(
"SkillConversationIdFactoryOptions(): bot_framework_skill cannot be None."
)

self.from_bot_oauth_scope = from_bot_oauth_scope
self.from_bot_id = from_bot_id
self.activity = activity
self.bot_framework_skill = bot_framework_skill
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 botbuilder.schema import ConversationReference


class SkillConversationReference:
"""
ConversationReference implementation for Skills ConversationIdFactory.
"""

def __init__(self, conversation_reference: ConversationReference, oauth_scope: str):
if conversation_reference is None:
raise TypeError(
"SkillConversationReference(): conversation_reference cannot be None."
)

if oauth_scope is None:
raise TypeError("SkillConversationReference(): oauth_scope cannot be None.")

self.conversation_reference = conversation_reference
self.oauth_scope = oauth_scope
34 changes: 27 additions & 7 deletions libraries/botbuilder-core/botbuilder/core/skills/skill_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
)
from botframework.connector.auth import (
AuthenticationConfiguration,
AuthenticationConstants,
ChannelProvider,
ClaimsIdentity,
CredentialProvider,
GovernmentConstants,
)

from .skill_conversation_id_factory import SkillConversationIdFactory
from .skill_conversation_reference import SkillConversationReference
from .conversation_id_factory import ConversationIdFactoryBase


class SkillHandler(ChannelServiceHandler):
Expand All @@ -30,7 +32,7 @@ def __init__(
self,
adapter: BotAdapter,
bot: Bot,
conversation_id_factory: SkillConversationIdFactory,
conversation_id_factory: ConversationIdFactoryBase,
credential_provider: CredentialProvider,
auth_configuration: AuthenticationConfiguration,
channel_provider: ChannelProvider = None,
Expand Down Expand Up @@ -118,14 +120,29 @@ async def _process_activity(
reply_to_activity_id: str,
activity: Activity,
) -> ResourceResponse:
conversation_reference = await self._conversation_id_factory.get_conversation_reference(
conversation_reference_result = await self._conversation_id_factory.get_conversation_reference(
conversation_id
)

oauth_scope = None
conversation_reference = None
if isinstance(conversation_reference_result, SkillConversationReference):
oauth_scope = conversation_reference_result.oauth_scope
conversation_reference = (
conversation_reference_result.conversation_reference
)
else:
conversation_reference = conversation_reference_result
oauth_scope = (
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
if self._channel_provider and self._channel_provider.is_government()
else AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
)

if not conversation_reference:
raise KeyError("ConversationReference not found")

skill_conversation_reference = ConversationReference(
activity_conversation_reference = ConversationReference(
activity_id=activity.id,
user=activity.from_property,
bot=activity.recipient,
Expand All @@ -137,7 +154,7 @@ async def _process_activity(
async def callback(context: TurnContext):
context.turn_state[
SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY
] = skill_conversation_reference
] = activity_conversation_reference
TurnContext.apply_conversation_reference(activity, conversation_reference)
context.activity.id = reply_to_activity_id

Expand All @@ -154,7 +171,10 @@ async def callback(context: TurnContext):
await context.send_activity(activity)

await self._adapter.continue_conversation(
conversation_reference, callback, claims_identity=claims_identity
conversation_reference,
callback,
claims_identity=claims_identity,
audience=oauth_scope,
)
return ResourceResponse(id=str(uuid4()))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from botbuilder.core import (
BotFrameworkHttpClient,
InvokeResponse,
)
from botbuilder.core.skills import (
ConversationIdFactoryBase,
SkillConversationIdFactoryOptions,
BotFrameworkSkill,
)
from botbuilder.schema import Activity
from botframework.connector.auth import (
AuthenticationConstants,
ChannelProvider,
GovernmentConstants,
SimpleCredentialProvider,
)


class SkillHttpClient(BotFrameworkHttpClient):
def __init__(
self,
credential_provider: SimpleCredentialProvider,
skill_conversation_id_factory: ConversationIdFactoryBase,
channel_provider: ChannelProvider = None,
):
if not skill_conversation_id_factory:
raise TypeError(
"SkillHttpClient(): skill_conversation_id_factory can't be None"
)

super().__init__(credential_provider)

self._skill_conversation_id_factory = skill_conversation_id_factory
self._channel_provider = channel_provider

async def post_activity_to_skill(
self,
from_bot_id: str,
to_skill: BotFrameworkSkill,
service_url: str,
activity: Activity,
originating_audience: str = None,
) -> InvokeResponse:

if originating_audience is None:
originating_audience = (
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
if self._channel_provider is not None
and self._channel_provider.IsGovernment()
else AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
)

options = SkillConversationIdFactoryOptions(
from_bot_oauth_scope=originating_audience,
from_bot_id=from_bot_id,
activity=activity,
bot_framework_skill=to_skill,
)

skill_conversation_id = await self._skill_conversation_id_factory.create_skill_conversation_id(
options
)

return await super().post_activity(
from_bot_id,
to_skill.app_id,
to_skill.skill_endpoint,
service_url,
skill_conversation_id,
activity,
)
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class ConversationIdFactoryForTest(ConversationIdFactoryBase):
def __init__(self):
self._conversation_refs: Dict[str, str] = {}

async def create_skill_conversation_id(
async def create_skill_conversation_id( # pylint: disable=W0221
self, conversation_reference: ConversationReference
) -> str:
cr_json = json.dumps(conversation_reference.serialize())
Expand Down