Repository: microsoft/botbuilder-python

Issue:
CloudAdapter' object has no attribute 'create_connector_client'
Version 4.14.7

In my MS Teams bot i want to loop over all members that are already in the Team when i add the bot. 

To do this i wrote

```
    async def on_installation_update_add(self, turn_context: TurnContext):
        # Get the team details
        team_details = await TeamsInfo.get_team_details(turn_context)
        
        if team_details:
            team_id = team_details.id
            # Retrieve all members of the team
            team_members = await TeamsInfo.get_team_members(turn_context, team_id)
            for member in team_members:
                if member.id != turn_context.activity.recipient.id:  # Avoid messaging the bot itself
                    # Prepare the message
```

It seems that TeamsInfo.get_team_members(turn_context, team_id) is not compatible with CloudAdapter. 

settings is not a known attribute of class <class 'botbuilder.schema.teams._models_py3.TeamsChannelData'> and will be ignored
source is not a known attribute of class <class 'botbuilder.schema.teams._models_py3.TeamsChannelData'> and will be ignored

 [on_turn_error] unhandled error: 'CloudAdapter' object has no attribute 'create_connector_client'
Traceback (most recent call last):
  File "\env\lib\site-packages\botbuilder\core\bot_adapter.py", line 174, in run_pipeline
    return await self._middleware.receive_activity_with_status(
  File "\env\lib\site-packages\botbuilder\core\middleware_set.py", line 69, in receive_activity_with_status
    return await self.receive_activity_internal(context, callback)
  File "\env\lib\site-packages\botbuilder\core\middleware_set.py", line 79, in receive_activity_internal
    return await callback(context)
  File "/app\bots\main_bot.py", line 30, in on_turn
    await self.teams_bot.on_turn(turn_context)
  File "\env\lib\site-packages\botbuilder\core\activity_handler.py", line 92, in on_turn
    await self.on_installation_update(turn_context)
  File "\env\lib\site-packages\botbuilder\core\activity_handler.py", line 384, in on_installation_update
    return await self.on_installation_update_add(turn_context)
  File "/app\bots\teams_bot.py", line 42, in on_installation_update_add
    team_details = await TeamsInfo.get_team_details(turn_context)
  File "\env\lib\site-packages\botbuilder\core\teams\teams_info.py", line 120, in get_team_details
    teams_connector = await TeamsInfo.get_teams_connector_client(turn_context)
  File "\env\lib\site-packages\botbuilder\core\teams\teams_info.py", line 305, in get_teams_connector_client
    connector_client = await TeamsInfo._get_connector_client(turn_context)
  File "\env\lib\site-packages\botbuilder\core\teams\teams_info.py", line 321, in _get_connector_client
    return await turn_context.adapter.create_connector_client(
AttributeError: 'CloudAdapter' object has no attribute 'create_connector_client'


Thanks for having a look



Discussion:
Thanks @BasileBerckmoes,  I can look into this.
@rampaged thank you! I was exploring the code myself to see if i could contribute but i need to progress my understanding of the framework first
Hi @BasileBerckmoes, 

I'm able to reproduce this issue in Teams. I used sample [58.teams-start-thread-in-channel](https://github.com/microsoft/BotBuilder-Samples/tree/main/archive/samples/python/58.teams-start-thread-in-channel) to test this issue. 

I added this piece of code to reproduce the issue in `on_message_activity` method:
```py
        team_details = await TeamsInfo.get_team_details(turn_context)

        if team_details:
            team_id = team_details.id
            # Retrieve all members of the team
            team_members = await TeamsInfo.get_team_members(turn_context, team_id)
            for member in team_members:
                if member.id != turn_context.activity.recipient.id:  # Avoid messaging the bot itself
                    # Prepare the message
                    await turn_context.send_activity(
                        "some message here"
                    )


```

Attached a bot that demonstrates the problem. Here are the steps I followed to verify it:
[58.teams-thread.zip](https://github.com/microsoft/botbuilder-python/files/13996535/58.teams-thread.zip)

1. Unzip the attached bot and follow this [readme](https://github.com/microsoft/BotBuilder-Samples/tree/main/archive/samples/python/58.teams-start-thread-in-channel#readme) to set up and configure the bot locally
2. Sideload the bot in Teams and add it to a Teams team in a channel 
3. start a new post and @ mention the bot (TeamsStartThreadInChannel) 
![Screenshot from 2024-01-19 19-47-09](https://github.com/microsoft/botbuilder-python/assets/38049078/46c00745-5e46-4d9a-9509-2f01a902f40a)
4. Notice the error:
``` js
 [on_turn_error] unhandled error: 'CloudAdapter' object has no attribute 'create_connector_client'
Traceback (most recent call last):
  File "/home/rampage/Documents/bots/BotBuilder-Samples/archive/samples/python/58.teams-start-thread-in-channel/venv/lib/python3.10/site-packages/botbuild
er/core/bot_adapter.py", line 174, in run_pipeline
    return await self._middleware.receive_activity_with_status(
  File "/home/rampage/Documents/bots/BotBuilder-Samples/archive/samples/python/58.teams-start-thread-in-channel/venv/lib/python3.10/site-packages/botbuild
er/core/middleware_set.py", line 69, in receive_activity_with_status
    return await self.receive_activity_internal(context, callback)
  File "/home/rampage/Documents/bots/BotBuilder-Samples/archive/samples/python/58.teams-start-thread-in-channel/venv/lib/python3.10/site-packages/botbuild
er/core/middleware_set.py", line 79, in receive_activity_internal
    return await callback(context)
  File "/home/rampage/Documents/bots/BotBuilder-Samples/archive/samples/python/58.teams-start-thread-in-channel/venv/lib/python3.10/site-packages/botbuild
er/core/activity_handler.py", line 70, in on_turn
    await self.on_message_activity(turn_context)
  File "/home/rampage/Documents/bots/BotBuilder-Samples/archive/samples/python/58.teams-start-thread-in-channel/bots/teams_start_thread_in_channel.py", li
ne 17, in on_message_activity
    team_details = await TeamsInfo.get_team_details(turn_context)
  File "/home/rampage/Documents/bots/BotBuilder-Samples/archive/samples/python/58.teams-start-thread-in-channel/venv/lib/python3.10/site-packages/botbuild
er/core/teams/teams_info.py", line 120, in get_team_details
    teams_connector = await TeamsInfo.get_teams_connector_client(turn_context)
  File "/home/rampage/Documents/bots/BotBuilder-Samples/archive/samples/python/58.teams-start-thread-in-channel/venv/lib/python3.10/site-packages/botbuild
er/core/teams/teams_info.py", line 305, in get_teams_connector_client
    connector_client = await TeamsInfo._get_connector_client(turn_context)
  File "/home/rampage/Documents/bots/BotBuilder-Samples/archive/samples/python/58.teams-start-thread-in-channel/venv/lib/python3.10/site-packages/botbuild
er/core/teams/teams_info.py", line 321, in _get_connector_client
    return await turn_context.adapter.create_connector_client(
AttributeError: 'CloudAdapter' object has no attribute 'create_connector_client'

```

Call stack diagram:
```md

teams_start_thread_in_channel.py
|__ on_message_activity (line 17)
    |
    |__ TeamsInfo.get_team_details
        |
        |__ TeamsInfo.get_teams_connector_client 
            |
            |__ TeamsInfo._get_connector_client (line 320)
                |
                |__ turn_context.adapter.create_connector_client (line 321) <---- error occurs

```
CC @stevkan to handle this issue internally with Microsoft engineers. 
@BasileBerckmoes, 

I'm in the same boat, teaching myself the framework as well.

I explored the python bot framework sdk source code and tweaked [cloud_adapter.py](https://github.com/microsoft/botbuilder-python/blob/e14f4f4e6a26b439872187afab6f322758730209/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py) and [teams_info.py](https://github.com/microsoft/botbuilder-python/blob/e14f4f4e6a26b439872187afab6f322758730209/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py), which I think resolved the issue. However, I'm still uncertain if this is the right approach since I'm learning the framework and  need feedback from the engineers.

#

### Observation:
In the javascript sdk side, the implementation detail for [getConnectorClient](https://github.com/microsoft/botbuilder-js/blob/6b2656b7dd149c5f9869ddcb6dc53d1f7e022c37/libraries/botbuilder/src/teamsInfo.ts#L627C20-L637) in [teamsInfo.ts](https://github.com/microsoft/botbuilder-js/blob/6b2656b7dd149c5f9869ddcb6dc53d1f7e022c37/libraries/botbuilder/src/teamsInfo.ts) is as follows:
``` js
    private static getConnectorClient(context: TurnContext): ConnectorClient {
        const client =
            context.adapter && 'createConnectorClient' in context.adapter
                ? (context.adapter as BotFrameworkAdapter).createConnectorClient(context.activity.serviceUrl)
                : context.turnState?.get<ConnectorClient>(context.adapter.ConnectorClientKey);
        if (!client) {
            throw new Error('This method requires a connector client.');
        }

        return client;
    }
```

as opposed to [_get_connector_client ](https://github.com/microsoft/botbuilder-python/blob/e14f4f4e6a26b439872187afab6f322758730209/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py#L320-L323) method in [teams_info.py](https://github.com/microsoft/botbuilder-python/blob/e14f4f4e6a26b439872187afab6f322758730209/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py) on the python sdk:
```py
    @staticmethod
    async def _get_connector_client(turn_context: TurnContext) -> ConnectorClient:
        return await turn_context.adapter.create_connector_client(
            turn_context.activity.service_url
        )
```

In JavaScript, `getConnectorClient` checks if the "createConnectorClient" method exists in "context.adapter" and calls it, otherwise it retrieves a "ConnectorClient" from "context.turnState" using a predefined key.

So my idea was to implement the same logic as `getConnectorClient` method but in python sdk.

#

### Changes:

updated [_get_connector_client ](https://github.com/microsoft/botbuilder-python/blob/e14f4f4e6a26b439872187afab6f322758730209/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py#L320-L323)method in [teams_info.py](https://github.com/microsoft/botbuilder-python/blob/e14f4f4e6a26b439872187afab6f322758730209/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py):
``` diff
+    from botbuilder.integration.aiohttp import CloudAdapter

    async def _get_connector_client(turn_context: TurnContext) -> ConnectorClient:
+        if isinstance(turn_context.adapter, CloudAdapter):
+           return await turn_context.adapter.create_connector_client(turn_context)
+        else:
            return await turn_context.adapter.create_connector_client(
                turn_context.activity.service_url
            )
```

and in [CloudAdapter](https://github.com/microsoft/botbuilder-python/blob/e14f4f4e6a26b439872187afab6f322758730209/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py#L44) class, I added this method:
```py
    async def create_connector_client(self, turn_context: TurnContext):
        connector_client: ConnectorClient = turn_context.turn_state.get(
            self.BOT_CONNECTOR_CLIENT_KEY
        )

        return connector_client

```

finally, in [58.teams-start-thread-in-channel](https://github.com/microsoft/BotBuilder-Samples/tree/main/archive/samples/python/58.teams-start-thread-in-channel) bot sample(not sdk), in file [teams_start_thread_in_channel.py](https://github.com/microsoft/BotBuilder-Samples/blob/main/archive/samples/python/58.teams-start-thread-in-channel/bots/teams_start_thread_in_channel.py), i changed:
```py
connector_client = await turn_context.adapter.create_connector_client(turn_context.activity.service_url)
```

to:
```py
connector_client = await turn_context.adapter.create_connector_client(turn_context)
```
#

### Result:
```py
        # Get the team details
        team_details = await TeamsInfo.get_team_details(turn_context)

        if team_details:
            team_id = team_details.id
            # Retrieve all members of the team
            team_members = await TeamsInfo.get_team_members(turn_context, team_id)
            for member in team_members:
                if member.id != turn_context.activity.recipient.id:  # Avoid messaging the bot itself
                    # Prepare the message
                    await turn_context.send_activity(
                        f"name {member.name}"
                    )

```
![Screenshot from 2024-01-20 02-03-52](https://github.com/microsoft/botbuilder-python/assets/38049078/2100068a-cd13-4e88-8c12-4b436f11bcd8)



Resolving patch:
diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py
--- a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py
+++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py
@@ -10,7 +10,7 @@
     teams_get_meeting_info,
     teams_get_channel_data,
 )
-from botbuilder.core import CloudAdapterBase, BotFrameworkAdapter, TurnContext
+from botbuilder.core import CloudAdapterBase, BotFrameworkAdapter, TurnContext, BotAdapter
 from botbuilder.schema import Activity, ConversationParameters, ConversationReference
 from botbuilder.schema.teams import (
     ChannelInfo,
@@ -318,10 +318,15 @@ def get_team_id(turn_context: TurnContext):
 
     @staticmethod
     async def _get_connector_client(turn_context: TurnContext) -> ConnectorClient:
-        return await turn_context.adapter.create_connector_client(
-            turn_context.activity.service_url
+        connector_client = turn_context.turn_state.get(
+            BotAdapter.BOT_CONNECTOR_CLIENT_KEY
         )
 
+        if connector_client is None:
+            raise ValueError('This method requires a connector client.')
+
+        return connector_client
+
     @staticmethod
     async def _get_members(
         connector_client: ConnectorClient, conversation_id: str


Test patch:
diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py
--- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py
+++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py
@@ -592,6 +592,9 @@ async def test_on_teams_members_added_activity(self):
 
         turn_context = TurnContext(SimpleAdapter(), activity)
 
+        mock_connector_client = await SimpleAdapter.create_connector_client(self, turn_context.activity.service_url)
+        turn_context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY] = mock_connector_client
+
         # Act
         bot = TestingTeamsActivityHandler()
         await bot.on_turn(turn_context)