From 7e01415b6f32c60ccb4fa42c170ffdbeb7094548 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Fri, 5 Dec 2025 09:12:08 -0800 Subject: [PATCH 1/3] Adding extra documentation --- .../client/connection_settings.py | 19 ++++++++++-- .../copilotstudio/client/copilot_client.py | 31 ++++++++++++++++++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/connection_settings.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/connection_settings.py index 6b8e6bc7..7a21c966 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/connection_settings.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/connection_settings.py @@ -15,10 +15,22 @@ def __init__( self, environment_id: str, agent_identifier: str, - cloud: Optional[PowerPlatformCloud], - copilot_agent_type: Optional[AgentType], - custom_power_platform_cloud: Optional[str], + cloud: Optional[PowerPlatformCloud] = None, + copilot_agent_type: Optional[AgentType] = None, + custom_power_platform_cloud: Optional[str] = None, + client_session_kwargs: Optional[dict] = None, ) -> None: + """Initialize connection settings. + + :param environment_id: The ID of the environment to connect to. + :param agent_identifier: The identifier of the agent to use for the connection. + :param cloud: The PowerPlatformCloud to use for the connection. + :param copilot_agent_type: The AgentType to use for the Copilot. + :param custom_power_platform_cloud: The custom PowerPlatformCloud URL. + :param client_session_kwargs: Additional keyword arguments for initialization + of the underlying Aiohttp ClientSession. + """ + self.environment_id = environment_id self.agent_identifier = agent_identifier @@ -30,3 +42,4 @@ def __init__( self.cloud = cloud or PowerPlatformCloud.PROD self.copilot_agent_type = copilot_agent_type or AgentType.PUBLISHED self.custom_power_platform_cloud = custom_power_platform_cloud + self.client_session_kwargs = client_session_kwargs or {} diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/copilot_client.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/copilot_client.py index 68c26466..bc308cf9 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/copilot_client.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/copilot_client.py @@ -28,7 +28,17 @@ def __init__( async def post_request( self, url: str, data: dict, headers: dict ) -> AsyncIterable[Activity]: - async with aiohttp.ClientSession() as session: + """Send a POST request to the specified URL with the given data and headers. + + :param url: The URL to which the POST request is sent. + :param data: The data to be sent in the POST request body. + :param headers: The headers to be included in the POST request. + :return: An asynchronous iterable of Activity objects received in the response. + """ + + async with aiohttp.ClientSession( + **self.settings.client_session_kwargs + ) as session: async with session.post(url, json=data, headers=headers) as response: if response.status != 200: # self.logger(f"Error sending request: {response.status}") @@ -57,6 +67,12 @@ async def post_request( async def start_conversation( self, emit_start_conversation_event: bool = True ) -> AsyncIterable[Activity]: + """Start a new conversation and optionally emit a start conversation event. + + :param emit_start_conversation_event: A boolean flag indicating whether to emit a start conversation event. + :return: An asynchronous iterable of Activity objects received in the response. + """ + url = PowerPlatformEnvironment.get_copilot_studio_connection_url( settings=self.settings ) @@ -73,6 +89,13 @@ async def start_conversation( async def ask_question( self, question: str, conversation_id: Optional[str] = None ) -> AsyncIterable[Activity]: + """Ask a question in the specified conversation. + + :param question: The question to be asked. + :param conversation_id: The ID of the conversation in which the question is asked. If not provided, the current conversation ID is used. + :return: An asynchronous iterable of Activity objects received in the response. + """ + activity = Activity( type="message", text=question, @@ -87,6 +110,12 @@ async def ask_question( async def ask_question_with_activity( self, activity: Activity ) -> AsyncIterable[Activity]: + """Ask a question using an Activity object. + + :param activity: The Activity object representing the question to be asked. + :return: An asynchronous iterable of Activity objects received in the response. + """ + if not activity: raise ValueError( "CopilotClient.ask_question_with_activity: Activity cannot be None" From 99f95360a331d61cbe0e0f24c4e0b8ce7aebb14c Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Fri, 5 Dec 2025 10:45:13 -0800 Subject: [PATCH 2/3] Adding unit tests for CopilotClient --- .../copilotstudio/client/__init__.py | 3 + .../copilotstudio/client/agent_type.py | 4 +- .../client/connection_settings.py | 9 +- .../copilotstudio/client/copilot_client.py | 8 +- ..._to_engine_connection_settings_protocol.py | 3 + .../client/execute_turn_request.py | 3 + .../client/power_platform_cloud.py | 4 +- .../client/power_platform_environment.py | 3 + .../test_copilot_client.py | 87 +++++++++++++++++++ 9 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 tests/copilotstudio_client/test_copilot_client.py diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/__init__.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/__init__.py index 8b7e1178..250e50e6 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/__init__.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/__init__.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from .agent_type import AgentType from .connection_settings import ConnectionSettings from .copilot_client import CopilotClient diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/agent_type.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/agent_type.py index 03d4489f..b4eaf268 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/agent_type.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/agent_type.py @@ -1,5 +1,7 @@ -from enum import Enum +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from enum import Enum class AgentType(str, Enum): PUBLISHED = "published" diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/connection_settings.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/connection_settings.py index 7a21c966..777c545e 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/connection_settings.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/connection_settings.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Optional from .direct_to_engine_connection_settings_protocol import ( DirectToEngineConnectionSettingsProtocol, @@ -18,7 +21,7 @@ def __init__( cloud: Optional[PowerPlatformCloud] = None, copilot_agent_type: Optional[AgentType] = None, custom_power_platform_cloud: Optional[str] = None, - client_session_kwargs: Optional[dict] = None, + client_session_settings: Optional[dict] = None, ) -> None: """Initialize connection settings. @@ -27,7 +30,7 @@ def __init__( :param cloud: The PowerPlatformCloud to use for the connection. :param copilot_agent_type: The AgentType to use for the Copilot. :param custom_power_platform_cloud: The custom PowerPlatformCloud URL. - :param client_session_kwargs: Additional keyword arguments for initialization + :param client_session_settings: Additional arguments for initialization of the underlying Aiohttp ClientSession. """ @@ -42,4 +45,4 @@ def __init__( self.cloud = cloud or PowerPlatformCloud.PROD self.copilot_agent_type = copilot_agent_type or AgentType.PUBLISHED self.custom_power_platform_cloud = custom_power_platform_cloud - self.client_session_kwargs = client_session_kwargs or {} + self.client_session_settings = client_session_settings or {} diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/copilot_client.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/copilot_client.py index bc308cf9..893bfb10 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/copilot_client.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/copilot_client.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + import aiohttp from typing import AsyncIterable, Callable, Optional @@ -9,6 +12,8 @@ class CopilotClient: + """A client for interacting with the Copilot service.""" + EVENT_STREAM_TYPE = "text/event-stream" APPLICATION_JSON_TYPE = "application/json" @@ -37,9 +42,10 @@ async def post_request( """ async with aiohttp.ClientSession( - **self.settings.client_session_kwargs + **self.settings.client_session_settings ) as session: async with session.post(url, json=data, headers=headers) as response: + if response.status != 200: # self.logger(f"Error sending request: {response.status}") raise aiohttp.ClientError( diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/direct_to_engine_connection_settings_protocol.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/direct_to_engine_connection_settings_protocol.py index b4750c31..c6d35b7c 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/direct_to_engine_connection_settings_protocol.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/direct_to_engine_connection_settings_protocol.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Protocol, Optional from .agent_type import AgentType diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/execute_turn_request.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/execute_turn_request.py index 3fc45351..6bedd3a5 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/execute_turn_request.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/execute_turn_request.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from microsoft_agents.activity import AgentsModel, Activity diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_cloud.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_cloud.py index 87d75e9a..08580b7d 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_cloud.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_cloud.py @@ -1,5 +1,7 @@ -from enum import Enum +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from enum import Enum class PowerPlatformCloud(str, Enum): """ 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 75ccd6f0..0afc31ee 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,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from microsoft_agents.copilotstudio.client.errors import copilot_studio_errors from urllib.parse import urlparse, urlunparse from typing import Optional diff --git a/tests/copilotstudio_client/test_copilot_client.py b/tests/copilotstudio_client/test_copilot_client.py new file mode 100644 index 00000000..c5eef944 --- /dev/null +++ b/tests/copilotstudio_client/test_copilot_client.py @@ -0,0 +1,87 @@ +import pytest + +from contextlib import asynccontextmanager + +from microsoft_agents.activity import Activity + +from microsoft_agents.copilotstudio.client import ( + ConnectionSettings, + CopilotClient, + PowerPlatformEnvironment +) + +from aiohttp import ClientSession, ClientError + +@pytest.mark.asyncio +async def test_copilot_client_error(mocker): + # Define the connection settings + connection_settings = ConnectionSettings( + "environment-id", + "agent-id", + client_session_settings={"base_url": "https://api.copilotstudio.com"} + ) + + mock_session = mocker.MagicMock(spec=ClientSession) + mock_session.__aenter__.return_value = mock_session + + @asynccontextmanager + async def response(): + mock_response = mocker.Mock() + mock_response.status = 401 + yield mock_response + + mock_session.post.return_value = response() + + mocker.patch("aiohttp.ClientSession.__new__", return_value=mock_session) + + # Create a CopilotClient instance + copilot_client = CopilotClient(connection_settings, "token") + + with pytest.raises(ClientError): + async for message in copilot_client.start_conversation(): + # Process the message received from the conversation + print(message) + +@pytest.mark.asyncio +async def test_copilot_client_basic(mocker): + # Define the connection settings + connection_settings = ConnectionSettings( + "environment-id", + "agent-id", + client_session_settings={"base_url": "https://api.copilotstudio.com"} + ) + + mock_session = mocker.MagicMock(spec=ClientSession) + mock_session.__aenter__.return_value = mock_session + + @asynccontextmanager + async def response(): + mock_response = mocker.Mock() + mock_response.status = 200 + + activity = Activity(type="message", text="Hello, world!", conversation={"id": "1234567890"}) + activity_json = activity.model_dump_json(exclude_unset=True) + + async def content(): + yield "event: activity".encode() + yield f"data: {activity_json}".encode() + + mock_response.content = content() + + yield mock_response + + mock_session.post.return_value = response() + + mocker.patch("aiohttp.ClientSession.__new__", return_value=mock_session) + + # Create a CopilotClient instance + copilot_client = CopilotClient(connection_settings, "token") + + count = 0 + async for message in copilot_client.start_conversation(): + count += 1 + assert message.type == "message" + assert message.text == "Hello, world!" + assert message.conversation.id == "1234567890" + + assert count == 1 \ No newline at end of file From 1b12c30bf63bbeed8b7a58aad43235be388f4516 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Fri, 5 Dec 2025 10:45:28 -0800 Subject: [PATCH 3/3] Formatting --- .../copilotstudio/client/agent_type.py | 1 + .../copilotstudio/client/power_platform_cloud.py | 1 + tests/copilotstudio_client/test_copilot_client.py | 14 +++++++++----- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/agent_type.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/agent_type.py index b4eaf268..77a95a32 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/agent_type.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/agent_type.py @@ -3,6 +3,7 @@ from enum import Enum + class AgentType(str, Enum): PUBLISHED = "published" PREBUILT = "prebuilt" diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_cloud.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_cloud.py index 08580b7d..c76ef976 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_cloud.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_cloud.py @@ -3,6 +3,7 @@ from enum import Enum + class PowerPlatformCloud(str, Enum): """ Enum representing different Power Platform Clouds. diff --git a/tests/copilotstudio_client/test_copilot_client.py b/tests/copilotstudio_client/test_copilot_client.py index c5eef944..1c235051 100644 --- a/tests/copilotstudio_client/test_copilot_client.py +++ b/tests/copilotstudio_client/test_copilot_client.py @@ -7,18 +7,19 @@ from microsoft_agents.copilotstudio.client import ( ConnectionSettings, CopilotClient, - PowerPlatformEnvironment + PowerPlatformEnvironment, ) from aiohttp import ClientSession, ClientError + @pytest.mark.asyncio async def test_copilot_client_error(mocker): # Define the connection settings connection_settings = ConnectionSettings( "environment-id", "agent-id", - client_session_settings={"base_url": "https://api.copilotstudio.com"} + client_session_settings={"base_url": "https://api.copilotstudio.com"}, ) mock_session = mocker.MagicMock(spec=ClientSession) @@ -42,13 +43,14 @@ async def response(): # Process the message received from the conversation print(message) + @pytest.mark.asyncio async def test_copilot_client_basic(mocker): # Define the connection settings connection_settings = ConnectionSettings( "environment-id", "agent-id", - client_session_settings={"base_url": "https://api.copilotstudio.com"} + client_session_settings={"base_url": "https://api.copilotstudio.com"}, ) mock_session = mocker.MagicMock(spec=ClientSession) @@ -59,7 +61,9 @@ async def response(): mock_response = mocker.Mock() mock_response.status = 200 - activity = Activity(type="message", text="Hello, world!", conversation={"id": "1234567890"}) + activity = Activity( + type="message", text="Hello, world!", conversation={"id": "1234567890"} + ) activity_json = activity.model_dump_json(exclude_unset=True) async def content(): @@ -84,4 +88,4 @@ async def content(): assert message.text == "Hello, world!" assert message.conversation.id == "1234567890" - assert count == 1 \ No newline at end of file + assert count == 1