|
| 1 | +# Copyright (c) Microsoft. All rights reserved. |
| 2 | + |
1 | 3 | from __future__ import annotations |
2 | 4 |
|
3 | 5 | from collections.abc import Awaitable, Callable, Iterable |
|
6 | 8 | from microsoft_agents.activity import ChannelId |
7 | 9 | from microsoft_agents.hosting.core import TurnContext |
8 | 10 | from microsoft_agents.hosting.core.app.state import TurnState |
| 11 | + |
| 12 | +from .models.agent_lifecycle_event import AgentLifecycleEvent |
9 | 13 | from .models.agent_notification_activity import AgentNotificationActivity, NotificationTypes |
10 | 14 | from .models.agent_subchannel import AgentSubChannel |
11 | | -from .models.agent_lifecycle_event import AgentLifecycleEvent |
12 | 15 |
|
13 | 16 | TContext = TypeVar("TContext", bound=TurnContext) |
14 | 17 | TState = TypeVar("TState", bound=TurnState) |
15 | 18 |
|
| 19 | +#: Type alias for agent notification handler functions. |
| 20 | +#: |
| 21 | +#: Agent handlers are async functions that process notifications from Microsoft 365 |
| 22 | +#: applications. They receive the turn context, application state, and a typed |
| 23 | +#: notification activity wrapper. |
| 24 | +#: |
| 25 | +#: Args: |
| 26 | +#: context: The turn context for the current conversation turn. |
| 27 | +#: state: The application state for the current turn. |
| 28 | +#: notification: The typed notification activity with parsed entities. |
| 29 | +#: |
| 30 | +#: Example: |
| 31 | +#: ```python |
| 32 | +#: async def handle_email( |
| 33 | +#: context: TurnContext, |
| 34 | +#: state: TurnState, |
| 35 | +#: notification: AgentNotificationActivity |
| 36 | +#: ) -> None: |
| 37 | +#: email = notification.email |
| 38 | +#: if email: |
| 39 | +#: print(f"Processing email: {email.id}") |
| 40 | +#: ``` |
16 | 41 | AgentHandler = Callable[[TContext, TState, AgentNotificationActivity], Awaitable[None]] |
17 | 42 |
|
18 | 43 |
|
19 | 44 | class AgentNotification: |
| 45 | + """Handler for agent notifications from Microsoft 365 applications. |
| 46 | +
|
| 47 | + This class provides decorators for registering handlers that respond to notifications |
| 48 | + from various Microsoft 365 channels and subchannels. It supports routing based on |
| 49 | + channel ID, subchannel, and lifecycle events. |
| 50 | +
|
| 51 | + Args: |
| 52 | + app: The application instance that will handle the routed notifications. |
| 53 | + known_subchannels: Optional iterable of recognized subchannels. If None, |
| 54 | + defaults to all values in the AgentSubChannel enum. |
| 55 | + known_lifecycle_events: Optional iterable of recognized lifecycle events. If None, |
| 56 | + defaults to all values in the AgentLifecycleEvent enum. |
| 57 | +
|
| 58 | + Example: |
| 59 | + ```python |
| 60 | + from microsoft_agents.hosting import Application |
| 61 | + from microsoft_agents_a365.notifications import AgentNotification |
| 62 | +
|
| 63 | + app = Application() |
| 64 | + notifications = AgentNotification(app) |
| 65 | +
|
| 66 | + @notifications.on_email() |
| 67 | + async def handle_email(context, state, notification): |
| 68 | + email = notification.email |
| 69 | + if email: |
| 70 | + await context.send_activity(f"Received email: {email.id}") |
| 71 | + ``` |
| 72 | + """ |
| 73 | + |
20 | 74 | def __init__( |
21 | 75 | self, |
22 | 76 | app: Any, |
@@ -56,6 +110,31 @@ def on_agent_notification( |
56 | 110 | channel_id: ChannelId, |
57 | 111 | **kwargs: Any, |
58 | 112 | ): |
| 113 | + """Register a handler for notifications from a specific channel and subchannel. |
| 114 | +
|
| 115 | + This decorator registers a handler function to be called when a notification is |
| 116 | + received from the specified channel and optional subchannel. The handler will |
| 117 | + receive a typed AgentNotificationActivity wrapper. |
| 118 | +
|
| 119 | + Args: |
| 120 | + channel_id: The channel ID specifying the channel and optional subchannel |
| 121 | + to listen for. Use "*" as the subchannel to match all subchannels. |
| 122 | + **kwargs: Additional keyword arguments passed to the app's add_route method. |
| 123 | +
|
| 124 | + Returns: |
| 125 | + A decorator function that registers the handler with the application. |
| 126 | +
|
| 127 | + Example: |
| 128 | + ```python |
| 129 | + from microsoft_agents.activity import ChannelId |
| 130 | +
|
| 131 | + @notifications.on_agent_notification( |
| 132 | + ChannelId(channel="agents", sub_channel="email") |
| 133 | + ) |
| 134 | + async def handle_custom_channel(context, state, notification): |
| 135 | + print(f"Received notification on {notification.channel}/{notification.sub_channel}") |
| 136 | + ``` |
| 137 | + """ |
59 | 138 | registered_channel = channel_id.channel.lower() |
60 | 139 | registered_subchannel = (channel_id.sub_channel or "*").lower() |
61 | 140 |
|
@@ -90,6 +169,26 @@ def on_agent_lifecycle_notification( |
90 | 169 | lifecycle_event: str, |
91 | 170 | **kwargs: Any, |
92 | 171 | ): |
| 172 | + """Register a handler for agent lifecycle event notifications. |
| 173 | +
|
| 174 | + This decorator registers a handler function to be called when lifecycle events |
| 175 | + occur, such as user creation, deletion, or workload onboarding updates. |
| 176 | +
|
| 177 | + Args: |
| 178 | + lifecycle_event: The lifecycle event to listen for. Use "*" to match all |
| 179 | + lifecycle events, or specify a specific event from AgentLifecycleEvent. |
| 180 | + **kwargs: Additional keyword arguments passed to the app's add_route method. |
| 181 | +
|
| 182 | + Returns: |
| 183 | + A decorator function that registers the handler with the application. |
| 184 | +
|
| 185 | + Example: |
| 186 | + ```python |
| 187 | + @notifications.on_agent_lifecycle_notification("agenticuseridentitycreated") |
| 188 | + async def handle_user_created(context, state, notification): |
| 189 | + print("New user created") |
| 190 | + ``` |
| 191 | + """ |
93 | 192 | def route_selector(context: TurnContext) -> bool: |
94 | 193 | ch = context.activity.channel_id |
95 | 194 | received_channel = ch.channel if ch else "" |
@@ -121,62 +220,235 @@ def decorator(handler: AgentHandler): |
121 | 220 | def on_email( |
122 | 221 | self, **kwargs: Any |
123 | 222 | ) -> Callable[[AgentHandler], Callable[[TurnContext, TurnState], Awaitable[None]]]: |
| 223 | + """Register a handler for Outlook email notifications. |
| 224 | +
|
| 225 | + This is a convenience decorator that registers a handler for notifications |
| 226 | + from the email subchannel. |
| 227 | +
|
| 228 | + Args: |
| 229 | + **kwargs: Additional keyword arguments passed to the app's add_route method. |
| 230 | +
|
| 231 | + Returns: |
| 232 | + A decorator function that registers the handler with the application. |
| 233 | +
|
| 234 | + Example: |
| 235 | + ```python |
| 236 | + @notifications.on_email() |
| 237 | + async def handle_email(context, state, notification): |
| 238 | + email = notification.email |
| 239 | + if email: |
| 240 | + print(f"Received email: {email.id}") |
| 241 | + # Send a response |
| 242 | + response = EmailResponse.create_email_response_activity( |
| 243 | + "<p>Thank you for your email.</p>" |
| 244 | + ) |
| 245 | + await context.send_activity(response) |
| 246 | + ``` |
| 247 | + """ |
124 | 248 | return self.on_agent_notification( |
125 | 249 | ChannelId(channel="agents", sub_channel=AgentSubChannel.EMAIL), **kwargs |
126 | 250 | ) |
127 | 251 |
|
128 | 252 | def on_word( |
129 | 253 | self, **kwargs: Any |
130 | 254 | ) -> Callable[[AgentHandler], Callable[[TurnContext, TurnState], Awaitable[None]]]: |
| 255 | + """Register a handler for Microsoft Word comment notifications. |
| 256 | +
|
| 257 | + This is a convenience decorator that registers a handler for notifications |
| 258 | + from the Word subchannel. |
| 259 | +
|
| 260 | + Args: |
| 261 | + **kwargs: Additional keyword arguments passed to the app's add_route method. |
| 262 | +
|
| 263 | + Returns: |
| 264 | + A decorator function that registers the handler with the application. |
| 265 | +
|
| 266 | + Example: |
| 267 | + ```python |
| 268 | + @notifications.on_word() |
| 269 | + async def handle_word_comment(context, state, notification): |
| 270 | + comment = notification.wpx_comment |
| 271 | + if comment: |
| 272 | + print(f"Received Word comment: {comment.comment_id}") |
| 273 | + ``` |
| 274 | + """ |
131 | 275 | return self.on_agent_notification( |
132 | 276 | ChannelId(channel="agents", sub_channel=AgentSubChannel.WORD), **kwargs |
133 | 277 | ) |
134 | 278 |
|
135 | 279 | def on_excel( |
136 | 280 | self, **kwargs: Any |
137 | 281 | ) -> Callable[[AgentHandler], Callable[[TurnContext, TurnState], Awaitable[None]]]: |
| 282 | + """Register a handler for Microsoft Excel comment notifications. |
| 283 | +
|
| 284 | + This is a convenience decorator that registers a handler for notifications |
| 285 | + from the Excel subchannel. |
| 286 | +
|
| 287 | + Args: |
| 288 | + **kwargs: Additional keyword arguments passed to the app's add_route method. |
| 289 | +
|
| 290 | + Returns: |
| 291 | + A decorator function that registers the handler with the application. |
| 292 | +
|
| 293 | + Example: |
| 294 | + ```python |
| 295 | + @notifications.on_excel() |
| 296 | + async def handle_excel_comment(context, state, notification): |
| 297 | + comment = notification.wpx_comment |
| 298 | + if comment: |
| 299 | + print(f"Received Excel comment: {comment.comment_id}") |
| 300 | + ``` |
| 301 | + """ |
138 | 302 | return self.on_agent_notification( |
139 | 303 | ChannelId(channel="agents", sub_channel=AgentSubChannel.EXCEL), **kwargs |
140 | 304 | ) |
141 | 305 |
|
142 | 306 | def on_powerpoint( |
143 | 307 | self, **kwargs: Any |
144 | 308 | ) -> Callable[[AgentHandler], Callable[[TurnContext, TurnState], Awaitable[None]]]: |
| 309 | + """Register a handler for Microsoft PowerPoint comment notifications. |
| 310 | +
|
| 311 | + This is a convenience decorator that registers a handler for notifications |
| 312 | + from the PowerPoint subchannel. |
| 313 | +
|
| 314 | + Args: |
| 315 | + **kwargs: Additional keyword arguments passed to the app's add_route method. |
| 316 | +
|
| 317 | + Returns: |
| 318 | + A decorator function that registers the handler with the application. |
| 319 | +
|
| 320 | + Example: |
| 321 | + ```python |
| 322 | + @notifications.on_powerpoint() |
| 323 | + async def handle_powerpoint_comment(context, state, notification): |
| 324 | + comment = notification.wpx_comment |
| 325 | + if comment: |
| 326 | + print(f"Received PowerPoint comment: {comment.comment_id}") |
| 327 | + ``` |
| 328 | + """ |
145 | 329 | return self.on_agent_notification( |
146 | 330 | ChannelId(channel="agents", sub_channel=AgentSubChannel.POWERPOINT), **kwargs |
147 | 331 | ) |
148 | 332 |
|
149 | 333 | def on_lifecycle( |
150 | 334 | self, **kwargs: Any |
151 | 335 | ) -> Callable[[AgentHandler], Callable[[TurnContext, TurnState], Awaitable[None]]]: |
| 336 | + """Register a handler for all agent lifecycle event notifications. |
| 337 | +
|
| 338 | + This is a convenience decorator that registers a handler for all lifecycle |
| 339 | + events using the wildcard "*" matcher. |
| 340 | +
|
| 341 | + Args: |
| 342 | + **kwargs: Additional keyword arguments passed to the app's add_route method. |
| 343 | +
|
| 344 | + Returns: |
| 345 | + A decorator function that registers the handler with the application. |
| 346 | +
|
| 347 | + Example: |
| 348 | + ```python |
| 349 | + @notifications.on_lifecycle() |
| 350 | + async def handle_any_lifecycle_event(context, state, notification): |
| 351 | + print(f"Lifecycle event type: {notification.notification_type}") |
| 352 | + ``` |
| 353 | + """ |
152 | 354 | return self.on_lifecycle_notification("*", **kwargs) |
153 | 355 |
|
154 | 356 | def on_user_created( |
155 | 357 | self, **kwargs: Any |
156 | 358 | ) -> Callable[[AgentHandler], Callable[[TurnContext, TurnState], Awaitable[None]]]: |
| 359 | + """Register a handler for user creation lifecycle events. |
| 360 | +
|
| 361 | + This is a convenience decorator that registers a handler specifically for |
| 362 | + agentic user identity creation events. |
| 363 | +
|
| 364 | + Args: |
| 365 | + **kwargs: Additional keyword arguments passed to the app's add_route method. |
| 366 | +
|
| 367 | + Returns: |
| 368 | + A decorator function that registers the handler with the application. |
| 369 | +
|
| 370 | + Example: |
| 371 | + ```python |
| 372 | + @notifications.on_user_created() |
| 373 | + async def handle_user_created(context, state, notification): |
| 374 | + print("New agentic user identity created") |
| 375 | + ``` |
| 376 | + """ |
157 | 377 | return self.on_lifecycle_notification(AgentLifecycleEvent.USERCREATED, **kwargs) |
158 | 378 |
|
159 | 379 | def on_user_workload_onboarding( |
160 | 380 | self, **kwargs: Any |
161 | 381 | ) -> Callable[[AgentHandler], Callable[[TurnContext, TurnState], Awaitable[None]]]: |
| 382 | + """Register a handler for user workload onboarding update events. |
| 383 | +
|
| 384 | + This is a convenience decorator that registers a handler for events that occur |
| 385 | + when a user's workload onboarding status is updated. |
| 386 | +
|
| 387 | + Args: |
| 388 | + **kwargs: Additional keyword arguments passed to the app's add_route method. |
| 389 | +
|
| 390 | + Returns: |
| 391 | + A decorator function that registers the handler with the application. |
| 392 | +
|
| 393 | + Example: |
| 394 | + ```python |
| 395 | + @notifications.on_user_workload_onboarding() |
| 396 | + async def handle_onboarding_update(context, state, notification): |
| 397 | + print("User workload onboarding status updated") |
| 398 | + ``` |
| 399 | + """ |
162 | 400 | return self.on_lifecycle_notification( |
163 | 401 | AgentLifecycleEvent.USERWORKLOADONBOARDINGUPDATED, **kwargs |
164 | 402 | ) |
165 | 403 |
|
166 | 404 | def on_user_deleted( |
167 | 405 | self, **kwargs: Any |
168 | 406 | ) -> Callable[[AgentHandler], Callable[[TurnContext, TurnState], Awaitable[None]]]: |
| 407 | + """Register a handler for user deletion lifecycle events. |
| 408 | +
|
| 409 | + This is a convenience decorator that registers a handler specifically for |
| 410 | + agentic user identity deletion events. |
| 411 | +
|
| 412 | + Args: |
| 413 | + **kwargs: Additional keyword arguments passed to the app's add_route method. |
| 414 | +
|
| 415 | + Returns: |
| 416 | + A decorator function that registers the handler with the application. |
| 417 | +
|
| 418 | + Example: |
| 419 | + ```python |
| 420 | + @notifications.on_user_deleted() |
| 421 | + async def handle_user_deleted(context, state, notification): |
| 422 | + print("Agentic user identity deleted") |
| 423 | + ``` |
| 424 | + """ |
169 | 425 | return self.on_lifecycle_notification(AgentLifecycleEvent.USERDELETED, **kwargs) |
170 | 426 |
|
171 | 427 | @staticmethod |
172 | 428 | def _normalize_subchannel(value: str | AgentSubChannel | None) -> str: |
| 429 | + """Normalize a subchannel value to a lowercase string. |
| 430 | +
|
| 431 | + Args: |
| 432 | + value: The subchannel value to normalize, either as an enum or string. |
| 433 | +
|
| 434 | + Returns: |
| 435 | + The normalized lowercase subchannel string, or empty string if None. |
| 436 | + """ |
173 | 437 | if value is None: |
174 | 438 | return "" |
175 | 439 | resolved = value.value if isinstance(value, AgentSubChannel) else str(value) |
176 | 440 | return resolved.lower().strip() |
177 | 441 |
|
178 | 442 | @staticmethod |
179 | 443 | def _normalize_lifecycleevent(value: str | AgentLifecycleEvent | None) -> str: |
| 444 | + """Normalize a lifecycle event value to a lowercase string. |
| 445 | +
|
| 446 | + Args: |
| 447 | + value: The lifecycle event value to normalize, either as an enum or string. |
| 448 | +
|
| 449 | + Returns: |
| 450 | + The normalized lowercase lifecycle event string, or empty string if None. |
| 451 | + """ |
180 | 452 | if value is None: |
181 | 453 | return "" |
182 | 454 | resolved = value.value if isinstance(value, AgentLifecycleEvent) else str(value) |
|
0 commit comments