Closed BasileBerckmoes closed 9 months ago
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
[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 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.
#
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.
#
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)
#
# 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}"
)
@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 https://github.com/microsoft/botbuilder-python/pull/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.
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
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