Skip to content

Commit 85ec376

Browse files
ceciliaavilaGiulianoDolceTalianoSantorosw-joelmut
authored
[microsoft#1687][PORT][SkillConversationIdFactory] Add SkillConversationIdFactory to Python (microsoft#1691)
* Add SkillConversationIdFactory class * Update get_skill_conversation_reference to support deprecated method * Add unit test for SkillConversationIdFactory and update skill dialog test * Fix SkillConversationIdFactory behavior and complete unit test * Update tests * Apply black and pylint * Add copyright licence Co-authored-by: Giuliano Taliano <54330145+GiulianoDolceTalianoSantoro@users.noreply.github.com> Co-authored-by: sw-joelmut <joel.mut@southworks.com>
1 parent 06f5054 commit 85ec376

File tree

8 files changed

+391
-65
lines changed

8 files changed

+391
-65
lines changed

libraries/botbuilder-core/botbuilder/core/skills/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .bot_framework_skill import BotFrameworkSkill
99
from .bot_framework_client import BotFrameworkClient
1010
from .conversation_id_factory import ConversationIdFactoryBase
11+
from .skill_conversation_id_factory import SkillConversationIdFactory
1112
from .skill_handler import SkillHandler
1213
from .skill_conversation_id_factory_options import SkillConversationIdFactoryOptions
1314
from .skill_conversation_reference import SkillConversationReference
@@ -16,6 +17,7 @@
1617
"BotFrameworkSkill",
1718
"BotFrameworkClient",
1819
"ConversationIdFactoryBase",
20+
"SkillConversationIdFactory",
1921
"SkillConversationIdFactoryOptions",
2022
"SkillConversationReference",
2123
"SkillHandler",

libraries/botbuilder-core/botbuilder/core/skills/conversation_id_factory.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33

4-
from abc import ABC, abstractmethod
4+
from abc import ABC
55
from typing import Union
66
from botbuilder.schema import ConversationReference
77
from .skill_conversation_id_factory_options import SkillConversationIdFactoryOptions
@@ -17,7 +17,6 @@ class ConversationIdFactoryBase(ABC):
1717
SkillConversationReferences and deletion.
1818
"""
1919

20-
@abstractmethod
2120
async def create_skill_conversation_id(
2221
self,
2322
options_or_conversation_reference: Union[
@@ -41,23 +40,32 @@ async def create_skill_conversation_id(
4140
"""
4241
raise NotImplementedError()
4342

44-
@abstractmethod
4543
async def get_conversation_reference(
4644
self, skill_conversation_id: str
47-
) -> Union[SkillConversationReference, ConversationReference]:
45+
) -> ConversationReference:
46+
"""
47+
[DEPRECATED] Method is deprecated, please use get_skill_conversation_reference() instead.
48+
49+
Retrieves a :class:`ConversationReference` using a conversation id passed in.
50+
51+
:param skill_conversation_id: The conversation id for which to retrieve the :class:`ConversationReference`.
52+
:type skill_conversation_id: str
53+
:returns: `ConversationReference` for the specified ID.
54+
"""
55+
raise NotImplementedError()
56+
57+
async def get_skill_conversation_reference(
58+
self, skill_conversation_id: str
59+
) -> SkillConversationReference:
4860
"""
4961
Retrieves a :class:`SkillConversationReference` using a conversation id passed in.
5062
5163
:param skill_conversation_id: The conversation id for which to retrieve the :class:`SkillConversationReference`.
5264
:type skill_conversation_id: str
53-
54-
.. note::
55-
SkillConversationReference is the preferred return type, while the :class:`SkillConversationReference`
56-
type is provided for backwards compatability.
65+
:returns: `SkillConversationReference` for the specified ID.
5766
"""
5867
raise NotImplementedError()
5968

60-
@abstractmethod
6169
async def delete_conversation_reference(self, skill_conversation_id: str):
6270
"""
6371
Removes any reference to objects keyed on the conversation id passed in.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from uuid import uuid4 as uuid
5+
from botbuilder.core import TurnContext, Storage
6+
from .conversation_id_factory import ConversationIdFactoryBase
7+
from .skill_conversation_id_factory_options import SkillConversationIdFactoryOptions
8+
from .skill_conversation_reference import SkillConversationReference
9+
from .skill_conversation_reference import ConversationReference
10+
11+
12+
class SkillConversationIdFactory(ConversationIdFactoryBase):
13+
def __init__(self, storage: Storage):
14+
if not storage:
15+
raise TypeError("storage can't be None")
16+
17+
self._storage = storage
18+
19+
async def create_skill_conversation_id( # pylint: disable=arguments-differ
20+
self, options: SkillConversationIdFactoryOptions
21+
) -> str:
22+
"""
23+
Creates a new `SkillConversationReference`.
24+
25+
:param options: Creation options to use when creating the `SkillConversationReference`.
26+
:type options: :class:`botbuilder.core.skills.SkillConversationIdFactoryOptions`
27+
:return: ID of the created `SkillConversationReference`.
28+
"""
29+
30+
if not options:
31+
raise TypeError("options can't be None")
32+
33+
conversation_reference = TurnContext.get_conversation_reference(
34+
options.activity
35+
)
36+
37+
skill_conversation_id = str(uuid())
38+
39+
# Create the SkillConversationReference instance.
40+
skill_conversation_reference = SkillConversationReference(
41+
conversation_reference=conversation_reference,
42+
oauth_scope=options.from_bot_oauth_scope,
43+
)
44+
45+
# Store the SkillConversationReference using the skill_conversation_id as a key.
46+
skill_conversation_info = {skill_conversation_id: skill_conversation_reference}
47+
48+
await self._storage.write(skill_conversation_info)
49+
50+
# Return the generated skill_conversation_id (that will be also used as the conversation ID to call the skill).
51+
return skill_conversation_id
52+
53+
async def get_conversation_reference(
54+
self, skill_conversation_id: str
55+
) -> ConversationReference:
56+
return await super().get_conversation_reference(skill_conversation_id)
57+
58+
async def get_skill_conversation_reference(
59+
self, skill_conversation_id: str
60+
) -> SkillConversationReference:
61+
"""
62+
Retrieve a `SkillConversationReference` with the specified ID.
63+
64+
:param skill_conversation_id: The ID of the `SkillConversationReference` to retrieve.
65+
:type skill_conversation_id: str
66+
:return: `SkillConversationReference` for the specified ID; None if not found.
67+
"""
68+
69+
if not skill_conversation_id:
70+
raise TypeError("skill_conversation_id can't be None")
71+
72+
# Get the SkillConversationReference from storage for the given skill_conversation_id.
73+
skill_conversation_reference = await self._storage.read([skill_conversation_id])
74+
75+
return skill_conversation_reference.get(skill_conversation_id)
76+
77+
async def delete_conversation_reference(self, skill_conversation_id: str):
78+
"""
79+
Deletes the `SkillConversationReference` with the specified ID.
80+
81+
:param skill_conversation_id: The ID of the `SkillConversationReference` to be deleted.
82+
:type skill_conversation_id: str
83+
"""
84+
85+
# Delete the SkillConversationReference from storage.
86+
await self._storage.delete([skill_conversation_id])

libraries/botbuilder-core/botbuilder/core/skills/skill_handler.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Licensed under the MIT License.
33

44
from uuid import uuid4
5+
from logging import Logger, getLogger
56

67
from botbuilder.core import Bot, BotAdapter, ChannelServiceHandler, TurnContext
78
from botbuilder.schema import (
@@ -37,7 +38,7 @@ def __init__(
3738
credential_provider: CredentialProvider,
3839
auth_configuration: AuthenticationConfiguration,
3940
channel_provider: ChannelProvider = None,
40-
logger: object = None,
41+
logger: Logger = None,
4142
):
4243
super().__init__(credential_provider, auth_configuration, channel_provider)
4344

@@ -51,7 +52,7 @@ def __init__(
5152
self._adapter = adapter
5253
self._bot = bot
5354
self._conversation_id_factory = conversation_id_factory
54-
self._logger = logger
55+
self._logger = logger or getLogger()
5556

5657
async def on_send_to_conversation(
5758
self, claims_identity: ClaimsIdentity, conversation_id: str, activity: Activity,
@@ -181,20 +182,26 @@ async def callback(turn_context: TurnContext):
181182
async def _get_skill_conversation_reference(
182183
self, conversation_id: str
183184
) -> SkillConversationReference:
184-
# Get the SkillsConversationReference
185-
conversation_reference_result = await self._conversation_id_factory.get_conversation_reference(
186-
conversation_id
187-
)
185+
try:
186+
skill_conversation_reference = await self._conversation_id_factory.get_skill_conversation_reference(
187+
conversation_id
188+
)
189+
except NotImplementedError:
190+
self._logger.warning(
191+
"Got NotImplementedError when trying to call get_skill_conversation_reference() "
192+
"on the SkillConversationIdFactory, attempting to use deprecated "
193+
"get_conversation_reference() method instead."
194+
)
195+
196+
# Attempt to get SkillConversationReference using deprecated method.
197+
# this catch should be removed once we remove the deprecated method.
198+
# We need to use the deprecated method for backward compatibility.
199+
conversation_reference = await self._conversation_id_factory.get_conversation_reference(
200+
conversation_id
201+
)
188202

189-
# ConversationIdFactory can return either a SkillConversationReference (the newer way),
190-
# or a ConversationReference (the old way, but still here for compatibility). If a
191-
# ConversationReference is returned, build a new SkillConversationReference to simplify
192-
# the remainder of this method.
193-
if isinstance(conversation_reference_result, SkillConversationReference):
194-
skill_conversation_reference: SkillConversationReference = conversation_reference_result
195-
else:
196203
skill_conversation_reference: SkillConversationReference = SkillConversationReference(
197-
conversation_reference=conversation_reference_result,
204+
conversation_reference=conversation_reference,
198205
oauth_scope=(
199206
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
200207
if self._channel_provider and self._channel_provider.is_government()
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from uuid import uuid4 as uuid
5+
from aiounittest import AsyncTestCase
6+
from botbuilder.core import MemoryStorage
7+
from botbuilder.schema import (
8+
Activity,
9+
ConversationAccount,
10+
ConversationReference,
11+
)
12+
from botbuilder.core.skills import (
13+
BotFrameworkSkill,
14+
SkillConversationIdFactory,
15+
SkillConversationIdFactoryOptions,
16+
)
17+
18+
19+
class SkillConversationIdFactoryForTest(AsyncTestCase):
20+
SERVICE_URL = "http://testbot.com/api/messages"
21+
SKILL_ID = "skill"
22+
23+
@classmethod
24+
def setUpClass(cls):
25+
cls._skill_conversation_id_factory = SkillConversationIdFactory(MemoryStorage())
26+
cls._application_id = str(uuid())
27+
cls._bot_id = str(uuid())
28+
29+
async def test_skill_conversation_id_factory_happy_path(self):
30+
conversation_reference = self._build_conversation_reference()
31+
32+
# Create skill conversation
33+
skill_conversation_id = await self._skill_conversation_id_factory.create_skill_conversation_id(
34+
options=SkillConversationIdFactoryOptions(
35+
activity=self._build_message_activity(conversation_reference),
36+
bot_framework_skill=self._build_bot_framework_skill(),
37+
from_bot_id=self._bot_id,
38+
from_bot_oauth_scope=self._bot_id,
39+
)
40+
)
41+
42+
assert (
43+
skill_conversation_id and skill_conversation_id.strip()
44+
), "Expected a valid skill conversation ID to be created"
45+
46+
# Retrieve skill conversation
47+
retrieved_conversation_reference = await self._skill_conversation_id_factory.get_skill_conversation_reference(
48+
skill_conversation_id
49+
)
50+
51+
# Delete
52+
await self._skill_conversation_id_factory.delete_conversation_reference(
53+
skill_conversation_id
54+
)
55+
56+
# Retrieve again
57+
deleted_conversation_reference = await self._skill_conversation_id_factory.get_skill_conversation_reference(
58+
skill_conversation_id
59+
)
60+
61+
self.assertIsNotNone(retrieved_conversation_reference)
62+
self.assertIsNotNone(retrieved_conversation_reference.conversation_reference)
63+
self.assertEqual(
64+
conversation_reference,
65+
retrieved_conversation_reference.conversation_reference,
66+
)
67+
self.assertIsNone(deleted_conversation_reference)
68+
69+
async def test_id_is_unique_each_time(self):
70+
conversation_reference = self._build_conversation_reference()
71+
72+
# Create skill conversation
73+
first_id = await self._skill_conversation_id_factory.create_skill_conversation_id(
74+
options=SkillConversationIdFactoryOptions(
75+
activity=self._build_message_activity(conversation_reference),
76+
bot_framework_skill=self._build_bot_framework_skill(),
77+
from_bot_id=self._bot_id,
78+
from_bot_oauth_scope=self._bot_id,
79+
)
80+
)
81+
82+
second_id = await self._skill_conversation_id_factory.create_skill_conversation_id(
83+
options=SkillConversationIdFactoryOptions(
84+
activity=self._build_message_activity(conversation_reference),
85+
bot_framework_skill=self._build_bot_framework_skill(),
86+
from_bot_id=self._bot_id,
87+
from_bot_oauth_scope=self._bot_id,
88+
)
89+
)
90+
91+
# Ensure that we get a different conversation_id each time we call create_skill_conversation_id
92+
self.assertNotEqual(first_id, second_id)
93+
94+
def _build_conversation_reference(self) -> ConversationReference:
95+
return ConversationReference(
96+
conversation=ConversationAccount(id=str(uuid())),
97+
service_url=self.SERVICE_URL,
98+
)
99+
100+
def _build_message_activity(
101+
self, conversation_reference: ConversationReference
102+
) -> Activity:
103+
if not conversation_reference:
104+
raise TypeError(str(conversation_reference))
105+
106+
activity = Activity.create_message_activity()
107+
activity.apply_conversation_reference(conversation_reference)
108+
109+
return activity
110+
111+
def _build_bot_framework_skill(self) -> BotFrameworkSkill:
112+
return BotFrameworkSkill(
113+
app_id=self._application_id,
114+
id=self.SKILL_ID,
115+
skill_endpoint=self.SERVICE_URL,
116+
)

0 commit comments

Comments
 (0)