Repository: microsoft/botbuilder-python

Issue:
Avoid sending typing activity when bot is invoked as skill
We should port this once the C# PR is merged.
See [parent](https://github.com/microsoft/botframework-sdk/issues/6049)


Discussion:

Resolving patch:
diff --git a/libraries/botbuilder-core/botbuilder/core/show_typing_middleware.py b/libraries/botbuilder-core/botbuilder/core/show_typing_middleware.py
--- a/libraries/botbuilder-core/botbuilder/core/show_typing_middleware.py
+++ b/libraries/botbuilder-core/botbuilder/core/show_typing_middleware.py
@@ -4,7 +4,9 @@
 from typing import Awaitable, Callable
 
 from botbuilder.schema import Activity, ActivityTypes
+from botframework.connector.auth import ClaimsIdentity, SkillValidation
 
+from .bot_adapter import BotAdapter
 from .middleware_set import Middleware
 from .turn_context import TurnContext
 
@@ -82,9 +84,12 @@ async def aux():
         def stop_interval():
             timer.set_clear_timer()
 
-        # if it's a message, start sending typing activities until the
-        # bot logic is done.
-        if context.activity.type == ActivityTypes.message:
+        # Start a timer to periodically send the typing activity
+        # (bots running as skills should not send typing activity)
+        if (
+            context.activity.type == ActivityTypes.message
+            and not ShowTypingMiddleware._is_skill_bot(context)
+        ):
             start_interval(context, self._delay, self._period)
 
         # call the bot logic
@@ -93,3 +98,10 @@ def stop_interval():
         stop_interval()
 
         return result
+
+    @staticmethod
+    def _is_skill_bot(context: TurnContext) -> bool:
+        claims_identity = context.turn_state.get(BotAdapter.BOT_IDENTITY_KEY)
+        return isinstance(
+            claims_identity, ClaimsIdentity
+        ) and SkillValidation.is_skill_claim(claims_identity.claims)


Test patch:
diff --git a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py
--- a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py
+++ b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py
@@ -153,7 +153,7 @@ async def process_activity(
             self._conversation_lock.release()
 
         activity.timestamp = activity.timestamp or datetime.utcnow()
-        await self.run_pipeline(TurnContext(self, activity), logic)
+        await self.run_pipeline(self.create_turn_context(activity), logic)
 
     async def send_activities(
         self, context, activities: List[Activity]
@@ -227,7 +227,7 @@ async def create_conversation(
             members_removed=[],
             conversation=ConversationAccount(id=str(uuid.uuid4())),
         )
-        context = TurnContext(self, update)
+        context = self.create_turn_context(update)
         return await callback(context)
 
     async def receive_activity(self, activity):
@@ -252,7 +252,7 @@ async def receive_activity(self, activity):
             request.id = str(self._next_id)
 
         # Create context object and run middleware.
-        context = TurnContext(self, request)
+        context = self.create_turn_context(request)
         return await self.run_pipeline(context, self.logic)
 
     def get_next_activity(self) -> Activity:
@@ -534,6 +534,9 @@ async def exchange_token_from_credentials(
 
         return None
 
+    def create_turn_context(self, activity: Activity) -> TurnContext:
+        return TurnContext(self, activity)
+
 
 class TestFlow:
     __test__ = False
diff --git a/libraries/botbuilder-core/tests/test_show_typing_middleware.py b/libraries/botbuilder-core/tests/test_show_typing_middleware.py
--- a/libraries/botbuilder-core/tests/test_show_typing_middleware.py
+++ b/libraries/botbuilder-core/tests/test_show_typing_middleware.py
@@ -1,11 +1,31 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 import asyncio
+from uuid import uuid4
 import aiounittest
 
-from botbuilder.core import ShowTypingMiddleware
+from botbuilder.core import ShowTypingMiddleware, TurnContext
 from botbuilder.core.adapters import TestAdapter
-from botbuilder.schema import ActivityTypes
+from botbuilder.schema import Activity, ActivityTypes
+from botframework.connector.auth import AuthenticationConstants, ClaimsIdentity
+
+
+class SkillTestAdapter(TestAdapter):
+    def create_turn_context(self, activity: Activity) -> TurnContext:
+        turn_context = super().create_turn_context(activity)
+
+        claims_identity = ClaimsIdentity(
+            claims={
+                AuthenticationConstants.VERSION_CLAIM: "2.0",
+                AuthenticationConstants.AUDIENCE_CLAIM: str(uuid4()),
+                AuthenticationConstants.AUTHORIZED_PARTY: str(uuid4()),
+            },
+            is_authenticated=True,
+        )
+
+        turn_context.turn_state[self.BOT_IDENTITY_KEY] = claims_identity
+
+        return turn_context
 
 
 class TestShowTypingMiddleware(aiounittest.AsyncTestCase):
@@ -65,3 +85,14 @@ def assert_is_message(activity, description):  # pylint: disable=unused-argument
 
         step1 = await adapter.send("foo")
         await step1.assert_reply(assert_is_message)
+
+    async def test_not_send_not_send_typing_indicator_when_bot_running_as_skill(self):
+        async def aux(context):
+            await asyncio.sleep(1)
+            await context.send_activity(f"echo:{context.activity.text}")
+
+        skill_adapter = SkillTestAdapter(aux)
+        skill_adapter.use(ShowTypingMiddleware(0.001, 1))
+
+        step1 = await skill_adapter.send("foo")
+        await step1.assert_reply("echo:foo")