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..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,10 +44,7 @@ def channel_id(self, value: Any): elif isinstance(value, str): self._channel_id = ChannelId(value) else: - raise ValueError( - f"Invalid type for channel_id: {type(value)}. " - "Expected ChannelId or str." - ) + 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 c038d0f5..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,9 +219,7 @@ def _validate_channel_id( activity.channel_id.sub_channel and activity.channel_id.sub_channel != product_info.id ): - raise Exception( - "Conflict between channel_id.sub_channel and productInfo entity" - ) + raise Exception(str(activity_errors.ChannelIdProductInfoConflict)) activity.channel_id = ChannelId( channel=activity.channel_id.channel, sub_channel=product_info.id, @@ -256,9 +255,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: - raise Exception( - "Conflict between channel_id.sub_channel and productInfo entity" - ) + 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 ad4890fe..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,14 +54,12 @@ def __new__( """ if isinstance(value, str): if channel or sub_channel: - raise ValueError( - "If value is provided, channel and sub_channel must be None" - ) + raise ValueError(str(activity_errors.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(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..e25666ba --- /dev/null +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents Activity package. +""" + +from microsoft_agents.hosting.core.errors 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_resources.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py new file mode 100644 index 00000000..b35f81ce --- /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 microsoft_agents.hosting.core.errors 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..2458a070 --- /dev/null +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents Authentication MSAL package. +""" + +from microsoft_agents.hosting.core.errors 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_resources.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/errors/error_resources.py new file mode 100644 index 00000000..b0a347c3 --- /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 microsoft_agents.hosting.core.errors 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 956caf04..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 @@ -26,6 +26,7 @@ AccessTokenProviderBase, AgentAuthConfiguration, ) +from microsoft_agents.authentication.msal.errors import authentication_errors 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(authentication_errors.InvalidInstanceUrl)) local_scopes = self._resolve_scopes_list(instance_uri, scopes) self._create_client_application() @@ -86,7 +87,11 @@ 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( + authentication_errors.FailedToAcquireToken.format( + str(auth_result_payload) + ) + ) return res @@ -106,7 +111,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(authentication_errors.OnBehalfOfFlowNotSupportedManagedIdentity) ) elif isinstance(self._msal_auth_client, ConfidentialClientApplication): # TODO: Handling token error / acquisition failed @@ -123,7 +128,9 @@ 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( + authentication_errors.FailedToAcquireToken.format(str(token)) + ) return token["access_token"] @@ -131,7 +138,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__}" + authentication_errors.OnBehalfOfFlowNotSupportedAuthType.format( + self._msal_auth_client.__class__.__name__ + ) ) def _create_client_application(self) -> None: @@ -187,7 +196,9 @@ 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(authentication_errors.AuthenticationTypeNotSupported) + ) self._msal_auth_client = ConfidentialClientApplication( client_id=self._msal_configuration.CLIENT_ID, @@ -233,7 +244,9 @@ async def get_agentic_application_token( """ if not agent_app_instance_id: - raise ValueError("Agent application instance Id must be provided.") + raise ValueError( + str(authentication_errors.AgentApplicationInstanceIdRequired) + ) logger.info( "Attempting to get agentic application token from agent_app_instance_id %s", @@ -267,7 +280,9 @@ async def get_agentic_instance_token( """ if not agent_app_instance_id: - raise ValueError("Agent application instance Id must be provided.") + raise ValueError( + str(authentication_errors.AgentApplicationInstanceIdRequired) + ) logger.info( "Attempting to get agentic instance token from agent_app_instance_id %s", @@ -283,7 +298,9 @@ 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}" + authentication_errors.FailedToAcquireAgenticInstanceToken.format( + agent_app_instance_id + ) ) authority = ( @@ -306,7 +323,9 @@ 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}" + authentication_errors.FailedToAcquireAgenticInstanceToken.format( + agent_app_instance_id + ) ) # future scenario where we don't know the blueprint id upfront @@ -316,7 +335,11 @@ 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( + authentication_errors.FailedToAcquireToken.format( + str(agentic_instance_token) + ) + ) logger.debug( "Agentic blueprint id: %s", @@ -345,7 +368,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(authentication_errors.AgentApplicationInstanceIdAndUserIdRequired) ) logger.info( @@ -364,7 +387,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}" + authentication_errors.FailedToAcquireInstanceOrAgentToken.format( + agent_app_instance_id, agentic_user_id + ) ) authority = ( 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..a19b7c1a --- /dev/null +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents Copilot Studio Client package. +""" + +from microsoft_agents.hosting.core.errors 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_resources.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/errors/error_resources.py new file mode 100644 index 00000000..3b470d6e --- /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 microsoft_agents.hosting.core.errors 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 78589fdf..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,3 +1,4 @@ +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 @@ -22,13 +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( - "cloud_base_address must be provided when PowerPlatformCloud is Other" - ) + raise ValueError(str(copilot_studio_errors.CloudBaseAddressRequired)) if not settings.environment_id: - raise ValueError("EnvironmentId must be provided") + raise ValueError(str(copilot_studio_errors.EnvironmentIdRequired)) if not settings.agent_identifier: - raise ValueError("AgentIdentifier must be provided") + raise ValueError(str(copilot_studio_errors.AgentIdentifierRequired)) if settings.cloud and settings.cloud != PowerPlatformCloud.UNKNOWN: cloud = settings.cloud if cloud == PowerPlatformCloud.OTHER: @@ -40,7 +39,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(copilot_studio_errors.CustomCloudOrBaseAddressRequired) ) if settings.copilot_agent_type: agent_type = settings.copilot_agent_type @@ -60,9 +59,7 @@ def get_token_audience( cloud_base_address: Optional[str] = None, ) -> str: if cloud == PowerPlatformCloud.OTHER and not cloud_base_address: - raise ValueError( - "cloud_base_address must be provided when PowerPlatformCloud is Other" - ) + 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: @@ -79,7 +76,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(copilot_studio_errors.CustomCloudOrBaseAddressRequired) ) cloud_base_address = cloud_base_address or "api.unknown.powerplatform.com" @@ -116,9 +113,7 @@ def get_environment_endpoint( cloud_base_address: Optional[str] = None, ) -> str: if cloud == PowerPlatformCloud.OTHER and not cloud_base_address: - raise ValueError( - "cloud_base_address must be provided when PowerPlatformCloud is Other" - ) + 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-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..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,8 @@ SensitivityUsageInfo, ) -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 +99,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 +135,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 +151,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-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/README.md b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/README.md new file mode 100644 index 00000000..11f400a2 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/README.md @@ -0,0 +1,123 @@ +# Error Resources for Microsoft Agents SDK - Hosting Core + +This module provides centralized error messages with error codes and help URLs for hosting operations in the Microsoft Agents SDK. + +## Overview + +Error messages are organized by package, with each package maintaining its own error resources: + +- **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 + +### Hosting Core Errors + +```python +from microsoft_agents.hosting.core import error_resources + +# Raise an error with a simple message +raise ValueError(str(error_resources.AdapterRequired)) + +# Raise an error with formatted arguments +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)) + +# 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 + +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 + +| 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 a package: + +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. Provide an appropriate help URL anchor + +Example: + +```python +NewHostingError = ErrorMessage( + "Description of the error with {0} placeholder", + -63XXX, # Use next available code in hosting range + "help-url-anchor", +) +``` + +## Avoiding Circular Imports + +When using error resources in modules that might cause circular dependencies, use lazy imports: + +```python +def my_function(): + 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` and package-specific test files. + +## Contributing + +When refactoring existing error messages: + +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 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..08b6eb24 --- /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..ac6a1723 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py @@ -0,0 +1,202 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Hosting core error resources for Microsoft Agents SDK. + +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 + + +class ErrorResources: + """ + Error messages for hosting core operations. + + Error codes are organized by range: + - -63000 to -63999: Hosting errors + - -66000 to -66999: General/validation errors + """ + + # Hosting Errors (-63000 to -63999) + AdapterRequired = ErrorMessage( + "start_agent_process: adapter can't be None", + -63000, + "hosting-configuration", + ) + + AgentApplicationRequired = ErrorMessage( + "start_agent_process: agent_application can't be None", + -63001, + "hosting-configuration", + ) + + RequestRequired = ErrorMessage( + "CloudAdapter.process: request can't be None", + -63002, + "hosting-configuration", + ) + + AgentRequired = ErrorMessage( + "CloudAdapter.process: agent can't be None", + -63003, + "hosting-configuration", + ) + + StreamAlreadyEnded = ErrorMessage( + "The stream has already ended.", + -63004, + "streaming", + ) + + TurnContextRequired = ErrorMessage( + "TurnContext cannot be None.", + -63005, + "hosting-configuration", + ) + + ActivityRequired = ErrorMessage( + "Activity cannot be None.", + -63006, + "hosting-configuration", + ) + + AppIdRequired = ErrorMessage( + "AppId cannot be empty or None.", + -63007, + "hosting-configuration", + ) + + InvalidActivityType = ErrorMessage( + "Invalid or missing activity type.", + -63008, + "hosting-configuration", + ) + + ConversationIdRequired = ErrorMessage( + "Conversation ID cannot be empty or None.", + -63009, + "hosting-configuration", + ) + + AuthHeaderRequired = ErrorMessage( + "Authorization header is required.", + -63010, + "hosting-configuration", + ) + + InvalidAuthHeader = ErrorMessage( + "Invalid authorization header format.", + -63011, + "hosting-configuration", + ) + + ClaimsIdentityRequired = ErrorMessage( + "ClaimsIdentity is required.", + -63012, + "hosting-configuration", + ) + + ChannelServiceRouteNotFound = ErrorMessage( + "Channel service route not found for: {0}", + -63013, + "hosting-configuration", + ) + + TokenExchangeRequired = ErrorMessage( + "Token exchange requires a token exchange resource.", + -63014, + "hosting-configuration", + ) + + MissingHttpClient = ErrorMessage( + "HTTP client is required.", + -63015, + "hosting-configuration", + ) + + InvalidBotFrameworkActivity = ErrorMessage( + "Invalid Bot Framework Activity format.", + -63016, + "hosting-configuration", + ) + + CredentialsRequired = ErrorMessage( + "Credentials are required for authentication.", + -63017, + "hosting-configuration", + ) + + # General/Validation Errors (-66000 to -66999) + InvalidConfiguration = ErrorMessage( + "Invalid configuration: {0}", + -66000, + "configuration", + ) + + RequiredParameterMissing = ErrorMessage( + "Required parameter missing: {0}", + -66001, + "configuration", + ) + + InvalidParameterValue = ErrorMessage( + "Invalid parameter value for {0}: {1}", + -66002, + "configuration", + ) + + OperationNotSupported = ErrorMessage( + "Operation not supported: {0}", + -66003, + "configuration", + ) + + ResourceNotFound = ErrorMessage( + "Resource not found: {0}", + -66004, + "configuration", + ) + + UnexpectedError = ErrorMessage( + "An unexpected error occurred: {0}", + -66005, + "configuration", + ) + + InvalidStateObject = ErrorMessage( + "Invalid state object: {0}", + -66006, + "configuration", + ) + + SerializationError = ErrorMessage( + "Serialization error: {0}", + -66007, + "configuration", + ) + + DeserializationError = ErrorMessage( + "Deserialization error: {0}", + -66008, + "configuration", + ) + + TimeoutError = ErrorMessage( + "Operation timed out: {0}", + -66009, + "configuration", + ) + + NetworkError = ErrorMessage( + "Network error occurred: {0}", + -66010, + "configuration", + ) + + def __init__(self): + """Initialize ErrorResources singleton.""" + pass 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..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,8 @@ SensitivityUsageInfo, ) -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 +80,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 +116,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 +132,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 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..fcf1c7bf --- /dev/null +++ b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents Hosting Teams package. +""" + +from microsoft_agents.hosting.core.errors 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_resources.py b/libraries/microsoft-agents-hosting-teams/microsoft_agents/hosting/teams/errors/error_resources.py new file mode 100644 index 00000000..888e930a --- /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 microsoft_agents.hosting.core.errors 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 17a3d78e..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 @@ -7,6 +7,7 @@ from typing import Any, List 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) == "NotImplemented": + if str(err) == str(teams_errors.TeamsNotImplemented): return InvokeResponse(status=int(HTTPStatus.NOT_IMPLEMENTED)) - elif str(err) == "BadRequest": + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("BadRequest") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("BadRequest") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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("NotImplemented") + 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 1a4de969..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 @@ -23,7 +23,12 @@ 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, +) +from microsoft_agents.hosting.teams.errors import teams_errors class TeamsInfo: @@ -52,7 +57,7 @@ async def get_meeting_participant( ValueError: If required parameters are missing. """ if not context: - raise ValueError("context is required.") + raise ValueError(str(teams_errors.TeamsContextRequired)) activity = context.activity teams_channel_data: dict = activity.channel_data @@ -61,13 +66,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(teams_errors.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(teams_errors.TeamsParticipantIdRequired)) if tenant_id is None: tenant_id = teams_channel_data.get("tenant", {}).get("id", None) @@ -100,7 +105,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(teams_errors.TeamsMeetingIdRequired)) rest_client = TeamsInfo._get_rest_client(context) result = await rest_client.fetch_meeting_info(meeting_id) @@ -128,7 +133,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(teams_errors.TeamsTeamIdRequired)) rest_client = TeamsInfo._get_rest_client(context) result = await rest_client.fetch_team_details(team_id) @@ -157,13 +162,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(teams_errors.TeamsTurnContextRequired)) if not activity: - raise ValueError("Activity cannot be None") + raise ValueError(str(teams_errors.TeamsActivityRequired)) if not teams_channel_id: - raise ValueError("The teams_channel_id cannot be None or empty") + raise ValueError(str(teams_errors.TeamsChannelIdRequired)) convo_params = ConversationParameters( is_group=True, @@ -239,7 +244,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(teams_errors.TeamsTeamIdRequired)) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.fetch_channel_list(team_id) @@ -278,7 +283,7 @@ async def get_paged_members( else None ) if not conversation_id: - raise ValueError("conversation_id is required.") + raise ValueError(str(teams_errors.TeamsConversationIdRequired)) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.get_conversation_paged_member( @@ -312,7 +317,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(teams_errors.TeamsConversationIdRequired)) return await TeamsInfo._get_member_internal( context, conversation_id, user_id @@ -345,7 +350,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(teams_errors.TeamsTeamIdRequired)) rest_client = TeamsInfo._get_rest_client(context) paged_results = await rest_client.get_conversation_paged_member( @@ -410,7 +415,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(teams_errors.TeamsMeetingIdRequired)) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.send_meeting_notification(meeting_id, notification) @@ -438,9 +443,11 @@ 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 +475,11 @@ 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 +506,13 @@ 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(teams_errors.TeamsTeamIdRequired)) rest_client = TeamsInfo._get_rest_client(context) return await rest_client.send_message_to_all_users_in_team( @@ -531,9 +542,11 @@ 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..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,6 +14,7 @@ ignore_error, is_status_code_error, ) +from microsoft_agents.storage.blob.errors import blob_storage_errors 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(blob_storage_errors.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." + blob_storage_errors.InvalidConfiguration.format( + "Credential is required when using a custom service URL" + ) ) return BlobServiceClient( account_url=self.config.url, credential=self.config.credential 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..15361d3f --- /dev/null +++ b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents Storage Blob package. +""" + +from microsoft_agents.hosting.core.errors 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_resources.py b/libraries/microsoft-agents-storage-blob/microsoft_agents/storage/blob/errors/error_resources.py new file mode 100644 index 00000000..4b4f781d --- /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 microsoft_agents.hosting.core.errors 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 fdd4f6f8..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,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.storage.cosmos.errors import storage_errors 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." + storage_errors.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(storage_errors.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(storage_errors.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(storage_errors.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]}." + 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]}." + ) ) 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..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,6 +2,7 @@ from typing import Union from azure.core.credentials import TokenCredential +from microsoft_agents.storage.cosmos.errors import storage_errors 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(storage_errors.CosmosDbConfigRequired)) if not config.cosmos_db_endpoint: - raise ValueError("CosmosDBStorage: cosmos_db_endpoint is required.") + raise ValueError(str(storage_errors.CosmosDbEndpointRequired)) if not config.auth_key: - raise ValueError("CosmosDBStorage: auth_key is required.") + raise ValueError(str(storage_errors.CosmosDbAuthKeyRequired)) if not config.database_id: - raise ValueError("CosmosDBStorage: database_id is required.") + raise ValueError(str(storage_errors.CosmosDbDatabaseIdRequired)) if not config.container_id: - raise ValueError("CosmosDBStorage: container_id is required.") + raise ValueError(str(storage_errors.CosmosDbContainerIdRequired)) CosmosDBStorageConfig._validate_suffix(config) @@ -85,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( - "compatibilityMode cannot be true while using a keySuffix." - ) + raise ValueError(str(storage_errors.CosmosDbCompatibilityModeRequired)) 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." + 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..6bdc216e --- /dev/null +++ b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Error resources for Microsoft Agents Storage Cosmos package. +""" + +from microsoft_agents.hosting.core.errors 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_resources.py b/libraries/microsoft-agents-storage-cosmos/microsoft_agents/storage/cosmos/errors/error_resources.py new file mode 100644 index 00000000..1137144a --- /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 microsoft_agents.hosting.core.errors 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 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..77ce3e67 --- /dev/null +++ b/tests/hosting_core/errors/test_error_resources.py @@ -0,0 +1,247 @@ +# 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 (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_hosting_errors_exist(self): + """Test that hosting errors are defined in hosting-core.""" + assert hasattr(error_resources, "AdapterRequired") + assert hasattr(error_resources, "AgentApplicationRequired") + assert hasattr(error_resources, "StreamAlreadyEnded") + assert hasattr(error_resources, "RequestRequired") + assert hasattr(error_resources, "AgentRequired") + + def test_general_errors_exist(self): + """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_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 + + +class TestDistributedErrorResources: + """Tests for error resources distributed across packages.""" + + 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_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_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_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")