Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from enum import Enum


Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -15,10 +18,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_settings: 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_settings: Additional arguments for initialization
of the underlying Aiohttp ClientSession.
"""

self.environment_id = environment_id
self.agent_identifier = agent_identifier

Expand All @@ -30,3 +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_settings = client_session_settings or {}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import aiohttp
from typing import AsyncIterable, Callable, Optional

Expand All @@ -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"

Expand All @@ -28,8 +33,19 @@ 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_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(
Expand Down Expand Up @@ -57,6 +73,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
)
Expand All @@ -73,6 +95,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,
Expand All @@ -87,6 +116,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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from microsoft_agents.activity import AgentsModel, Activity


Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from enum import Enum


Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
91 changes: 91 additions & 0 deletions tests/copilotstudio_client/test_copilot_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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