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
Expand Up @@ -2,10 +2,12 @@
# Licensed under the MIT License.

from abc import ABC
from typing import Any, Callable
from typing import Any, Callable, TypeVar

from .agents_model import AgentsModel

AgentsModelT = TypeVar("AgentsModelT", bound=AgentsModel)


class ModelFieldHelper(ABC):
"""Base class for model field processing prior to initialization of an AgentsModel"""
Expand Down Expand Up @@ -55,7 +57,7 @@ def pick_model_dict(**kwargs):
return model_dict


def pick_model(model_class: type[AgentsModel], **kwargs) -> AgentsModel:
def pick_model(model_class: type[AgentsModelT], **kwargs) -> AgentsModelT:
"""Picks model fields from the given keyword arguments.

Usage:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ def get_mentions(self) -> list[Mention]:
if not self.entities:
return []
raw_mentions = [
x for x in self.entities if x.type.lower() == EntityTypes.MENTION
x for x in self.entities if x.type.lower() == EntityTypes.MENTION.value
]

return Activity._convert_entity_list(raw_mentions, Mention)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,60 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from __future__ import annotations

import re
from typing import Optional
from typing import Optional, Awaitable, TypeVar, Protocol

from copy import copy, deepcopy
from copy import deepcopy
from collections.abc import Callable
from datetime import datetime, timezone
from microsoft_agents.activity import TurnContextProtocol
from microsoft_agents.activity import (
Activity,
ActivityTypes,
ConversationReference,
DeliveryModes,
InputHints,
Mention,
ResourceResponse,
DeliveryModes,
TurnContextProtocol,
)
Comment thread
rodrigobr-msft marked this conversation as resolved.
from microsoft_agents.activity._model_utils import pick_model, SkipNone
from microsoft_agents.activity.entity.entity_types import EntityTypes
from microsoft_agents.hosting.core.authorization.claims_identity import ClaimsIdentity
import microsoft_agents.hosting.core.telemetry.turn_context.spans as spans

OnSendActivitiesHandler = Callable[
["TurnContext", list[Activity], Callable[[], Awaitable[list[ResourceResponse]]]],
Awaitable[list[ResourceResponse]],
]
OnUpdateActivityHandler = Callable[
["TurnContext", Activity, Callable[[], Awaitable[ResourceResponse]]],
Awaitable[ResourceResponse],
]
OnDeleteActivityHandler = Callable[
["TurnContext", ConversationReference, Callable[[], Awaitable[None]]],
Awaitable[None],
]

T = TypeVar("T")
_ArgT = TypeVar("_ArgT")


class _AsyncFunc(Protocol[T]):
def __call__(self) -> Awaitable[T]: ...


class TurnContext(TurnContextProtocol):
# Same constant as in the BF Adapter, duplicating here to avoid circular dependency
_INVOKE_RESPONSE_KEY = "TurnContext.InvokeResponse"

_activity: Activity

_on_send_activities: list[OnSendActivitiesHandler]
_on_update_activity: list[OnUpdateActivityHandler]
_on_delete_activity: list[OnDeleteActivityHandler]

def __init__(
self,
adapter_or_context,
Expand All @@ -37,8 +63,9 @@ def __init__(
):
"""
Creates a new TurnContext instance.
:param adapter_or_context:
:param request:
:param adapter_or_context: The adapter instance or an existing TurnContext.
:param request: The incoming Activity.
:param identity: The ClaimsIdentity associated with the request.
"""
if isinstance(adapter_or_context, TurnContext):
adapter_or_context.copy_to(self)
Expand All @@ -48,15 +75,9 @@ def __init__(
self._activity = request # exception thrown if None further down
self.responses: list[Activity] = []
self._services: dict = {}
self._on_send_activities: Callable[
["TurnContext", list[Activity], Callable], list[ResourceResponse]
] = []
self._on_update_activity: Callable[
["TurnContext", Activity, Callable], ResourceResponse
] = []
self._on_delete_activity: Callable[
["TurnContext", ConversationReference, Callable], None
] = []
self._on_send_activities = []
self._on_update_activity = []
self._on_delete_activity = []
self._responded: bool = False
self._identity = identity

Expand Down Expand Up @@ -235,7 +256,7 @@ def activity_validator(activity: Activity) -> Activity:
]

# send activities through adapter
async def logic():
async def logic() -> list[ResourceResponse]:
nonlocal sent_non_trace_activity

if self.activity.delivery_mode == DeliveryModes.expect_replies:
Expand All @@ -259,7 +280,7 @@ async def logic():
self.responded = True
return responses

return await self._emit(self._on_send_activities, output, logic())
return await self._emit(self._on_send_activities, output, logic)

async def update_activity(self, activity: Activity):
"""
Expand All @@ -272,7 +293,7 @@ async def update_activity(self, activity: Activity):
return await self._emit(
self._on_update_activity,
TurnContext.apply_conversation_reference(activity, reference),
self.adapter.update_activity(self, activity),
lambda: self.adapter.update_activity(self, activity),
)

async def delete_activity(self, id_or_reference: str | ConversationReference):
Expand All @@ -286,58 +307,72 @@ async def delete_activity(self, id_or_reference: str | ConversationReference):
reference.activity_id = id_or_reference
else:
reference = id_or_reference

return await self._emit(
self._on_delete_activity,
reference,
self.adapter.delete_activity(self, reference),
lambda: self.adapter.delete_activity(self, reference),
)

def on_send_activities(self, handler) -> "TurnContext":
def on_send_activities(self, handler: OnSendActivitiesHandler) -> TurnContext:
"""
Registers a handler to be notified of and potentially intercept the sending of activities.
:param handler:
:param handler: the handler to register
:type handler: OnSendActivitiesHandler
:return:
"""
self._on_send_activities.append(handler)
return self

def on_update_activity(self, handler) -> "TurnContext":
def on_update_activity(self, handler: OnUpdateActivityHandler) -> TurnContext:
"""
Registers a handler to be notified of and potentially intercept an activity being updated.
:param handler:
:param handler: the handler to register
:type handler: OnUpdateActivityHandler
:return:
"""
self._on_update_activity.append(handler)
return self

def on_delete_activity(self, handler) -> "TurnContext":
def on_delete_activity(self, handler: OnDeleteActivityHandler) -> TurnContext:
"""
Registers a handler to be notified of and potentially intercept an activity being deleted.
:param handler:
:param handler: the handler to register
:type handler: OnDeleteActivityHandler
:return:
"""
self._on_delete_activity.append(handler)
return self

async def _emit(self, plugins, arg, logic):
handlers = copy(plugins)
async def _emit(
self,
handlers: list[Callable[[TurnContext, _ArgT, _AsyncFunc[T]], Awaitable[T]]],
arg: _ArgT,
logic: _AsyncFunc[T],
) -> T:
"""Emits an event to the registered handlers, allowing them to intercept and modify the behavior of the logic function.

async def emit_next(i: int):
context = self
try:
if i < len(handlers):
:param handlers: The list of registered handlers to invoke.
:param arg: The argument to pass to the handlers.
:param logic: The logic function to invoke after all handlers have been called.
:return: The result of the logic function, potentially modified by the handlers.
"""

async def next_handler():
await emit_next(i + 1)
handlers = list(handlers)

await handlers[i](context, arg, next_handler)
async def emit_next(i: int) -> T:
call_next: _AsyncFunc[T]
if i + 1 < len(handlers):
call_next = lambda: emit_next(i + 1)
else:
call_next = logic

except Exception as error:
raise error
return await handlers[i](self, arg, call_next)

await emit_next(0)
# logic does not use parentheses because it's a coroutine
return await logic
if len(handlers) > 0:
return await emit_next(0)

return await logic()

async def send_trace_activity(
self,
Expand All @@ -346,13 +381,14 @@ async def send_trace_activity(
value_type: str | None = None,
label: str | None = None,
) -> ResourceResponse:
trace_activity = Activity(
trace_activity = pick_model(
Activity,
type=ActivityTypes.trace,
timestamp=datetime.now(timezone.utc),
name=name,
value=value,
value_type=value_type,
label=label,
value_type=SkipNone(value_type),
label=SkipNone(label),
)

return await self.send_activity(trace_activity)
Expand All @@ -371,7 +407,8 @@ def apply_conversation_reference(
:return:
"""
activity.channel_id = reference.channel_id
activity.locale = reference.locale
if reference.locale:
activity.locale = reference.locale
activity.service_url = reference.service_url
activity.conversation = reference.conversation
Comment thread
rodrigobr-msft marked this conversation as resolved.
if is_incoming:
Expand Down Expand Up @@ -405,19 +442,14 @@ def remove_recipient_mention(activity: Activity) -> str:
@staticmethod
def remove_mention_text(activity: Activity, identifier: str) -> str:
"""
TODO: manual test for this function as it was replaced from manual code to re.escape

Previously: This was a copy of the re.escape function in Python 3.8. This was done
because the 3.6.x version didn't escape in the same way and handling
agent names with regex characters in it would fail in TurnContext.remove_mention_text
without escaping the text.
Remove a mention matching the given account identifier from activity.text.
"""
mentions = TurnContext.get_mentions(activity)
for mention in mentions:
if mention.additional_properties["mentioned"]["id"] == identifier:
if mention.mentioned and mention.mentioned.id == identifier:
mention_name_match = re.match(
Comment thread
rodrigobr-msft marked this conversation as resolved.
r"<at(.*)>(.*?)<\/at>",
re.escape(mention.additional_properties.get("text", "")),
re.escape(mention.text or ""),
re.IGNORECASE,
)
Comment thread
rodrigobr-msft marked this conversation as resolved.
if mention_name_match:
Expand All @@ -429,10 +461,9 @@ def remove_mention_text(activity: Activity, identifier: str) -> str:

@staticmethod
def get_mentions(activity: Activity) -> list[Mention]:
result: list[Mention] = []
if activity.entities is not None:
for entity in activity.entities:
if entity.type.lower() == EntityTypes.MENTION:
result.append(entity)
"""Get all mentions from the activity.

return result
:param activity: The activity to get mentions from.
:return: A list of Mention objects.
"""
return activity.get_mentions()
Loading
Loading