Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions dev/integration/tests/test_copilot_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import pytest

from aiohttp.web import Request, Response, Application

from microsoft_agents.activity import Activity

from microsoft_agents.copilotstudio.client import (
CopilotClient,
ConnectionSettings,
PowerPlatformEnvironment
)

from microsoft_agents.testing.integration.core import (
AiohttpRunner
)

def mock_mcs_handler(activity: Activity) -> Awaitable[[Request], Response]:
"""Creates a mock handler for MCS endpoint returning the given activity."""
async def handler(request: Request) -> Response:
activity_data = activity.model_dump_json(exclude_unset=True)
return Response(
body=activity_data
)
return handler

def mock_mcs_endpoint(
mocker,
activity: Activity,
path: str,
port: int
) -> AiohttpRunner:
"""Mock MCS responses for testing."""

PowerPlatformEnvironment.get_copilot_studio_connection_url = mocker.MagicMock(
return_value=f"http://localhost:{port}{path}"
)

app = Application()
app.router.add_post(path, mock_mcs_handler(activity))

return AiohttpRunner(app, port=port)


@pytest.mark.asyncio
async def test_start_conversation_and_ask_question(mocker):

activity = Activity(
type="message"
)

runner = mock_mcs_endpoint(mocker, activity, "/mcs-endpoint", port=8081)

await with runner:
settings = ConnectionSettings("environment-id", "agent-id")
client = CopilotClient(settings=settings, token="test-token")

async for conv_activity in client.start_conversation():
assert conv_activity.type == "message"

async for question_activity in client.ask_question("Hello!"):
assert question_activity.type == "message"
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def __init__(
cloud: Optional[PowerPlatformCloud],
copilot_agent_type: Optional[AgentType],
custom_power_platform_cloud: Optional[str],
client_session_defaults: Optional[dict] = None,
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type hint dict is too generic. Consider using Dict[str, Any] from the typing module for better type safety, or even better, define the expected structure more explicitly if the aiohttp.ClientSession constructor parameters are known.

Copilot uses AI. Check for mistakes.
) -> None:
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client_session_defaults parameter lacks documentation. Consider adding a docstring comment to the __init__ method or inline documentation explaining what this parameter is for, what keys are supported, and providing usage examples.

Suggested change
) -> None:
) -> None:
"""
Initializes a new instance of ConnectionSettings.
Args:
environment_id (str): The ID of the Power Platform environment.
agent_identifier (str): The identifier for the agent.
cloud (Optional[PowerPlatformCloud]): The Power Platform cloud to use. Defaults to PROD if not specified.
copilot_agent_type (Optional[AgentType]): The type of Copilot agent. Defaults to PUBLISHED if not specified.
custom_power_platform_cloud (Optional[str]): Custom cloud endpoint, if applicable.
client_session_defaults (Optional[dict]): Optional dictionary of default values for the client session.
Supported keys may include configuration options such as authentication tokens,
user preferences, or other session-specific settings. The exact keys depend on the
client implementation.
Example:
client_session_defaults = {
"auth_token": "your_token_here",
"locale": "en-US",
"timeout": 30
}
settings = ConnectionSettings(
environment_id="env123",
agent_identifier="agent456",
cloud=PowerPlatformCloud.PROD,
copilot_agent_type=AgentType.PUBLISHED,
custom_power_platform_cloud=None,
client_session_defaults=client_session_defaults
)
"""

Copilot uses AI. Check for mistakes.
self.environment_id = environment_id
self.agent_identifier = agent_identifier
Expand All @@ -30,3 +31,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_defaults = client_session_defaults or {}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client_session_defaults attribute should be added to the DirectToEngineConnectionSettingsProtocol to maintain protocol conformance. Since ConnectionSettings implements this protocol, all attributes should be declared in the protocol interface.

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ def __init__(
async def post_request(
self, url: str, data: dict, headers: dict
) -> AsyncIterable[Activity]:
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(
**self.settings.client_session_defaults
) as session:
async with session.post(url, json=data, headers=headers) as response:
Comment on lines +31 to 34
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing arbitrary user-provided dictionary values to aiohttp.ClientSession could lead to runtime errors if invalid parameters are provided. Consider adding validation for the dictionary keys/values, or at least wrapping this in a try-except block to provide a more helpful error message if invalid session parameters are provided.

Suggested change
async with aiohttp.ClientSession(
**self.settings.client_session_defaults
) as session:
async with session.post(url, json=data, headers=headers) as response:
try:
async with aiohttp.ClientSession(
**self.settings.client_session_defaults
) 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(
f"Error sending request: {response.status}"
)
# Set conversation ID from response header when status is 200
conversation_id_header = response.headers.get("x-ms-conversationid")
if conversation_id_header:
self._current_conversation_id = conversation_id_header
event_type = None
async for line in response.content:
if line.startswith(b"event:"):
event_type = line[6:].decode("utf-8").strip()
if line.startswith(b"data:") and event_type == "activity":
activity_data = line[5:].decode("utf-8").strip()
activity = Activity.model_validate_json(activity_data)
if activity.type == ActivityTypes.message:
self._current_conversation_id = activity.conversation.id
yield activity
except TypeError as e:
raise ValueError(
f"Invalid parameters provided to aiohttp.ClientSession: {e}. "
f"Check self.settings.client_session_defaults for invalid keys/values."
) from e

Copilot uses AI. Check for mistakes.
if response.status != 200:
# self.logger(f"Error sending request: {response.status}")
Expand Down