8000 Latest updates on skills libraries - internals, http_helper and bf_sk… · itsmokha/botbuilder-python@4393bc4 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4393bc4

Browse files
committed
Latest updates on skills libraries - internals, http_helper and bf_skill_client pending
1 parent 563b602 commit 4393bc4

File tree

7 files changed

+647
-146
lines changed

7 files changed

+647
-146
lines changed

libraries/botbuilder-core/botbuilder/core/bot_adapter.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from abc import ABC, abstractmethod
55
from typing import List, Callable, Awaitable
66
from botbuilder.schema import Activity, ConversationReference
7+
from botframework.connector.auth import ClaimsIdentity
78

89
from . import conversation_reference_extension
910
from .bot_assert import BotAssert
@@ -77,6 +78,14 @@ async def continue_conversation(
7778
)
7879
return await self.run_pipeline(context, callback)
7980

81+
async def process_activity_with_claims(
82+
self,
83+
identity: ClaimsIdentity,
84+
activity: Activity,
85+
callback: Callable[[TurnContext], Awaitable],
86+
): # pylint: disable=unused-argument
87+
raise NotImplementedError()
88+
8089
async def run_pipeline(
8190
self, context: TurnContext, callback: Callable[[TurnContext], Awaitable] = None
8291
):

libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import os
88
from typing import List, Callable, Awaitable, Union, Dict
99
from msrest.serialization import Model
10+
from botbuilder.core import InvokeResponse
1011
from botbuilder.schema import (
1112
Activity,
13+
ActivityTypes,
1214
ConversationAccount,
1315
ConversationParameters,
1416
ConversationReference,
@@ -19,10 +21,13 @@
1921
from botframework.connector.auth import (
2022
AuthenticationConstants,
2123
ChannelValidation,
24+
ClaimsIdentity,
25+
CredentialProvider,
2226
GovernmentChannelValidation,
2327
GovernmentConstants,
2428
MicrosoftAppCredentials,
2529
JwtTokenValidation,
30+
SkillValidation,
2631
SimpleCredentialProvider,
2732
)
2833
from botframework.connector.token_api import TokenApiClient
@@ -71,17 +76,20 @@ def __init__(
7176
oauth_endpoint: str = None,
7277
open_id_metadata: str = None,
7378
channel_service: str = None,
79+
credential_provider: CredentialProvider = None,
7480
):
7581
self.app_id = app_id
7682
self.app_password = app_password
7783
self.channel_auth_tenant = channel_auth_tenant
7884
self.oauth_endpoint = oauth_endpoint
7985
self.open_id_metadata = open_id_metadata
8086
self.channel_service = channel_service
87+
self.credential_provider = credential_provider
8188

8289

8390
class BotFrameworkAdapter(BotAdapter, UserTokenProvider):
8491
_INVOKE_RESPONSE_KEY = "BotFrameworkAdapter.InvokeResponse"
92+
_BOT_IDENTITY_KEY = "BotIdentity"
8593

8694
def __init__(self, settings: BotFrameworkAdapterSettings):
8795
super(BotFrameworkAdapter, self).__init__()
@@ -98,8 +106,11 @@ def __init__(self, settings: BotFrameworkAdapterSettings):
98106
self.settings.app_password,
99107
self.settings.channel_auth_tenant,
100108
)
101-
self._credential_provider = SimpleCredentialProvider(
102-
self.settings.app_id, self.settings.app_password
109+
self._credential_provider = (
110+
settings.credential_provider
111+
or SimpleCredentialProvider(
112+
self.settings.app_id, self.settings.app_password
113+
)
103114
)
104115
self._is_emulating_oauth_cards = False
105116

@@ -117,6 +128,8 @@ def __init__(self, settings: BotFrameworkAdapterSettings):
117128
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
118129
)
119130

131+
self._APP_CREDENTIALS_CACHE: Dict[str, MicrosoftAppCredentials] = {}
132+
120133
async def continue_conversation(
121134
self, bot_id: str, reference: ConversationReference, callback: Callable
122135
):
@@ -227,6 +240,40 @@ async def process_activity(self, req, auth_header: str, logic: Callable):
227240

228241
return await self.run_pipeline(context, logic)
229242

243+
async def process_activity_with_claims(
244+
self,
245+
identity: ClaimsIdentity,
246+
activity: Activity,
247+
logic: Callable[[TurnContext], Awaitable],
248+
) -> InvokeResponse:
249+
if not activity:
250+
raise TypeError(f"{Activity.__name__} can not be None")
251+
252+
context = TurnContext(self, activity)
253+
254+
context.turn_state[BotFrameworkAdapter._BOT_IDENTITY_KEY] = identity
255+
context.turn_state["BotCallbackHandler"] = logic
256+
257+
client = await self.create_connector_client_with_claims(
258+
activity.service_url, identity
259+
)
260+
context.turn_state[client.__class__.__name__] = client
261+
262+
await self.run_pipeline(context, logic)
263+
264+
# Handle Invoke scenarios, which deviate from the request/response model in that
265+
# the Bot will return a specific body and return code.
266+
if activity.type == ActivityTypes.invoke:
267+
activity_invoke_response = context.turn_state.get(self._INVOKE_RESPONSE_KEY)
268+
if not activity_invoke_response:
269+
return InvokeResponse(status=501)
270+
271+
return activity_invoke_response
272+
273+
# For all non-invoke scenarios, the HTTP layers above don't have to mess
274+
# with the Body and return codes.
275+
return None
276+
230277
async def authenticate_request(self, request: Activity, auth_header: str):
231278
"""
232279
Allows for the overriding of authentication in unit tests.
@@ -579,6 +626,31 @@ def create_connector_client(self, service_url: str) -> ConnectorClient:
579626
client.config.add_user_agent(USER_AGENT)
580627
return client
581628

629+
def create_connector_client_with_claims(
630+
self, service_url: str, identity: ClaimsIdentity = None
631+
) -> ConnectorClient:
632+
credentials: MicrosoftAppCredentials = None
633+
if identity:
634+
# For requests from channel App Id is in Audience claim of JWT token. For emulator it is in AppId claim. For
635+
# unauthenticated requests we have anonymous identity provided auth is disabled.
636+
# For Activities coming from Emulator AppId claim contains the Bot's AAD AppId.
637+
bot_app_id_claim = identity.claims.get(
638+
AuthenticationConstants.AUDIENCE_CLAIM
639+
) or identity.claims.get(AuthenticationConstants.APP_ID_CLAIM)
640+
641+
# For anonymous requests (requests with no header) appId is not set in claims.
642+
if bot_app_id_claim:
643+
scope = None
644+
if SkillValidation.is_skill_claim(identity.claims):
645+
# The skill connector has the target skill in the OAuthScope.
646+
scope = JwtTokenValidation.get_app_id_from_claims(identity.claims)
647+
648+
credentials = await self._get_app_credentials(bot_app_id_claim, scope)
649+
650+
client = ConnectorClient(credentials, base_url=service_url)
651+
client.config.add_user_agent(USER_AGENT)
652+
return client
653+
582654
def create_token_api_client(self, service_url: str) -> TokenApiClient:
583655
client = TokenApiClient(self._credentials, service_url)
584656
client.config.add_user_agent(USER_AGENT)
@@ -593,7 +665,6 @@ async def emulate_oauth_cards(
593665
await EmulatorApiClient.emulate_oauth_cards(self._credentials, url, emulate)
594666

595667
def oauth_api_url(self, context_or_service_url: Union[TurnContext, str]) -> str:
596-
url = None
597668
if self._is_emulating_oauth_cards:
598669
url = (
599670
context_or_service_url.activity.service_url
@@ -622,3 +693,36 @@ def check_emulating_oauth_cards(self, context: TurnContext):
622693
)
623694
):
624695
self._is_emulating_oauth_cards = True
696+
697+
async def _get_app_credentials(
698+
self, app_id: str, oauth_scope: str
699+
) -> MicrosoftAppCredentials:
700+
if not app_id:
701+
return MicrosoftAppCredentials(None, None)
702+
703+
cache_key = f"{app_id}{oauth_scope}"
704+
app_credentials = self._APP_CREDENTIALS_CACHE.get(cache_key)
705+
706+
if app_credentials:
707+
return app_credentials
708+
709+
# If app credentials were provided, use them as they are the preferred choice moving forward
710+
if self._credentials.microsoft_app_id:
711+
# Cache credentials
712+
self._APP_CREDENTIALS_CACHE[cache_key] = self._credentials
713+
return self._credentials
714+
715+
app_password = await self._credential_provider.get_app_password(app_id)
716+
app_credentials = MicrosoftAppCredentials(
717+
app_id, app_password, oauth_scope=oauth_scope
718+
)
719+
if JwtTokenValidation.is_government(self.settings.channel_service):
720+
app_credentials.oauth_endpoint = (
721+
GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL
722+
)
723+
app_credentials.oauth_scope = (
724+
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
725+
)
726+
727+
self._APP_CREDENTIALS_CACHE[cache_key] = app_credentials
728+
return app_credentials

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

Lines changed: 0 additions & 3 deletions
This file was deleted.

libraries/botbuilder-skills/botbuilder/skills/channel_api_middleware.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,29 @@
44
import uuid
55
from typing import Callable, Awaitable
66

7-
from botbuilder.core import BotFrameworkAdapter, Middleware, TurnContext
7+
from botbuilder.core import BotAdapter, Middleware, TurnContext
88
from botbuilder.schema import Activity, ActivityTypes, ResourceResponse
99
from .channel_api_args import ChannelApiArgs
1010
from .channel_api_methods import ChannelApiMethods
11-
from .skill_host_adapter import SkillHostAdapter
11+
from .skill_client import SkillClient
1212

1313

1414
class ChannelApiMiddleware(Middleware):
15-
def __init__(self, skill_adapter: SkillHostAdapter):
16-
self._skill_adapter = skill_adapter
15+
def __init__(self, skill_adapter: SkillClient):
16+
self._SKILL_ADAPTER = skill_adapter
1717

1818
async def on_turn(
1919
self, context: TurnContext, logic: Callable[[TurnContext], Awaitable]
2020
):
21-
# register the skill adapter
22-
context.turn_state[self._skill_adapter.__class__.__name__] = self._skill_adapter
23-
2421
if (
2522
context.activity.type == ActivityTypes.invoke
26-
and context.activity.name == SkillHostAdapter.INVOKE_ACTIVITY_NAME
23+
and context.activity.name == SkillClient.INVOKE_ACTIVITY_NAME
2724
):
2825
# process invoke activity TODO: (implement next 2 lines)
2926
invoke_activity = context.activity.as_invoke_activity()
3027
invoke_args: ChannelApiArgs = invoke_activity.value
3128

32-
await self.call_channel_api(context, logic, invoke_args)
29+
await self._process_skill_activity(context, logic, invoke_args)
3330
else:
3431
await logic()
3532

@@ -39,6 +36,7 @@ async def _process_end_of_conversation(
3936
logic: Callable[[TurnContext], Awaitable],
4037
activity_payload: Activity,
4138
):
39+
# TODO: implement 'as_end_of_conversation_activity'
4240
end_of_conversation = activity_payload.as_end_of_conversation_activity()
4341
context.activity.type = end_of_conversation.type
4442
context.activity.text = end_of_conversation.text
@@ -53,14 +51,14 @@ async def _process_end_of_conversation(
5351

5452
await logic()
5553

56-
async def _call_chanel_api(
54+
async def _process_skill_activity(
5755
self,
5856
context: TurnContext,
5957
logic: Callable[[TurnContext], Awaitable],
6058
invoke_args: ChannelApiArgs,
6159
):
6260
try:
63-
adapter: BotFrameworkAdapter = context.adapter
61+
adapter: BotAdapter = context.adapter
6462

6563
if invoke_args.method == ChannelApiMethods.SEND_TO_CONVERSATION:
6664
activity_payload: Activity = invoke_args.args[0]
@@ -75,7 +73,7 @@ async def _call_chanel_api(
7573

7674
elif invoke_args.method == ChannelApiMethods.REPLY_TO_ACTIVITY:
7775
activity_payload: Activity = invoke_args.args[1]
78-
activity_payload.reply_to_id = invoke_args[0]
76+
activity_payload.reply_to_id = invoke_args.args[0]
7977

8078
if activity_payload.type == ActivityTypes.end_of_conversation:
8179
await ChannelApiMiddleware._process_end_of_conversation(
@@ -90,7 +88,7 @@ async def _call_chanel_api(
9088
invoke_args.result = await context.update_activity(invoke_args.args[0])
9189

9290
elif invoke_args.method == ChannelApiMethods.DELETE_ACTIVITY:
93-
invoke_args.result = await context.delete_activity(invoke_args[0])
91+
invoke_args.result = await context.delete_activity(invoke_args.args[0])
9492

9593
elif invoke_args == ChannelApiMethods.SEND_CONVERSATION_HISTORY:
9694
raise NotImplementedError(
@@ -109,13 +107,13 @@ async def _call_chanel_api(
109107
elif invoke_args == ChannelApiMethods.DELETE_CONVERSATION_MEMBER:
110108
if adapter:
111109
invoke_args.result = await adapter.delete_conversation_member(
112-
context, invoke_args[0]
110+
context, invoke_args.args[0]
113111
)
114112

115113
elif invoke_args == ChannelApiMethods.GET_ACTIVITY_MEMBERS:
116114
if adapter:
117115
invoke_args.result = await adapter.get_activity_members(
118-
context, invoke_args[0]
116+
context, invoke_args.args[0]
119117
)
120118

121119
elif invoke_args == ChannelApiMethods.UPLOAD_ATTACHMENT:

0 commit comments

Comments
 (0)
0