microsoft/botbuilder-python

CloudAdapter' object has no attribute 'create_connector_client'

BasileBerckmoes opened this issue · 13 comments

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

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 to test this issue.

I added this piece of code to reproduce the issue in on_message_activity method:

        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

  1. Unzip the attached bot and follow this 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
  4. Notice the error:
 [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:

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 and 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 in teamsInfo.ts is as follows:

    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 method in teams_info.py on the python sdk:

    @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 method in teams_info.py:

+    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 class, I added this method:

    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 bot sample(not sdk), in file teams_start_thread_in_channel.py, i changed:

connector_client = await turn_context.adapter.create_connector_client(turn_context.activity.service_url)

to:

connector_client = await turn_context.adapter.create_connector_client(turn_context)

Result:

        # 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

@rampaged Hi Ram. Not sure why JS does what it does. But I avoid type checking ('isinstance(turn_context.adapter, CloudAdapter)) unless I absolutely need to. The JS version is just check if the 'createConnectorClient' method is available (no idea why, could be historical).

It would be typical for the stack to store the Connector in the turnState, and I wonder if we also don't have to check that out. That is, it's created and stored for that turn to avoid repeatedly creating new ones.

I'll compare to DotNet. My guess is something was missed when porting the CloudAdapter.

@rampaged Check out the impl of TeamsInfo in DotNet. It has methods for GetConnectorClient (which just gets from TurnState), and GetTeamsConnectorClient (which creates a new TeamConnectorClient).

ConnectorClient would be added to TurnState in difference places for BotFrameworkAdapter vs CloudAdapter. DotNet will also remove it from TurnState in specific places.

Thanks for the feedback @tracyboehrer. Looking into it.

Thanks for the pointers @tracyboehrer.

I made an update to the _get_connector_client method to retrieve the ConnectorClient directly from turn_context.turn_state.

Now it uses hasattr to check if the create_connector_client method exists, which seems to keep things compatible with different adapters without needing type checking.

    @staticmethod
    async def _get_connector_client(turn_context: TurnContext) -> ConnectorClient:
        if hasattr(turn_context.adapter, 'create_connector_client'):
            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

Then for bot sample 58.teams-start-thread-in-channel, in file teams_start_thread_in_channel.py, i changed:

# works only with BotFrameworkAdapter
connector_client = await turn_context.adapter.create_connector_client(turn_context.activity.service_url)

to

# works for both CloudAdapter and BotFrameworkAdapter
connector_client = await TeamsInfo._get_connector_client(turn_context)

This change seems to maintain compatibility with both CloudAdapter and BotFrameworkAdapter.

I've updated the PR #2062. Let me know your thoughts.

Great work @rampaged!

@rampaged is there any way i can ask you some questions? The teams samples are not ported yet to the CloudAdaptor and im having a few difficulties. Should i put my questions here or can we chat somewhere else?

Hey @BasileBerckmoes, Of course. Happy to help.

Note that for "how-to" questions, the Microsoft Bot Framework team  prefers  posting questions on
Stack Overflow with the #botframework tag.

The official Bot Framework Github repos are the preferred platform for submitting bug fixes and feature requests.