From 745ffec6dfb60c0795abe43e27723fe8bd515708 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:20:25 +0000 Subject: [PATCH 01/16] Initial plan From 3fb85ffab7e93781a5d387e96a64c0017e764bcd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:36:41 +0000 Subject: [PATCH 02/16] Add error resources infrastructure and refactor authentication-msal errors Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../authentication/msal/msal_auth.py | 31 +- .../microsoft_agents/hosting/core/__init__.py | 6 + .../hosting/core/errors/__init__.py | 17 + .../hosting/core/errors/error_message.py | 68 +++ .../hosting/core/errors/error_resources.py | 503 ++++++++++++++++++ tests/hosting_core/errors/__init__.py | 2 + .../errors/test_error_resources.py | 161 ++++++ 7 files changed, 775 insertions(+), 13 deletions(-) create mode 100644 libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/__init__.py create mode 100644 libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_message.py create mode 100644 libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py create mode 100644 tests/hosting_core/errors/__init__.py create mode 100644 tests/hosting_core/errors/test_error_resources.py diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py index 956caf04..fdb9e672 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py @@ -25,6 +25,7 @@ AuthTypes, AccessTokenProviderBase, AgentAuthConfiguration, + error_resources, ) logger = logging.getLogger(__name__) @@ -65,7 +66,7 @@ async def get_access_token( ) valid_uri, instance_uri = self._uri_validator(resource_url) if not valid_uri: - raise ValueError("Invalid instance URL") + raise ValueError(str(error_resources.InvalidInstanceUrl)) local_scopes = self._resolve_scopes_list(instance_uri, scopes) self._create_client_application() @@ -86,7 +87,7 @@ async def get_access_token( res = auth_result_payload.get("access_token") if auth_result_payload else None if not res: logger.error("Failed to acquire token for resource %s", auth_result_payload) - raise ValueError(f"Failed to acquire token. {str(auth_result_payload)}") + raise ValueError(error_resources.FailedToAcquireToken.format(str(auth_result_payload))) return res @@ -106,7 +107,7 @@ async def acquire_token_on_behalf_of( "Attempted on-behalf-of flow with Managed Identity authentication." ) raise NotImplementedError( - "On-behalf-of flow is not supported with Managed Identity authentication." + str(error_resources.OnBehalfOfFlowNotSupportedManagedIdentity) ) elif isinstance(self._msal_auth_client, ConfidentialClientApplication): # TODO: Handling token error / acquisition failed @@ -123,7 +124,7 @@ async def acquire_token_on_behalf_of( logger.error( f"Failed to acquire token on behalf of user: {user_assertion}" ) - raise ValueError(f"Failed to acquire token. {str(token)}") + raise ValueError(error_resources.FailedToAcquireToken.format(str(token))) return token["access_token"] @@ -131,7 +132,9 @@ async def acquire_token_on_behalf_of( f"On-behalf-of flow is not supported with the current authentication type: {self._msal_auth_client.__class__.__name__}" ) raise NotImplementedError( - f"On-behalf-of flow is not supported with the current authentication type: {self._msal_auth_client.__class__.__name__}" + error_resources.OnBehalfOfFlowNotSupportedAuthType.format( + self._msal_auth_client.__class__.__name__ + ) ) def _create_client_application(self) -> None: @@ -187,7 +190,7 @@ def _create_client_application(self) -> None: logger.error( f"Unsupported authentication type: {self._msal_configuration.AUTH_TYPE}" ) - raise NotImplementedError("Authentication type not supported") + raise NotImplementedError(str(error_resources.AuthenticationTypeNotSupported)) self._msal_auth_client = ConfidentialClientApplication( client_id=self._msal_configuration.CLIENT_ID, @@ -233,7 +236,7 @@ async def get_agentic_application_token( """ if not agent_app_instance_id: - raise ValueError("Agent application instance Id must be provided.") + raise ValueError(str(error_resources.AgentApplicationInstanceIdRequired)) logger.info( "Attempting to get agentic application token from agent_app_instance_id %s", @@ -267,7 +270,7 @@ async def get_agentic_instance_token( """ if not agent_app_instance_id: - raise ValueError("Agent application instance Id must be provided.") + raise ValueError(str(error_resources.AgentApplicationInstanceIdRequired)) logger.info( "Attempting to get agentic instance token from agent_app_instance_id %s", @@ -283,7 +286,7 @@ async def get_agentic_instance_token( agent_app_instance_id, ) raise Exception( - f"Failed to acquire agentic instance token or agent token for agent_app_instance_id {agent_app_instance_id}" + error_resources.FailedToAcquireAgenticInstanceToken.format(agent_app_instance_id) ) authority = ( @@ -306,7 +309,7 @@ async def get_agentic_instance_token( agent_app_instance_id, ) raise Exception( - f"Failed to acquire agentic instance token or agent token for agent_app_instance_id {agent_app_instance_id}" + error_resources.FailedToAcquireAgenticInstanceToken.format(agent_app_instance_id) ) # future scenario where we don't know the blueprint id upfront @@ -316,7 +319,7 @@ async def get_agentic_instance_token( logger.error( "Failed to acquire agentic instance token, %s", agentic_instance_token ) - raise ValueError(f"Failed to acquire token. {str(agentic_instance_token)}") + raise ValueError(error_resources.FailedToAcquireToken.format(str(agentic_instance_token))) logger.debug( "Agentic blueprint id: %s", @@ -345,7 +348,7 @@ async def get_agentic_user_token( """ if not agent_app_instance_id or not agentic_user_id: raise ValueError( - "Agent application instance Id and agentic user Id must be provided." + str(error_resources.AgentApplicationInstanceIdAndUserIdRequired) ) logger.info( @@ -364,7 +367,9 @@ async def get_agentic_user_token( agentic_user_id, ) raise Exception( - 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}" + error_resources.FailedToAcquireInstanceOrAgentToken.format( + agent_app_instance_id, agentic_user_id + ) ) authority = ( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/__init__.py index 90d6f0ec..9abce32c 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/__init__.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/__init__.py @@ -82,6 +82,9 @@ from .storage import Storage from .storage.memory_storage import MemoryStorage +# Error Resources +from .errors import error_resources, ErrorMessage, ErrorResources + # Define the package's public interface __all__ = [ @@ -148,4 +151,7 @@ "MemoryStorage", "AgenticUserAuthorization", "Authorization", + "error_resources", + "ErrorMessage", + "ErrorResources", ] diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/__init__.py new file mode 100644 index 00000000..d072fbd3 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents SDK. + +This module provides centralized error messages with error codes and help URLs +following the pattern established in the C# SDK. +""" + +from .error_message import ErrorMessage +from .error_resources import ErrorResources + +# Singleton instance +error_resources = ErrorResources() + +__all__ = ["ErrorMessage", "ErrorResources", "error_resources"] diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_message.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_message.py new file mode 100644 index 00000000..fd07a9c7 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_message.py @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +ErrorMessage class for formatting error messages with error codes and help URLs. +""" + + +class ErrorMessage: + """ + Represents a formatted error message with error code and help URL. + + This class formats error messages according to the Microsoft Agents SDK pattern: + - Original error message + - Error Code: [negative number] + - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor + """ + + def __init__( + self, + message_template: str, + error_code: int, + help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", + ): + """ + Initialize an ErrorMessage. + + :param message_template: The error message template (may include format placeholders) + :type message_template: str + :param error_code: The error code (should be negative) + :type error_code: int + :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) + :type help_url_anchor: str + """ + self.message_template = message_template + self.error_code = error_code + self.help_url_anchor = help_url_anchor + self.base_url = "https://aka.ms/M365AgentsErrorCodes" + + def format(self, *args, **kwargs) -> str: + """ + Format the error message with the provided arguments. + + :param args: Positional arguments for string formatting + :param kwargs: Keyword arguments for string formatting + :return: Formatted error message with error code and help URL + :rtype: str + """ + # Format the main message + if args or kwargs: + message = self.message_template.format(*args, **kwargs) + else: + message = self.message_template + + # Append error code and help URL + return ( + f"{message}\n\n" + f"Error Code: {self.error_code}\n" + f"Help URL: {self.base_url}/#{self.help_url_anchor}" + ) + + def __str__(self) -> str: + """Return the formatted error message without any arguments.""" + return self.format() + + def __repr__(self) -> str: + """Return a representation of the ErrorMessage.""" + return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py new file mode 100644 index 00000000..b20b35c0 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py @@ -0,0 +1,503 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Central error resource file for Microsoft Agents SDK. + +This module contains all error messages used throughout the SDK, each with: +- A formatting string +- An error code (aligned with C# SDK pattern) +- A help URL anchor + +Error codes are negative integers following the Microsoft Agents SDK convention. +""" + +from .error_message import ErrorMessage + + +class ErrorResources: + """ + Central repository of all error messages used in the Microsoft Agents SDK. + + Error codes are organized by range: + - -60000 to -60099: Authentication errors + - -60100 to -60199: Storage errors + - -60200 to -60299: Teams-specific errors + - -60300 to -60399: Hosting errors + - -60400 to -60499: Activity errors + - -60500 to -60599: Copilot Studio errors + - -60600 to -60699: General/validation errors + """ + + # Authentication Errors (-60000 to -60099) + FailedToAcquireToken = ErrorMessage( + "Failed to acquire token. {0}", + -60012, + "agentic-identity-with-the-m365-agents-sdk", + ) + + InvalidInstanceUrl = ErrorMessage( + "Invalid instance URL", + -60013, + "agentic-identity-with-the-m365-agents-sdk", + ) + + OnBehalfOfFlowNotSupportedManagedIdentity = ErrorMessage( + "On-behalf-of flow is not supported with Managed Identity authentication.", + -60014, + "agentic-identity-with-the-m365-agents-sdk", + ) + + OnBehalfOfFlowNotSupportedAuthType = ErrorMessage( + "On-behalf-of flow is not supported with the current authentication type: {0}", + -60015, + "agentic-identity-with-the-m365-agents-sdk", + ) + + AuthenticationTypeNotSupported = ErrorMessage( + "Authentication type not supported", + -60016, + "agentic-identity-with-the-m365-agents-sdk", + ) + + AgentApplicationInstanceIdRequired = ErrorMessage( + "Agent application instance Id must be provided.", + -60017, + "agentic-identity-with-the-m365-agents-sdk", + ) + + FailedToAcquireAgenticInstanceToken = ErrorMessage( + "Failed to acquire agentic instance token or agent token for agent_app_instance_id {0}", + -60018, + "agentic-identity-with-the-m365-agents-sdk", + ) + + AgentApplicationInstanceIdAndUserIdRequired = ErrorMessage( + "Agent application instance Id and agentic user Id must be provided.", + -60019, + "agentic-identity-with-the-m365-agents-sdk", + ) + + FailedToAcquireInstanceOrAgentToken = ErrorMessage( + "Failed to acquire instance token or agent token for agent_app_instance_id {0} and agentic_user_id {1}", + -60020, + "agentic-identity-with-the-m365-agents-sdk", + ) + + # Storage Errors (-60100 to -60199) + CosmosDbConfigRequired = ErrorMessage( + "CosmosDBStorage: CosmosDBConfig is required.", + -60100, + "storage-configuration", + ) + + CosmosDbEndpointRequired = ErrorMessage( + "CosmosDBStorage: cosmos_db_endpoint is required.", + -60101, + "storage-configuration", + ) + + CosmosDbAuthKeyRequired = ErrorMessage( + "CosmosDBStorage: auth_key is required.", + -60102, + "storage-configuration", + ) + + CosmosDbDatabaseIdRequired = ErrorMessage( + "CosmosDBStorage: database_id is required.", + -60103, + "storage-configuration", + ) + + CosmosDbContainerIdRequired = ErrorMessage( + "CosmosDBStorage: container_id is required.", + -60104, + "storage-configuration", + ) + + CosmosDbKeyCannotBeEmpty = ErrorMessage( + "CosmosDBStorage: Key cannot be empty.", + -60105, + "storage-configuration", + ) + + CosmosDbPartitionKeyInvalid = ErrorMessage( + "CosmosDBStorage: PartitionKey of {0} cannot be used with a CosmosDbPartitionedStorageOptions.PartitionKey of {1}.", + -60106, + "storage-configuration", + ) + + CosmosDbPartitionKeyPathInvalid = ErrorMessage( + "CosmosDBStorage: PartitionKeyPath must match cosmosDbPartitionedStorageOptions value of {0}", + -60107, + "storage-configuration", + ) + + CosmosDbCompatibilityModeRequired = ErrorMessage( + "CosmosDBStorage: compatibilityMode cannot be set when using partitionKey options.", + -60108, + "storage-configuration", + ) + + CosmosDbPartitionKeyNotFound = ErrorMessage( + "CosmosDBStorage: Partition key '{0}' missing from state, you may be missing custom state implementation.", + -60109, + "storage-configuration", + ) + + CosmosDbInvalidPartitionKeyValue = ErrorMessage( + "CosmosDBStorage: Invalid PartitionKey property on item with id {0}", + -60110, + "storage-configuration", + ) + + BlobStorageConfigRequired = ErrorMessage( + "BlobStorage: BlobStorageConfig is required.", + -60120, + "storage-configuration", + ) + + BlobConnectionStringOrUrlRequired = ErrorMessage( + "BlobStorage: either connection_string or container_url is required.", + -60121, + "storage-configuration", + ) + + BlobContainerNameRequired = ErrorMessage( + "BlobStorage: container_name is required.", + -60122, + "storage-configuration", + ) + + StorageKeyCannotBeEmpty = ErrorMessage( + "Storage: Key cannot be empty.", + -60130, + "storage-configuration", + ) + + StorageInvalidJsonBlob = ErrorMessage( + "Storage: Blob {0} could not be decoded as JSON. {1}", + -60131, + "storage-configuration", + ) + + # Teams Errors (-60200 to -60299) + TeamsBadRequest = ErrorMessage( + "BadRequest", + -60200, + "teams-integration", + ) + + TeamsNotImplemented = ErrorMessage( + "NotImplemented", + -60201, + "teams-integration", + ) + + TeamsContextRequired = ErrorMessage( + "context is required.", + -60202, + "teams-integration", + ) + + TeamsMeetingIdRequired = ErrorMessage( + "meeting_id is required.", + -60203, + "teams-integration", + ) + + TeamsParticipantIdRequired = ErrorMessage( + "participant_id is required.", + -60204, + "teams-integration", + ) + + TeamsTeamIdRequired = ErrorMessage( + "team_id is required.", + -60205, + "teams-integration", + ) + + TeamsTurnContextRequired = ErrorMessage( + "TurnContext cannot be None", + -60206, + "teams-integration", + ) + + TeamsActivityRequired = ErrorMessage( + "Activity cannot be None", + -60207, + "teams-integration", + ) + + TeamsChannelIdRequired = ErrorMessage( + "The teams_channel_id cannot be None or empty", + -60208, + "teams-integration", + ) + + TeamsConversationIdRequired = ErrorMessage( + "conversation_id is required.", + -60209, + "teams-integration", + ) + + # Hosting Errors (-60300 to -60399) + AdapterRequired = ErrorMessage( + "start_agent_process: adapter can't be None", + -60300, + "hosting-configuration", + ) + + AgentApplicationRequired = ErrorMessage( + "start_agent_process: agent_application can't be None", + -60301, + "hosting-configuration", + ) + + RequestRequired = ErrorMessage( + "CloudAdapter.process: request can't be None", + -60302, + "hosting-configuration", + ) + + AgentRequired = ErrorMessage( + "CloudAdapter.process: agent can't be None", + -60303, + "hosting-configuration", + ) + + StreamAlreadyEnded = ErrorMessage( + "The stream has already ended.", + -60304, + "streaming", + ) + + TurnContextRequired = ErrorMessage( + "TurnContext cannot be None.", + -60305, + "hosting-configuration", + ) + + ActivityRequired = ErrorMessage( + "Activity cannot be None.", + -60306, + "hosting-configuration", + ) + + AppIdRequired = ErrorMessage( + "AppId cannot be empty or None.", + -60307, + "hosting-configuration", + ) + + InvalidActivityType = ErrorMessage( + "Invalid or missing activity type.", + -60308, + "hosting-configuration", + ) + + ConversationIdRequired = ErrorMessage( + "Conversation ID cannot be empty or None.", + -60309, + "hosting-configuration", + ) + + AuthHeaderRequired = ErrorMessage( + "Authorization header is required.", + -60310, + "hosting-configuration", + ) + + InvalidAuthHeader = ErrorMessage( + "Invalid authorization header format.", + -60311, + "hosting-configuration", + ) + + ClaimsIdentityRequired = ErrorMessage( + "ClaimsIdentity is required.", + -60312, + "hosting-configuration", + ) + + ChannelServiceRouteNotFound = ErrorMessage( + "Channel service route not found for: {0}", + -60313, + "hosting-configuration", + ) + + TokenExchangeRequired = ErrorMessage( + "Token exchange requires a token exchange resource.", + -60314, + "hosting-configuration", + ) + + MissingHttpClient = ErrorMessage( + "HTTP client is required.", + -60315, + "hosting-configuration", + ) + + InvalidBotFrameworkActivity = ErrorMessage( + "Invalid Bot Framework Activity format.", + -60316, + "hosting-configuration", + ) + + CredentialsRequired = ErrorMessage( + "Credentials are required for authentication.", + -60317, + "hosting-configuration", + ) + + # Activity Errors (-60400 to -60499) + InvalidChannelIdType = ErrorMessage( + "Invalid type for channel_id: {0}. Expected ChannelId or str.", + -60400, + "activity-schema", + ) + + ChannelIdProductInfoConflict = ErrorMessage( + "Conflict between channel_id.sub_channel and productInfo entity", + -60401, + "activity-schema", + ) + + ChannelIdValueConflict = ErrorMessage( + "If value is provided, channel and sub_channel must be None", + -60402, + "activity-schema", + ) + + ChannelIdValueMustBeNonEmpty = ErrorMessage( + "value must be a non empty string if provided", + -60403, + "activity-schema", + ) + + InvalidFromPropertyType = ErrorMessage( + "Invalid type for from_property: {0}. Expected ChannelAccount or dict.", + -60404, + "activity-schema", + ) + + InvalidRecipientType = ErrorMessage( + "Invalid type for recipient: {0}. Expected ChannelAccount or dict.", + -60405, + "activity-schema", + ) + + # Copilot Studio Errors (-60500 to -60599) + CloudBaseAddressRequired = ErrorMessage( + "cloud_base_address must be provided when PowerPlatformCloud is Other", + -60500, + "copilot-studio-client", + ) + + EnvironmentIdRequired = ErrorMessage( + "EnvironmentId must be provided", + -60501, + "copilot-studio-client", + ) + + AgentIdentifierRequired = ErrorMessage( + "AgentIdentifier must be provided", + -60502, + "copilot-studio-client", + ) + + CustomCloudOrBaseAddressRequired = ErrorMessage( + "Either CustomPowerPlatformCloud or cloud_base_address must be provided when PowerPlatformCloud is Other", + -60503, + "copilot-studio-client", + ) + + InvalidConnectionSettingsType = ErrorMessage( + "connection_settings must be of type DirectToEngineConnectionSettings", + -60504, + "copilot-studio-client", + ) + + PowerPlatformEnvironmentRequired = ErrorMessage( + "PowerPlatformEnvironment must be provided", + -60505, + "copilot-studio-client", + ) + + AccessTokenProviderRequired = ErrorMessage( + "AccessTokenProvider must be provided", + -60506, + "copilot-studio-client", + ) + + # General/Validation Errors (-60600 to -60699) + InvalidConfiguration = ErrorMessage( + "Invalid configuration: {0}", + -60600, + "configuration", + ) + + RequiredParameterMissing = ErrorMessage( + "Required parameter missing: {0}", + -60601, + "configuration", + ) + + InvalidParameterValue = ErrorMessage( + "Invalid parameter value for {0}: {1}", + -60602, + "configuration", + ) + + OperationNotSupported = ErrorMessage( + "Operation not supported: {0}", + -60603, + "configuration", + ) + + ResourceNotFound = ErrorMessage( + "Resource not found: {0}", + -60604, + "configuration", + ) + + UnexpectedError = ErrorMessage( + "An unexpected error occurred: {0}", + -60605, + "configuration", + ) + + InvalidStateObject = ErrorMessage( + "Invalid state object: {0}", + -60606, + "configuration", + ) + + SerializationError = ErrorMessage( + "Serialization error: {0}", + -60607, + "configuration", + ) + + DeserializationError = ErrorMessage( + "Deserialization error: {0}", + -60608, + "configuration", + ) + + TimeoutError = ErrorMessage( + "Operation timed out: {0}", + -60609, + "configuration", + ) + + NetworkError = ErrorMessage( + "Network error occurred: {0}", + -60610, + "configuration", + ) + + def __init__(self): + """Initialize ErrorResources singleton.""" + pass diff --git a/tests/hosting_core/errors/__init__.py b/tests/hosting_core/errors/__init__.py new file mode 100644 index 00000000..5b7f7a92 --- /dev/null +++ b/tests/hosting_core/errors/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/tests/hosting_core/errors/test_error_resources.py b/tests/hosting_core/errors/test_error_resources.py new file mode 100644 index 00000000..86f1fa3f --- /dev/null +++ b/tests/hosting_core/errors/test_error_resources.py @@ -0,0 +1,161 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Tests for error resources and error message formatting. +""" + +import pytest +from microsoft_agents.hosting.core.errors import ( + ErrorMessage, + ErrorResources, + error_resources, +) + + +class TestErrorMessage: + """Tests for ErrorMessage class.""" + + def test_error_message_basic(self): + """Test basic error message creation.""" + error = ErrorMessage("Test error message", -60000, "test-anchor") + assert error.message_template == "Test error message" + assert error.error_code == -60000 + assert error.help_url_anchor == "test-anchor" + + def test_error_message_format_no_args(self): + """Test formatting error message without arguments.""" + error = ErrorMessage("Simple error", -60001, "test") + formatted = error.format() + assert "Simple error" in formatted + assert "Error Code: -60001" in formatted + assert "Help URL: https://aka.ms/M365AgentsErrorCodes/#test" in formatted + + def test_error_message_format_with_positional_args(self): + """Test formatting error message with positional arguments.""" + error = ErrorMessage("Error with {0} and {1}", -60002, "test") + formatted = error.format("arg1", "arg2") + assert "Error with arg1 and arg2" in formatted + assert "Error Code: -60002" in formatted + + def test_error_message_format_with_keyword_args(self): + """Test formatting error message with keyword arguments.""" + error = ErrorMessage("Error with {name} and {value}", -60003, "test") + formatted = error.format(name="test_name", value="test_value") + assert "Error with test_name and test_value" in formatted + assert "Error Code: -60003" in formatted + + def test_error_message_str(self): + """Test string representation of error message.""" + error = ErrorMessage("Test error", -60004, "test") + str_repr = str(error) + assert "Test error" in str_repr + assert "Error Code: -60004" in str_repr + + def test_error_message_repr(self): + """Test repr of error message.""" + error = ErrorMessage("Test error message", -60005, "test") + repr_str = repr(error) + assert "ErrorMessage" in repr_str + assert "-60005" in repr_str + + +class TestErrorResources: + """Tests for ErrorResources class.""" + + def test_error_resources_singleton(self): + """Test that error_resources is accessible.""" + assert error_resources is not None + assert isinstance(error_resources, ErrorResources) + + def test_authentication_errors_exist(self): + """Test that authentication errors are defined.""" + assert hasattr(error_resources, "FailedToAcquireToken") + assert hasattr(error_resources, "InvalidInstanceUrl") + assert hasattr(error_resources, "OnBehalfOfFlowNotSupportedManagedIdentity") + + def test_storage_errors_exist(self): + """Test that storage errors are defined.""" + assert hasattr(error_resources, "CosmosDbConfigRequired") + assert hasattr(error_resources, "CosmosDbEndpointRequired") + assert hasattr(error_resources, "StorageKeyCannotBeEmpty") + + def test_teams_errors_exist(self): + """Test that teams errors are defined.""" + assert hasattr(error_resources, "TeamsBadRequest") + assert hasattr(error_resources, "TeamsContextRequired") + assert hasattr(error_resources, "TeamsMeetingIdRequired") + + def test_hosting_errors_exist(self): + """Test that hosting errors are defined.""" + assert hasattr(error_resources, "AdapterRequired") + assert hasattr(error_resources, "AgentApplicationRequired") + assert hasattr(error_resources, "StreamAlreadyEnded") + + def test_activity_errors_exist(self): + """Test that activity errors are defined.""" + assert hasattr(error_resources, "InvalidChannelIdType") + assert hasattr(error_resources, "ChannelIdProductInfoConflict") + + def test_copilot_studio_errors_exist(self): + """Test that copilot studio errors are defined.""" + assert hasattr(error_resources, "CloudBaseAddressRequired") + assert hasattr(error_resources, "EnvironmentIdRequired") + + def test_general_errors_exist(self): + """Test that general errors are defined.""" + assert hasattr(error_resources, "InvalidConfiguration") + assert hasattr(error_resources, "RequiredParameterMissing") + + def test_error_code_ranges(self): + """Test that error codes are in expected ranges.""" + # Authentication errors: -60000 to -60099 + assert -60099 <= error_resources.FailedToAcquireToken.error_code <= -60000 + + # Storage errors: -60100 to -60199 + assert -60199 <= error_resources.CosmosDbConfigRequired.error_code <= -60100 + + # Teams errors: -60200 to -60299 + assert -60299 <= error_resources.TeamsBadRequest.error_code <= -60200 + + # Hosting errors: -60300 to -60399 + assert -60399 <= error_resources.AdapterRequired.error_code <= -60300 + + # Activity errors: -60400 to -60499 + assert -60499 <= error_resources.InvalidChannelIdType.error_code <= -60400 + + # Copilot Studio errors: -60500 to -60599 + assert -60599 <= error_resources.CloudBaseAddressRequired.error_code <= -60500 + + # General errors: -60600 to -60699 + assert -60699 <= error_resources.InvalidConfiguration.error_code <= -60600 + + def test_failed_to_acquire_token_format(self): + """Test FailedToAcquireToken error formatting.""" + error = error_resources.FailedToAcquireToken + formatted = error.format("test_payload") + assert "Failed to acquire token. test_payload" in formatted + assert "Error Code: -60012" in formatted + assert "agentic-identity-with-the-m365-agents-sdk" in formatted + + def test_cosmos_db_config_required(self): + """Test CosmosDbConfigRequired error formatting.""" + error = error_resources.CosmosDbConfigRequired + formatted = str(error) + assert "CosmosDBStorage: CosmosDBConfig is required." in formatted + assert "Error Code: -60100" in formatted + + def test_teams_context_required(self): + """Test TeamsContextRequired error formatting.""" + error = error_resources.TeamsContextRequired + formatted = str(error) + assert "context is required." in formatted + assert "Error Code: -60202" in formatted + + def test_error_with_multiple_arguments(self): + """Test error with multiple format arguments.""" + error = error_resources.CosmosDbPartitionKeyInvalid + formatted = error.format("key1", "key2") + assert "key1" in formatted + assert "key2" in formatted + assert "Error Code: -60106" in formatted From 35e5905823f85eb2cb2bbf41c69c85d409bcd5f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:41:53 +0000 Subject: [PATCH 03/16] Refactor CosmosDB storage errors to use error resources Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../storage/cosmos/cosmos_db_storage.py | 16 ++++++++++------ .../storage/cosmos/cosmos_db_storage_config.py | 17 ++++++++++------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage.py index fdd4f6f8..cc2fdc2f 100644 --- a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage.py +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage.py @@ -20,6 +20,7 @@ from microsoft_agents.hosting.core.storage import AsyncStorageBase, StoreItem from microsoft_agents.hosting.core.storage._type_aliases import JSON from microsoft_agents.hosting.core.storage.error_handling import ignore_error +from microsoft_agents.hosting.core import error_resources from .cosmos_db_storage_config import CosmosDBStorageConfig from .key_ops import sanitize_key @@ -55,7 +56,9 @@ def _create_client(self) -> CosmosClient: if self._config.url: if not self._config.credential: raise ValueError( - "CosmosDBStorage: Credential is required when using a custom service URL." + error_resources.InvalidConfiguration.format( + "Credential is required when using a custom service URL" + ) ) return CosmosClient( account_url=self._config.url, credential=self._config.credential @@ -89,7 +92,7 @@ async def _read_item( ) -> tuple[Union[str, None], Union[StoreItemT, None]]: if key == "": - raise ValueError("CosmosDBStorage: Key cannot be empty.") + raise ValueError(str(error_resources.CosmosDbKeyCannotBeEmpty)) escaped_key: str = self._sanitize(key) read_item_response: CosmosDict = await ignore_error( @@ -106,7 +109,7 @@ async def _read_item( async def _write_item(self, key: str, item: StoreItem) -> None: if key == "": - raise ValueError("CosmosDBStorage: Key cannot be empty.") + raise ValueError(str(error_resources.CosmosDbKeyCannotBeEmpty)) escaped_key: str = self._sanitize(key) @@ -119,7 +122,7 @@ async def _write_item(self, key: str, item: StoreItem) -> None: async def _delete_item(self, key: str) -> None: if key == "": - raise ValueError("CosmosDBStorage: Key cannot be empty.") + raise ValueError(str(error_resources.CosmosDbKeyCannotBeEmpty)) escaped_key: str = self._sanitize(key) @@ -156,8 +159,9 @@ async def _create_container(self) -> None: self._compatability_mode_partition_key = True elif "/id" not in paths: raise Exception( - f"Custom Partition Key Paths are not supported. {self._config.container_id} " - "has a custom Partition Key Path of {paths[0]}." + error_resources.InvalidConfiguration.format( + f"Custom Partition Key Paths are not supported. {self._config.container_id} has a custom Partition Key Path of {paths[0]}." + ) ) else: raise err diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py index a5476e64..6c4d73b8 100644 --- a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py @@ -2,6 +2,7 @@ from typing import Union from azure.core.credentials import TokenCredential +from microsoft_agents.hosting.core import error_resources from .key_ops import sanitize_key @@ -69,15 +70,15 @@ def validate_cosmos_db_config(config: "CosmosDBStorageConfig") -> None: This is used prior to the creation of the CosmosDBStorage object.""" if not config: - raise ValueError("CosmosDBStorage: CosmosDBConfig is required.") + raise ValueError(str(error_resources.CosmosDbConfigRequired)) if not config.cosmos_db_endpoint: - raise ValueError("CosmosDBStorage: cosmos_db_endpoint is required.") + raise ValueError(str(error_resources.CosmosDbEndpointRequired)) if not config.auth_key: - raise ValueError("CosmosDBStorage: auth_key is required.") + raise ValueError(str(error_resources.CosmosDbAuthKeyRequired)) if not config.database_id: - raise ValueError("CosmosDBStorage: database_id is required.") + raise ValueError(str(error_resources.CosmosDbDatabaseIdRequired)) if not config.container_id: - raise ValueError("CosmosDBStorage: container_id is required.") + raise ValueError(str(error_resources.CosmosDbContainerIdRequired)) CosmosDBStorageConfig._validate_suffix(config) @@ -86,10 +87,12 @@ def _validate_suffix(config: "CosmosDBStorageConfig") -> None: if config.key_suffix: if config.compatibility_mode: raise ValueError( - "compatibilityMode cannot be true while using a keySuffix." + error_resources.CosmosDbCompatibilityModeRequired.format() ) suffix_escaped: str = sanitize_key(config.key_suffix) if suffix_escaped != config.key_suffix: raise ValueError( - f"Cannot use invalid Row Key characters: {config.key_suffix} in keySuffix." + error_resources.InvalidParameterValue.format( + "keySuffix", config.key_suffix + ) ) From 076fb6796ea52ef5ad2cdb866cfc933b3de3b94e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:45:58 +0000 Subject: [PATCH 04/16] Refactor blob storage and Teams errors to use error resources Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../hosting/teams/teams_activity_handler.py | 56 +++++++++---------- .../hosting/teams/teams_info.py | 46 +++++++-------- .../storage/blob/blob_storage.py | 7 ++- 3 files changed, 56 insertions(+), 53 deletions(-) diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_activity_handler.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_activity_handler.py index 17a3d78e..d811dc18 100644 --- a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_activity_handler.py +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_activity_handler.py @@ -6,7 +6,7 @@ from http import HTTPStatus from typing import Any, List -from microsoft_agents.hosting.core import ActivityHandler, TurnContext +from microsoft_agents.hosting.core import ActivityHandler, TurnContext, error_resources from microsoft_agents.activity import ( InvokeResponse, ChannelAccount, @@ -155,9 +155,9 @@ async def on_invoke_activity(self, turn_context: TurnContext) -> InvokeResponse: else: return await super().on_invoke_activity(turn_context) except Exception as err: - if str(err) == "NotImplemented": + if str(err) == str(error_resources.TeamsNotImplemented): return InvokeResponse(status=int(HTTPStatus.NOT_IMPLEMENTED)) - elif str(err) == "BadRequest": + elif str(err) == str(error_resources.TeamsBadRequest): return InvokeResponse(status=int(HTTPStatus.BAD_REQUEST)) raise @@ -170,7 +170,7 @@ async def on_teams_card_action_invoke( :param turn_context: The context object for the turn. :return: An InvokeResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_config_fetch( self, turn_context: TurnContext, config_data: Any @@ -182,7 +182,7 @@ async def on_teams_config_fetch( :param config_data: The config data. :return: A ConfigResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_config_submit( self, turn_context: TurnContext, config_data: Any @@ -194,7 +194,7 @@ async def on_teams_config_submit( :param config_data: The config data. :return: A ConfigResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_file_consent( self, @@ -217,7 +217,7 @@ async def on_teams_file_consent( turn_context, file_consent_card_response ) else: - raise ValueError("BadRequest") + raise ValueError(str(error_resources.TeamsBadRequest)) async def on_teams_file_consent_accept( self, @@ -231,7 +231,7 @@ async def on_teams_file_consent_accept( :param file_consent_card_response: The file consent card response. :return: None """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_file_consent_decline( self, @@ -245,7 +245,7 @@ async def on_teams_file_consent_decline( :param file_consent_card_response: The file consent card response. :return: None """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_o365_connector_card_action( self, turn_context: TurnContext, query: O365ConnectorCardActionQuery @@ -257,7 +257,7 @@ async def on_teams_o365_connector_card_action( :param query: The O365 connector card action query. :return: None """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_signin_verify_state( self, turn_context: TurnContext, query: SigninStateVerificationQuery @@ -269,7 +269,7 @@ async def on_teams_signin_verify_state( :param query: The sign-in state verification query. :return: None """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_signin_token_exchange( self, turn_context: TurnContext, query: SigninStateVerificationQuery @@ -281,7 +281,7 @@ async def on_teams_signin_token_exchange( :param query: The sign-in state verification query. :return: None """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_app_based_link_query( self, turn_context: TurnContext, query: AppBasedLinkQuery @@ -293,7 +293,7 @@ async def on_teams_app_based_link_query( :param query: The app-based link query. :return: A MessagingExtensionResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_anonymous_app_based_link_query( self, turn_context: TurnContext, query: AppBasedLinkQuery @@ -305,7 +305,7 @@ async def on_teams_anonymous_app_based_link_query( :param query: The app-based link query. :return: A MessagingExtensionResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_messaging_extension_query( self, turn_context: TurnContext, query: MessagingExtensionQuery @@ -317,7 +317,7 @@ async def on_teams_messaging_extension_query( :param query: The messaging extension query. :return: A MessagingExtensionResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_messaging_extension_select_item( self, turn_context: TurnContext, query: Any @@ -329,7 +329,7 @@ async def on_teams_messaging_extension_select_item( :param query: The query. :return: A MessagingExtensionResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_messaging_extension_submit_action_dispatch( self, turn_context: TurnContext, action: MessagingExtensionAction @@ -351,7 +351,7 @@ async def on_teams_messaging_extension_submit_action_dispatch( turn_context, action ) else: - raise ValueError("BadRequest") + raise ValueError(str(error_resources.TeamsBadRequest)) else: return await self.on_teams_messaging_extension_submit_action( turn_context, action @@ -367,7 +367,7 @@ async def on_teams_messaging_extension_submit_action( :param action: The messaging extension action. :return: A MessagingExtensionActionResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_messaging_extension_message_preview_edit( self, turn_context: TurnContext, action: MessagingExtensionAction @@ -379,7 +379,7 @@ async def on_teams_messaging_extension_message_preview_edit( :param action: The messaging extension action. :return: A MessagingExtensionActionResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_messaging_extension_message_preview_send( self, turn_context: TurnContext, action: MessagingExtensionAction @@ -391,7 +391,7 @@ async def on_teams_messaging_extension_message_preview_send( :param action: The messaging extension action. :return: A MessagingExtensionActionResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_messaging_extension_fetch_task( self, turn_context: TurnContext, action: MessagingExtensionAction @@ -403,7 +403,7 @@ async def on_teams_messaging_extension_fetch_task( :param action: The messaging extension action. :return: A MessagingExtensionActionResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_messaging_extension_configuration_query_setting_url( self, turn_context: TurnContext, query: MessagingExtensionQuery @@ -415,7 +415,7 @@ async def on_teams_messaging_extension_configuration_query_setting_url( :param query: The messaging extension query. :return: A MessagingExtensionResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_messaging_extension_configuration_setting( self, turn_context: TurnContext, settings: Any @@ -427,7 +427,7 @@ async def on_teams_messaging_extension_configuration_setting( :param settings: The settings. :return: None """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_messaging_extension_card_button_clicked( self, turn_context: TurnContext, card_data: Any @@ -439,7 +439,7 @@ async def on_teams_messaging_extension_card_button_clicked( :param card_data: The card data. :return: None """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_task_module_fetch( self, turn_context: TurnContext, task_module_request: TaskModuleRequest @@ -451,7 +451,7 @@ async def on_teams_task_module_fetch( :param task_module_request: The task module request. :return: A TaskModuleResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_task_module_submit( self, turn_context: TurnContext, task_module_request: TaskModuleRequest @@ -463,7 +463,7 @@ async def on_teams_task_module_submit( :param task_module_request: The task module request. :return: A TaskModuleResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_tab_fetch( self, turn_context: TurnContext, tab_request: TabRequest @@ -475,7 +475,7 @@ async def on_teams_tab_fetch( :param tab_request: The tab request. :return: A TabResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_teams_tab_submit( self, turn_context: TurnContext, tab_submit: TabSubmit @@ -487,7 +487,7 @@ async def on_teams_tab_submit( :param tab_submit: The tab submit. :return: A TabResponse. """ - raise NotImplementedError("NotImplemented") + raise NotImplementedError(str(error_resources.TeamsNotImplemented)) async def on_conversation_update_activity(self, turn_context: TurnContext): """ diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py index 1a4de969..e3412a0f 100644 --- a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py @@ -23,7 +23,7 @@ ChannelInfo, ) from microsoft_agents.hosting.core.connector.teams import TeamsConnectorClient -from microsoft_agents.hosting.core import ChannelServiceAdapter, TurnContext +from microsoft_agents.hosting.core import ChannelServiceAdapter, TurnContext, error_resources class TeamsInfo: @@ -52,7 +52,7 @@ async def get_meeting_participant( ValueError: If required parameters are missing. """ if not context: - raise ValueError("context is required.") + raise ValueError(str(error_resources.TeamsContextRequired)) activity = context.activity teams_channel_data: dict = activity.channel_data @@ -61,13 +61,13 @@ async def get_meeting_participant( meeting_id = teams_channel_data.get("meeting", {}).get("id", None) if not meeting_id: - raise ValueError("meeting_id is required.") + raise ValueError(str(error_resources.TeamsMeetingIdRequired)) if participant_id is None: participant_id = getattr(activity.from_property, "aad_object_id", None) if not participant_id: - raise ValueError("participant_id is required.") + raise ValueError(str(error_resources.TeamsParticipantIdRequired)) if tenant_id is None: tenant_id = teams_channel_data.get("tenant", {}).get("id", None) @@ -100,7 +100,7 @@ async def get_meeting_info( meeting_id = teams_channel_data.get("meeting", {}).get("id", None) if not meeting_id: - raise ValueError("meeting_id is required.") + raise ValueError(str(error_resources.TeamsMeetingIdRequired)) rest_client = TeamsInfo._get_rest_client(context) result = await rest_client.fetch_meeting_info(meeting_id) @@ -128,7 +128,7 @@ async def get_team_details( team_id = teams_channel_data.get("team", {}).get("id", None) if not team_id: - raise ValueError("team_id is required.") + raise ValueError(str(error_resources.TeamsTeamIdRequired)) rest_client = TeamsInfo._get_rest_client(context) result = await rest_client.fetch_team_details(team_id) @@ -157,13 +157,13 @@ async def send_message_to_teams_channel( ValueError: If required parameters are missing. """ if not context: - raise ValueError("TurnContext cannot be None") + raise ValueError(str(error_resources.TeamsTurnContextRequired)) if not activity: - raise ValueError("Activity cannot be None") + raise ValueError(str(error_resources.TeamsActivityRequired)) if not teams_channel_id: - raise ValueError("The teams_channel_id cannot be None or empty") + raise ValueError(str(error_resources.TeamsChannelIdRequired)) convo_params = ConversationParameters( is_group=True, @@ -239,7 +239,7 @@ async def get_team_channels( team_id = teams_channel_data.get("team", {}).get("id", None) if not team_id: - raise ValueError("team_id is required.") + raise ValueError(str(error_resources.TeamsTeamIdRequired)) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.fetch_channel_list(team_id) @@ -278,7 +278,7 @@ async def get_paged_members( else None ) if not conversation_id: - raise ValueError("conversation_id is required.") + raise ValueError(str(error_resources.TeamsConversationIdRequired)) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.get_conversation_paged_member( @@ -312,7 +312,7 @@ async def get_member(context: TurnContext, user_id: str) -> TeamsChannelAccount: else None ) if not conversation_id: - raise ValueError("conversation_id is required.") + raise ValueError(str(error_resources.TeamsConversationIdRequired)) return await TeamsInfo._get_member_internal( context, conversation_id, user_id @@ -345,7 +345,7 @@ async def get_paged_team_members( team_id = teams_channel_data.get("team", {}).get("id", None) if not team_id: - raise ValueError("team_id is required.") + raise ValueError(str(error_resources.TeamsTeamIdRequired)) rest_client = TeamsInfo._get_rest_client(context) paged_results = await rest_client.get_conversation_paged_member( @@ -410,7 +410,7 @@ async def send_meeting_notification( meeting_id = teams_channel_data.get("meeting", {}).get("id", None) if not meeting_id: - raise ValueError("meeting_id is required.") + raise ValueError(str(error_resources.TeamsMeetingIdRequired)) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.send_meeting_notification(meeting_id, notification) @@ -438,9 +438,9 @@ async def send_message_to_list_of_users( ValueError: If required parameters are missing. """ if not activity: - raise ValueError("activity is required.") + raise ValueError(str(error_resources.ActivityRequired)) if not tenant_id: - raise ValueError("tenant_id is required.") + raise ValueError(error_resources.RequiredParameterMissing.format("tenant_id")) if not members or len(members) == 0: raise ValueError("members list is required.") @@ -468,9 +468,9 @@ async def send_message_to_all_users_in_tenant( ValueError: If required parameters are missing. """ if not activity: - raise ValueError("activity is required.") + raise ValueError(str(error_resources.ActivityRequired)) if not tenant_id: - raise ValueError("tenant_id is required.") + raise ValueError(error_resources.RequiredParameterMissing.format("tenant_id")) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.send_message_to_all_users_in_tenant( @@ -497,11 +497,11 @@ async def send_message_to_all_users_in_team( ValueError: If required parameters are missing. """ if not activity: - raise ValueError("activity is required.") + raise ValueError(str(error_resources.ActivityRequired)) if not tenant_id: - raise ValueError("tenant_id is required.") + raise ValueError(error_resources.RequiredParameterMissing.format("tenant_id")) if not team_id: - raise ValueError("team_id is required.") + raise ValueError(str(error_resources.TeamsTeamIdRequired)) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.send_message_to_all_users_in_team( @@ -531,9 +531,9 @@ async def send_message_to_list_of_channels( ValueError: If required parameters are missing. """ if not activity: - raise ValueError("activity is required.") + raise ValueError(str(error_resources.ActivityRequired)) if not tenant_id: - raise ValueError("tenant_id is required.") + raise ValueError(error_resources.RequiredParameterMissing.format("tenant_id")) if not members or len(members) == 0: raise ValueError("members list is required.") diff --git a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/blob_storage.py b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/blob_storage.py index 2a7634b7..107d5931 100644 --- a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/blob_storage.py +++ b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/blob_storage.py @@ -14,6 +14,7 @@ ignore_error, is_status_code_error, ) +from microsoft_agents.hosting.core import error_resources from .blob_storage_config import BlobStorageConfig @@ -25,7 +26,7 @@ class BlobStorage(AsyncStorageBase): def __init__(self, config: BlobStorageConfig): if not config.container_name: - raise ValueError("BlobStorage: Container name is required.") + raise ValueError(str(error_resources.BlobContainerNameRequired)) self.config = config @@ -39,7 +40,9 @@ def _create_client(self) -> BlobServiceClient: if self.config.url: # connect with URL and credentials if not self.config.credential: raise ValueError( - "BlobStorage: Credential is required when using a custom service URL." + error_resources.InvalidConfiguration.format( + "Credential is required when using a custom service URL" + ) ) return BlobServiceClient( account_url=self.config.url, credential=self.config.credential From 6bbd39b69f8c0720ae90cc53562cebdf5f31a001 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:48:29 +0000 Subject: [PATCH 05/16] Refactor hosting, activity, and copilot studio errors to use error resources Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../activity/_channel_id_field_mixin.py | 2 ++ .../microsoft_agents/activity/activity.py | 6 ++++-- .../microsoft_agents/activity/channel_id.py | 6 ++++-- .../client/power_platform_environment.py | 15 ++++++++------- .../hosting/aiohttp/_start_agent_process.py | 5 +++-- .../aiohttp/app/streaming/streaming_response.py | 9 +++++---- .../hosting/aiohttp/cloud_adapter.py | 5 +++-- .../hosting/fastapi/_start_agent_process.py | 5 +++-- .../fastapi/app/streaming/streaming_response.py | 9 +++++---- .../hosting/fastapi/cloud_adapter.py | 5 +++-- 10 files changed, 40 insertions(+), 27 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py index 77f60c02..f695c409 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from microsoft_agents.hosting.core import error_resources + from __future__ import annotations import logging diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index c038d0f5..6a5e8639 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from microsoft_agents.hosting.core import error_resources + from __future__ import annotations import logging @@ -219,7 +221,7 @@ def _validate_channel_id( and activity.channel_id.sub_channel != product_info.id ): raise Exception( - "Conflict between channel_id.sub_channel and productInfo entity" + str(error_resources.ChannelIdProductInfoConflict) ) activity.channel_id = ChannelId( channel=activity.channel_id.channel, @@ -257,7 +259,7 @@ def _serialize_sub_channel_data( if self.channel_id and self.channel_id.sub_channel: if product_info and product_info.get("id") != self.channel_id.sub_channel: raise Exception( - "Conflict between channel_id.sub_channel and productInfo entity" + str(error_resources.ChannelIdProductInfoConflict) ) elif not product_info: if not serialized.get("entities"): diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py index ad4890fe..fe6d5780 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from microsoft_agents.hosting.core import error_resources + from __future__ import annotations from typing import Optional, Any @@ -53,13 +55,13 @@ def __new__( if isinstance(value, str): if channel or sub_channel: raise ValueError( - "If value is provided, channel and sub_channel must be None" + str(error_resources.ChannelIdValueConflict) ) value = value.strip() if value: return str.__new__(cls, value) - raise TypeError("value must be a non empty string if provided") + raise TypeError(str(error_resources.ChannelIdValueMustBeNonEmpty)) else: if ( not isinstance(channel, str) diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py index 78589fdf..c821b745 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py @@ -1,3 +1,4 @@ +from microsoft_agents.hosting.core import error_resources from urllib.parse import urlparse, urlunparse from typing import Optional from .connection_settings import ConnectionSettings @@ -23,12 +24,12 @@ def get_copilot_studio_connection_url( ) -> str: if cloud == PowerPlatformCloud.OTHER and not cloud_base_address: raise ValueError( - "cloud_base_address must be provided when PowerPlatformCloud is Other" + str(error_resources.CloudBaseAddressRequired) ) if not settings.environment_id: - raise ValueError("EnvironmentId must be provided") + raise ValueError(str(error_resources.EnvironmentIdRequired)) if not settings.agent_identifier: - raise ValueError("AgentIdentifier must be provided") + raise ValueError(str(error_resources.AgentIdentifierRequired)) if settings.cloud and settings.cloud != PowerPlatformCloud.UNKNOWN: cloud = settings.cloud if cloud == PowerPlatformCloud.OTHER: @@ -40,7 +41,7 @@ def get_copilot_studio_connection_url( cloud_base_address = settings.custom_power_platform_cloud else: raise ValueError( - "Either CustomPowerPlatformCloud or cloud_base_address must be provided when PowerPlatformCloud is Other" + str(error_resources.CustomCloudOrBaseAddressRequired) ) if settings.copilot_agent_type: agent_type = settings.copilot_agent_type @@ -61,7 +62,7 @@ def get_token_audience( ) -> str: if cloud == PowerPlatformCloud.OTHER and not cloud_base_address: raise ValueError( - "cloud_base_address must be provided when PowerPlatformCloud is Other" + str(error_resources.CloudBaseAddressRequired) ) if not settings and cloud == PowerPlatformCloud.UNKNOWN: raise ValueError("Either settings or cloud must be provided") @@ -79,7 +80,7 @@ def get_token_audience( cloud_base_address = settings.custom_power_platform_cloud else: raise ValueError( - "Either CustomPowerPlatformCloud or cloud_base_address must be provided when PowerPlatformCloud is Other" + str(error_resources.CustomCloudOrBaseAddressRequired) ) cloud_base_address = cloud_base_address or "api.unknown.powerplatform.com" @@ -117,7 +118,7 @@ def get_environment_endpoint( ) -> str: if cloud == PowerPlatformCloud.OTHER and not cloud_base_address: raise ValueError( - "cloud_base_address must be provided when PowerPlatformCloud is Other" + str(error_resources.CloudBaseAddressRequired) ) cloud_base_address = cloud_base_address or "api.unknown.powerplatform.com" normalized_resource_id = environment_id.lower().replace("-", "") diff --git a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/_start_agent_process.py b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/_start_agent_process.py index 0aa93f8b..bba42c8f 100644 --- a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/_start_agent_process.py +++ b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/_start_agent_process.py @@ -1,5 +1,6 @@ from typing import Optional from aiohttp.web import Request, Response +from microsoft_agents.hosting.core import error_resources from microsoft_agents.hosting.core.app import AgentApplication from .cloud_adapter import CloudAdapter @@ -15,9 +16,9 @@ async def start_agent_process( agent_application (AgentApplication): The agent application to run. """ if not adapter: - raise TypeError("start_agent_process: adapter can't be None") + raise TypeError(str(error_resources.AdapterRequired)) if not agent_application: - raise TypeError("start_agent_process: agent_application can't be None") + raise TypeError(str(error_resources.AgentApplicationRequired)) # Start the agent application with the provided adapter return await adapter.process( diff --git a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/app/streaming/streaming_response.py b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/app/streaming/streaming_response.py index db57858b..0d789c71 100644 --- a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/app/streaming/streaming_response.py +++ b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/app/streaming/streaming_response.py @@ -17,7 +17,8 @@ ) if TYPE_CHECKING: - from microsoft_agents.hosting.core.turn_context import TurnContext + from microsoft_agents.hosting.core import error_resources +from microsoft_agents.hosting.core.turn_context import TurnContext from .citation import Citation from .citation_util import CitationUtil @@ -99,7 +100,7 @@ def queue_informative_update(self, text: str) -> None: return if self._ended: - raise RuntimeError("The stream has already ended.") + raise RuntimeError(str(error_resources.StreamAlreadyEnded)) # Queue a typing activity def create_activity(): @@ -135,7 +136,7 @@ def queue_text_chunk( if self._cancelled: return if self._ended: - raise RuntimeError("The stream has already ended.") + raise RuntimeError(str(error_resources.StreamAlreadyEnded)) # Update full message text self._message += text @@ -151,7 +152,7 @@ async def end_stream(self) -> None: Ends the stream by sending the final message to the client. """ if self._ended: - raise RuntimeError("The stream has already ended.") + raise RuntimeError(str(error_resources.StreamAlreadyEnded)) # Queue final message self._ended = True diff --git a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/cloud_adapter.py b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/cloud_adapter.py index 1ef106c3..a8833f22 100644 --- a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/cloud_adapter.py +++ b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/cloud_adapter.py @@ -12,6 +12,7 @@ HTTPUnauthorized, HTTPUnsupportedMediaType, ) +from microsoft_agents.hosting.core import error_resources from microsoft_agents.hosting.core.authorization import ( ClaimsIdentity, Connections, @@ -70,9 +71,9 @@ async def on_turn_error(context: TurnContext, error: Exception): async def process(self, request: Request, agent: Agent) -> Optional[Response]: if not request: - raise TypeError("CloudAdapter.process: request can't be None") + raise TypeError(str(error_resources.RequestRequired)) if not agent: - raise TypeError("CloudAdapter.process: agent can't be None") + raise TypeError(str(error_resources.AgentRequired)) if request.method == "POST": # Deserialize the incoming Activity diff --git a/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/_start_agent_process.py b/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/_start_agent_process.py index 13396ca8..ebaf2e43 100644 --- a/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/_start_agent_process.py +++ b/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/_start_agent_process.py @@ -1,5 +1,6 @@ from typing import Optional from fastapi import Request, Response +from microsoft_agents.hosting.core import error_resources from microsoft_agents.hosting.core.app import AgentApplication from .cloud_adapter import CloudAdapter @@ -15,9 +16,9 @@ async def start_agent_process( agent_application (AgentApplication): The agent application to run. """ if not adapter: - raise TypeError("start_agent_process: adapter can't be None") + raise TypeError(str(error_resources.AdapterRequired)) if not agent_application: - raise TypeError("start_agent_process: agent_application can't be None") + raise TypeError(str(error_resources.AgentApplicationRequired)) # Start the agent application with the provided adapter return await adapter.process( diff --git a/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/app/streaming/streaming_response.py b/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/app/streaming/streaming_response.py index b68d6d0d..7d666bb7 100644 --- a/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/app/streaming/streaming_response.py +++ b/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/app/streaming/streaming_response.py @@ -17,7 +17,8 @@ ) if TYPE_CHECKING: - from microsoft_agents.hosting.core.turn_context import TurnContext + from microsoft_agents.hosting.core import error_resources +from microsoft_agents.hosting.core.turn_context import TurnContext from .citation import Citation from .citation_util import CitationUtil @@ -80,7 +81,7 @@ def queue_informative_update(self, text: str) -> None: return if self._ended: - raise RuntimeError("The stream has already ended.") + raise RuntimeError(str(error_resources.StreamAlreadyEnded)) # Queue a typing activity def create_activity(): @@ -116,7 +117,7 @@ def queue_text_chunk( if self._cancelled: return if self._ended: - raise RuntimeError("The stream has already ended.") + raise RuntimeError(str(error_resources.StreamAlreadyEnded)) # Update full message text self._message += text @@ -132,7 +133,7 @@ async def end_stream(self) -> None: Ends the stream by sending the final message to the client. """ if self._ended: - raise RuntimeError("The stream has already ended.") + raise RuntimeError(str(error_resources.StreamAlreadyEnded)) # Queue final message self._ended = True diff --git a/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/cloud_adapter.py b/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/cloud_adapter.py index 3383c793..1a8f912a 100644 --- a/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/cloud_adapter.py +++ b/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/cloud_adapter.py @@ -5,6 +5,7 @@ from fastapi import Request, Response, HTTPException from fastapi.responses import JSONResponse +from microsoft_agents.hosting.core import error_resources from microsoft_agents.hosting.core.authorization import ( ClaimsIdentity, Connections, @@ -63,9 +64,9 @@ async def on_turn_error(context: TurnContext, error: Exception): async def process(self, request: Request, agent: Agent) -> Optional[Response]: if not request: - raise TypeError("CloudAdapter.process: request can't be None") + raise TypeError(str(error_resources.RequestRequired)) if not agent: - raise TypeError("CloudAdapter.process: agent can't be None") + raise TypeError(str(error_resources.AgentRequired)) if request.method == "POST": # Deserialize the incoming Activity From 687c45c8594625872ecad78da2a51033028120e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:52:18 +0000 Subject: [PATCH 06/16] Fix import order for __future__ annotations in activity module Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../microsoft_agents/activity/_channel_id_field_mixin.py | 4 ++-- .../microsoft_agents/activity/activity.py | 4 ++-- .../microsoft_agents/activity/channel_id.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py index f695c409..3f69610f 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from microsoft_agents.hosting.core import error_resources - from __future__ import annotations +from microsoft_agents.hosting.core import error_resources + import logging from typing import Optional, Any diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index 6a5e8639..280c09c9 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from microsoft_agents.hosting.core import error_resources - from __future__ import annotations +from microsoft_agents.hosting.core import error_resources + import logging from copy import copy from datetime import datetime, timezone diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py index fe6d5780..ff154eae 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from microsoft_agents.hosting.core import error_resources - from __future__ import annotations +from microsoft_agents.hosting.core import error_resources + from typing import Optional, Any from pydantic_core import CoreSchema, core_schema From a81d33657dde9fcec184e8a3a5eac2051410272d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:57:43 +0000 Subject: [PATCH 07/16] Fix circular import by using lazy imports in activity module Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../microsoft_agents/activity/_channel_id_field_mixin.py | 6 ++---- .../microsoft_agents/activity/activity.py | 4 ++-- .../microsoft_agents/activity/channel_id.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py index 3f69610f..80e8fc84 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py @@ -3,8 +3,6 @@ from __future__ import annotations -from microsoft_agents.hosting.core import error_resources - import logging from typing import Optional, Any @@ -44,9 +42,9 @@ def channel_id(self, value: Any): elif isinstance(value, str): self._channel_id = ChannelId(value) else: + from microsoft_agents.hosting.core import error_resources raise ValueError( - f"Invalid type for channel_id: {type(value)}. " - "Expected ChannelId or str." + error_resources.InvalidChannelIdType.format(type(value)) ) def _set_validated_channel_id(self, data: Any) -> None: diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index 280c09c9..5991ec1a 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -3,8 +3,6 @@ from __future__ import annotations -from microsoft_agents.hosting.core import error_resources - import logging from copy import copy from datetime import datetime, timezone @@ -220,6 +218,7 @@ def _validate_channel_id( activity.channel_id.sub_channel and activity.channel_id.sub_channel != product_info.id ): + from microsoft_agents.hosting.core import error_resources raise Exception( str(error_resources.ChannelIdProductInfoConflict) ) @@ -258,6 +257,7 @@ def _serialize_sub_channel_data( # self.channel_id is the source of truth for serialization if self.channel_id and self.channel_id.sub_channel: if product_info and product_info.get("id") != self.channel_id.sub_channel: + from microsoft_agents.hosting.core import error_resources raise Exception( str(error_resources.ChannelIdProductInfoConflict) ) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py index ff154eae..a55e6822 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py @@ -3,8 +3,6 @@ from __future__ import annotations -from microsoft_agents.hosting.core import error_resources - from typing import Optional, Any from pydantic_core import CoreSchema, core_schema @@ -54,6 +52,7 @@ def __new__( """ if isinstance(value, str): if channel or sub_channel: + from microsoft_agents.hosting.core import error_resources raise ValueError( str(error_resources.ChannelIdValueConflict) ) @@ -61,6 +60,7 @@ def __new__( value = value.strip() if value: return str.__new__(cls, value) + from microsoft_agents.hosting.core import error_resources raise TypeError(str(error_resources.ChannelIdValueMustBeNonEmpty)) else: if ( From dd5d925d114b345fb944db833a8eed75a2f821e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:59:45 +0000 Subject: [PATCH 08/16] Add comprehensive documentation for error resources module Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../hosting/core/errors/README.md | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/README.md diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/README.md b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/README.md new file mode 100644 index 00000000..84c7bd8b --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/README.md @@ -0,0 +1,186 @@ +# Error Resources for Microsoft Agents SDK + +This module provides centralized error messages with error codes and help URLs for the Microsoft Agents SDK, following the pattern established in the C# SDK. + +## Overview + +All error messages in the Microsoft Agents SDK are now centralized in the `error_resources` module. Each error includes: + +1. **Formatted message string** - Can include placeholders for dynamic values +2. **Error code** - A unique negative integer identifying the error +3. **Help URL** - A link to documentation with hashtag anchor + +## Usage + +### Basic Usage + +```python +from microsoft_agents.hosting.core import error_resources + +# Raise an error with a simple message +raise ValueError(str(error_resources.CosmosDbConfigRequired)) + +# Raise an error with formatted arguments +raise ValueError(error_resources.FailedToAcquireToken.format(auth_payload)) + +# Multiple arguments +raise ValueError(error_resources.CosmosDbPartitionKeyInvalid.format(key1, key2)) +``` + +### Example Output + +When an error is raised, it will look like: + +``` +Failed to acquire token. {'error': 'invalid_grant'} + +Error Code: -60012 +Help URL: https://aka.ms/M365AgentsErrorCodes/#agentic-identity-with-the-m365-agents-sdk +``` + +## Error Code Ranges + +Error codes are organized by category in the following ranges: + +| Range | Category | Example | +|-------|----------|---------| +| -60000 to -60099 | Authentication | FailedToAcquireToken (-60012) | +| -60100 to -60199 | Storage | CosmosDbConfigRequired (-60100) | +| -60200 to -60299 | Teams | TeamsBadRequest (-60200) | +| -60300 to -60399 | Hosting | AdapterRequired (-60300) | +| -60400 to -60499 | Activity | InvalidChannelIdType (-60400) | +| -60500 to -60599 | Copilot Studio | CloudBaseAddressRequired (-60500) | +| -60600 to -60699 | General/Validation | InvalidConfiguration (-60600) | + +## Available Error Resources + +### Authentication Errors + +- `FailedToAcquireToken` - Failed to acquire authentication token +- `InvalidInstanceUrl` - Invalid instance URL provided +- `OnBehalfOfFlowNotSupportedManagedIdentity` - On-behalf-of flow not supported with managed identity +- `OnBehalfOfFlowNotSupportedAuthType` - On-behalf-of flow not supported with current auth type +- `AuthenticationTypeNotSupported` - Authentication type not supported +- `AgentApplicationInstanceIdRequired` - Agent application instance ID required +- `FailedToAcquireAgenticInstanceToken` - Failed to acquire agentic instance token +- `AgentApplicationInstanceIdAndUserIdRequired` - Both agent app instance ID and user ID required +- `FailedToAcquireInstanceOrAgentToken` - Failed to acquire instance or agent token + +### Storage Errors + +- `CosmosDbConfigRequired` - CosmosDB configuration required +- `CosmosDbEndpointRequired` - CosmosDB endpoint required +- `CosmosDbAuthKeyRequired` - CosmosDB auth key required +- `CosmosDbDatabaseIdRequired` - CosmosDB database ID required +- `CosmosDbContainerIdRequired` - CosmosDB container ID required +- `CosmosDbKeyCannotBeEmpty` - CosmosDB key cannot be empty +- `BlobStorageConfigRequired` - Blob storage configuration required +- `BlobContainerNameRequired` - Blob container name required +- `StorageKeyCannotBeEmpty` - Storage key cannot be empty + +### Teams Errors + +- `TeamsBadRequest` - Bad request +- `TeamsNotImplemented` - Not implemented +- `TeamsContextRequired` - Context required +- `TeamsMeetingIdRequired` - Meeting ID required +- `TeamsParticipantIdRequired` - Participant ID required +- `TeamsTeamIdRequired` - Team ID required +- `TeamsTurnContextRequired` - TurnContext required +- `TeamsActivityRequired` - Activity required +- `TeamsChannelIdRequired` - Teams channel ID required +- `TeamsConversationIdRequired` - Conversation ID required + +### Hosting Errors + +- `AdapterRequired` - Adapter required +- `AgentApplicationRequired` - Agent application required +- `RequestRequired` - Request required +- `AgentRequired` - Agent required +- `StreamAlreadyEnded` - Stream already ended +- `TurnContextRequired` - TurnContext required +- `ActivityRequired` - Activity required + +### Activity Errors + +- `InvalidChannelIdType` - Invalid channel ID type +- `ChannelIdProductInfoConflict` - Conflict between channel ID and product info +- `ChannelIdValueConflict` - Value and channel cannot both be provided +- `ChannelIdValueMustBeNonEmpty` - Channel ID value must be non-empty + +### Copilot Studio Errors + +- `CloudBaseAddressRequired` - Cloud base address required +- `EnvironmentIdRequired` - Environment ID required +- `AgentIdentifierRequired` - Agent identifier required +- `CustomCloudOrBaseAddressRequired` - Custom cloud or base address required +- `PowerPlatformEnvironmentRequired` - Power Platform environment required +- `AccessTokenProviderRequired` - Access token provider required + +### General/Validation Errors + +- `InvalidConfiguration` - Invalid configuration +- `RequiredParameterMissing` - Required parameter missing +- `InvalidParameterValue` - Invalid parameter value +- `OperationNotSupported` - Operation not supported +- `ResourceNotFound` - Resource not found +- `UnexpectedError` - Unexpected error occurred + +## Adding New Error Messages + +To add a new error message: + +1. Open `error_resources.py` +2. Add a new `ErrorMessage` instance to the `ErrorResources` class +3. Follow the naming convention: `PascalCaseErrorName` +4. Assign an error code in the appropriate range +5. Provide an appropriate help URL anchor + +Example: + +```python +NewErrorType = ErrorMessage( + "Description of the error with {0} placeholder", + -60XXX, # Use appropriate range + "help-url-anchor", +) +``` + +## Avoiding Circular Imports + +When using error_resources in modules that are imported by hosting-core's `__init__.py`, use lazy imports to avoid circular dependencies: + +```python +# Instead of module-level import: +# from microsoft_agents.hosting.core import error_resources + +# Use lazy import where needed: +def my_function(): + from microsoft_agents.hosting.core import error_resources + raise ValueError(str(error_resources.SomeError)) +``` + +## Testing + +Tests for error resources are located in `tests/hosting_core/errors/test_error_resources.py`. + +To run tests: +```bash +pytest tests/hosting_core/errors/test_error_resources.py -v +``` + +## Contributing + +When refactoring existing error messages: + +1. Find the error message in the code +2. Identify or create an appropriate error resource +3. Replace the hardcoded string with `str(error_resources.ErrorName)` or `error_resources.ErrorName.format(args)` +4. For errors with placeholders, use `.format()` with appropriate arguments +5. Update tests if needed + +## Future Work + +- Help URLs will be updated with correct deep-link hashtags once documentation is available +- Additional error messages can be added as needed +- Error codes can be aligned with C# SDK as that evolves From 7f338856cb3e3bd9a80d223ba1abccd13ccee465 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 21:02:55 +0000 Subject: [PATCH 09/16] 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> --- .../hosting/aiohttp/app/streaming/streaming_response.py | 3 +-- .../hosting/fastapi/app/streaming/streaming_response.py | 3 +-- .../storage/cosmos/cosmos_db_storage_config.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/app/streaming/streaming_response.py b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/app/streaming/streaming_response.py index 0d789c71..a86fc721 100644 --- a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/app/streaming/streaming_response.py +++ b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/app/streaming/streaming_response.py @@ -16,8 +16,7 @@ SensitivityUsageInfo, ) -if TYPE_CHECKING: - from microsoft_agents.hosting.core import error_resources +from microsoft_agents.hosting.core import error_resources from microsoft_agents.hosting.core.turn_context import TurnContext from .citation import Citation diff --git a/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/app/streaming/streaming_response.py b/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/app/streaming/streaming_response.py index 7d666bb7..7d837dfe 100644 --- a/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/app/streaming/streaming_response.py +++ b/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/app/streaming/streaming_response.py @@ -16,8 +16,7 @@ SensitivityUsageInfo, ) -if TYPE_CHECKING: - from microsoft_agents.hosting.core import error_resources +from microsoft_agents.hosting.core import error_resources from microsoft_agents.hosting.core.turn_context import TurnContext from .citation import Citation diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py index 6c4d73b8..e78185f1 100644 --- a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py @@ -87,7 +87,7 @@ def _validate_suffix(config: "CosmosDBStorageConfig") -> None: if config.key_suffix: if config.compatibility_mode: raise ValueError( - error_resources.CosmosDbCompatibilityModeRequired.format() + str(error_resources.CosmosDbCompatibilityModeRequired) ) suffix_escaped: str = sanitize_key(config.key_suffix) if suffix_escaped != config.key_suffix: From 86ac5aab1e58868ac3e6e7e7a14045e963f4c048 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:34:27 +0000 Subject: [PATCH 10/16] Format all changed files with Black to fix build errors Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../activity/_channel_id_field_mixin.py | 5 ++-- .../microsoft_agents/activity/activity.py | 10 ++++---- .../microsoft_agents/activity/channel_id.py | 6 ++--- .../authentication/msal/msal_auth.py | 24 ++++++++++++++----- .../client/power_platform_environment.py | 20 ++++------------ .../hosting/core/errors/error_message.py | 6 ++--- .../hosting/core/errors/error_resources.py | 2 +- .../hosting/teams/teams_info.py | 22 +++++++++++++---- .../cosmos/cosmos_db_storage_config.py | 4 +--- 9 files changed, 54 insertions(+), 45 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py index 80e8fc84..e969c08c 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py @@ -43,9 +43,8 @@ def channel_id(self, value: Any): self._channel_id = ChannelId(value) else: from microsoft_agents.hosting.core import error_resources - raise ValueError( - error_resources.InvalidChannelIdType.format(type(value)) - ) + + raise ValueError(error_resources.InvalidChannelIdType.format(type(value))) def _set_validated_channel_id(self, data: Any) -> None: """Sets the channel_id after validating it as a ChannelId model.""" diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index 5991ec1a..fd9b188f 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -219,9 +219,8 @@ def _validate_channel_id( and activity.channel_id.sub_channel != product_info.id ): from microsoft_agents.hosting.core import error_resources - raise Exception( - str(error_resources.ChannelIdProductInfoConflict) - ) + + raise Exception(str(error_resources.ChannelIdProductInfoConflict)) activity.channel_id = ChannelId( channel=activity.channel_id.channel, sub_channel=product_info.id, @@ -258,9 +257,8 @@ def _serialize_sub_channel_data( if self.channel_id and self.channel_id.sub_channel: if product_info and product_info.get("id") != self.channel_id.sub_channel: from microsoft_agents.hosting.core import error_resources - raise Exception( - str(error_resources.ChannelIdProductInfoConflict) - ) + + raise Exception(str(error_resources.ChannelIdProductInfoConflict)) elif not product_info: if not serialized.get("entities"): serialized["entities"] = [] diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py index a55e6822..f94e456d 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py @@ -53,14 +53,14 @@ def __new__( if isinstance(value, str): if channel or sub_channel: from microsoft_agents.hosting.core import error_resources - raise ValueError( - str(error_resources.ChannelIdValueConflict) - ) + + raise ValueError(str(error_resources.ChannelIdValueConflict)) value = value.strip() if value: return str.__new__(cls, value) from microsoft_agents.hosting.core import error_resources + raise TypeError(str(error_resources.ChannelIdValueMustBeNonEmpty)) else: if ( diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py index fdb9e672..81c2546a 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py @@ -87,7 +87,9 @@ async def get_access_token( res = auth_result_payload.get("access_token") if auth_result_payload else None if not res: logger.error("Failed to acquire token for resource %s", auth_result_payload) - raise ValueError(error_resources.FailedToAcquireToken.format(str(auth_result_payload))) + raise ValueError( + error_resources.FailedToAcquireToken.format(str(auth_result_payload)) + ) return res @@ -124,7 +126,9 @@ async def acquire_token_on_behalf_of( logger.error( f"Failed to acquire token on behalf of user: {user_assertion}" ) - raise ValueError(error_resources.FailedToAcquireToken.format(str(token))) + raise ValueError( + error_resources.FailedToAcquireToken.format(str(token)) + ) return token["access_token"] @@ -190,7 +194,9 @@ def _create_client_application(self) -> None: logger.error( f"Unsupported authentication type: {self._msal_configuration.AUTH_TYPE}" ) - raise NotImplementedError(str(error_resources.AuthenticationTypeNotSupported)) + raise NotImplementedError( + str(error_resources.AuthenticationTypeNotSupported) + ) self._msal_auth_client = ConfidentialClientApplication( client_id=self._msal_configuration.CLIENT_ID, @@ -286,7 +292,9 @@ async def get_agentic_instance_token( agent_app_instance_id, ) raise Exception( - error_resources.FailedToAcquireAgenticInstanceToken.format(agent_app_instance_id) + error_resources.FailedToAcquireAgenticInstanceToken.format( + agent_app_instance_id + ) ) authority = ( @@ -309,7 +317,9 @@ async def get_agentic_instance_token( agent_app_instance_id, ) raise Exception( - error_resources.FailedToAcquireAgenticInstanceToken.format(agent_app_instance_id) + error_resources.FailedToAcquireAgenticInstanceToken.format( + agent_app_instance_id + ) ) # future scenario where we don't know the blueprint id upfront @@ -319,7 +329,9 @@ async def get_agentic_instance_token( logger.error( "Failed to acquire agentic instance token, %s", agentic_instance_token ) - raise ValueError(error_resources.FailedToAcquireToken.format(str(agentic_instance_token))) + raise ValueError( + error_resources.FailedToAcquireToken.format(str(agentic_instance_token)) + ) logger.debug( "Agentic blueprint id: %s", diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py index c821b745..569a1178 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py @@ -23,9 +23,7 @@ def get_copilot_studio_connection_url( cloud_base_address: Optional[str] = None, ) -> str: if cloud == PowerPlatformCloud.OTHER and not cloud_base_address: - raise ValueError( - str(error_resources.CloudBaseAddressRequired) - ) + raise ValueError(str(error_resources.CloudBaseAddressRequired)) if not settings.environment_id: raise ValueError(str(error_resources.EnvironmentIdRequired)) if not settings.agent_identifier: @@ -40,9 +38,7 @@ def get_copilot_studio_connection_url( elif settings.custom_power_platform_cloud: cloud_base_address = settings.custom_power_platform_cloud else: - raise ValueError( - str(error_resources.CustomCloudOrBaseAddressRequired) - ) + raise ValueError(str(error_resources.CustomCloudOrBaseAddressRequired)) if settings.copilot_agent_type: agent_type = settings.copilot_agent_type @@ -61,9 +57,7 @@ def get_token_audience( cloud_base_address: Optional[str] = None, ) -> str: if cloud == PowerPlatformCloud.OTHER and not cloud_base_address: - raise ValueError( - str(error_resources.CloudBaseAddressRequired) - ) + raise ValueError(str(error_resources.CloudBaseAddressRequired)) if not settings and cloud == PowerPlatformCloud.UNKNOWN: raise ValueError("Either settings or cloud must be provided") if settings and settings.cloud and settings.cloud != PowerPlatformCloud.UNKNOWN: @@ -79,9 +73,7 @@ def get_token_audience( cloud = PowerPlatformCloud.OTHER cloud_base_address = settings.custom_power_platform_cloud else: - raise ValueError( - str(error_resources.CustomCloudOrBaseAddressRequired) - ) + raise ValueError(str(error_resources.CustomCloudOrBaseAddressRequired)) cloud_base_address = cloud_base_address or "api.unknown.powerplatform.com" return f"https://{PowerPlatformEnvironment.get_endpoint_suffix(cloud, cloud_base_address)}/.default" @@ -117,9 +109,7 @@ def get_environment_endpoint( cloud_base_address: Optional[str] = None, ) -> str: if cloud == PowerPlatformCloud.OTHER and not cloud_base_address: - raise ValueError( - str(error_resources.CloudBaseAddressRequired) - ) + raise ValueError(str(error_resources.CloudBaseAddressRequired)) cloud_base_address = cloud_base_address or "api.unknown.powerplatform.com" normalized_resource_id = environment_id.lower().replace("-", "") id_suffix_length = PowerPlatformEnvironment.get_id_suffix_length(cloud) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_message.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_message.py index fd07a9c7..08b6eb24 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_message.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_message.py @@ -9,7 +9,7 @@ class ErrorMessage: """ Represents a formatted error message with error code and help URL. - + This class formats error messages according to the Microsoft Agents SDK pattern: - Original error message - Error Code: [negative number] @@ -24,7 +24,7 @@ def __init__( ): """ Initialize an ErrorMessage. - + :param message_template: The error message template (may include format placeholders) :type message_template: str :param error_code: The error code (should be negative) @@ -40,7 +40,7 @@ def __init__( def format(self, *args, **kwargs) -> str: """ Format the error message with the provided arguments. - + :param args: Positional arguments for string formatting :param kwargs: Keyword arguments for string formatting :return: Formatted error message with error code and help URL diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py index b20b35c0..b89b2e1e 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py @@ -18,7 +18,7 @@ class ErrorResources: """ Central repository of all error messages used in the Microsoft Agents SDK. - + Error codes are organized by range: - -60000 to -60099: Authentication errors - -60100 to -60199: Storage errors diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py index e3412a0f..3de4f39a 100644 --- a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py @@ -23,7 +23,11 @@ ChannelInfo, ) from microsoft_agents.hosting.core.connector.teams import TeamsConnectorClient -from microsoft_agents.hosting.core import ChannelServiceAdapter, TurnContext, error_resources +from microsoft_agents.hosting.core import ( + ChannelServiceAdapter, + TurnContext, + error_resources, +) class TeamsInfo: @@ -440,7 +444,9 @@ async def send_message_to_list_of_users( if not activity: raise ValueError(str(error_resources.ActivityRequired)) if not tenant_id: - raise ValueError(error_resources.RequiredParameterMissing.format("tenant_id")) + raise ValueError( + error_resources.RequiredParameterMissing.format("tenant_id") + ) if not members or len(members) == 0: raise ValueError("members list is required.") @@ -470,7 +476,9 @@ async def send_message_to_all_users_in_tenant( if not activity: raise ValueError(str(error_resources.ActivityRequired)) if not tenant_id: - raise ValueError(error_resources.RequiredParameterMissing.format("tenant_id")) + raise ValueError( + error_resources.RequiredParameterMissing.format("tenant_id") + ) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.send_message_to_all_users_in_tenant( @@ -499,7 +507,9 @@ async def send_message_to_all_users_in_team( if not activity: raise ValueError(str(error_resources.ActivityRequired)) if not tenant_id: - raise ValueError(error_resources.RequiredParameterMissing.format("tenant_id")) + raise ValueError( + error_resources.RequiredParameterMissing.format("tenant_id") + ) if not team_id: raise ValueError(str(error_resources.TeamsTeamIdRequired)) @@ -533,7 +543,9 @@ async def send_message_to_list_of_channels( if not activity: raise ValueError(str(error_resources.ActivityRequired)) if not tenant_id: - raise ValueError(error_resources.RequiredParameterMissing.format("tenant_id")) + raise ValueError( + error_resources.RequiredParameterMissing.format("tenant_id") + ) if not members or len(members) == 0: raise ValueError("members list is required.") diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py index e78185f1..5d59719d 100644 --- a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py @@ -86,9 +86,7 @@ def validate_cosmos_db_config(config: "CosmosDBStorageConfig") -> None: def _validate_suffix(config: "CosmosDBStorageConfig") -> None: if config.key_suffix: if config.compatibility_mode: - raise ValueError( - str(error_resources.CosmosDbCompatibilityModeRequired) - ) + raise ValueError(str(error_resources.CosmosDbCompatibilityModeRequired)) suffix_escaped: str = sanitize_key(config.key_suffix) if suffix_escaped != config.key_suffix: raise ValueError( From 3d657763e30e09f56d1294d8ce02a109d408cc84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:47:40 +0000 Subject: [PATCH 11/16] Fix test failure by adding specific error for invalid CosmosDB key suffix characters Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../microsoft_agents/hosting/core/errors/error_resources.py | 6 ++++++ .../storage/cosmos/cosmos_db_storage_config.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py index b89b2e1e..1ff4e848 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py @@ -151,6 +151,12 @@ class ErrorResources: "storage-configuration", ) + CosmosDbInvalidKeySuffixCharacters = ErrorMessage( + "Cannot use invalid Row Key characters: {0} in keySuffix.", + -60111, + "storage-configuration", + ) + BlobStorageConfigRequired = ErrorMessage( "BlobStorage: BlobStorageConfig is required.", -60120, diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py index 5d59719d..ff3f2a87 100644 --- a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py @@ -90,7 +90,7 @@ def _validate_suffix(config: "CosmosDBStorageConfig") -> None: suffix_escaped: str = sanitize_key(config.key_suffix) if suffix_escaped != config.key_suffix: raise ValueError( - error_resources.InvalidParameterValue.format( - "keySuffix", config.key_suffix + error_resources.CosmosDbInvalidKeySuffixCharacters.format( + config.key_suffix ) ) From b06f3b0e7abac5665dd3376ab8798cb69a1d5618 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:13:36 +0000 Subject: [PATCH 12/16] 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> --- .../activity/_channel_id_field_mixin.py | 4 +- .../microsoft_agents/activity/activity.py | 8 +- .../microsoft_agents/activity/channel_id.py | 8 +- .../activity/errors/__init__.py | 14 + .../activity/errors/error_message.py | 68 ++++ .../activity/errors/error_resources.py | 58 +++ .../authentication/msal/errors/__init__.py | 14 + .../msal/errors/error_message.py | 68 ++++ .../msal/errors/error_resources.py | 76 ++++ .../authentication/msal/msal_auth.py | 36 +- .../copilotstudio/client/errors/__init__.py | 14 + .../client/errors/error_message.py | 68 ++++ .../client/errors/error_resources.py | 64 +++ .../client/power_platform_environment.py | 20 +- .../hosting/core/errors/README.md | 191 +++------ .../hosting/core/errors/error_resources.py | 383 ++---------------- .../hosting/teams/errors/__init__.py | 14 + .../hosting/teams/errors/error_message.py | 68 ++++ .../hosting/teams/errors/error_resources.py | 82 ++++ .../hosting/teams/teams_activity_handler.py | 57 +-- .../hosting/teams/teams_info.py | 54 ++- .../storage/blob/blob_storage.py | 6 +- .../storage/blob/errors/__init__.py | 14 + .../storage/blob/errors/error_message.py | 68 ++++ .../storage/blob/errors/error_resources.py | 46 +++ .../storage/cosmos/cosmos_db_storage.py | 12 +- .../cosmos/cosmos_db_storage_config.py | 16 +- .../storage/cosmos/errors/__init__.py | 14 + .../storage/cosmos/errors/error_message.py | 68 ++++ .../storage/cosmos/errors/error_resources.py | 100 +++++ 30 files changed, 1133 insertions(+), 580 deletions(-) create mode 100644 libraries/microsoft-agents-activity/microsoft_agents/activity/errors/__init__.py create mode 100644 libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_message.py create mode 100644 libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py create mode 100644 libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/__init__.py create mode 100644 libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_message.py create mode 100644 libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_resources.py create mode 100644 libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/__init__.py create mode 100644 libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_message.py create mode 100644 libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_resources.py create mode 100644 libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/__init__.py create mode 100644 libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_message.py create mode 100644 libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_resources.py create mode 100644 libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/__init__.py create mode 100644 libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_message.py create mode 100644 libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_resources.py create mode 100644 libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/__init__.py create mode 100644 libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_message.py create mode 100644 libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_resources.py diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py index e969c08c..8d301afd 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py @@ -42,9 +42,9 @@ def channel_id(self, value: Any): elif isinstance(value, str): self._channel_id = ChannelId(value) else: - from microsoft_agents.hosting.core import error_resources + from microsoft_agents.activity.errors import activity_errors - raise ValueError(error_resources.InvalidChannelIdType.format(type(value))) + raise ValueError(activity_errors.InvalidChannelIdType.format(type(value))) def _set_validated_channel_id(self, data: Any) -> None: """Sets the channel_id after validating it as a ChannelId model.""" diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index fd9b188f..9663b584 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -218,9 +218,9 @@ def _validate_channel_id( activity.channel_id.sub_channel and activity.channel_id.sub_channel != product_info.id ): - from microsoft_agents.hosting.core import error_resources + from microsoft_agents.activity.errors import activity_errors - raise Exception(str(error_resources.ChannelIdProductInfoConflict)) + raise Exception(str(activity_errors.ChannelIdProductInfoConflict)) activity.channel_id = ChannelId( channel=activity.channel_id.channel, sub_channel=product_info.id, @@ -256,9 +256,9 @@ def _serialize_sub_channel_data( # self.channel_id is the source of truth for serialization if self.channel_id and self.channel_id.sub_channel: if product_info and product_info.get("id") != self.channel_id.sub_channel: - from microsoft_agents.hosting.core import error_resources + from microsoft_agents.activity.errors import activity_errors - raise Exception(str(error_resources.ChannelIdProductInfoConflict)) + raise Exception(str(activity_errors.ChannelIdProductInfoConflict)) elif not product_info: if not serialized.get("entities"): serialized["entities"] = [] diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py index f94e456d..e23acc32 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py @@ -52,16 +52,16 @@ def __new__( """ if isinstance(value, str): if channel or sub_channel: - from microsoft_agents.hosting.core import error_resources + from microsoft_agents.activity.errors import activity_errors - raise ValueError(str(error_resources.ChannelIdValueConflict)) + raise ValueError(str(activity_errors.ChannelIdValueConflict)) value = value.strip() if value: return str.__new__(cls, value) - from microsoft_agents.hosting.core import error_resources + from microsoft_agents.activity.errors import activity_errors - raise TypeError(str(error_resources.ChannelIdValueMustBeNonEmpty)) + raise TypeError(str(activity_errors.ChannelIdValueMustBeNonEmpty)) else: if ( not isinstance(channel, str) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/__init__.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/__init__.py new file mode 100644 index 00000000..90283f53 --- /dev/null +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents Activity package. +""" + +from .error_message import ErrorMessage +from .error_resources import ActivityErrorResources + +# Singleton instance +activity_errors = ActivityErrorResources() + +__all__ = ["ErrorMessage", "ActivityErrorResources", "activity_errors"] diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_message.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_message.py new file mode 100644 index 00000000..08b6eb24 --- /dev/null +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_message.py @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +ErrorMessage class for formatting error messages with error codes and help URLs. +""" + + +class ErrorMessage: + """ + Represents a formatted error message with error code and help URL. + + This class formats error messages according to the Microsoft Agents SDK pattern: + - Original error message + - Error Code: [negative number] + - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor + """ + + def __init__( + self, + message_template: str, + error_code: int, + help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", + ): + """ + Initialize an ErrorMessage. + + :param message_template: The error message template (may include format placeholders) + :type message_template: str + :param error_code: The error code (should be negative) + :type error_code: int + :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) + :type help_url_anchor: str + """ + self.message_template = message_template + self.error_code = error_code + self.help_url_anchor = help_url_anchor + self.base_url = "https://aka.ms/M365AgentsErrorCodes" + + def format(self, *args, **kwargs) -> str: + """ + Format the error message with the provided arguments. + + :param args: Positional arguments for string formatting + :param kwargs: Keyword arguments for string formatting + :return: Formatted error message with error code and help URL + :rtype: str + """ + # Format the main message + if args or kwargs: + message = self.message_template.format(*args, **kwargs) + else: + message = self.message_template + + # Append error code and help URL + return ( + f"{message}\n\n" + f"Error Code: {self.error_code}\n" + f"Help URL: {self.base_url}/#{self.help_url_anchor}" + ) + + def __str__(self) -> str: + """Return the formatted error message without any arguments.""" + return self.format() + + def __repr__(self) -> str: + """Return a representation of the ErrorMessage.""" + return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py new file mode 100644 index 00000000..14ee59c0 --- /dev/null +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py @@ -0,0 +1,58 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Activity error resources for Microsoft Agents SDK. + +Error codes are in the range -64000 to -64999. +""" + +from .error_message import ErrorMessage + + +class ActivityErrorResources: + """ + Error messages for activity operations. + + Error codes are organized in the range -64000 to -64999. + """ + + InvalidChannelIdType = ErrorMessage( + "Invalid type for channel_id: {0}. Expected ChannelId or str.", + -64000, + "activity-schema", + ) + + ChannelIdProductInfoConflict = ErrorMessage( + "Conflict between channel_id.sub_channel and productInfo entity", + -64001, + "activity-schema", + ) + + ChannelIdValueConflict = ErrorMessage( + "If value is provided, channel and sub_channel must be None", + -64002, + "activity-schema", + ) + + ChannelIdValueMustBeNonEmpty = ErrorMessage( + "value must be a non empty string if provided", + -64003, + "activity-schema", + ) + + InvalidFromPropertyType = ErrorMessage( + "Invalid type for from_property: {0}. Expected ChannelAccount or dict.", + -64004, + "activity-schema", + ) + + InvalidRecipientType = ErrorMessage( + "Invalid type for recipient: {0}. Expected ChannelAccount or dict.", + -64005, + "activity-schema", + ) + + def __init__(self): + """Initialize ActivityErrorResources.""" + pass diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/__init__.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/__init__.py new file mode 100644 index 00000000..0c77b9fe --- /dev/null +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents Authentication MSAL package. +""" + +from .error_message import ErrorMessage +from .error_resources import AuthenticationErrorResources + +# Singleton instance +authentication_errors = AuthenticationErrorResources() + +__all__ = ["ErrorMessage", "AuthenticationErrorResources", "authentication_errors"] diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_message.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_message.py new file mode 100644 index 00000000..08b6eb24 --- /dev/null +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_message.py @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +ErrorMessage class for formatting error messages with error codes and help URLs. +""" + + +class ErrorMessage: + """ + Represents a formatted error message with error code and help URL. + + This class formats error messages according to the Microsoft Agents SDK pattern: + - Original error message + - Error Code: [negative number] + - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor + """ + + def __init__( + self, + message_template: str, + error_code: int, + help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", + ): + """ + Initialize an ErrorMessage. + + :param message_template: The error message template (may include format placeholders) + :type message_template: str + :param error_code: The error code (should be negative) + :type error_code: int + :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) + :type help_url_anchor: str + """ + self.message_template = message_template + self.error_code = error_code + self.help_url_anchor = help_url_anchor + self.base_url = "https://aka.ms/M365AgentsErrorCodes" + + def format(self, *args, **kwargs) -> str: + """ + Format the error message with the provided arguments. + + :param args: Positional arguments for string formatting + :param kwargs: Keyword arguments for string formatting + :return: Formatted error message with error code and help URL + :rtype: str + """ + # Format the main message + if args or kwargs: + message = self.message_template.format(*args, **kwargs) + else: + message = self.message_template + + # Append error code and help URL + return ( + f"{message}\n\n" + f"Error Code: {self.error_code}\n" + f"Help URL: {self.base_url}/#{self.help_url_anchor}" + ) + + def __str__(self) -> str: + """Return the formatted error message without any arguments.""" + return self.format() + + def __repr__(self) -> str: + """Return a representation of the ErrorMessage.""" + return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_resources.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_resources.py new file mode 100644 index 00000000..bbf461ad --- /dev/null +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_resources.py @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Authentication error resources for Microsoft Agents SDK. + +Error codes are in the range -60000 to -60999. +""" + +from .error_message import ErrorMessage + + +class AuthenticationErrorResources: + """ + Error messages for authentication operations. + + Error codes are organized in the range -60000 to -60999. + """ + + FailedToAcquireToken = ErrorMessage( + "Failed to acquire token. {0}", + -60012, + "agentic-identity-with-the-m365-agents-sdk", + ) + + InvalidInstanceUrl = ErrorMessage( + "Invalid instance URL", + -60013, + "agentic-identity-with-the-m365-agents-sdk", + ) + + OnBehalfOfFlowNotSupportedManagedIdentity = ErrorMessage( + "On-behalf-of flow is not supported with Managed Identity authentication.", + -60014, + "agentic-identity-with-the-m365-agents-sdk", + ) + + OnBehalfOfFlowNotSupportedAuthType = ErrorMessage( + "On-behalf-of flow is not supported with the current authentication type: {0}", + -60015, + "agentic-identity-with-the-m365-agents-sdk", + ) + + AuthenticationTypeNotSupported = ErrorMessage( + "Authentication type not supported", + -60016, + "agentic-identity-with-the-m365-agents-sdk", + ) + + AgentApplicationInstanceIdRequired = ErrorMessage( + "Agent application instance Id must be provided.", + -60017, + "agentic-identity-with-the-m365-agents-sdk", + ) + + FailedToAcquireAgenticInstanceToken = ErrorMessage( + "Failed to acquire agentic instance token or agent token for agent_app_instance_id {0}", + -60018, + "agentic-identity-with-the-m365-agents-sdk", + ) + + AgentApplicationInstanceIdAndUserIdRequired = ErrorMessage( + "Agent application instance Id and agentic user Id must be provided.", + -60019, + "agentic-identity-with-the-m365-agents-sdk", + ) + + FailedToAcquireInstanceOrAgentToken = ErrorMessage( + "Failed to acquire instance token or agent token for agent_app_instance_id {0} and agentic_user_id {1}", + -60020, + "agentic-identity-with-the-m365-agents-sdk", + ) + + def __init__(self): + """Initialize AuthenticationErrorResources.""" + pass diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py index 81c2546a..72e118ed 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py @@ -25,8 +25,8 @@ AuthTypes, AccessTokenProviderBase, AgentAuthConfiguration, - error_resources, ) +from microsoft_agents.authentication.msal.errors import authentication_errors logger = logging.getLogger(__name__) @@ -66,7 +66,7 @@ async def get_access_token( ) valid_uri, instance_uri = self._uri_validator(resource_url) if not valid_uri: - raise ValueError(str(error_resources.InvalidInstanceUrl)) + raise ValueError(str(authentication_errors.InvalidInstanceUrl)) local_scopes = self._resolve_scopes_list(instance_uri, scopes) self._create_client_application() @@ -88,7 +88,9 @@ async def get_access_token( if not res: logger.error("Failed to acquire token for resource %s", auth_result_payload) raise ValueError( - error_resources.FailedToAcquireToken.format(str(auth_result_payload)) + authentication_errors.FailedToAcquireToken.format( + str(auth_result_payload) + ) ) return res @@ -109,7 +111,7 @@ async def acquire_token_on_behalf_of( "Attempted on-behalf-of flow with Managed Identity authentication." ) raise NotImplementedError( - str(error_resources.OnBehalfOfFlowNotSupportedManagedIdentity) + str(authentication_errors.OnBehalfOfFlowNotSupportedManagedIdentity) ) elif isinstance(self._msal_auth_client, ConfidentialClientApplication): # TODO: Handling token error / acquisition failed @@ -127,7 +129,7 @@ async def acquire_token_on_behalf_of( f"Failed to acquire token on behalf of user: {user_assertion}" ) raise ValueError( - error_resources.FailedToAcquireToken.format(str(token)) + authentication_errors.FailedToAcquireToken.format(str(token)) ) return token["access_token"] @@ -136,7 +138,7 @@ async def acquire_token_on_behalf_of( f"On-behalf-of flow is not supported with the current authentication type: {self._msal_auth_client.__class__.__name__}" ) raise NotImplementedError( - error_resources.OnBehalfOfFlowNotSupportedAuthType.format( + authentication_errors.OnBehalfOfFlowNotSupportedAuthType.format( self._msal_auth_client.__class__.__name__ ) ) @@ -195,7 +197,7 @@ def _create_client_application(self) -> None: f"Unsupported authentication type: {self._msal_configuration.AUTH_TYPE}" ) raise NotImplementedError( - str(error_resources.AuthenticationTypeNotSupported) + str(authentication_errors.AuthenticationTypeNotSupported) ) self._msal_auth_client = ConfidentialClientApplication( @@ -242,7 +244,9 @@ async def get_agentic_application_token( """ if not agent_app_instance_id: - raise ValueError(str(error_resources.AgentApplicationInstanceIdRequired)) + raise ValueError( + str(authentication_errors.AgentApplicationInstanceIdRequired) + ) logger.info( "Attempting to get agentic application token from agent_app_instance_id %s", @@ -276,7 +280,9 @@ async def get_agentic_instance_token( """ if not agent_app_instance_id: - raise ValueError(str(error_resources.AgentApplicationInstanceIdRequired)) + raise ValueError( + str(authentication_errors.AgentApplicationInstanceIdRequired) + ) logger.info( "Attempting to get agentic instance token from agent_app_instance_id %s", @@ -292,7 +298,7 @@ async def get_agentic_instance_token( agent_app_instance_id, ) raise Exception( - error_resources.FailedToAcquireAgenticInstanceToken.format( + authentication_errors.FailedToAcquireAgenticInstanceToken.format( agent_app_instance_id ) ) @@ -317,7 +323,7 @@ async def get_agentic_instance_token( agent_app_instance_id, ) raise Exception( - error_resources.FailedToAcquireAgenticInstanceToken.format( + authentication_errors.FailedToAcquireAgenticInstanceToken.format( agent_app_instance_id ) ) @@ -330,7 +336,9 @@ async def get_agentic_instance_token( "Failed to acquire agentic instance token, %s", agentic_instance_token ) raise ValueError( - error_resources.FailedToAcquireToken.format(str(agentic_instance_token)) + authentication_errors.FailedToAcquireToken.format( + str(agentic_instance_token) + ) ) logger.debug( @@ -360,7 +368,7 @@ async def get_agentic_user_token( """ if not agent_app_instance_id or not agentic_user_id: raise ValueError( - str(error_resources.AgentApplicationInstanceIdAndUserIdRequired) + str(authentication_errors.AgentApplicationInstanceIdAndUserIdRequired) ) logger.info( @@ -379,7 +387,7 @@ async def get_agentic_user_token( agentic_user_id, ) raise Exception( - error_resources.FailedToAcquireInstanceOrAgentToken.format( + authentication_errors.FailedToAcquireInstanceOrAgentToken.format( agent_app_instance_id, agentic_user_id ) ) diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/__init__.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/__init__.py new file mode 100644 index 00000000..26d65f8a --- /dev/null +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents Copilot Studio Client package. +""" + +from .error_message import ErrorMessage +from .error_resources import CopilotStudioErrorResources + +# Singleton instance +copilot_studio_errors = CopilotStudioErrorResources() + +__all__ = ["ErrorMessage", "CopilotStudioErrorResources", "copilot_studio_errors"] diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_message.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_message.py new file mode 100644 index 00000000..08b6eb24 --- /dev/null +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_message.py @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +ErrorMessage class for formatting error messages with error codes and help URLs. +""" + + +class ErrorMessage: + """ + Represents a formatted error message with error code and help URL. + + This class formats error messages according to the Microsoft Agents SDK pattern: + - Original error message + - Error Code: [negative number] + - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor + """ + + def __init__( + self, + message_template: str, + error_code: int, + help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", + ): + """ + Initialize an ErrorMessage. + + :param message_template: The error message template (may include format placeholders) + :type message_template: str + :param error_code: The error code (should be negative) + :type error_code: int + :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) + :type help_url_anchor: str + """ + self.message_template = message_template + self.error_code = error_code + self.help_url_anchor = help_url_anchor + self.base_url = "https://aka.ms/M365AgentsErrorCodes" + + def format(self, *args, **kwargs) -> str: + """ + Format the error message with the provided arguments. + + :param args: Positional arguments for string formatting + :param kwargs: Keyword arguments for string formatting + :return: Formatted error message with error code and help URL + :rtype: str + """ + # Format the main message + if args or kwargs: + message = self.message_template.format(*args, **kwargs) + else: + message = self.message_template + + # Append error code and help URL + return ( + f"{message}\n\n" + f"Error Code: {self.error_code}\n" + f"Help URL: {self.base_url}/#{self.help_url_anchor}" + ) + + def __str__(self) -> str: + """Return the formatted error message without any arguments.""" + return self.format() + + def __repr__(self) -> str: + """Return a representation of the ErrorMessage.""" + return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_resources.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_resources.py new file mode 100644 index 00000000..f267a6a4 --- /dev/null +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_resources.py @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Copilot Studio error resources for Microsoft Agents SDK. + +Error codes are in the range -65000 to -65999. +""" + +from .error_message import ErrorMessage + + +class CopilotStudioErrorResources: + """ + Error messages for Copilot Studio operations. + + Error codes are organized in the range -65000 to -65999. + """ + + CloudBaseAddressRequired = ErrorMessage( + "cloud_base_address must be provided when PowerPlatformCloud is Other", + -65000, + "copilot-studio-client", + ) + + EnvironmentIdRequired = ErrorMessage( + "EnvironmentId must be provided", + -65001, + "copilot-studio-client", + ) + + AgentIdentifierRequired = ErrorMessage( + "AgentIdentifier must be provided", + -65002, + "copilot-studio-client", + ) + + CustomCloudOrBaseAddressRequired = ErrorMessage( + "Either CustomPowerPlatformCloud or cloud_base_address must be provided when PowerPlatformCloud is Other", + -65003, + "copilot-studio-client", + ) + + InvalidConnectionSettingsType = ErrorMessage( + "connection_settings must be of type DirectToEngineConnectionSettings", + -65004, + "copilot-studio-client", + ) + + PowerPlatformEnvironmentRequired = ErrorMessage( + "PowerPlatformEnvironment must be provided", + -65005, + "copilot-studio-client", + ) + + AccessTokenProviderRequired = ErrorMessage( + "AccessTokenProvider must be provided", + -65006, + "copilot-studio-client", + ) + + def __init__(self): + """Initialize CopilotStudioErrorResources.""" + pass diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py index 569a1178..75ccd6f0 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py @@ -1,4 +1,4 @@ -from microsoft_agents.hosting.core import error_resources +from microsoft_agents.copilotstudio.client.errors import copilot_studio_errors from urllib.parse import urlparse, urlunparse from typing import Optional from .connection_settings import ConnectionSettings @@ -23,11 +23,11 @@ def get_copilot_studio_connection_url( cloud_base_address: Optional[str] = None, ) -> str: if cloud == PowerPlatformCloud.OTHER and not cloud_base_address: - raise ValueError(str(error_resources.CloudBaseAddressRequired)) + raise ValueError(str(copilot_studio_errors.CloudBaseAddressRequired)) if not settings.environment_id: - raise ValueError(str(error_resources.EnvironmentIdRequired)) + raise ValueError(str(copilot_studio_errors.EnvironmentIdRequired)) if not settings.agent_identifier: - raise ValueError(str(error_resources.AgentIdentifierRequired)) + raise ValueError(str(copilot_studio_errors.AgentIdentifierRequired)) if settings.cloud and settings.cloud != PowerPlatformCloud.UNKNOWN: cloud = settings.cloud if cloud == PowerPlatformCloud.OTHER: @@ -38,7 +38,9 @@ def get_copilot_studio_connection_url( elif settings.custom_power_platform_cloud: cloud_base_address = settings.custom_power_platform_cloud else: - raise ValueError(str(error_resources.CustomCloudOrBaseAddressRequired)) + raise ValueError( + str(copilot_studio_errors.CustomCloudOrBaseAddressRequired) + ) if settings.copilot_agent_type: agent_type = settings.copilot_agent_type @@ -57,7 +59,7 @@ def get_token_audience( cloud_base_address: Optional[str] = None, ) -> str: if cloud == PowerPlatformCloud.OTHER and not cloud_base_address: - raise ValueError(str(error_resources.CloudBaseAddressRequired)) + raise ValueError(str(copilot_studio_errors.CloudBaseAddressRequired)) if not settings and cloud == PowerPlatformCloud.UNKNOWN: raise ValueError("Either settings or cloud must be provided") if settings and settings.cloud and settings.cloud != PowerPlatformCloud.UNKNOWN: @@ -73,7 +75,9 @@ def get_token_audience( cloud = PowerPlatformCloud.OTHER cloud_base_address = settings.custom_power_platform_cloud else: - raise ValueError(str(error_resources.CustomCloudOrBaseAddressRequired)) + raise ValueError( + str(copilot_studio_errors.CustomCloudOrBaseAddressRequired) + ) cloud_base_address = cloud_base_address or "api.unknown.powerplatform.com" return f"https://{PowerPlatformEnvironment.get_endpoint_suffix(cloud, cloud_base_address)}/.default" @@ -109,7 +113,7 @@ def get_environment_endpoint( cloud_base_address: Optional[str] = None, ) -> str: if cloud == PowerPlatformCloud.OTHER and not cloud_base_address: - raise ValueError(str(error_resources.CloudBaseAddressRequired)) + raise ValueError(str(copilot_studio_errors.CloudBaseAddressRequired)) cloud_base_address = cloud_base_address or "api.unknown.powerplatform.com" normalized_resource_id = environment_id.lower().replace("-", "") id_suffix_length = PowerPlatformEnvironment.get_id_suffix_length(cloud) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/README.md b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/README.md index 84c7bd8b..11f400a2 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/README.md +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/README.md @@ -1,33 +1,61 @@ -# Error Resources for Microsoft Agents SDK +# Error Resources for Microsoft Agents SDK - Hosting Core -This module provides centralized error messages with error codes and help URLs for the Microsoft Agents SDK, following the pattern established in the C# SDK. +This module provides centralized error messages with error codes and help URLs for hosting operations in the Microsoft Agents SDK. ## Overview -All error messages in the Microsoft Agents SDK are now centralized in the `error_resources` module. Each error includes: +Error messages are organized by package, with each package maintaining its own error resources: -1. **Formatted message string** - Can include placeholders for dynamic values -2. **Error code** - A unique negative integer identifying the error -3. **Help URL** - A link to documentation with hashtag anchor +- **Authentication** (`microsoft-agents-authentication-msal`): -60000 to -60999 +- **Storage - Cosmos** (`microsoft-agents-storage-cosmos`): -61000 to -61999 +- **Storage - Blob** (`microsoft-agents-storage-blob`): -61100 to -61199 +- **Teams** (`microsoft-agents-hosting-teams`): -62000 to -62999 +- **Hosting** (`microsoft-agents-hosting-core`): -63000 to -63999 +- **Activity** (`microsoft-agents-activity`): -64000 to -64999 +- **Copilot Studio** (`microsoft-agents-copilotstudio-client`): -65000 to -65999 +- **General/Validation** (`microsoft-agents-hosting-core`): -66000 to -66999 ## Usage -### Basic Usage +### Hosting Core Errors ```python from microsoft_agents.hosting.core import error_resources # Raise an error with a simple message -raise ValueError(str(error_resources.CosmosDbConfigRequired)) +raise ValueError(str(error_resources.AdapterRequired)) # Raise an error with formatted arguments -raise ValueError(error_resources.FailedToAcquireToken.format(auth_payload)) +raise ValueError(error_resources.ChannelServiceRouteNotFound.format("route_name")) +``` + +### Package-Specific Errors + +Each package exports its own error resources: + +```python +# Authentication errors +from microsoft_agents.authentication.msal.errors import authentication_errors +raise ValueError(authentication_errors.FailedToAcquireToken.format(payload)) + +# Storage errors +from microsoft_agents.storage.cosmos.errors import storage_errors +raise ValueError(str(storage_errors.CosmosDbConfigRequired)) + +# Teams errors +from microsoft_agents.hosting.teams.errors import teams_errors +raise ValueError(str(teams_errors.TeamsContextRequired)) -# Multiple arguments -raise ValueError(error_resources.CosmosDbPartitionKeyInvalid.format(key1, key2)) +# Activity errors +from microsoft_agents.activity.errors import activity_errors +raise ValueError(activity_errors.InvalidChannelIdType.format(type(value))) + +# Copilot Studio errors +from microsoft_agents.copilotstudio.client.errors import copilot_studio_errors +raise ValueError(str(copilot_studio_errors.EnvironmentIdRequired)) ``` -### Example Output +## Example Output When an error is raised, it will look like: @@ -40,147 +68,56 @@ Help URL: https://aka.ms/M365AgentsErrorCodes/#agentic-identity-with-the-m365-ag ## Error Code Ranges -Error codes are organized by category in the following ranges: - -| Range | Category | Example | -|-------|----------|---------| -| -60000 to -60099 | Authentication | FailedToAcquireToken (-60012) | -| -60100 to -60199 | Storage | CosmosDbConfigRequired (-60100) | -| -60200 to -60299 | Teams | TeamsBadRequest (-60200) | -| -60300 to -60399 | Hosting | AdapterRequired (-60300) | -| -60400 to -60499 | Activity | InvalidChannelIdType (-60400) | -| -60500 to -60599 | Copilot Studio | CloudBaseAddressRequired (-60500) | -| -60600 to -60699 | General/Validation | InvalidConfiguration (-60600) | - -## Available Error Resources - -### Authentication Errors - -- `FailedToAcquireToken` - Failed to acquire authentication token -- `InvalidInstanceUrl` - Invalid instance URL provided -- `OnBehalfOfFlowNotSupportedManagedIdentity` - On-behalf-of flow not supported with managed identity -- `OnBehalfOfFlowNotSupportedAuthType` - On-behalf-of flow not supported with current auth type -- `AuthenticationTypeNotSupported` - Authentication type not supported -- `AgentApplicationInstanceIdRequired` - Agent application instance ID required -- `FailedToAcquireAgenticInstanceToken` - Failed to acquire agentic instance token -- `AgentApplicationInstanceIdAndUserIdRequired` - Both agent app instance ID and user ID required -- `FailedToAcquireInstanceOrAgentToken` - Failed to acquire instance or agent token - -### Storage Errors - -- `CosmosDbConfigRequired` - CosmosDB configuration required -- `CosmosDbEndpointRequired` - CosmosDB endpoint required -- `CosmosDbAuthKeyRequired` - CosmosDB auth key required -- `CosmosDbDatabaseIdRequired` - CosmosDB database ID required -- `CosmosDbContainerIdRequired` - CosmosDB container ID required -- `CosmosDbKeyCannotBeEmpty` - CosmosDB key cannot be empty -- `BlobStorageConfigRequired` - Blob storage configuration required -- `BlobContainerNameRequired` - Blob container name required -- `StorageKeyCannotBeEmpty` - Storage key cannot be empty - -### Teams Errors - -- `TeamsBadRequest` - Bad request -- `TeamsNotImplemented` - Not implemented -- `TeamsContextRequired` - Context required -- `TeamsMeetingIdRequired` - Meeting ID required -- `TeamsParticipantIdRequired` - Participant ID required -- `TeamsTeamIdRequired` - Team ID required -- `TeamsTurnContextRequired` - TurnContext required -- `TeamsActivityRequired` - Activity required -- `TeamsChannelIdRequired` - Teams channel ID required -- `TeamsConversationIdRequired` - Conversation ID required - -### Hosting Errors - -- `AdapterRequired` - Adapter required -- `AgentApplicationRequired` - Agent application required -- `RequestRequired` - Request required -- `AgentRequired` - Agent required -- `StreamAlreadyEnded` - Stream already ended -- `TurnContextRequired` - TurnContext required -- `ActivityRequired` - Activity required - -### Activity Errors - -- `InvalidChannelIdType` - Invalid channel ID type -- `ChannelIdProductInfoConflict` - Conflict between channel ID and product info -- `ChannelIdValueConflict` - Value and channel cannot both be provided -- `ChannelIdValueMustBeNonEmpty` - Channel ID value must be non-empty - -### Copilot Studio Errors - -- `CloudBaseAddressRequired` - Cloud base address required -- `EnvironmentIdRequired` - Environment ID required -- `AgentIdentifierRequired` - Agent identifier required -- `CustomCloudOrBaseAddressRequired` - Custom cloud or base address required -- `PowerPlatformEnvironmentRequired` - Power Platform environment required -- `AccessTokenProviderRequired` - Access token provider required - -### General/Validation Errors - -- `InvalidConfiguration` - Invalid configuration -- `RequiredParameterMissing` - Required parameter missing -- `InvalidParameterValue` - Invalid parameter value -- `OperationNotSupported` - Operation not supported -- `ResourceNotFound` - Resource not found -- `UnexpectedError` - Unexpected error occurred +| Range | Package | Category | Example | +|-------|---------|----------|---------| +| -60000 to -60999 | microsoft-agents-authentication-msal | Authentication | FailedToAcquireToken (-60012) | +| -61000 to -61999 | microsoft-agents-storage-cosmos | Storage (Cosmos) | CosmosDbConfigRequired (-61000) | +| -61100 to -61199 | microsoft-agents-storage-blob | Storage (Blob) | BlobStorageConfigRequired (-61100) | +| -62000 to -62999 | microsoft-agents-hosting-teams | Teams | TeamsBadRequest (-62000) | +| -63000 to -63999 | microsoft-agents-hosting-core | Hosting | AdapterRequired (-63000) | +| -64000 to -64999 | microsoft-agents-activity | Activity | InvalidChannelIdType (-64000) | +| -65000 to -65999 | microsoft-agents-copilotstudio-client | Copilot Studio | CloudBaseAddressRequired (-65000) | +| -66000 to -66999 | microsoft-agents-hosting-core | General/Validation | InvalidConfiguration (-66000) | ## Adding New Error Messages -To add a new error message: +To add a new error message to a package: -1. Open `error_resources.py` -2. Add a new `ErrorMessage` instance to the `ErrorResources` class +1. Navigate to the package's `errors/error_resources.py` file +2. Add a new `ErrorMessage` instance with an appropriate error code within the package's range 3. Follow the naming convention: `PascalCaseErrorName` -4. Assign an error code in the appropriate range -5. Provide an appropriate help URL anchor +4. Provide an appropriate help URL anchor Example: ```python -NewErrorType = ErrorMessage( +NewHostingError = ErrorMessage( "Description of the error with {0} placeholder", - -60XXX, # Use appropriate range + -63XXX, # Use next available code in hosting range "help-url-anchor", ) ``` ## Avoiding Circular Imports -When using error_resources in modules that are imported by hosting-core's `__init__.py`, use lazy imports to avoid circular dependencies: +When using error resources in modules that might cause circular dependencies, use lazy imports: ```python -# Instead of module-level import: -# from microsoft_agents.hosting.core import error_resources - -# Use lazy import where needed: def my_function(): - from microsoft_agents.hosting.core import error_resources - raise ValueError(str(error_resources.SomeError)) + from microsoft_agents.activity.errors import activity_errors + raise ValueError(str(activity_errors.SomeError)) ``` ## Testing -Tests for error resources are located in `tests/hosting_core/errors/test_error_resources.py`. - -To run tests: -```bash -pytest tests/hosting_core/errors/test_error_resources.py -v -``` +Tests for error resources are located in `tests/hosting_core/errors/test_error_resources.py` and package-specific test files. ## Contributing When refactoring existing error messages: -1. Find the error message in the code -2. Identify or create an appropriate error resource -3. Replace the hardcoded string with `str(error_resources.ErrorName)` or `error_resources.ErrorName.format(args)` -4. For errors with placeholders, use `.format()` with appropriate arguments +1. Identify the package where the error belongs +2. Find or create the appropriate error resource in that package +3. Replace hardcoded strings with the error resource reference +4. Format with Black 5. Update tests if needed - -## Future Work - -- Help URLs will be updated with correct deep-link hashtags once documentation is available -- Additional error messages can be added as needed -- Error codes can be aligned with C# SDK as that evolves diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py index 1ff4e848..ac6a1723 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py @@ -2,14 +2,11 @@ # Licensed under the MIT License. """ -Central error resource file for Microsoft Agents SDK. +Hosting core error resources for Microsoft Agents SDK. -This module contains all error messages used throughout the SDK, each with: -- A formatting string -- An error code (aligned with C# SDK pattern) -- A help URL anchor - -Error codes are negative integers following the Microsoft Agents SDK convention. +This module contains error messages for hosting operations. +Error codes are in the range -63000 to -63999 for hosting errors. +General/validation errors are in the range -66000 to -66999. """ from .error_message import ErrorMessage @@ -17,490 +14,186 @@ class ErrorResources: """ - Central repository of all error messages used in the Microsoft Agents SDK. + Error messages for hosting core operations. Error codes are organized by range: - - -60000 to -60099: Authentication errors - - -60100 to -60199: Storage errors - - -60200 to -60299: Teams-specific errors - - -60300 to -60399: Hosting errors - - -60400 to -60499: Activity errors - - -60500 to -60599: Copilot Studio errors - - -60600 to -60699: General/validation errors + - -63000 to -63999: Hosting errors + - -66000 to -66999: General/validation errors """ - # Authentication Errors (-60000 to -60099) - FailedToAcquireToken = ErrorMessage( - "Failed to acquire token. {0}", - -60012, - "agentic-identity-with-the-m365-agents-sdk", - ) - - InvalidInstanceUrl = ErrorMessage( - "Invalid instance URL", - -60013, - "agentic-identity-with-the-m365-agents-sdk", - ) - - OnBehalfOfFlowNotSupportedManagedIdentity = ErrorMessage( - "On-behalf-of flow is not supported with Managed Identity authentication.", - -60014, - "agentic-identity-with-the-m365-agents-sdk", - ) - - OnBehalfOfFlowNotSupportedAuthType = ErrorMessage( - "On-behalf-of flow is not supported with the current authentication type: {0}", - -60015, - "agentic-identity-with-the-m365-agents-sdk", - ) - - AuthenticationTypeNotSupported = ErrorMessage( - "Authentication type not supported", - -60016, - "agentic-identity-with-the-m365-agents-sdk", - ) - - AgentApplicationInstanceIdRequired = ErrorMessage( - "Agent application instance Id must be provided.", - -60017, - "agentic-identity-with-the-m365-agents-sdk", - ) - - FailedToAcquireAgenticInstanceToken = ErrorMessage( - "Failed to acquire agentic instance token or agent token for agent_app_instance_id {0}", - -60018, - "agentic-identity-with-the-m365-agents-sdk", - ) - - AgentApplicationInstanceIdAndUserIdRequired = ErrorMessage( - "Agent application instance Id and agentic user Id must be provided.", - -60019, - "agentic-identity-with-the-m365-agents-sdk", - ) - - FailedToAcquireInstanceOrAgentToken = ErrorMessage( - "Failed to acquire instance token or agent token for agent_app_instance_id {0} and agentic_user_id {1}", - -60020, - "agentic-identity-with-the-m365-agents-sdk", - ) - - # Storage Errors (-60100 to -60199) - CosmosDbConfigRequired = ErrorMessage( - "CosmosDBStorage: CosmosDBConfig is required.", - -60100, - "storage-configuration", - ) - - CosmosDbEndpointRequired = ErrorMessage( - "CosmosDBStorage: cosmos_db_endpoint is required.", - -60101, - "storage-configuration", - ) - - CosmosDbAuthKeyRequired = ErrorMessage( - "CosmosDBStorage: auth_key is required.", - -60102, - "storage-configuration", - ) - - CosmosDbDatabaseIdRequired = ErrorMessage( - "CosmosDBStorage: database_id is required.", - -60103, - "storage-configuration", - ) - - CosmosDbContainerIdRequired = ErrorMessage( - "CosmosDBStorage: container_id is required.", - -60104, - "storage-configuration", - ) - - CosmosDbKeyCannotBeEmpty = ErrorMessage( - "CosmosDBStorage: Key cannot be empty.", - -60105, - "storage-configuration", - ) - - CosmosDbPartitionKeyInvalid = ErrorMessage( - "CosmosDBStorage: PartitionKey of {0} cannot be used with a CosmosDbPartitionedStorageOptions.PartitionKey of {1}.", - -60106, - "storage-configuration", - ) - - CosmosDbPartitionKeyPathInvalid = ErrorMessage( - "CosmosDBStorage: PartitionKeyPath must match cosmosDbPartitionedStorageOptions value of {0}", - -60107, - "storage-configuration", - ) - - CosmosDbCompatibilityModeRequired = ErrorMessage( - "CosmosDBStorage: compatibilityMode cannot be set when using partitionKey options.", - -60108, - "storage-configuration", - ) - - CosmosDbPartitionKeyNotFound = ErrorMessage( - "CosmosDBStorage: Partition key '{0}' missing from state, you may be missing custom state implementation.", - -60109, - "storage-configuration", - ) - - CosmosDbInvalidPartitionKeyValue = ErrorMessage( - "CosmosDBStorage: Invalid PartitionKey property on item with id {0}", - -60110, - "storage-configuration", - ) - - CosmosDbInvalidKeySuffixCharacters = ErrorMessage( - "Cannot use invalid Row Key characters: {0} in keySuffix.", - -60111, - "storage-configuration", - ) - - BlobStorageConfigRequired = ErrorMessage( - "BlobStorage: BlobStorageConfig is required.", - -60120, - "storage-configuration", - ) - - BlobConnectionStringOrUrlRequired = ErrorMessage( - "BlobStorage: either connection_string or container_url is required.", - -60121, - "storage-configuration", - ) - - BlobContainerNameRequired = ErrorMessage( - "BlobStorage: container_name is required.", - -60122, - "storage-configuration", - ) - - StorageKeyCannotBeEmpty = ErrorMessage( - "Storage: Key cannot be empty.", - -60130, - "storage-configuration", - ) - - StorageInvalidJsonBlob = ErrorMessage( - "Storage: Blob {0} could not be decoded as JSON. {1}", - -60131, - "storage-configuration", - ) - - # Teams Errors (-60200 to -60299) - TeamsBadRequest = ErrorMessage( - "BadRequest", - -60200, - "teams-integration", - ) - - TeamsNotImplemented = ErrorMessage( - "NotImplemented", - -60201, - "teams-integration", - ) - - TeamsContextRequired = ErrorMessage( - "context is required.", - -60202, - "teams-integration", - ) - - TeamsMeetingIdRequired = ErrorMessage( - "meeting_id is required.", - -60203, - "teams-integration", - ) - - TeamsParticipantIdRequired = ErrorMessage( - "participant_id is required.", - -60204, - "teams-integration", - ) - - TeamsTeamIdRequired = ErrorMessage( - "team_id is required.", - -60205, - "teams-integration", - ) - - TeamsTurnContextRequired = ErrorMessage( - "TurnContext cannot be None", - -60206, - "teams-integration", - ) - - TeamsActivityRequired = ErrorMessage( - "Activity cannot be None", - -60207, - "teams-integration", - ) - - TeamsChannelIdRequired = ErrorMessage( - "The teams_channel_id cannot be None or empty", - -60208, - "teams-integration", - ) - - TeamsConversationIdRequired = ErrorMessage( - "conversation_id is required.", - -60209, - "teams-integration", - ) - - # Hosting Errors (-60300 to -60399) + # Hosting Errors (-63000 to -63999) AdapterRequired = ErrorMessage( "start_agent_process: adapter can't be None", - -60300, + -63000, "hosting-configuration", ) AgentApplicationRequired = ErrorMessage( "start_agent_process: agent_application can't be None", - -60301, + -63001, "hosting-configuration", ) RequestRequired = ErrorMessage( "CloudAdapter.process: request can't be None", - -60302, + -63002, "hosting-configuration", ) AgentRequired = ErrorMessage( "CloudAdapter.process: agent can't be None", - -60303, + -63003, "hosting-configuration", ) StreamAlreadyEnded = ErrorMessage( "The stream has already ended.", - -60304, + -63004, "streaming", ) TurnContextRequired = ErrorMessage( "TurnContext cannot be None.", - -60305, + -63005, "hosting-configuration", ) ActivityRequired = ErrorMessage( "Activity cannot be None.", - -60306, + -63006, "hosting-configuration", ) AppIdRequired = ErrorMessage( "AppId cannot be empty or None.", - -60307, + -63007, "hosting-configuration", ) InvalidActivityType = ErrorMessage( "Invalid or missing activity type.", - -60308, + -63008, "hosting-configuration", ) ConversationIdRequired = ErrorMessage( "Conversation ID cannot be empty or None.", - -60309, + -63009, "hosting-configuration", ) AuthHeaderRequired = ErrorMessage( "Authorization header is required.", - -60310, + -63010, "hosting-configuration", ) InvalidAuthHeader = ErrorMessage( "Invalid authorization header format.", - -60311, + -63011, "hosting-configuration", ) ClaimsIdentityRequired = ErrorMessage( "ClaimsIdentity is required.", - -60312, + -63012, "hosting-configuration", ) ChannelServiceRouteNotFound = ErrorMessage( "Channel service route not found for: {0}", - -60313, + -63013, "hosting-configuration", ) TokenExchangeRequired = ErrorMessage( "Token exchange requires a token exchange resource.", - -60314, + -63014, "hosting-configuration", ) MissingHttpClient = ErrorMessage( "HTTP client is required.", - -60315, + -63015, "hosting-configuration", ) InvalidBotFrameworkActivity = ErrorMessage( "Invalid Bot Framework Activity format.", - -60316, + -63016, "hosting-configuration", ) CredentialsRequired = ErrorMessage( "Credentials are required for authentication.", - -60317, + -63017, "hosting-configuration", ) - # Activity Errors (-60400 to -60499) - InvalidChannelIdType = ErrorMessage( - "Invalid type for channel_id: {0}. Expected ChannelId or str.", - -60400, - "activity-schema", - ) - - ChannelIdProductInfoConflict = ErrorMessage( - "Conflict between channel_id.sub_channel and productInfo entity", - -60401, - "activity-schema", - ) - - ChannelIdValueConflict = ErrorMessage( - "If value is provided, channel and sub_channel must be None", - -60402, - "activity-schema", - ) - - ChannelIdValueMustBeNonEmpty = ErrorMessage( - "value must be a non empty string if provided", - -60403, - "activity-schema", - ) - - InvalidFromPropertyType = ErrorMessage( - "Invalid type for from_property: {0}. Expected ChannelAccount or dict.", - -60404, - "activity-schema", - ) - - InvalidRecipientType = ErrorMessage( - "Invalid type for recipient: {0}. Expected ChannelAccount or dict.", - -60405, - "activity-schema", - ) - - # Copilot Studio Errors (-60500 to -60599) - CloudBaseAddressRequired = ErrorMessage( - "cloud_base_address must be provided when PowerPlatformCloud is Other", - -60500, - "copilot-studio-client", - ) - - EnvironmentIdRequired = ErrorMessage( - "EnvironmentId must be provided", - -60501, - "copilot-studio-client", - ) - - AgentIdentifierRequired = ErrorMessage( - "AgentIdentifier must be provided", - -60502, - "copilot-studio-client", - ) - - CustomCloudOrBaseAddressRequired = ErrorMessage( - "Either CustomPowerPlatformCloud or cloud_base_address must be provided when PowerPlatformCloud is Other", - -60503, - "copilot-studio-client", - ) - - InvalidConnectionSettingsType = ErrorMessage( - "connection_settings must be of type DirectToEngineConnectionSettings", - -60504, - "copilot-studio-client", - ) - - PowerPlatformEnvironmentRequired = ErrorMessage( - "PowerPlatformEnvironment must be provided", - -60505, - "copilot-studio-client", - ) - - AccessTokenProviderRequired = ErrorMessage( - "AccessTokenProvider must be provided", - -60506, - "copilot-studio-client", - ) - - # General/Validation Errors (-60600 to -60699) + # General/Validation Errors (-66000 to -66999) InvalidConfiguration = ErrorMessage( "Invalid configuration: {0}", - -60600, + -66000, "configuration", ) RequiredParameterMissing = ErrorMessage( "Required parameter missing: {0}", - -60601, + -66001, "configuration", ) InvalidParameterValue = ErrorMessage( "Invalid parameter value for {0}: {1}", - -60602, + -66002, "configuration", ) OperationNotSupported = ErrorMessage( "Operation not supported: {0}", - -60603, + -66003, "configuration", ) ResourceNotFound = ErrorMessage( "Resource not found: {0}", - -60604, + -66004, "configuration", ) UnexpectedError = ErrorMessage( "An unexpected error occurred: {0}", - -60605, + -66005, "configuration", ) InvalidStateObject = ErrorMessage( "Invalid state object: {0}", - -60606, + -66006, "configuration", ) SerializationError = ErrorMessage( "Serialization error: {0}", - -60607, + -66007, "configuration", ) DeserializationError = ErrorMessage( "Deserialization error: {0}", - -60608, + -66008, "configuration", ) TimeoutError = ErrorMessage( "Operation timed out: {0}", - -60609, + -66009, "configuration", ) NetworkError = ErrorMessage( "Network error occurred: {0}", - -60610, + -66010, "configuration", ) diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/__init__.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/__init__.py new file mode 100644 index 00000000..fdee7825 --- /dev/null +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents Hosting Teams package. +""" + +from .error_message import ErrorMessage +from .error_resources import TeamsErrorResources + +# Singleton instance +teams_errors = TeamsErrorResources() + +__all__ = ["ErrorMessage", "TeamsErrorResources", "teams_errors"] diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_message.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_message.py new file mode 100644 index 00000000..08b6eb24 --- /dev/null +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_message.py @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +ErrorMessage class for formatting error messages with error codes and help URLs. +""" + + +class ErrorMessage: + """ + Represents a formatted error message with error code and help URL. + + This class formats error messages according to the Microsoft Agents SDK pattern: + - Original error message + - Error Code: [negative number] + - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor + """ + + def __init__( + self, + message_template: str, + error_code: int, + help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", + ): + """ + Initialize an ErrorMessage. + + :param message_template: The error message template (may include format placeholders) + :type message_template: str + :param error_code: The error code (should be negative) + :type error_code: int + :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) + :type help_url_anchor: str + """ + self.message_template = message_template + self.error_code = error_code + self.help_url_anchor = help_url_anchor + self.base_url = "https://aka.ms/M365AgentsErrorCodes" + + def format(self, *args, **kwargs) -> str: + """ + Format the error message with the provided arguments. + + :param args: Positional arguments for string formatting + :param kwargs: Keyword arguments for string formatting + :return: Formatted error message with error code and help URL + :rtype: str + """ + # Format the main message + if args or kwargs: + message = self.message_template.format(*args, **kwargs) + else: + message = self.message_template + + # Append error code and help URL + return ( + f"{message}\n\n" + f"Error Code: {self.error_code}\n" + f"Help URL: {self.base_url}/#{self.help_url_anchor}" + ) + + def __str__(self) -> str: + """Return the formatted error message without any arguments.""" + return self.format() + + def __repr__(self) -> str: + """Return a representation of the ErrorMessage.""" + return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_resources.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_resources.py new file mode 100644 index 00000000..6456868b --- /dev/null +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_resources.py @@ -0,0 +1,82 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Teams error resources for Microsoft Agents SDK. + +Error codes are in the range -62000 to -62999. +""" + +from .error_message import ErrorMessage + + +class TeamsErrorResources: + """ + Error messages for Teams operations. + + Error codes are organized in the range -62000 to -62999. + """ + + TeamsBadRequest = ErrorMessage( + "BadRequest", + -62000, + "teams-integration", + ) + + TeamsNotImplemented = ErrorMessage( + "NotImplemented", + -62001, + "teams-integration", + ) + + TeamsContextRequired = ErrorMessage( + "context is required.", + -62002, + "teams-integration", + ) + + TeamsMeetingIdRequired = ErrorMessage( + "meeting_id is required.", + -62003, + "teams-integration", + ) + + TeamsParticipantIdRequired = ErrorMessage( + "participant_id is required.", + -62004, + "teams-integration", + ) + + TeamsTeamIdRequired = ErrorMessage( + "team_id is required.", + -62005, + "teams-integration", + ) + + TeamsTurnContextRequired = ErrorMessage( + "TurnContext cannot be None", + -62006, + "teams-integration", + ) + + TeamsActivityRequired = ErrorMessage( + "Activity cannot be None", + -62007, + "teams-integration", + ) + + TeamsChannelIdRequired = ErrorMessage( + "The teams_channel_id cannot be None or empty", + -62008, + "teams-integration", + ) + + TeamsConversationIdRequired = ErrorMessage( + "conversation_id is required.", + -62009, + "teams-integration", + ) + + def __init__(self): + """Initialize TeamsErrorResources.""" + pass diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_activity_handler.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_activity_handler.py index d811dc18..ba753bb2 100644 --- a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_activity_handler.py +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_activity_handler.py @@ -6,7 +6,8 @@ from http import HTTPStatus from typing import Any, List -from microsoft_agents.hosting.core import ActivityHandler, TurnContext, error_resources +from microsoft_agents.hosting.core import ActivityHandler, TurnContext +from microsoft_agents.hosting.teams.errors import teams_errors from microsoft_agents.activity import ( InvokeResponse, ChannelAccount, @@ -155,9 +156,9 @@ async def on_invoke_activity(self, turn_context: TurnContext) -> InvokeResponse: else: return await super().on_invoke_activity(turn_context) except Exception as err: - if str(err) == str(error_resources.TeamsNotImplemented): + if str(err) == str(teams_errors.TeamsNotImplemented): return InvokeResponse(status=int(HTTPStatus.NOT_IMPLEMENTED)) - elif str(err) == str(error_resources.TeamsBadRequest): + elif str(err) == str(teams_errors.TeamsBadRequest): return InvokeResponse(status=int(HTTPStatus.BAD_REQUEST)) raise @@ -170,7 +171,7 @@ async def on_teams_card_action_invoke( :param turn_context: The context object for the turn. :return: An InvokeResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_config_fetch( self, turn_context: TurnContext, config_data: Any @@ -182,7 +183,7 @@ async def on_teams_config_fetch( :param config_data: The config data. :return: A ConfigResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_config_submit( self, turn_context: TurnContext, config_data: Any @@ -194,7 +195,7 @@ async def on_teams_config_submit( :param config_data: The config data. :return: A ConfigResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_file_consent( self, @@ -217,7 +218,7 @@ async def on_teams_file_consent( turn_context, file_consent_card_response ) else: - raise ValueError(str(error_resources.TeamsBadRequest)) + raise ValueError(str(teams_errors.TeamsBadRequest)) async def on_teams_file_consent_accept( self, @@ -231,7 +232,7 @@ async def on_teams_file_consent_accept( :param file_consent_card_response: The file consent card response. :return: None """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_file_consent_decline( self, @@ -245,7 +246,7 @@ async def on_teams_file_consent_decline( :param file_consent_card_response: The file consent card response. :return: None """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_o365_connector_card_action( self, turn_context: TurnContext, query: O365ConnectorCardActionQuery @@ -257,7 +258,7 @@ async def on_teams_o365_connector_card_action( :param query: The O365 connector card action query. :return: None """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_signin_verify_state( self, turn_context: TurnContext, query: SigninStateVerificationQuery @@ -269,7 +270,7 @@ async def on_teams_signin_verify_state( :param query: The sign-in state verification query. :return: None """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_signin_token_exchange( self, turn_context: TurnContext, query: SigninStateVerificationQuery @@ -281,7 +282,7 @@ async def on_teams_signin_token_exchange( :param query: The sign-in state verification query. :return: None """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_app_based_link_query( self, turn_context: TurnContext, query: AppBasedLinkQuery @@ -293,7 +294,7 @@ async def on_teams_app_based_link_query( :param query: The app-based link query. :return: A MessagingExtensionResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_anonymous_app_based_link_query( self, turn_context: TurnContext, query: AppBasedLinkQuery @@ -305,7 +306,7 @@ async def on_teams_anonymous_app_based_link_query( :param query: The app-based link query. :return: A MessagingExtensionResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_messaging_extension_query( self, turn_context: TurnContext, query: MessagingExtensionQuery @@ -317,7 +318,7 @@ async def on_teams_messaging_extension_query( :param query: The messaging extension query. :return: A MessagingExtensionResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_messaging_extension_select_item( self, turn_context: TurnContext, query: Any @@ -329,7 +330,7 @@ async def on_teams_messaging_extension_select_item( :param query: The query. :return: A MessagingExtensionResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_messaging_extension_submit_action_dispatch( self, turn_context: TurnContext, action: MessagingExtensionAction @@ -351,7 +352,7 @@ async def on_teams_messaging_extension_submit_action_dispatch( turn_context, action ) else: - raise ValueError(str(error_resources.TeamsBadRequest)) + raise ValueError(str(teams_errors.TeamsBadRequest)) else: return await self.on_teams_messaging_extension_submit_action( turn_context, action @@ -367,7 +368,7 @@ async def on_teams_messaging_extension_submit_action( :param action: The messaging extension action. :return: A MessagingExtensionActionResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_messaging_extension_message_preview_edit( self, turn_context: TurnContext, action: MessagingExtensionAction @@ -379,7 +380,7 @@ async def on_teams_messaging_extension_message_preview_edit( :param action: The messaging extension action. :return: A MessagingExtensionActionResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_messaging_extension_message_preview_send( self, turn_context: TurnContext, action: MessagingExtensionAction @@ -391,7 +392,7 @@ async def on_teams_messaging_extension_message_preview_send( :param action: The messaging extension action. :return: A MessagingExtensionActionResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_messaging_extension_fetch_task( self, turn_context: TurnContext, action: MessagingExtensionAction @@ -403,7 +404,7 @@ async def on_teams_messaging_extension_fetch_task( :param action: The messaging extension action. :return: A MessagingExtensionActionResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_messaging_extension_configuration_query_setting_url( self, turn_context: TurnContext, query: MessagingExtensionQuery @@ -415,7 +416,7 @@ async def on_teams_messaging_extension_configuration_query_setting_url( :param query: The messaging extension query. :return: A MessagingExtensionResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_messaging_extension_configuration_setting( self, turn_context: TurnContext, settings: Any @@ -427,7 +428,7 @@ async def on_teams_messaging_extension_configuration_setting( :param settings: The settings. :return: None """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_messaging_extension_card_button_clicked( self, turn_context: TurnContext, card_data: Any @@ -439,7 +440,7 @@ async def on_teams_messaging_extension_card_button_clicked( :param card_data: The card data. :return: None """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_task_module_fetch( self, turn_context: TurnContext, task_module_request: TaskModuleRequest @@ -451,7 +452,7 @@ async def on_teams_task_module_fetch( :param task_module_request: The task module request. :return: A TaskModuleResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_task_module_submit( self, turn_context: TurnContext, task_module_request: TaskModuleRequest @@ -463,7 +464,7 @@ async def on_teams_task_module_submit( :param task_module_request: The task module request. :return: A TaskModuleResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_tab_fetch( self, turn_context: TurnContext, tab_request: TabRequest @@ -475,7 +476,7 @@ async def on_teams_tab_fetch( :param tab_request: The tab request. :return: A TabResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_teams_tab_submit( self, turn_context: TurnContext, tab_submit: TabSubmit @@ -487,7 +488,7 @@ async def on_teams_tab_submit( :param tab_submit: The tab submit. :return: A TabResponse. """ - raise NotImplementedError(str(error_resources.TeamsNotImplemented)) + raise NotImplementedError(str(teams_errors.TeamsNotImplemented)) async def on_conversation_update_activity(self, turn_context: TurnContext): """ diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py index 3de4f39a..6e06af35 100644 --- a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py @@ -26,8 +26,8 @@ from microsoft_agents.hosting.core import ( ChannelServiceAdapter, TurnContext, - error_resources, ) +from microsoft_agents.hosting.teams.errors import teams_errors class TeamsInfo: @@ -56,7 +56,7 @@ async def get_meeting_participant( ValueError: If required parameters are missing. """ if not context: - raise ValueError(str(error_resources.TeamsContextRequired)) + raise ValueError(str(teams_errors.TeamsContextRequired)) activity = context.activity teams_channel_data: dict = activity.channel_data @@ -65,13 +65,13 @@ async def get_meeting_participant( meeting_id = teams_channel_data.get("meeting", {}).get("id", None) if not meeting_id: - raise ValueError(str(error_resources.TeamsMeetingIdRequired)) + raise ValueError(str(teams_errors.TeamsMeetingIdRequired)) if participant_id is None: participant_id = getattr(activity.from_property, "aad_object_id", None) if not participant_id: - raise ValueError(str(error_resources.TeamsParticipantIdRequired)) + raise ValueError(str(teams_errors.TeamsParticipantIdRequired)) if tenant_id is None: tenant_id = teams_channel_data.get("tenant", {}).get("id", None) @@ -104,7 +104,7 @@ async def get_meeting_info( meeting_id = teams_channel_data.get("meeting", {}).get("id", None) if not meeting_id: - raise ValueError(str(error_resources.TeamsMeetingIdRequired)) + raise ValueError(str(teams_errors.TeamsMeetingIdRequired)) rest_client = TeamsInfo._get_rest_client(context) result = await rest_client.fetch_meeting_info(meeting_id) @@ -132,7 +132,7 @@ async def get_team_details( team_id = teams_channel_data.get("team", {}).get("id", None) if not team_id: - raise ValueError(str(error_resources.TeamsTeamIdRequired)) + raise ValueError(str(teams_errors.TeamsTeamIdRequired)) rest_client = TeamsInfo._get_rest_client(context) result = await rest_client.fetch_team_details(team_id) @@ -161,13 +161,13 @@ async def send_message_to_teams_channel( ValueError: If required parameters are missing. """ if not context: - raise ValueError(str(error_resources.TeamsTurnContextRequired)) + raise ValueError(str(teams_errors.TeamsTurnContextRequired)) if not activity: - raise ValueError(str(error_resources.TeamsActivityRequired)) + raise ValueError(str(teams_errors.TeamsActivityRequired)) if not teams_channel_id: - raise ValueError(str(error_resources.TeamsChannelIdRequired)) + raise ValueError(str(teams_errors.TeamsChannelIdRequired)) convo_params = ConversationParameters( is_group=True, @@ -243,7 +243,7 @@ async def get_team_channels( team_id = teams_channel_data.get("team", {}).get("id", None) if not team_id: - raise ValueError(str(error_resources.TeamsTeamIdRequired)) + raise ValueError(str(teams_errors.TeamsTeamIdRequired)) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.fetch_channel_list(team_id) @@ -282,7 +282,7 @@ async def get_paged_members( else None ) if not conversation_id: - raise ValueError(str(error_resources.TeamsConversationIdRequired)) + raise ValueError(str(teams_errors.TeamsConversationIdRequired)) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.get_conversation_paged_member( @@ -316,7 +316,7 @@ async def get_member(context: TurnContext, user_id: str) -> TeamsChannelAccount: else None ) if not conversation_id: - raise ValueError(str(error_resources.TeamsConversationIdRequired)) + raise ValueError(str(teams_errors.TeamsConversationIdRequired)) return await TeamsInfo._get_member_internal( context, conversation_id, user_id @@ -349,7 +349,7 @@ async def get_paged_team_members( team_id = teams_channel_data.get("team", {}).get("id", None) if not team_id: - raise ValueError(str(error_resources.TeamsTeamIdRequired)) + raise ValueError(str(teams_errors.TeamsTeamIdRequired)) rest_client = TeamsInfo._get_rest_client(context) paged_results = await rest_client.get_conversation_paged_member( @@ -414,7 +414,7 @@ async def send_meeting_notification( meeting_id = teams_channel_data.get("meeting", {}).get("id", None) if not meeting_id: - raise ValueError(str(error_resources.TeamsMeetingIdRequired)) + raise ValueError(str(teams_errors.TeamsMeetingIdRequired)) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.send_meeting_notification(meeting_id, notification) @@ -442,11 +442,9 @@ async def send_message_to_list_of_users( ValueError: If required parameters are missing. """ if not activity: - raise ValueError(str(error_resources.ActivityRequired)) + raise ValueError(str(teams_errors.ActivityRequired)) if not tenant_id: - raise ValueError( - error_resources.RequiredParameterMissing.format("tenant_id") - ) + raise ValueError(teams_errors.RequiredParameterMissing.format("tenant_id")) if not members or len(members) == 0: raise ValueError("members list is required.") @@ -474,11 +472,9 @@ async def send_message_to_all_users_in_tenant( ValueError: If required parameters are missing. """ if not activity: - raise ValueError(str(error_resources.ActivityRequired)) + raise ValueError(str(teams_errors.ActivityRequired)) if not tenant_id: - raise ValueError( - error_resources.RequiredParameterMissing.format("tenant_id") - ) + raise ValueError(teams_errors.RequiredParameterMissing.format("tenant_id")) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.send_message_to_all_users_in_tenant( @@ -505,13 +501,11 @@ async def send_message_to_all_users_in_team( ValueError: If required parameters are missing. """ if not activity: - raise ValueError(str(error_resources.ActivityRequired)) + raise ValueError(str(teams_errors.ActivityRequired)) if not tenant_id: - raise ValueError( - error_resources.RequiredParameterMissing.format("tenant_id") - ) + raise ValueError(teams_errors.RequiredParameterMissing.format("tenant_id")) if not team_id: - raise ValueError(str(error_resources.TeamsTeamIdRequired)) + raise ValueError(str(teams_errors.TeamsTeamIdRequired)) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.send_message_to_all_users_in_team( @@ -541,11 +535,9 @@ async def send_message_to_list_of_channels( ValueError: If required parameters are missing. """ if not activity: - raise ValueError(str(error_resources.ActivityRequired)) + raise ValueError(str(teams_errors.ActivityRequired)) if not tenant_id: - raise ValueError( - error_resources.RequiredParameterMissing.format("tenant_id") - ) + raise ValueError(teams_errors.RequiredParameterMissing.format("tenant_id")) if not members or len(members) == 0: raise ValueError("members list is required.") diff --git a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/blob_storage.py b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/blob_storage.py index 107d5931..aed86416 100644 --- a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/blob_storage.py +++ b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/blob_storage.py @@ -14,7 +14,7 @@ ignore_error, is_status_code_error, ) -from microsoft_agents.hosting.core import error_resources +from microsoft_agents.storage.blob.errors import blob_storage_errors from .blob_storage_config import BlobStorageConfig @@ -26,7 +26,7 @@ class BlobStorage(AsyncStorageBase): def __init__(self, config: BlobStorageConfig): if not config.container_name: - raise ValueError(str(error_resources.BlobContainerNameRequired)) + raise ValueError(str(blob_storage_errors.BlobContainerNameRequired)) self.config = config @@ -40,7 +40,7 @@ def _create_client(self) -> BlobServiceClient: if self.config.url: # connect with URL and credentials if not self.config.credential: raise ValueError( - error_resources.InvalidConfiguration.format( + blob_storage_errors.InvalidConfiguration.format( "Credential is required when using a custom service URL" ) ) diff --git a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/__init__.py b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/__init__.py new file mode 100644 index 00000000..3fb309fc --- /dev/null +++ b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents Storage Blob package. +""" + +from .error_message import ErrorMessage +from .error_resources import BlobStorageErrorResources + +# Singleton instance +blob_storage_errors = BlobStorageErrorResources() + +__all__ = ["ErrorMessage", "BlobStorageErrorResources", "blob_storage_errors"] diff --git a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_message.py b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_message.py new file mode 100644 index 00000000..08b6eb24 --- /dev/null +++ b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_message.py @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +ErrorMessage class for formatting error messages with error codes and help URLs. +""" + + +class ErrorMessage: + """ + Represents a formatted error message with error code and help URL. + + This class formats error messages according to the Microsoft Agents SDK pattern: + - Original error message + - Error Code: [negative number] + - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor + """ + + def __init__( + self, + message_template: str, + error_code: int, + help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", + ): + """ + Initialize an ErrorMessage. + + :param message_template: The error message template (may include format placeholders) + :type message_template: str + :param error_code: The error code (should be negative) + :type error_code: int + :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) + :type help_url_anchor: str + """ + self.message_template = message_template + self.error_code = error_code + self.help_url_anchor = help_url_anchor + self.base_url = "https://aka.ms/M365AgentsErrorCodes" + + def format(self, *args, **kwargs) -> str: + """ + Format the error message with the provided arguments. + + :param args: Positional arguments for string formatting + :param kwargs: Keyword arguments for string formatting + :return: Formatted error message with error code and help URL + :rtype: str + """ + # Format the main message + if args or kwargs: + message = self.message_template.format(*args, **kwargs) + else: + message = self.message_template + + # Append error code and help URL + return ( + f"{message}\n\n" + f"Error Code: {self.error_code}\n" + f"Help URL: {self.base_url}/#{self.help_url_anchor}" + ) + + def __str__(self) -> str: + """Return the formatted error message without any arguments.""" + return self.format() + + def __repr__(self) -> str: + """Return a representation of the ErrorMessage.""" + return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_resources.py b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_resources.py new file mode 100644 index 00000000..316b5841 --- /dev/null +++ b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_resources.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Blob storage error resources for Microsoft Agents SDK. + +Error codes are in the range -61100 to -61199. +""" + +from .error_message import ErrorMessage + + +class BlobStorageErrorResources: + """ + Error messages for blob storage operations. + + Error codes are organized in the range -61100 to -61199. + """ + + BlobStorageConfigRequired = ErrorMessage( + "BlobStorage: BlobStorageConfig is required.", + -61100, + "storage-configuration", + ) + + BlobConnectionStringOrUrlRequired = ErrorMessage( + "BlobStorage: either connection_string or container_url is required.", + -61101, + "storage-configuration", + ) + + BlobContainerNameRequired = ErrorMessage( + "BlobStorage: container_name is required.", + -61102, + "storage-configuration", + ) + + InvalidConfiguration = ErrorMessage( + "Invalid configuration: {0}", + -61103, + "configuration", + ) + + def __init__(self): + """Initialize BlobStorageErrorResources.""" + pass diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage.py index cc2fdc2f..f693205f 100644 --- a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage.py +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage.py @@ -20,7 +20,7 @@ from microsoft_agents.hosting.core.storage import AsyncStorageBase, StoreItem from microsoft_agents.hosting.core.storage._type_aliases import JSON from microsoft_agents.hosting.core.storage.error_handling import ignore_error -from microsoft_agents.hosting.core import error_resources +from microsoft_agents.storage.cosmos.errors import storage_errors from .cosmos_db_storage_config import CosmosDBStorageConfig from .key_ops import sanitize_key @@ -56,7 +56,7 @@ def _create_client(self) -> CosmosClient: if self._config.url: if not self._config.credential: raise ValueError( - error_resources.InvalidConfiguration.format( + storage_errors.InvalidConfiguration.format( "Credential is required when using a custom service URL" ) ) @@ -92,7 +92,7 @@ async def _read_item( ) -> tuple[Union[str, None], Union[StoreItemT, None]]: if key == "": - raise ValueError(str(error_resources.CosmosDbKeyCannotBeEmpty)) + raise ValueError(str(storage_errors.CosmosDbKeyCannotBeEmpty)) escaped_key: str = self._sanitize(key) read_item_response: CosmosDict = await ignore_error( @@ -109,7 +109,7 @@ async def _read_item( async def _write_item(self, key: str, item: StoreItem) -> None: if key == "": - raise ValueError(str(error_resources.CosmosDbKeyCannotBeEmpty)) + raise ValueError(str(storage_errors.CosmosDbKeyCannotBeEmpty)) escaped_key: str = self._sanitize(key) @@ -122,7 +122,7 @@ async def _write_item(self, key: str, item: StoreItem) -> None: async def _delete_item(self, key: str) -> None: if key == "": - raise ValueError(str(error_resources.CosmosDbKeyCannotBeEmpty)) + raise ValueError(str(storage_errors.CosmosDbKeyCannotBeEmpty)) escaped_key: str = self._sanitize(key) @@ -159,7 +159,7 @@ async def _create_container(self) -> None: self._compatability_mode_partition_key = True elif "/id" not in paths: raise Exception( - error_resources.InvalidConfiguration.format( + storage_errors.InvalidConfiguration.format( f"Custom Partition Key Paths are not supported. {self._config.container_id} has a custom Partition Key Path of {paths[0]}." ) ) diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py index ff3f2a87..c06a79fb 100644 --- a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/cosmos_db_storage_config.py @@ -2,7 +2,7 @@ from typing import Union from azure.core.credentials import TokenCredential -from microsoft_agents.hosting.core import error_resources +from microsoft_agents.storage.cosmos.errors import storage_errors from .key_ops import sanitize_key @@ -70,15 +70,15 @@ def validate_cosmos_db_config(config: "CosmosDBStorageConfig") -> None: This is used prior to the creation of the CosmosDBStorage object.""" if not config: - raise ValueError(str(error_resources.CosmosDbConfigRequired)) + raise ValueError(str(storage_errors.CosmosDbConfigRequired)) if not config.cosmos_db_endpoint: - raise ValueError(str(error_resources.CosmosDbEndpointRequired)) + raise ValueError(str(storage_errors.CosmosDbEndpointRequired)) if not config.auth_key: - raise ValueError(str(error_resources.CosmosDbAuthKeyRequired)) + raise ValueError(str(storage_errors.CosmosDbAuthKeyRequired)) if not config.database_id: - raise ValueError(str(error_resources.CosmosDbDatabaseIdRequired)) + raise ValueError(str(storage_errors.CosmosDbDatabaseIdRequired)) if not config.container_id: - raise ValueError(str(error_resources.CosmosDbContainerIdRequired)) + raise ValueError(str(storage_errors.CosmosDbContainerIdRequired)) CosmosDBStorageConfig._validate_suffix(config) @@ -86,11 +86,11 @@ def validate_cosmos_db_config(config: "CosmosDBStorageConfig") -> None: def _validate_suffix(config: "CosmosDBStorageConfig") -> None: if config.key_suffix: if config.compatibility_mode: - raise ValueError(str(error_resources.CosmosDbCompatibilityModeRequired)) + raise ValueError(str(storage_errors.CosmosDbCompatibilityModeRequired)) suffix_escaped: str = sanitize_key(config.key_suffix) if suffix_escaped != config.key_suffix: raise ValueError( - error_resources.CosmosDbInvalidKeySuffixCharacters.format( + storage_errors.CosmosDbInvalidKeySuffixCharacters.format( config.key_suffix ) ) diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/__init__.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/__init__.py new file mode 100644 index 00000000..b7c4b380 --- /dev/null +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents Storage Cosmos package. +""" + +from .error_message import ErrorMessage +from .error_resources import StorageErrorResources + +# Singleton instance +storage_errors = StorageErrorResources() + +__all__ = ["ErrorMessage", "StorageErrorResources", "storage_errors"] diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_message.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_message.py new file mode 100644 index 00000000..08b6eb24 --- /dev/null +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_message.py @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +ErrorMessage class for formatting error messages with error codes and help URLs. +""" + + +class ErrorMessage: + """ + Represents a formatted error message with error code and help URL. + + This class formats error messages according to the Microsoft Agents SDK pattern: + - Original error message + - Error Code: [negative number] + - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor + """ + + def __init__( + self, + message_template: str, + error_code: int, + help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", + ): + """ + Initialize an ErrorMessage. + + :param message_template: The error message template (may include format placeholders) + :type message_template: str + :param error_code: The error code (should be negative) + :type error_code: int + :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) + :type help_url_anchor: str + """ + self.message_template = message_template + self.error_code = error_code + self.help_url_anchor = help_url_anchor + self.base_url = "https://aka.ms/M365AgentsErrorCodes" + + def format(self, *args, **kwargs) -> str: + """ + Format the error message with the provided arguments. + + :param args: Positional arguments for string formatting + :param kwargs: Keyword arguments for string formatting + :return: Formatted error message with error code and help URL + :rtype: str + """ + # Format the main message + if args or kwargs: + message = self.message_template.format(*args, **kwargs) + else: + message = self.message_template + + # Append error code and help URL + return ( + f"{message}\n\n" + f"Error Code: {self.error_code}\n" + f"Help URL: {self.base_url}/#{self.help_url_anchor}" + ) + + def __str__(self) -> str: + """Return the formatted error message without any arguments.""" + return self.format() + + def __repr__(self) -> str: + """Return a representation of the ErrorMessage.""" + return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_resources.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_resources.py new file mode 100644 index 00000000..1fcae2bc --- /dev/null +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_resources.py @@ -0,0 +1,100 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Storage error resources for Microsoft Agents SDK (CosmosDB). + +Error codes are in the range -61000 to -61999. +""" + +from .error_message import ErrorMessage + + +class StorageErrorResources: + """ + Error messages for storage operations (CosmosDB). + + Error codes are organized in the range -61000 to -61999. + """ + + CosmosDbConfigRequired = ErrorMessage( + "CosmosDBStorage: CosmosDBConfig is required.", + -61000, + "storage-configuration", + ) + + CosmosDbEndpointRequired = ErrorMessage( + "CosmosDBStorage: cosmos_db_endpoint is required.", + -61001, + "storage-configuration", + ) + + CosmosDbAuthKeyRequired = ErrorMessage( + "CosmosDBStorage: auth_key is required.", + -61002, + "storage-configuration", + ) + + CosmosDbDatabaseIdRequired = ErrorMessage( + "CosmosDBStorage: database_id is required.", + -61003, + "storage-configuration", + ) + + CosmosDbContainerIdRequired = ErrorMessage( + "CosmosDBStorage: container_id is required.", + -61004, + "storage-configuration", + ) + + CosmosDbKeyCannotBeEmpty = ErrorMessage( + "CosmosDBStorage: Key cannot be empty.", + -61005, + "storage-configuration", + ) + + CosmosDbPartitionKeyInvalid = ErrorMessage( + "CosmosDBStorage: PartitionKey of {0} cannot be used with a CosmosDbPartitionedStorageOptions.PartitionKey of {1}.", + -61006, + "storage-configuration", + ) + + CosmosDbPartitionKeyPathInvalid = ErrorMessage( + "CosmosDBStorage: PartitionKeyPath must match cosmosDbPartitionedStorageOptions value of {0}", + -61007, + "storage-configuration", + ) + + CosmosDbCompatibilityModeRequired = ErrorMessage( + "CosmosDBStorage: compatibilityMode cannot be set when using partitionKey options.", + -61008, + "storage-configuration", + ) + + CosmosDbPartitionKeyNotFound = ErrorMessage( + "CosmosDBStorage: Partition key '{0}' missing from state, you may be missing custom state implementation.", + -61009, + "storage-configuration", + ) + + CosmosDbInvalidPartitionKeyValue = ErrorMessage( + "CosmosDBStorage: Invalid PartitionKey property on item with id {0}", + -61010, + "storage-configuration", + ) + + CosmosDbInvalidKeySuffixCharacters = ErrorMessage( + "Cannot use invalid Row Key characters: {0} in keySuffix.", + -61011, + "storage-configuration", + ) + + InvalidConfiguration = ErrorMessage( + "Invalid configuration: {0}", + -61012, + "configuration", + ) + + def __init__(self): + """Initialize StorageErrorResources.""" + pass From 9d6e802209c51c15570ca2f5ec08eba2ec73b1b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:25:01 +0000 Subject: [PATCH 13/16] 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> --- .../errors/test_error_resources.py | 240 ++++++++++++------ 1 file changed, 163 insertions(+), 77 deletions(-) diff --git a/tests/hosting_core/errors/test_error_resources.py b/tests/hosting_core/errors/test_error_resources.py index 86f1fa3f..77ce3e67 100644 --- a/tests/hosting_core/errors/test_error_resources.py +++ b/tests/hosting_core/errors/test_error_resources.py @@ -61,101 +61,187 @@ def test_error_message_repr(self): class TestErrorResources: - """Tests for ErrorResources class.""" + """Tests for ErrorResources class (hosting and general errors).""" def test_error_resources_singleton(self): """Test that error_resources is accessible.""" assert error_resources is not None assert isinstance(error_resources, ErrorResources) - def test_authentication_errors_exist(self): - """Test that authentication errors are defined.""" - assert hasattr(error_resources, "FailedToAcquireToken") - assert hasattr(error_resources, "InvalidInstanceUrl") - assert hasattr(error_resources, "OnBehalfOfFlowNotSupportedManagedIdentity") - - def test_storage_errors_exist(self): - """Test that storage errors are defined.""" - assert hasattr(error_resources, "CosmosDbConfigRequired") - assert hasattr(error_resources, "CosmosDbEndpointRequired") - assert hasattr(error_resources, "StorageKeyCannotBeEmpty") - - def test_teams_errors_exist(self): - """Test that teams errors are defined.""" - assert hasattr(error_resources, "TeamsBadRequest") - assert hasattr(error_resources, "TeamsContextRequired") - assert hasattr(error_resources, "TeamsMeetingIdRequired") - def test_hosting_errors_exist(self): - """Test that hosting errors are defined.""" + """Test that hosting errors are defined in hosting-core.""" assert hasattr(error_resources, "AdapterRequired") assert hasattr(error_resources, "AgentApplicationRequired") assert hasattr(error_resources, "StreamAlreadyEnded") - - def test_activity_errors_exist(self): - """Test that activity errors are defined.""" - assert hasattr(error_resources, "InvalidChannelIdType") - assert hasattr(error_resources, "ChannelIdProductInfoConflict") - - def test_copilot_studio_errors_exist(self): - """Test that copilot studio errors are defined.""" - assert hasattr(error_resources, "CloudBaseAddressRequired") - assert hasattr(error_resources, "EnvironmentIdRequired") + assert hasattr(error_resources, "RequestRequired") + assert hasattr(error_resources, "AgentRequired") def test_general_errors_exist(self): - """Test that general errors are defined.""" + """Test that general errors are defined in hosting-core.""" assert hasattr(error_resources, "InvalidConfiguration") assert hasattr(error_resources, "RequiredParameterMissing") + assert hasattr(error_resources, "InvalidParameterValue") + + def test_hosting_error_code_ranges(self): + """Test that hosting error codes are in expected range.""" + # Hosting errors: -63000 to -63999 + assert -63999 <= error_resources.AdapterRequired.error_code <= -63000 + assert -63999 <= error_resources.StreamAlreadyEnded.error_code <= -63000 + + def test_general_error_code_ranges(self): + """Test that general error codes are in expected range.""" + # General errors: -66000 to -66999 + assert -66999 <= error_resources.InvalidConfiguration.error_code <= -66000 + assert -66999 <= error_resources.RequiredParameterMissing.error_code <= -66000 + + def test_adapter_required_format(self): + """Test AdapterRequired error formatting.""" + error = error_resources.AdapterRequired + formatted = str(error) + assert "adapter can't be None" in formatted + assert "Error Code: -63000" in formatted - def test_error_code_ranges(self): - """Test that error codes are in expected ranges.""" - # Authentication errors: -60000 to -60099 - assert -60099 <= error_resources.FailedToAcquireToken.error_code <= -60000 - - # Storage errors: -60100 to -60199 - assert -60199 <= error_resources.CosmosDbConfigRequired.error_code <= -60100 - - # Teams errors: -60200 to -60299 - assert -60299 <= error_resources.TeamsBadRequest.error_code <= -60200 - - # Hosting errors: -60300 to -60399 - assert -60399 <= error_resources.AdapterRequired.error_code <= -60300 + def test_invalid_configuration_format(self): + """Test InvalidConfiguration error formatting.""" + error = error_resources.InvalidConfiguration + formatted = error.format("test config error") + assert "Invalid configuration: test config error" in formatted + assert "Error Code: -66000" in formatted - # Activity errors: -60400 to -60499 - assert -60499 <= error_resources.InvalidChannelIdType.error_code <= -60400 - # Copilot Studio errors: -60500 to -60599 - assert -60599 <= error_resources.CloudBaseAddressRequired.error_code <= -60500 +class TestDistributedErrorResources: + """Tests for error resources distributed across packages.""" - # General errors: -60600 to -60699 - assert -60699 <= error_resources.InvalidConfiguration.error_code <= -60600 + def test_authentication_errors_exist(self): + """Test that authentication errors are defined in their package.""" + try: + from microsoft_agents.authentication.msal.errors import ( + authentication_errors, + ) + + assert hasattr(authentication_errors, "FailedToAcquireToken") + assert hasattr(authentication_errors, "InvalidInstanceUrl") + assert hasattr( + authentication_errors, "OnBehalfOfFlowNotSupportedManagedIdentity" + ) + # Test error code range: -60000 to -60999 + assert ( + -60999 + <= authentication_errors.FailedToAcquireToken.error_code + <= -60000 + ) + except ImportError: + pytest.skip("Authentication package not available") + + def test_storage_cosmos_errors_exist(self): + """Test that storage cosmos errors are defined in their package.""" + try: + from microsoft_agents.storage.cosmos.errors import storage_errors + + assert hasattr(storage_errors, "CosmosDbConfigRequired") + assert hasattr(storage_errors, "CosmosDbEndpointRequired") + assert hasattr(storage_errors, "CosmosDbKeyCannotBeEmpty") + # Test error code range: -61000 to -61999 + assert -61999 <= storage_errors.CosmosDbConfigRequired.error_code <= -61000 + except ImportError: + pytest.skip("Storage Cosmos package not available") + + def test_storage_blob_errors_exist(self): + """Test that storage blob errors are defined in their package.""" + try: + from microsoft_agents.storage.blob.errors import blob_storage_errors + + assert hasattr(blob_storage_errors, "BlobStorageConfigRequired") + assert hasattr(blob_storage_errors, "BlobContainerNameRequired") + # Test error code range: -61100 to -61199 + assert ( + -61199 + <= blob_storage_errors.BlobStorageConfigRequired.error_code + <= -61100 + ) + except ImportError: + pytest.skip("Storage Blob package not available") - def test_failed_to_acquire_token_format(self): - """Test FailedToAcquireToken error formatting.""" - error = error_resources.FailedToAcquireToken - formatted = error.format("test_payload") - assert "Failed to acquire token. test_payload" in formatted - assert "Error Code: -60012" in formatted - assert "agentic-identity-with-the-m365-agents-sdk" in formatted + def test_teams_errors_exist(self): + """Test that teams errors are defined in their package.""" + try: + from microsoft_agents.hosting.teams.errors import teams_errors + + assert hasattr(teams_errors, "TeamsBadRequest") + assert hasattr(teams_errors, "TeamsContextRequired") + assert hasattr(teams_errors, "TeamsMeetingIdRequired") + # Test error code range: -62000 to -62999 + assert -62999 <= teams_errors.TeamsBadRequest.error_code <= -62000 + except ImportError: + pytest.skip("Teams package not available") - def test_cosmos_db_config_required(self): - """Test CosmosDbConfigRequired error formatting.""" - error = error_resources.CosmosDbConfigRequired - formatted = str(error) - assert "CosmosDBStorage: CosmosDBConfig is required." in formatted - assert "Error Code: -60100" in formatted + def test_activity_errors_exist(self): + """Test that activity errors are defined in their package.""" + try: + from microsoft_agents.activity.errors import activity_errors + + assert hasattr(activity_errors, "InvalidChannelIdType") + assert hasattr(activity_errors, "ChannelIdProductInfoConflict") + assert hasattr(activity_errors, "ChannelIdValueConflict") + # Test error code range: -64000 to -64999 + assert -64999 <= activity_errors.InvalidChannelIdType.error_code <= -64000 + except ImportError: + pytest.skip("Activity package not available") - def test_teams_context_required(self): - """Test TeamsContextRequired error formatting.""" - error = error_resources.TeamsContextRequired - formatted = str(error) - assert "context is required." in formatted - assert "Error Code: -60202" in formatted - - def test_error_with_multiple_arguments(self): - """Test error with multiple format arguments.""" - error = error_resources.CosmosDbPartitionKeyInvalid - formatted = error.format("key1", "key2") - assert "key1" in formatted - assert "key2" in formatted - assert "Error Code: -60106" in formatted + def test_copilot_studio_errors_exist(self): + """Test that copilot studio errors are defined in their package.""" + try: + from microsoft_agents.copilotstudio.client.errors import ( + copilot_studio_errors, + ) + + assert hasattr(copilot_studio_errors, "CloudBaseAddressRequired") + assert hasattr(copilot_studio_errors, "EnvironmentIdRequired") + assert hasattr(copilot_studio_errors, "AgentIdentifierRequired") + # Test error code range: -65000 to -65999 + assert ( + -65999 + <= copilot_studio_errors.CloudBaseAddressRequired.error_code + <= -65000 + ) + except ImportError: + pytest.skip("Copilot Studio package not available") + + def test_authentication_error_format(self): + """Test authentication error formatting.""" + try: + from microsoft_agents.authentication.msal.errors import ( + authentication_errors, + ) + + error = authentication_errors.FailedToAcquireToken + formatted = error.format("test_payload") + assert "Failed to acquire token. test_payload" in formatted + assert "Error Code: -60012" in formatted + assert "agentic-identity-with-the-m365-agents-sdk" in formatted + except ImportError: + pytest.skip("Authentication package not available") + + def test_storage_error_format(self): + """Test storage error formatting.""" + try: + from microsoft_agents.storage.cosmos.errors import storage_errors + + error = storage_errors.CosmosDbConfigRequired + formatted = str(error) + assert "CosmosDBStorage: CosmosDBConfig is required." in formatted + assert "Error Code: -61000" in formatted + except ImportError: + pytest.skip("Storage Cosmos package not available") + + def test_teams_error_format(self): + """Test teams error formatting.""" + try: + from microsoft_agents.hosting.teams.errors import teams_errors + + error = teams_errors.TeamsContextRequired + formatted = str(error) + assert "context is required." in formatted + assert "Error Code: -62002" in formatted + except ImportError: + pytest.skip("Teams package not available") From b15c6bfc1f2d5b022f120e028ceb53e22e2dba3c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 19:07:15 +0000 Subject: [PATCH 14/16] 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> --- .../activity/_channel_id_field_mixin.py | 4 ++-- .../microsoft_agents/activity/activity.py | 5 +---- .../microsoft_agents/activity/channel_id.py | 6 ++---- .../hosting/teams/teams_info.py | 17 +++++++++++++---- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py index 8d301afd..38101b87 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/_channel_id_field_mixin.py @@ -14,6 +14,8 @@ model_serializer, ) +from microsoft_agents.activity.errors import activity_errors + from .channel_id import ChannelId logger = logging.getLogger(__name__) @@ -42,8 +44,6 @@ def channel_id(self, value: Any): elif isinstance(value, str): self._channel_id = ChannelId(value) else: - from microsoft_agents.activity.errors import activity_errors - raise ValueError(activity_errors.InvalidChannelIdType.format(type(value))) def _set_validated_channel_id(self, data: Any) -> None: diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index 9663b584..5ea03b26 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -44,6 +44,7 @@ from .channel_id import ChannelId from ._model_utils import pick_model, SkipNone from ._type_aliases import NonEmptyString +from microsoft_agents.activity.errors import activity_errors logger = logging.getLogger(__name__) @@ -218,8 +219,6 @@ def _validate_channel_id( activity.channel_id.sub_channel and activity.channel_id.sub_channel != product_info.id ): - from microsoft_agents.activity.errors import activity_errors - raise Exception(str(activity_errors.ChannelIdProductInfoConflict)) activity.channel_id = ChannelId( channel=activity.channel_id.channel, @@ -256,8 +255,6 @@ def _serialize_sub_channel_data( # self.channel_id is the source of truth for serialization if self.channel_id and self.channel_id.sub_channel: if product_info and product_info.get("id") != self.channel_id.sub_channel: - from microsoft_agents.activity.errors import activity_errors - raise Exception(str(activity_errors.ChannelIdProductInfoConflict)) elif not product_info: if not serialized.get("entities"): diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py index e23acc32..e8192d6c 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py @@ -8,6 +8,8 @@ from pydantic_core import CoreSchema, core_schema from pydantic import GetCoreSchemaHandler +from microsoft_agents.activity.errors import activity_errors + class ChannelId(str): """A ChannelId represents a channel and optional sub-channel in the format 'channel:sub_channel'.""" @@ -52,15 +54,11 @@ def __new__( """ if isinstance(value, str): if channel or sub_channel: - from microsoft_agents.activity.errors import activity_errors - raise ValueError(str(activity_errors.ChannelIdValueConflict)) value = value.strip() if value: return str.__new__(cls, value) - from microsoft_agents.activity.errors import activity_errors - raise TypeError(str(activity_errors.ChannelIdValueMustBeNonEmpty)) else: if ( diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py index 6e06af35..63e9104a 100644 --- a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py @@ -26,6 +26,7 @@ from microsoft_agents.hosting.core import ( ChannelServiceAdapter, TurnContext, + error_resources, ) from microsoft_agents.hosting.teams.errors import teams_errors @@ -444,7 +445,9 @@ async def send_message_to_list_of_users( if not activity: raise ValueError(str(teams_errors.ActivityRequired)) if not tenant_id: - raise ValueError(teams_errors.RequiredParameterMissing.format("tenant_id")) + raise ValueError( + error_resources.RequiredParameterMissing.format("tenant_id") + ) if not members or len(members) == 0: raise ValueError("members list is required.") @@ -474,7 +477,9 @@ async def send_message_to_all_users_in_tenant( if not activity: raise ValueError(str(teams_errors.ActivityRequired)) if not tenant_id: - raise ValueError(teams_errors.RequiredParameterMissing.format("tenant_id")) + raise ValueError( + error_resources.RequiredParameterMissing.format("tenant_id") + ) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.send_message_to_all_users_in_tenant( @@ -503,7 +508,9 @@ async def send_message_to_all_users_in_team( if not activity: raise ValueError(str(teams_errors.ActivityRequired)) if not tenant_id: - raise ValueError(teams_errors.RequiredParameterMissing.format("tenant_id")) + raise ValueError( + error_resources.RequiredParameterMissing.format("tenant_id") + ) if not team_id: raise ValueError(str(teams_errors.TeamsTeamIdRequired)) @@ -537,7 +544,9 @@ async def send_message_to_list_of_channels( if not activity: raise ValueError(str(teams_errors.ActivityRequired)) if not tenant_id: - raise ValueError(teams_errors.RequiredParameterMissing.format("tenant_id")) + raise ValueError( + error_resources.RequiredParameterMissing.format("tenant_id") + ) if not members or len(members) == 0: raise ValueError("members list is required.") From 74678c7c70bd8b41e07544400decbc7e993d9beb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 19:12:32 +0000 Subject: [PATCH 15/16] 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> --- .../microsoft_agents/hosting/teams/teams_info.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py index 63e9104a..05cb1ab3 100644 --- a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/teams_info.py @@ -443,7 +443,7 @@ async def send_message_to_list_of_users( ValueError: If required parameters are missing. """ if not activity: - raise ValueError(str(teams_errors.ActivityRequired)) + raise ValueError(str(error_resources.ActivityRequired)) if not tenant_id: raise ValueError( error_resources.RequiredParameterMissing.format("tenant_id") @@ -475,7 +475,7 @@ async def send_message_to_all_users_in_tenant( ValueError: If required parameters are missing. """ if not activity: - raise ValueError(str(teams_errors.ActivityRequired)) + raise ValueError(str(error_resources.ActivityRequired)) if not tenant_id: raise ValueError( error_resources.RequiredParameterMissing.format("tenant_id") @@ -506,7 +506,7 @@ async def send_message_to_all_users_in_team( ValueError: If required parameters are missing. """ if not activity: - raise ValueError(str(teams_errors.ActivityRequired)) + raise ValueError(str(error_resources.ActivityRequired)) if not tenant_id: raise ValueError( error_resources.RequiredParameterMissing.format("tenant_id") @@ -542,7 +542,7 @@ async def send_message_to_list_of_channels( ValueError: If required parameters are missing. """ if not activity: - raise ValueError(str(teams_errors.ActivityRequired)) + raise ValueError(str(error_resources.ActivityRequired)) if not tenant_id: raise ValueError( error_resources.RequiredParameterMissing.format("tenant_id") From 3c7ad4973d18ab0b4c3dcc7a84a39b802263945d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 21:52:18 +0000 Subject: [PATCH 16/16] 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> --- .../activity/errors/__init__.py | 3 +- .../activity/errors/error_message.py | 68 ------------------- .../activity/errors/error_resources.py | 2 +- .../authentication/msal/errors/__init__.py | 3 +- .../msal/errors/error_message.py | 68 ------------------- .../msal/errors/error_resources.py | 2 +- .../copilotstudio/client/errors/__init__.py | 3 +- .../client/errors/error_message.py | 68 ------------------- .../client/errors/error_resources.py | 2 +- .../hosting/teams/errors/__init__.py | 3 +- .../hosting/teams/errors/error_message.py | 68 ------------------- .../hosting/teams/errors/error_resources.py | 2 +- .../storage/blob/errors/__init__.py | 3 +- .../storage/blob/errors/error_message.py | 68 ------------------- .../storage/blob/errors/error_resources.py | 2 +- .../storage/cosmos/errors/__init__.py | 3 +- .../storage/cosmos/errors/error_message.py | 68 ------------------- .../storage/cosmos/errors/error_resources.py | 2 +- 18 files changed, 18 insertions(+), 420 deletions(-) delete mode 100644 libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_message.py delete mode 100644 libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_message.py delete mode 100644 libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_message.py delete mode 100644 libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_message.py delete mode 100644 libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_message.py delete mode 100644 libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_message.py diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/__init__.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/__init__.py index 90283f53..e25666ba 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/__init__.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/__init__.py @@ -5,7 +5,8 @@ Error resources for Microsoft Agents Activity package. """ -from .error_message import ErrorMessage +from microsoft_agents.hosting.core.errors import ErrorMessage + from .error_resources import ActivityErrorResources # Singleton instance diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_message.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_message.py deleted file mode 100644 index 08b6eb24..00000000 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_message.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -ErrorMessage class for formatting error messages with error codes and help URLs. -""" - - -class ErrorMessage: - """ - Represents a formatted error message with error code and help URL. - - This class formats error messages according to the Microsoft Agents SDK pattern: - - Original error message - - Error Code: [negative number] - - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor - """ - - def __init__( - self, - message_template: str, - error_code: int, - help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", - ): - """ - Initialize an ErrorMessage. - - :param message_template: The error message template (may include format placeholders) - :type message_template: str - :param error_code: The error code (should be negative) - :type error_code: int - :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) - :type help_url_anchor: str - """ - self.message_template = message_template - self.error_code = error_code - self.help_url_anchor = help_url_anchor - self.base_url = "https://aka.ms/M365AgentsErrorCodes" - - def format(self, *args, **kwargs) -> str: - """ - Format the error message with the provided arguments. - - :param args: Positional arguments for string formatting - :param kwargs: Keyword arguments for string formatting - :return: Formatted error message with error code and help URL - :rtype: str - """ - # Format the main message - if args or kwargs: - message = self.message_template.format(*args, **kwargs) - else: - message = self.message_template - - # Append error code and help URL - return ( - f"{message}\n\n" - f"Error Code: {self.error_code}\n" - f"Help URL: {self.base_url}/#{self.help_url_anchor}" - ) - - def __str__(self) -> str: - """Return the formatted error message without any arguments.""" - return self.format() - - def __repr__(self) -> str: - """Return a representation of the ErrorMessage.""" - return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py index 14ee59c0..b35f81ce 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py @@ -7,7 +7,7 @@ Error codes are in the range -64000 to -64999. """ -from .error_message import ErrorMessage +from microsoft_agents.hosting.core.errors import ErrorMessage class ActivityErrorResources: diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/__init__.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/__init__.py index 0c77b9fe..2458a070 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/__init__.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/__init__.py @@ -5,7 +5,8 @@ Error resources for Microsoft Agents Authentication MSAL package. """ -from .error_message import ErrorMessage +from microsoft_agents.hosting.core.errors import ErrorMessage + from .error_resources import AuthenticationErrorResources # Singleton instance diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_message.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_message.py deleted file mode 100644 index 08b6eb24..00000000 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_message.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -ErrorMessage class for formatting error messages with error codes and help URLs. -""" - - -class ErrorMessage: - """ - Represents a formatted error message with error code and help URL. - - This class formats error messages according to the Microsoft Agents SDK pattern: - - Original error message - - Error Code: [negative number] - - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor - """ - - def __init__( - self, - message_template: str, - error_code: int, - help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", - ): - """ - Initialize an ErrorMessage. - - :param message_template: The error message template (may include format placeholders) - :type message_template: str - :param error_code: The error code (should be negative) - :type error_code: int - :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) - :type help_url_anchor: str - """ - self.message_template = message_template - self.error_code = error_code - self.help_url_anchor = help_url_anchor - self.base_url = "https://aka.ms/M365AgentsErrorCodes" - - def format(self, *args, **kwargs) -> str: - """ - Format the error message with the provided arguments. - - :param args: Positional arguments for string formatting - :param kwargs: Keyword arguments for string formatting - :return: Formatted error message with error code and help URL - :rtype: str - """ - # Format the main message - if args or kwargs: - message = self.message_template.format(*args, **kwargs) - else: - message = self.message_template - - # Append error code and help URL - return ( - f"{message}\n\n" - f"Error Code: {self.error_code}\n" - f"Help URL: {self.base_url}/#{self.help_url_anchor}" - ) - - def __str__(self) -> str: - """Return the formatted error message without any arguments.""" - return self.format() - - def __repr__(self) -> str: - """Return a representation of the ErrorMessage.""" - return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_resources.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_resources.py index bbf461ad..b0a347c3 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_resources.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_resources.py @@ -7,7 +7,7 @@ Error codes are in the range -60000 to -60999. """ -from .error_message import ErrorMessage +from microsoft_agents.hosting.core.errors import ErrorMessage class AuthenticationErrorResources: diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/__init__.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/__init__.py index 26d65f8a..a19b7c1a 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/__init__.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/__init__.py @@ -5,7 +5,8 @@ Error resources for Microsoft Agents Copilot Studio Client package. """ -from .error_message import ErrorMessage +from microsoft_agents.hosting.core.errors import ErrorMessage + from .error_resources import CopilotStudioErrorResources # Singleton instance diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_message.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_message.py deleted file mode 100644 index 08b6eb24..00000000 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_message.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -ErrorMessage class for formatting error messages with error codes and help URLs. -""" - - -class ErrorMessage: - """ - Represents a formatted error message with error code and help URL. - - This class formats error messages according to the Microsoft Agents SDK pattern: - - Original error message - - Error Code: [negative number] - - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor - """ - - def __init__( - self, - message_template: str, - error_code: int, - help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", - ): - """ - Initialize an ErrorMessage. - - :param message_template: The error message template (may include format placeholders) - :type message_template: str - :param error_code: The error code (should be negative) - :type error_code: int - :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) - :type help_url_anchor: str - """ - self.message_template = message_template - self.error_code = error_code - self.help_url_anchor = help_url_anchor - self.base_url = "https://aka.ms/M365AgentsErrorCodes" - - def format(self, *args, **kwargs) -> str: - """ - Format the error message with the provided arguments. - - :param args: Positional arguments for string formatting - :param kwargs: Keyword arguments for string formatting - :return: Formatted error message with error code and help URL - :rtype: str - """ - # Format the main message - if args or kwargs: - message = self.message_template.format(*args, **kwargs) - else: - message = self.message_template - - # Append error code and help URL - return ( - f"{message}\n\n" - f"Error Code: {self.error_code}\n" - f"Help URL: {self.base_url}/#{self.help_url_anchor}" - ) - - def __str__(self) -> str: - """Return the formatted error message without any arguments.""" - return self.format() - - def __repr__(self) -> str: - """Return a representation of the ErrorMessage.""" - return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_resources.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_resources.py index f267a6a4..3b470d6e 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_resources.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_resources.py @@ -7,7 +7,7 @@ Error codes are in the range -65000 to -65999. """ -from .error_message import ErrorMessage +from microsoft_agents.hosting.core.errors import ErrorMessage class CopilotStudioErrorResources: diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/__init__.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/__init__.py index fdee7825..fcf1c7bf 100644 --- a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/__init__.py +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/__init__.py @@ -5,7 +5,8 @@ Error resources for Microsoft Agents Hosting Teams package. """ -from .error_message import ErrorMessage +from microsoft_agents.hosting.core.errors import ErrorMessage + from .error_resources import TeamsErrorResources # Singleton instance diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_message.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_message.py deleted file mode 100644 index 08b6eb24..00000000 --- a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_message.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -ErrorMessage class for formatting error messages with error codes and help URLs. -""" - - -class ErrorMessage: - """ - Represents a formatted error message with error code and help URL. - - This class formats error messages according to the Microsoft Agents SDK pattern: - - Original error message - - Error Code: [negative number] - - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor - """ - - def __init__( - self, - message_template: str, - error_code: int, - help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", - ): - """ - Initialize an ErrorMessage. - - :param message_template: The error message template (may include format placeholders) - :type message_template: str - :param error_code: The error code (should be negative) - :type error_code: int - :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) - :type help_url_anchor: str - """ - self.message_template = message_template - self.error_code = error_code - self.help_url_anchor = help_url_anchor - self.base_url = "https://aka.ms/M365AgentsErrorCodes" - - def format(self, *args, **kwargs) -> str: - """ - Format the error message with the provided arguments. - - :param args: Positional arguments for string formatting - :param kwargs: Keyword arguments for string formatting - :return: Formatted error message with error code and help URL - :rtype: str - """ - # Format the main message - if args or kwargs: - message = self.message_template.format(*args, **kwargs) - else: - message = self.message_template - - # Append error code and help URL - return ( - f"{message}\n\n" - f"Error Code: {self.error_code}\n" - f"Help URL: {self.base_url}/#{self.help_url_anchor}" - ) - - def __str__(self) -> str: - """Return the formatted error message without any arguments.""" - return self.format() - - def __repr__(self) -> str: - """Return a representation of the ErrorMessage.""" - return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_resources.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_resources.py index 6456868b..888e930a 100644 --- a/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_resources.py +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_resources.py @@ -7,7 +7,7 @@ Error codes are in the range -62000 to -62999. """ -from .error_message import ErrorMessage +from microsoft_agents.hosting.core.errors import ErrorMessage class TeamsErrorResources: diff --git a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/__init__.py b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/__init__.py index 3fb309fc..15361d3f 100644 --- a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/__init__.py +++ b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/__init__.py @@ -5,7 +5,8 @@ Error resources for Microsoft Agents Storage Blob package. """ -from .error_message import ErrorMessage +from microsoft_agents.hosting.core.errors import ErrorMessage + from .error_resources import BlobStorageErrorResources # Singleton instance diff --git a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_message.py b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_message.py deleted file mode 100644 index 08b6eb24..00000000 --- a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_message.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -ErrorMessage class for formatting error messages with error codes and help URLs. -""" - - -class ErrorMessage: - """ - Represents a formatted error message with error code and help URL. - - This class formats error messages according to the Microsoft Agents SDK pattern: - - Original error message - - Error Code: [negative number] - - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor - """ - - def __init__( - self, - message_template: str, - error_code: int, - help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", - ): - """ - Initialize an ErrorMessage. - - :param message_template: The error message template (may include format placeholders) - :type message_template: str - :param error_code: The error code (should be negative) - :type error_code: int - :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) - :type help_url_anchor: str - """ - self.message_template = message_template - self.error_code = error_code - self.help_url_anchor = help_url_anchor - self.base_url = "https://aka.ms/M365AgentsErrorCodes" - - def format(self, *args, **kwargs) -> str: - """ - Format the error message with the provided arguments. - - :param args: Positional arguments for string formatting - :param kwargs: Keyword arguments for string formatting - :return: Formatted error message with error code and help URL - :rtype: str - """ - # Format the main message - if args or kwargs: - message = self.message_template.format(*args, **kwargs) - else: - message = self.message_template - - # Append error code and help URL - return ( - f"{message}\n\n" - f"Error Code: {self.error_code}\n" - f"Help URL: {self.base_url}/#{self.help_url_anchor}" - ) - - def __str__(self) -> str: - """Return the formatted error message without any arguments.""" - return self.format() - - def __repr__(self) -> str: - """Return a representation of the ErrorMessage.""" - return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_resources.py b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_resources.py index 316b5841..4b4f781d 100644 --- a/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_resources.py +++ b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_resources.py @@ -7,7 +7,7 @@ Error codes are in the range -61100 to -61199. """ -from .error_message import ErrorMessage +from microsoft_agents.hosting.core.errors import ErrorMessage class BlobStorageErrorResources: diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/__init__.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/__init__.py index b7c4b380..6bdc216e 100644 --- a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/__init__.py +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/__init__.py @@ -5,7 +5,8 @@ Error resources for Microsoft Agents Storage Cosmos package. """ -from .error_message import ErrorMessage +from microsoft_agents.hosting.core.errors import ErrorMessage + from .error_resources import StorageErrorResources # Singleton instance diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_message.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_message.py deleted file mode 100644 index 08b6eb24..00000000 --- a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_message.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -ErrorMessage class for formatting error messages with error codes and help URLs. -""" - - -class ErrorMessage: - """ - Represents a formatted error message with error code and help URL. - - This class formats error messages according to the Microsoft Agents SDK pattern: - - Original error message - - Error Code: [negative number] - - Help URL: https://aka.ms/M365AgentsErrorCodes/#anchor - """ - - def __init__( - self, - message_template: str, - error_code: int, - help_url_anchor: str = "agentic-identity-with-the-m365-agents-sdk", - ): - """ - Initialize an ErrorMessage. - - :param message_template: The error message template (may include format placeholders) - :type message_template: str - :param error_code: The error code (should be negative) - :type error_code: int - :param help_url_anchor: The anchor for the help URL (defaults to agentic identity) - :type help_url_anchor: str - """ - self.message_template = message_template - self.error_code = error_code - self.help_url_anchor = help_url_anchor - self.base_url = "https://aka.ms/M365AgentsErrorCodes" - - def format(self, *args, **kwargs) -> str: - """ - Format the error message with the provided arguments. - - :param args: Positional arguments for string formatting - :param kwargs: Keyword arguments for string formatting - :return: Formatted error message with error code and help URL - :rtype: str - """ - # Format the main message - if args or kwargs: - message = self.message_template.format(*args, **kwargs) - else: - message = self.message_template - - # Append error code and help URL - return ( - f"{message}\n\n" - f"Error Code: {self.error_code}\n" - f"Help URL: {self.base_url}/#{self.help_url_anchor}" - ) - - def __str__(self) -> str: - """Return the formatted error message without any arguments.""" - return self.format() - - def __repr__(self) -> str: - """Return a representation of the ErrorMessage.""" - return f"ErrorMessage(code={self.error_code}, message='{self.message_template[:50]}...')" diff --git a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_resources.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_resources.py index 1fcae2bc..1137144a 100644 --- a/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_resources.py +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_resources.py @@ -7,7 +7,7 @@ Error codes are in the range -61000 to -61999. """ -from .error_message import ErrorMessage +from microsoft_agents.hosting.core.errors import ErrorMessage class StorageErrorResources: