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