Skip to content
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
2 changes: 2 additions & 0 deletions libraries/botbuilder-core/botbuilder/core/skills/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .bot_framework_skill import BotFrameworkSkill
from .bot_framework_client import BotFrameworkClient
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
Expand All @@ -16,6 +17,7 @@
"BotFrameworkSkill",
"BotFrameworkClient",
"ConversationIdFactoryBase",
"SkillConversationIdFactory",
"SkillConversationIdFactoryOptions",
"SkillConversationReference",
"SkillHandler",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from abc import ABC, abstractmethod
from abc import ABC
from typing import Union
from botbuilder.schema import ConversationReference
from .skill_conversation_id_factory_options import SkillConversationIdFactoryOptions
Expand All @@ -17,7 +17,6 @@ class ConversationIdFactoryBase(ABC):
SkillConversationReferences and deletion.
"""

@abstractmethod
async def create_skill_conversation_id(
self,
options_or_conversation_reference: Union[
Expand All @@ -41,23 +40,32 @@ async def create_skill_conversation_id(
"""
raise NotImplementedError()

@abstractmethod
async def get_conversation_reference(
self, skill_conversation_id: str
) -> Union[SkillConversationReference, ConversationReference]:
) -> ConversationReference:
"""
[DEPRECATED] Method is deprecated, please use get_skill_conversation_reference() instead.

Retrieves a :class:`ConversationReference` using a conversation id passed in.

:param skill_conversation_id: The conversation id for which to retrieve the :class:`ConversationReference`.
:type skill_conversation_id: str
:returns: `ConversationReference` for the specified ID.
"""
raise NotImplementedError()

async def get_skill_conversation_reference(
self, skill_conversation_id: str
) -> SkillConversationReference:
"""
Retrieves a :class:`SkillConversationReference` using a conversation id passed in.

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

.. note::
SkillConversationReference is the preferred return type, while the :class:`SkillConversationReference`
type is provided for backwards compatability.
:returns: `SkillConversationReference` for the specified ID.
"""
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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from uuid import uuid4 as uuid
from botbuilder.core import TurnContext, Storage
from .conversation_id_factory import ConversationIdFactoryBase
from .skill_conversation_id_factory_options import SkillConversationIdFactoryOptions
from .skill_conversation_reference import SkillConversationReference
from .skill_conversation_reference import ConversationReference


class SkillConversationIdFactory(ConversationIdFactoryBase):
def __init__(self, storage: Storage):
if not storage:
raise TypeError("storage can't be None")

self._storage = storage

async def create_skill_conversation_id( # pylint: disable=arguments-differ
self, options: SkillConversationIdFactoryOptions
) -> str:
"""
Creates a new `SkillConversationReference`.

:param options: Creation options to use when creating the `SkillConversationReference`.
:type options: :class:`botbuilder.core.skills.SkillConversationIdFactoryOptions`
:return: ID of the created `SkillConversationReference`.
"""

if not options:
raise TypeError("options can't be None")

conversation_reference = TurnContext.get_conversation_reference(
options.activity
)

skill_conversation_id = str(uuid())

# Create the SkillConversationReference instance.
skill_conversation_reference = SkillConversationReference(
conversation_reference=conversation_reference,
oauth_scope=options.from_bot_oauth_scope,
)

# Store the SkillConversationReference using the skill_conversation_id as a key.
skill_conversation_info = {skill_conversation_id: skill_conversation_reference}

await self._storage.write(skill_conversation_info)

# Return the generated skill_conversation_id (that will be also used as the conversation ID to call the skill).
return skill_conversation_id

async def get_conversation_reference(
self, skill_conversation_id: str
) -> ConversationReference:
return await super().get_conversation_reference(skill_conversation_id)

async def get_skill_conversation_reference(
self, skill_conversation_id: str
) -> SkillConversationReference:
"""
Retrieve a `SkillConversationReference` with the specified ID.

:param skill_conversation_id: The ID of the `SkillConversationReference` to retrieve.
:type skill_conversation_id: str
:return: `SkillConversationReference` for the specified ID; None if not found.
"""

if not skill_conversation_id:
raise TypeError("skill_conversation_id can't be None")

# Get the SkillConversationReference from storage for the given skill_conversation_id.
skill_conversation_reference = await self._storage.read([skill_conversation_id])

return skill_conversation_reference.get(skill_conversation_id)

async def delete_conversation_reference(self, skill_conversation_id: str):
"""
Deletes the `SkillConversationReference` with the specified ID.

:param skill_conversation_id: The ID of the `SkillConversationReference` to be deleted.
:type skill_conversation_id: str
"""

# Delete the SkillConversationReference from storage.
await self._storage.delete([skill_conversation_id])
35 changes: 21 additions & 14 deletions libraries/botbuilder-core/botbuilder/core/skills/skill_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Licensed under the MIT License.

from uuid import uuid4
from logging import Logger, getLogger

from botbuilder.core import Bot, BotAdapter, ChannelServiceHandler, TurnContext
from botbuilder.schema import (
Expand Down Expand Up @@ -37,7 +38,7 @@ def __init__(
credential_provider: CredentialProvider,
auth_configuration: AuthenticationConfiguration,
channel_provider: ChannelProvider = None,
logger: object = None,
logger: Logger = None,
):
super().__init__(credential_provider, auth_configuration, channel_provider)

Expand All @@ -51,7 +52,7 @@ def __init__(
self._adapter = adapter
self._bot = bot
self._conversation_id_factory = conversation_id_factory
self._logger = logger
self._logger = logger or getLogger()

async def on_send_to_conversation(
self, claims_identity: ClaimsIdentity, conversation_id: str, activity: Activity,
Expand Down Expand Up @@ -181,20 +182,26 @@ async def callback(turn_context: TurnContext):
async def _get_skill_conversation_reference(
self, conversation_id: str
) -> SkillConversationReference:
# Get the SkillsConversationReference
conversation_reference_result = await self._conversation_id_factory.get_conversation_reference(
conversation_id
)
try:
skill_conversation_reference = await self._conversation_id_factory.get_skill_conversation_reference(
conversation_id
)
except NotImplementedError:
self._logger.warning(
"Got NotImplementedError when trying to call get_skill_conversation_reference() "
"on the SkillConversationIdFactory, attempting to use deprecated "
"get_conversation_reference() method instead."
)

# Attempt to get SkillConversationReference using deprecated method.
# this catch should be removed once we remove the deprecated method.
# We need to use the deprecated method for backward compatibility.
conversation_reference = await self._conversation_id_factory.get_conversation_reference(
conversation_id
)

# ConversationIdFactory can return either a SkillConversationReference (the newer way),
# or a ConversationReference (the old way, but still here for compatibility). If a
# ConversationReference is returned, build a new SkillConversationReference to simplify
# the remainder of this method.
if isinstance(conversation_reference_result, SkillConversationReference):
skill_conversation_reference: SkillConversationReference = conversation_reference_result
else:
skill_conversation_reference: SkillConversationReference = SkillConversationReference(
conversation_reference=conversation_reference_result,
conversation_reference=conversation_reference,
oauth_scope=(
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
if self._channel_provider and self._channel_provider.is_government()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from uuid import uuid4 as uuid
from aiounittest import AsyncTestCase
from botbuilder.core import MemoryStorage
from botbuilder.schema import (
Activity,
ConversationAccount,
ConversationReference,
)
from botbuilder.core.skills import (
BotFrameworkSkill,
SkillConversationIdFactory,
SkillConversationIdFactoryOptions,
)


class SkillConversationIdFactoryForTest(AsyncTestCase):
SERVICE_URL = "http://testbot.com/api/messages"
SKILL_ID = "skill"

@classmethod
def setUpClass(cls):
cls._skill_conversation_id_factory = SkillConversationIdFactory(MemoryStorage())
cls._application_id = str(uuid())
cls._bot_id = str(uuid())

async def test_skill_conversation_id_factory_happy_path(self):
conversation_reference = self._build_conversation_reference()

# Create skill conversation
skill_conversation_id = await self._skill_conversation_id_factory.create_skill_conversation_id(
options=SkillConversationIdFactoryOptions(
activity=self._build_message_activity(conversation_reference),
bot_framework_skill=self._build_bot_framework_skill(),
from_bot_id=self._bot_id,
from_bot_oauth_scope=self._bot_id,
)
)

assert (
skill_conversation_id and skill_conversation_id.strip()
), "Expected a valid skill conversation ID to be created"

# Retrieve skill conversation
retrieved_conversation_reference = await self._skill_conversation_id_factory.get_skill_conversation_reference(
skill_conversation_id
)

# Delete
await self._skill_conversation_id_factory.delete_conversation_reference(
skill_conversation_id
)

# Retrieve again
deleted_conversation_reference = await self._skill_conversation_id_factory.get_skill_conversation_reference(
skill_conversation_id
)

self.assertIsNotNone(retrieved_conversation_reference)
self.assertIsNotNone(retrieved_conversation_reference.conversation_reference)
self.assertEqual(
conversation_reference,
retrieved_conversation_reference.conversation_reference,
)
self.assertIsNone(deleted_conversation_reference)

async def test_id_is_unique_each_time(self):
conversation_reference = self._build_conversation_reference()

# Create skill conversation
first_id = await self._skill_conversation_id_factory.create_skill_conversation_id(
options=SkillConversationIdFactoryOptions(
activity=self._build_message_activity(conversation_reference),
bot_framework_skill=self._build_bot_framework_skill(),
from_bot_id=self._bot_id,
from_bot_oauth_scope=self._bot_id,
)
)

second_id = await self._skill_conversation_id_factory.create_skill_conversation_id(
options=SkillConversationIdFactoryOptions(
activity=self._build_message_activity(conversation_reference),
bot_framework_skill=self._build_bot_framework_skill(),
from_bot_id=self._bot_id,
from_bot_oauth_scope=self._bot_id,
)
)

# Ensure that we get a different conversation_id each time we call create_skill_conversation_id
self.assertNotEqual(first_id, second_id)

def _build_conversation_reference(self) -> ConversationReference:
return ConversationReference(
conversation=ConversationAccount(id=str(uuid())),
service_url=self.SERVICE_URL,
)

def _build_message_activity(
self, conversation_reference: ConversationReference
) -> Activity:
if not conversation_reference:
raise TypeError(str(conversation_reference))

activity = Activity.create_message_activity()
activity.apply_conversation_reference(conversation_reference)

return activity

def _build_bot_framework_skill(self) -> BotFrameworkSkill:
return BotFrameworkSkill(
app_id=self._application_id,
id=self.SKILL_ID,
skill_endpoint=self.SERVICE_URL,
)
Loading