Skip to content

Commit daabfef

Browse files
Copilotcleemullins
andauthored
Distribute error resources across packages with error codes and help URLs (#223)
* Initial plan * Add error resources infrastructure and refactor authentication-msal errors Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Refactor CosmosDB storage errors to use error resources Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Refactor blob storage and Teams errors to use error resources Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Refactor hosting, activity, and copilot studio errors to use error resources Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Fix import order for __future__ annotations in activity module Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Fix circular import by using lazy imports in activity module Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Add comprehensive documentation for error resources module Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Fix code review feedback: move imports out of TYPE_CHECKING and remove unnecessary format() call Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Format all changed files with Black to fix build errors Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Fix test failure by adding specific error for invalid CosmosDB key suffix characters Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Distribute error resources to individual packages with updated ranges - Created error resources in each package: - Authentication: -60000 to -60999 - Storage Cosmos: -61000 to -61999 - Storage Blob: -61100 to -61199 - Teams: -62000 to -62999 - Hosting: -63000 to -63999 - Activity: -64000 to -64999 - Copilot Studio: -65000 to -65999 - General: -66000 to -66999 - Updated all imports to use package-specific error resources - Updated README with new structure and ranges Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Update tests to reflect distributed error resources structure - Updated test_error_resources.py to test hosting-core errors only - Added new test class for distributed error resources across packages - Tests now import from package-specific error modules - Updated error code range assertions to match new ranges - All tests use pytest.skip for packages that may not be available Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Fix error resource references and move imports to top of files - Fixed teams_info.py to use error_resources.ActivityRequired and error_resources.RequiredParameterMissing from hosting-core instead of non-existent teams_errors attributes - Moved lazy imports to top of activity module files to improve code organization - Updated _channel_id_field_mixin.py, activity.py, and channel_id.py to import activity_errors at module level - All files formatted with Black Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Complete fix for teams_info.py error resource references - Fixed remaining instances of teams_errors.ActivityRequired to use error_resources.ActivityRequired - All four occurrences in send_message_to_list_of_users, send_message_to_all_users_in_tenant, send_message_to_all_users_in_team, and send_message_to_list_of_channels now correctly use hosting-core's error_resources Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> * Consolidate ErrorMessage class to hosting-core package - Removed duplicate ErrorMessage classes from all packages (activity, authentication-msal, copilotstudio-client, hosting-teams, storage-blob, storage-cosmos) - Updated all packages to import ErrorMessage from microsoft_agents.hosting.core.errors - Single source of truth for ErrorMessage class reduces maintenance and ensures consistency - All files formatted with Black Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com>
1 parent 48fbd9d commit daabfef

File tree

35 files changed

+1348
-125
lines changed

35 files changed

+1348
-125
lines changed

libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
model_serializer,
1515
)
1616

17+
from microsoft_agents.activity.errors import activity_errors
18+
1719
from .channel_id import ChannelId
1820

1921
logger = logging.getLogger(__name__)
@@ -42,10 +44,7 @@ def channel_id(self, value: Any):
4244
elif isinstance(value, str):
4345
self._channel_id = ChannelId(value)
4446
else:
45-
raise ValueError(
46-
f"Invalid type for channel_id: {type(value)}. "
47-
"Expected ChannelId or str."
48-
)
47+
raise ValueError(activity_errors.InvalidChannelIdType.format(type(value)))
4948

5049
def _set_validated_channel_id(self, data: Any) -> None:
5150
"""Sets the channel_id after validating it as a ChannelId model."""

libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from .channel_id import ChannelId
4545
from ._model_utils import pick_model, SkipNone
4646
from ._type_aliases import NonEmptyString
47+
from microsoft_agents.activity.errors import activity_errors
4748

4849
logger = logging.getLogger(__name__)
4950

@@ -218,9 +219,7 @@ def _validate_channel_id(
218219
activity.channel_id.sub_channel
219220
and activity.channel_id.sub_channel != product_info.id
220221
):
221-
raise Exception(
222-
"Conflict between channel_id.sub_channel and productInfo entity"
223-
)
222+
raise Exception(str(activity_errors.ChannelIdProductInfoConflict))
224223
activity.channel_id = ChannelId(
225224
channel=activity.channel_id.channel,
226225
sub_channel=product_info.id,
@@ -256,9 +255,7 @@ def _serialize_sub_channel_data(
256255
# self.channel_id is the source of truth for serialization
257256
if self.channel_id and self.channel_id.sub_channel:
258257
if product_info and product_info.get("id") != self.channel_id.sub_channel:
259-
raise Exception(
260-
"Conflict between channel_id.sub_channel and productInfo entity"
261-
)
258+
raise Exception(str(activity_errors.ChannelIdProductInfoConflict))
262259
elif not product_info:
263260
if not serialized.get("entities"):
264261
serialized["entities"] = []

libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from pydantic_core import CoreSchema, core_schema
99
from pydantic import GetCoreSchemaHandler
1010

11+
from microsoft_agents.activity.errors import activity_errors
12+
1113

1214
class ChannelId(str):
1315
"""A ChannelId represents a channel and optional sub-channel in the format 'channel:sub_channel'."""
@@ -52,14 +54,12 @@ def __new__(
5254
"""
5355
if isinstance(value, str):
5456
if channel or sub_channel:
55-
raise ValueError(
56-
"If value is provided, channel and sub_channel must be None"
57-
)
57+
raise ValueError(str(activity_errors.ChannelIdValueConflict))
5858

5959
value = value.strip()
6060
if value:
6161
return str.__new__(cls, value)
62-
raise TypeError("value must be a non empty string if provided")
62+
raise TypeError(str(activity_errors.ChannelIdValueMustBeNonEmpty))
6363
else:
6464
if (
6565
not isinstance(channel, str)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
"""
5+
Error resources for Microsoft Agents Activity package.
6+
"""
7+
8+
from microsoft_agents.hosting.core.errors import ErrorMessage
9+
10+
from .error_resources import ActivityErrorResources
11+
12+
# Singleton instance
13+
activity_errors = ActivityErrorResources()
14+
15+
__all__ = ["ErrorMessage", "ActivityErrorResources", "activity_errors"]
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
"""
5+
Activity error resources for Microsoft Agents SDK.
6+
7+
Error codes are in the range -64000 to -64999.
8+
"""
9+
10+
from microsoft_agents.hosting.core.errors import ErrorMessage
11+
12+
13+
class ActivityErrorResources:
14+
"""
15+
Error messages for activity operations.
16+
17+
Error codes are organized in the range -64000 to -64999.
18+
"""
19+
20+
InvalidChannelIdType = ErrorMessage(
21+
"Invalid type for channel_id: {0}. Expected ChannelId or str.",
22+
-64000,
23+
"activity-schema",
24+
)
25+
26+
ChannelIdProductInfoConflict = ErrorMessage(
27+
"Conflict between channel_id.sub_channel and productInfo entity",
28+
-64001,
29+
"activity-schema",
30+
)
31+
32+
ChannelIdValueConflict = ErrorMessage(
33+
"If value is provided, channel and sub_channel must be None",
34+
-64002,
35+
"activity-schema",
36+
)
37+
38+
ChannelIdValueMustBeNonEmpty = ErrorMessage(
39+
"value must be a non empty string if provided",
40+
-64003,
41+
"activity-schema",
42+
)
43+
44+
InvalidFromPropertyType = ErrorMessage(
45+
"Invalid type for from_property: {0}. Expected ChannelAccount or dict.",
46+
-64004,
47+
"activity-schema",
48+
)
49+
50+
InvalidRecipientType = ErrorMessage(
51+
"Invalid type for recipient: {0}. Expected ChannelAccount or dict.",
52+
-64005,
53+
"activity-schema",
54+
)
55+
56+
def __init__(self):
57+
"""Initialize ActivityErrorResources."""
58+
pass
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
"""
5+
Error resources for Microsoft Agents Authentication MSAL package.
6+
"""
7+
8+
from microsoft_agents.hosting.core.errors import ErrorMessage
9+
10+
from .error_resources import AuthenticationErrorResources
11+
12+
# Singleton instance
13+
authentication_errors = AuthenticationErrorResources()
14+
15+
__all__ = ["ErrorMessage", "AuthenticationErrorResources", "authentication_errors"]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
"""
5+
Authentication error resources for Microsoft Agents SDK.
6+
7+
Error codes are in the range -60000 to -60999.
8+
"""
9+
10+
from microsoft_agents.hosting.core.errors import ErrorMessage
11+
12+
13+
class AuthenticationErrorResources:
14+
"""
15+
Error messages for authentication operations.
16+
17+
Error codes are organized in the range -60000 to -60999.
18+
"""
19+
20+
FailedToAcquireToken = ErrorMessage(
21+
"Failed to acquire token. {0}",
22+
-60012,
23+
"agentic-identity-with-the-m365-agents-sdk",
24+
)
25+
26+
InvalidInstanceUrl = ErrorMessage(
27+
"Invalid instance URL",
28+
-60013,
29+
"agentic-identity-with-the-m365-agents-sdk",
30+
)
31+
32+
OnBehalfOfFlowNotSupportedManagedIdentity = ErrorMessage(
33+
"On-behalf-of flow is not supported with Managed Identity authentication.",
34+
-60014,
35+
"agentic-identity-with-the-m365-agents-sdk",
36+
)
37+
38+
OnBehalfOfFlowNotSupportedAuthType = ErrorMessage(
39+
"On-behalf-of flow is not supported with the current authentication type: {0}",
40+
-60015,
41+
"agentic-identity-with-the-m365-agents-sdk",
42+
)
43+
44+
AuthenticationTypeNotSupported = ErrorMessage(
45+
"Authentication type not supported",
46+
-60016,
47+
"agentic-identity-with-the-m365-agents-sdk",
48+
)
49+
50+
AgentApplicationInstanceIdRequired = ErrorMessage(
51+
"Agent application instance Id must be provided.",
52+
-60017,
53+
"agentic-identity-with-the-m365-agents-sdk",
54+
)
55+
56+
FailedToAcquireAgenticInstanceToken = ErrorMessage(
57+
"Failed to acquire agentic instance token or agent token for agent_app_instance_id {0}",
58+
-60018,
59+
"agentic-identity-with-the-m365-agents-sdk",
60+
)
61+
62+
AgentApplicationInstanceIdAndUserIdRequired = ErrorMessage(
63+
"Agent application instance Id and agentic user Id must be provided.",
64+
-60019,
65+
"agentic-identity-with-the-m365-agents-sdk",
66+
)
67+
68+
FailedToAcquireInstanceOrAgentToken = ErrorMessage(
69+
"Failed to acquire instance token or agent token for agent_app_instance_id {0} and agentic_user_id {1}",
70+
-60020,
71+
"agentic-identity-with-the-m365-agents-sdk",
72+
)
73+
74+
def __init__(self):
75+
"""Initialize AuthenticationErrorResources."""
76+
pass

libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
AccessTokenProviderBase,
2727
AgentAuthConfiguration,
2828
)
29+
from microsoft_agents.authentication.msal.errors import authentication_errors
2930

3031
logger = logging.getLogger(__name__)
3132

@@ -65,7 +66,7 @@ async def get_access_token(
6566
)
6667
valid_uri, instance_uri = self._uri_validator(resource_url)
6768
if not valid_uri:
68-
raise ValueError("Invalid instance URL")
69+
raise ValueError(str(authentication_errors.InvalidInstanceUrl))
6970

7071
local_scopes = self._resolve_scopes_list(instance_uri, scopes)
7172
self._create_client_application()
@@ -86,7 +87,11 @@ async def get_access_token(
8687
res = auth_result_payload.get("access_token") if auth_result_payload else None
8788
if not res:
8889
logger.error("Failed to acquire token for resource %s", auth_result_payload)
89-
raise ValueError(f"Failed to acquire token. {str(auth_result_payload)}")
90+
raise ValueError(
91+
authentication_errors.FailedToAcquireToken.format(
92+
str(auth_result_payload)
93+
)
94+
)
9095

9196
return res
9297

@@ -106,7 +111,7 @@ async def acquire_token_on_behalf_of(
106111
"Attempted on-behalf-of flow with Managed Identity authentication."
107112
)
108113
raise NotImplementedError(
109-
"On-behalf-of flow is not supported with Managed Identity authentication."
114+
str(authentication_errors.OnBehalfOfFlowNotSupportedManagedIdentity)
110115
)
111116
elif isinstance(self._msal_auth_client, ConfidentialClientApplication):
112117
# TODO: Handling token error / acquisition failed
@@ -123,15 +128,19 @@ async def acquire_token_on_behalf_of(
123128
logger.error(
124129
f"Failed to acquire token on behalf of user: {user_assertion}"
125130
)
126-
raise ValueError(f"Failed to acquire token. {str(token)}")
131+
raise ValueError(
132+
authentication_errors.FailedToAcquireToken.format(str(token))
133+
)
127134

128135
return token["access_token"]
129136

130137
logger.error(
131138
f"On-behalf-of flow is not supported with the current authentication type: {self._msal_auth_client.__class__.__name__}"
132139
)
133140
raise NotImplementedError(
134-
f"On-behalf-of flow is not supported with the current authentication type: {self._msal_auth_client.__class__.__name__}"
141+
authentication_errors.OnBehalfOfFlowNotSupportedAuthType.format(
142+
self._msal_auth_client.__class__.__name__
143+
)
135144
)
136145

137146
def _create_client_application(self) -> None:
@@ -187,7 +196,9 @@ def _create_client_application(self) -> None:
187196
logger.error(
188197
f"Unsupported authentication type: {self._msal_configuration.AUTH_TYPE}"
189198
)
190-
raise NotImplementedError("Authentication type not supported")
199+
raise NotImplementedError(
200+
str(authentication_errors.AuthenticationTypeNotSupported)
201+
)
191202

192203
self._msal_auth_client = ConfidentialClientApplication(
193204
client_id=self._msal_configuration.CLIENT_ID,
@@ -233,7 +244,9 @@ async def get_agentic_application_token(
233244
"""
234245

235246
if not agent_app_instance_id:
236-
raise ValueError("Agent application instance Id must be provided.")
247+
raise ValueError(
248+
str(authentication_errors.AgentApplicationInstanceIdRequired)
249+
)
237250

238251
logger.info(
239252
"Attempting to get agentic application token from agent_app_instance_id %s",
@@ -267,7 +280,9 @@ async def get_agentic_instance_token(
267280
"""
268281

269282
if not agent_app_instance_id:
270-
raise ValueError("Agent application instance Id must be provided.")
283+
raise ValueError(
284+
str(authentication_errors.AgentApplicationInstanceIdRequired)
285+
)
271286

272287
logger.info(
273288
"Attempting to get agentic instance token from agent_app_instance_id %s",
@@ -283,7 +298,9 @@ async def get_agentic_instance_token(
283298
agent_app_instance_id,
284299
)
285300
raise Exception(
286-
f"Failed to acquire agentic instance token or agent token for agent_app_instance_id {agent_app_instance_id}"
301+
authentication_errors.FailedToAcquireAgenticInstanceToken.format(
302+
agent_app_instance_id
303+
)
287304
)
288305

289306
authority = (
@@ -306,7 +323,9 @@ async def get_agentic_instance_token(
306323
agent_app_instance_id,
307324
)
308325
raise Exception(
309-
f"Failed to acquire agentic instance token or agent token for agent_app_instance_id {agent_app_instance_id}"
326+
authentication_errors.FailedToAcquireAgenticInstanceToken.format(
327+
agent_app_instance_id
328+
)
310329
)
311330

312331
# future scenario where we don't know the blueprint id upfront
@@ -316,7 +335,11 @@ async def get_agentic_instance_token(
316335
logger.error(
317336
"Failed to acquire agentic instance token, %s", agentic_instance_token
318337
)
319-
raise ValueError(f"Failed to acquire token. {str(agentic_instance_token)}")
338+
raise ValueError(
339+
authentication_errors.FailedToAcquireToken.format(
340+
str(agentic_instance_token)
341+
)
342+
)
320343

321344
logger.debug(
322345
"Agentic blueprint id: %s",
@@ -345,7 +368,7 @@ async def get_agentic_user_token(
345368
"""
346369
if not agent_app_instance_id or not agentic_user_id:
347370
raise ValueError(
348-
"Agent application instance Id and agentic user Id must be provided."
371+
str(authentication_errors.AgentApplicationInstanceIdAndUserIdRequired)
349372
)
350373

351374
logger.info(
@@ -364,7 +387,9 @@ async def get_agentic_user_token(
364387
agentic_user_id,
365388
)
366389
raise Exception(
367-
f"Failed to acquire instance token or agent token for agent_app_instance_id {agent_app_instance_id} and agentic_user_id {agentic_user_id}"
390+
authentication_errors.FailedToAcquireInstanceOrAgentToken.format(
391+
agent_app_instance_id, agentic_user_id
392+
)
368393
)
369394

370395
authority = (

0 commit comments

Comments
 (0)