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
27 changes: 26 additions & 1 deletion .azdo/ci-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ steps:

- script: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest black
python -m pip install flake8 pytest black pytest-asyncio build
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
displayName: 'Install dependencies'

Expand All @@ -34,6 +34,31 @@ steps:
flake8 . --count --exit-zero --show-source --statistics
displayName: 'Lint with flake8'

- script: |
mkdir -p dist
for dir in libraries/*; do
if [ -d "$dir" ]; then
for subdir in "$dir"/*; do
if [ -f "$subdir/pyproject.toml" ]; then
(cd "$subdir" && python -m build --outdir ../../../dist)
fi
done
fi
done
displayName: 'Build packages'

- script: |
python -m pip install ./dist/microsoft_agents_core*.whl
python -m pip install ./dist/microsoft_agents_authentication*.whl
python -m pip install ./dist/microsoft_agents_connector*.whl
python -m pip install ./dist/microsoft_agents_client*.whl
python -m pip install ./dist/microsoft_agents_botbuilder*.whl
python -m pip install ./dist/microsoft_agents_authorization_msal*.whl
python -m pip install ./dist/microsoft_agents_copilotstudio_client*.whl
python -m pip install ./dist/microsoft_agents_hosting_aiohttp*.whl
python -m pip install ./dist/microsoft_agents_storage*.whl
displayName: 'Install wheels'

- script: |
pytest
displayName: 'Test with pytest'
25 changes: 24 additions & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest black
python -m pip install flake8 pytest black pytest-asyncio build
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Check format with black
run: |
Expand All @@ -37,6 +37,29 @@ jobs:
run: |
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --show-source --statistics
- name: Build packages
run: |
mkdir -p dist
for dir in libraries/*; do
if [ -d "$dir" ]; then
for subdir in "$dir"/*; do
if [ -f "$subdir/pyproject.toml" ]; then
(cd "$subdir" && python -m build --outdir ../../../dist)
fi
done
fi
done
- name: Install wheels
run: |
python -m pip install ./dist/microsoft_agents_core*.whl
python -m pip install ./dist/microsoft_agents_authentication*.whl
python -m pip install ./dist/microsoft_agents_connector*.whl
python -m pip install ./dist/microsoft_agents_client*.whl
python -m pip install ./dist/microsoft_agents_botbuilder*.whl
python -m pip install ./dist/microsoft_agents_authorization_msal*.whl
python -m pip install ./dist/microsoft_agents_copilotstudio_client*.whl
python -m pip install ./dist/microsoft_agents_hosting_aiohttp*.whl
python -m pip install ./dist/microsoft_agents_storage*.whl
- name: Test with pytest
run: |
pytest
Original file line number Diff line number Diff line change
Expand Up @@ -159,21 +159,15 @@ async def on_conversation_update_activity(self, turn_context: TurnContextProtoco
- In a derived class, override this method to add logic that applies to all conversation update activities.
Add logic to apply before the member added or removed logic before the call to this base class method.
"""
if (
turn_context.activity.members_added is not None
and turn_context.activity.members_added
):
return await self.on_members_added_activity(
# TODO: confirm behavior of added and removed at the same time as C# doesn't support it
if turn_context.activity.members_added:
await self.on_members_added_activity(
turn_context.activity.members_added, turn_context
)
if (
turn_context.activity.members_removed is not None
and turn_context.activity.members_removed
):
return await self.on_members_removed_activity(
if turn_context.activity.members_removed:
await self.on_members_removed_activity(
turn_context.activity.members_removed, turn_context
)
return

async def on_members_added_activity(
self, members_added: list[ChannelAccount], turn_context: TurnContextProtocol
Expand Down Expand Up @@ -534,9 +528,15 @@ async def on_adaptive_card_invoke(

@staticmethod
def _create_invoke_response(body: BaseModel = None) -> InvokeResponse:
serialized_body = (
body.model_dump(mode="json", by_alias=True, exclude_none=True)
if body
else None
)

return InvokeResponse(
status=int(HTTPStatus.OK),
body=body.model_dump(mode="json", by_alias=True, exclude_none=True),
body=serialized_body,
)

def _get_adaptive_card_invoke_value(self, activity: Activity):
Expand All @@ -563,11 +563,11 @@ def _get_adaptive_card_invoke_value(self, activity: Activity):
)
raise _InvokeResponseException(HTTPStatus.BAD_REQUEST, response)

if invoke_value.action.get("type") != "Action.Execute":
if invoke_value.action.type != "Action.Execute":
response = self._create_adaptive_card_invoke_error_response(
HTTPStatus.BAD_REQUEST,
"NotSupported",
f"The action '{invoke_value.action.get('type')}' is not supported.",
f"The action '{invoke_value.action.type}' is not supported.",
)
raise _InvokeResponseException(HTTPStatus.BAD_REQUEST, response)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import pytest
from unittest.mock import AsyncMock
from microsoft.agents.botbuilder import ActivityHandler, TurnContext
from microsoft.agents.core.models import (
ActivityTypes,
ChannelAccount,
MessageReaction,
SignInConstants,
InvokeResponse,
)


class TestActivityHandler:
@pytest.fixture
def handler(self):
return ActivityHandler()

@pytest.fixture
def turn_context(self):
return AsyncMock(spec=TurnContext)

@pytest.mark.asyncio
async def test_on_turn_message_activity(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.type = ActivityTypes.message
handler.on_message_activity = AsyncMock()
await handler.on_turn(turn_context)
handler.on_message_activity.assert_called_once_with(turn_context)

@pytest.mark.asyncio
async def test_on_turn_conversation_update_activity(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.type = ActivityTypes.conversation_update
handler.on_conversation_update_activity = AsyncMock()
await handler.on_turn(turn_context)
handler.on_conversation_update_activity.assert_called_once_with(turn_context)

@pytest.mark.asyncio
async def test_on_turn_event_activity(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.type = ActivityTypes.event
handler.on_event_activity = AsyncMock()
await handler.on_turn(turn_context)
handler.on_event_activity.assert_called_once_with(turn_context)

@pytest.mark.asyncio
async def test_on_turn_unrecognized_activity_type(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.type = "unknown"
handler.on_unrecognized_activity_type = AsyncMock()
await handler.on_turn(turn_context)
handler.on_unrecognized_activity_type.assert_called_once_with(turn_context)

@pytest.mark.asyncio
async def test_on_message_activity(
self, handler: ActivityHandler, turn_context: TurnContext
):
await handler.on_message_activity(turn_context)
# No exception means the test passed

@pytest.mark.asyncio
async def test_on_conversation_update_activity_members_added(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.members_added = [ChannelAccount(id="user1")]
handler.on_members_added_activity = AsyncMock()
await handler.on_conversation_update_activity(turn_context)
handler.on_members_added_activity.assert_called_once_with(
turn_context.activity.members_added, turn_context
)

@pytest.mark.asyncio
async def test_on_conversation_update_activity_members_removed(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.members_removed = [ChannelAccount(id="user1")]
handler.on_members_removed_activity = AsyncMock()
await handler.on_conversation_update_activity(turn_context)
handler.on_members_removed_activity.assert_called_once_with(
turn_context.activity.members_removed, turn_context
)

@pytest.mark.asyncio
async def test_on_message_reaction_activity_reactions_added(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.reactions_added = [MessageReaction(type="like")]
handler.on_reactions_added = AsyncMock()
await handler.on_message_reaction_activity(turn_context)
handler.on_reactions_added.assert_called_once_with(
turn_context.activity.reactions_added, turn_context
)

@pytest.mark.asyncio
async def test_on_message_reaction_activity_reactions_removed(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.reactions_removed = [MessageReaction(type="like")]
handler.on_reactions_removed = AsyncMock()
await handler.on_message_reaction_activity(turn_context)
handler.on_reactions_removed.assert_called_once_with(
turn_context.activity.reactions_removed, turn_context
)

@pytest.mark.asyncio
async def test_on_event_activity_token_response(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.name = SignInConstants.token_response_event_name
handler.on_token_response_event = AsyncMock()
await handler.on_event_activity(turn_context)
handler.on_token_response_event.assert_called_once_with(turn_context)

@pytest.mark.asyncio
async def test_on_event_activity_other_event(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.name = "other_event"
handler.on_event = AsyncMock()
await handler.on_event_activity(turn_context)
handler.on_event.assert_called_once_with(turn_context)

@pytest.mark.asyncio
async def test_on_invoke_activity_sign_in(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.name = SignInConstants.verify_state_operation_name
handler.on_sign_in_invoke = AsyncMock()
response = await handler.on_invoke_activity(turn_context)
handler.on_sign_in_invoke.assert_called_once_with(turn_context)
assert isinstance(response, InvokeResponse)

@pytest.mark.asyncio
async def test_on_invoke_activity_adaptive_card(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.name = "adaptiveCard/action"
turn_context.activity.value = {"action": {"type": "Action.Execute"}}
handler.on_adaptive_card_invoke = AsyncMock(return_value=None)
response = await handler.on_invoke_activity(turn_context)
handler.on_adaptive_card_invoke.assert_called_once()
assert isinstance(response, InvokeResponse)

@pytest.mark.asyncio
async def test_on_invoke_activity_not_implemented(
self, handler: ActivityHandler, turn_context: TurnContext
):
turn_context.activity.name = "unknown"
response = await handler.on_invoke_activity(turn_context)

assert response.status == 501
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel


class AgentsModel(BaseModel):
class Config:
alias_generator = to_camel
populate_by_name = True
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)

"""
@model_serializer
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .agents_model import AgentsModel
from .agents_model import AgentsModel, ConfigDict
from ._type_aliases import NonEmptyString


Expand All @@ -9,7 +9,6 @@ class Entity(AgentsModel):
:type type: str
"""

class Config:
extra = "allow"
model_config = ConfigDict(extra="allow")

type: NonEmptyString