-
Notifications
You must be signed in to change notification settings - Fork 88
Expand file tree
/
Copy pathactivity.py
More file actions
805 lines (684 loc) · 32.5 KB
/
Copy pathactivity.py
File metadata and controls
805 lines (684 loc) · 32.5 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
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from __future__ import annotations
import logging
from copy import copy
from datetime import datetime, timezone
from typing import Any, cast
from pydantic import (
Field,
SerializeAsAny,
model_serializer,
model_validator,
SerializerFunctionWrapHandler,
ModelWrapValidatorHandler,
ValidationError,
)
from .activity_types import ActivityTypes
from .channel_account import ChannelAccount
from .conversation_account import ConversationAccount
from .message_reaction import MessageReaction
from .resource_response import ResourceResponse
from .suggested_actions import SuggestedActions
from .attachment import Attachment
from .entity import (
Entity,
EntityTypes,
Mention,
AIEntity,
ClientCitation,
ProductInfo,
SensitivityUsageInfo,
)
from .conversation_reference import ConversationReference
from .text_highlight import TextHighlight
from .semantic_action import SemanticAction
from .agents_model import AgentsModel
from .role_types import RoleTypes
from ._channel_id_field_mixin import _ChannelIdFieldMixin
from .channel_id import ChannelId
from ._model_utils import pick_model, SkipNone
from ._type_aliases import NonEmptyString
from microsoft_agents.activity.errors import activity_errors
logger = logging.getLogger(__name__)
# TODO: A2A Agent 2 is responding with None as id, had to mark it as optional (investigate)
class Activity(AgentsModel, _ChannelIdFieldMixin):
"""An Activity is the basic communication type for the protocol.
:param type: Contains the activity type. Possible values include:
'message', 'contactRelationUpdate', 'conversationUpdate', 'typing',
'endOfConversation', 'event', 'invoke', 'deleteUserData', 'messageUpdate',
'messageDelete', 'installationUpdate', 'messageReaction', 'suggestion',
'trace', 'handoff'
:type type: str or ~microsoft_agents.activity.ActivityTypes
:param id: Contains an ID that uniquely identifies the activity on the channel.
:type id: str
:param timestamp: Contains the date and time that the message was sent, in UTC, expressed in ISO-8601 format.
:type timestamp: datetime
:param local_timestamp: Contains the local date and time of the message expressed in ISO-8601 format.
For example, 2016-09-23T13:07:49.4714686-07:00.
:type local_timestamp: datetime
:param local_timezone: Contains the name of the local timezone of the message, expressed in IANA Time Zone database format.
For example, America/Los_Angeles.
:type local_timezone: str
:param service_url: Contains the URL that specifies the channel's service endpoint. Set by the channel.
:type service_url: str
:param channel_id: Contains an ID that uniquely identifies the channel (and possibly the sub-channel). Set by the channel.
:type channel_id: ~microsoft_agents.activity.ChannelId
:param from_property: Identifies the sender of the message.
:type from_property: ~microsoft_agents.activity.ChannelAccount
:param conversation: Identifies the conversation to which the activity belongs.
:type conversation: ~microsoft_agents.activity.ConversationAccount
:param recipient: Identifies the recipient of the message.
:type recipient: ~microsoft_agents.activity.ChannelAccount
:param text_format: Format of text fields Default:markdown. Possible values include: 'markdown', 'plain', 'xml'
:type text_format: str or ~microsoft_agents.activity.TextFormatTypes
:param attachment_layout: The layout hint for multiple attachments. Default: list. Possible values include: 'list', 'carousel'
:type attachment_layout: str or ~microsoft_agents.activity.AttachmentLayoutTypes
:param members_added: The collection of members added to the conversation.
:type members_added: list[~microsoft_agents.activity.ChannelAccount]
:param members_removed: The collection of members removed from the conversation.
:type members_removed: list[~microsoft_agents.activity.ChannelAccount]
:param reactions_added: The collection of reactions added to the conversation.
:type reactions_added: list[~microsoft_agents.activity.MessageReaction]
:param reactions_removed: The collection of reactions removed from the conversation.
:type reactions_removed: list[~microsoft_agents.activity.MessageReaction]
:param topic_name: The updated topic name of the conversation.
:type topic_name: str
:param history_disclosed: Indicates whether the prior history of the channel is disclosed.
:type history_disclosed: bool
:param locale: A locale name for the contents of the text field. The locale name is a combination of an ISO 639 two- or three-letter
culture code associated with a language and an ISO 3166 two-letter subculture code associated with a country or region.
The locale name can also correspond to a valid BCP-47 language tag.
:type locale: str
:param text: The text content of the message.
:type text: str
:param speak: The text to speak.
:type speak: str
:param input_hint: Indicates whether your agent is accepting, expecting, or ignoring user input after the message is delivered to the client.
Possible values include: 'acceptingInput', 'ignoringInput', 'expectingInput'
:type input_hint: str or ~microsoft_agents.activity.InputHints
:param summary: The text to display if the channel cannot render cards.
:type summary: str
:param suggested_actions: The suggested actions for the activity.
:type suggested_actions: ~microsoft_agents.activity.SuggestedActions
:param attachments: Attachments
:type attachments: list[~microsoft_agents.activity.Attachment]
:param entities: Represents the entities that were mentioned in the message.
:type entities: list[~microsoft_agents.activity.Entity]
:param channel_data: Contains channel-specific content.
:type channel_data: object
:param action: Indicates whether the recipient of a contactRelationUpdate was added or removed from the sender's contact list.
:type action: str
:param reply_to_id: Contains the ID of the message to which this message is a reply.
:type reply_to_id: str
:param label: A descriptive label for the activity.
:type label: str
:param value_type: The type of the activity's value object.
:type value_type: str
:param value: A value that is associated with the activity.
:type value: object
:param name: The name of the operation associated with an invoke or event activity.
:type name: str
:param relates_to: A reference to another conversation or activity.
:type relates_to: ~microsoft_agents.activity.ConversationReference
:param code: The a code for endOfConversation activities that indicates why the conversation ended. Possible values include: 'unknown',
'completedSuccessfully', 'userCancelled', 'botTimedOut', 'botIssuedInvalidMessage', 'channelFailed'
:type code: str or ~microsoft_agents.activity.EndOfConversationCodes
:param expiration: The time at which the activity should be considered to be "expired" and should not be presented to the recipient.
:type expiration: datetime
:param importance: The importance of the activity. Possible values include: 'low', 'normal', 'high'
:type importance: str or ~microsoft_agents.activity.ActivityImportance
:param delivery_mode: A delivery hint to signal to the recipient alternate delivery paths for the activity.
The default delivery mode is "default". Possible values include: 'normal', 'notification', 'expectReplies', 'ephemeral'
:type delivery_mode: str or ~microsoft_agents.activity.DeliveryModes
:param listen_for: List of phrases and references that speech and language priming systems should listen for
:type listen_for: list[str]
:param text_highlights: The collection of text fragments to highlight when the activity contains a ReplyToId value.
:type text_highlights: list[~microsoft_agents.activity.TextHighlight]
:param semantic_action: An optional programmatic action accompanying this request
:type semantic_action: ~microsoft_agents.activity.SemanticAction
:param caller_id: A string containing an IRI identifying the caller of an agent. This field is not intended to be transmitted over the wire,
but is instead populated by agents and clients based on cryptographically verifiable data that asserts the identity of the callers (e.g. tokens).
:type caller_id: str
"""
type: NonEmptyString
id: NonEmptyString | None = None
timestamp: datetime = None
local_timestamp: datetime = None
local_timezone: NonEmptyString = None
service_url: NonEmptyString = None
from_property: ChannelAccount = Field(None, alias="from")
conversation: ConversationAccount = None
recipient: ChannelAccount = None
text_format: NonEmptyString = None
attachment_layout: NonEmptyString = None
members_added: list[ChannelAccount] = None
members_removed: list[ChannelAccount] = None
reactions_added: list[MessageReaction] = None
reactions_removed: list[MessageReaction] = None
topic_name: NonEmptyString = None
history_disclosed: bool = None
locale: NonEmptyString = None
text: str = None
speak: str = None
input_hint: NonEmptyString = None
summary: NonEmptyString = None
suggested_actions: SuggestedActions = None
attachments: list[Attachment] = None
entities: list[SerializeAsAny[Entity]] = None
channel_data: object = None
action: NonEmptyString = None
reply_to_id: NonEmptyString = None
label: NonEmptyString = None
value_type: NonEmptyString = None
value: object = None
name: NonEmptyString = None
relates_to: ConversationReference = None
code: NonEmptyString = None
expiration: datetime = None
importance: NonEmptyString = None
delivery_mode: NonEmptyString = None
listen_for: list[NonEmptyString] = None
text_highlights: list[TextHighlight] = None
semantic_action: SemanticAction = None
caller_id: NonEmptyString = None
@model_validator(mode="wrap")
@classmethod
def _validate_channel_id(
cls, data: Any, handler: ModelWrapValidatorHandler[Activity]
) -> Activity:
"""Validate the Activity, ensuring consistency between channel_id.sub_channel and productInfo entity.
:param data: The input data to validate.
:param handler: The validation handler provided by Pydantic.
:return: The validated Activity instance.
"""
try:
# run Pydantic's standard validation first
activity = handler(data)
# needed to assign to a computed field
# needed because we override the mixin validator
activity._set_validated_channel_id(data)
# sync sub_channel with productInfo entity
product_info = activity.get_product_info_entity()
if product_info and activity.channel_id:
if (
activity.channel_id.sub_channel
and activity.channel_id.sub_channel != product_info.id
):
raise Exception(str(activity_errors.ChannelIdProductInfoConflict))
activity.channel_id = ChannelId(
channel=activity.channel_id.channel,
sub_channel=product_info.id,
)
return activity
except ValidationError as exc:
logger.error("Validation error for Activity: %s", exc, exc_info=True)
raise
@model_serializer(mode="wrap")
def _serialize_sub_channel_data(
self, handler: SerializerFunctionWrapHandler
) -> dict[str, object]:
"""Serialize the Activity, ensuring consistency between channel_id.sub_channel and productInfo entity.
:param handler: The serialization handler provided by Pydantic.
:return: A dictionary representing the serialized Activity.
"""
# run Pydantic's standard serialization first
serialized = handler(self)
if not self: # serialization can be called with None
return serialized
# find the ProductInfo entity
product_info = None
for i, entity in enumerate(serialized.get("entities") or []):
if entity.get("type", "") == EntityTypes.PRODUCT_INFO:
product_info = entity
break
# self.channel_id is the source of truth for serialization
if self.channel_id and self.channel_id.sub_channel:
if product_info and product_info.get("id") != self.channel_id.sub_channel:
raise Exception(str(activity_errors.ChannelIdProductInfoConflict))
elif not product_info:
if not serialized.get("entities"):
serialized["entities"] = []
serialized["entities"].append(
{
"type": EntityTypes.PRODUCT_INFO,
"id": self.channel_id.sub_channel,
}
)
# simply serialized channelId value in Activity and relatesTo
if "channelId" in serialized:
serialized["channelId"] = self.channel_id.channel
elif "channel_id" in serialized:
serialized["channel_id"] = self.channel_id.channel
elif product_info: # remove productInfo entity if sub_channel is not set
del serialized["entities"][i]
if not serialized["entities"]: # after removal above, list may be empty
del serialized["entities"]
# necessary due to computed_field serialization
self._remove_serialized_unset_channel_id(serialized)
return serialized
def apply_conversation_reference(
self, reference: ConversationReference, is_incoming: bool = False
):
"""
Updates this activity with the delivery information from an existing ConversationReference.
:param reference: The existing conversation reference.
:param is_incoming: Optional, True to treat the activity as an incoming activity, where the agent is the recipient; otherwise, False.
Default is False, and the activity will show the agent as the sender.
:returns: This activity, updated with the delivery information.
.. remarks::
Call GetConversationReference on an incoming activity to get a conversation reference that you can then use to update an
outgoing activity with the correct delivery information.
"""
self.channel_id = reference.channel_id
self.service_url = reference.service_url
self.conversation = reference.conversation
if reference.locale is not None:
self.locale = reference.locale
if is_incoming:
self.from_property = reference.user
self.recipient = reference.agent
if reference.activity_id is not None:
self.id = reference.activity_id
else:
self.from_property = reference.agent
self.recipient = reference.user
if reference.activity_id is not None:
self.reply_to_id = reference.activity_id
return self
def as_contact_relation_update_activity(self):
"""
Returns this activity as a ContactRelationUpdateActivity object; or None, if this is not that type of activity.
:returns: This activity as a message activity; or None.
"""
return (
self if self.__is_activity(ActivityTypes.contact_relation_update) else None
)
def as_conversation_update_activity(self):
"""
Returns this activity as a ConversationUpdateActivity object; or None, if this is not that type of activity.
:returns: This activity as a conversation update activity; or None.
"""
return self if self.__is_activity(ActivityTypes.conversation_update) else None
def as_end_of_conversation_activity(self):
"""
Returns this activity as an EndOfConversationActivity object; or None, if this is not that type of activity.
:returns: This activity as an end of conversation activity; or None.
"""
return self if self.__is_activity(ActivityTypes.end_of_conversation) else None
def as_event_activity(self):
"""
Returns this activity as an EventActivity object; or None, if this is not that type of activity.
:returns: This activity as an event activity; or None.
"""
return self if self.__is_activity(ActivityTypes.event) else None
def as_handoff_activity(self):
"""
Returns this activity as a HandoffActivity object; or None, if this is not that type of activity.
:returns: This activity as a handoff activity; or None.
"""
return self if self.__is_activity(ActivityTypes.handoff) else None
def as_installation_update_activity(self):
"""
Returns this activity as an InstallationUpdateActivity object; or None, if this is not that type of activity.
:returns: This activity as an installation update activity; or None.
"""
return self if self.__is_activity(ActivityTypes.installation_update) else None
def as_invoke_activity(self):
"""
Returns this activity as an InvokeActivity object; or None, if this is not that type of activity.
:returns: This activity as an invoke activity; or None.
"""
return self if self.__is_activity(ActivityTypes.invoke) else None
def as_message_activity(self):
"""
Returns this activity as a MessageActivity object; or None, if this is not that type of activity.
:returns: This activity as a message activity; or None.
"""
return self if self.__is_activity(ActivityTypes.message) else None
def as_message_delete_activity(self):
"""
Returns this activity as a MessageDeleteActivity object; or None, if this is not that type of activity.
:returns: This activity as a message delete request; or None.
"""
return self if self.__is_activity(ActivityTypes.message_delete) else None
def as_message_reaction_activity(self):
"""
Returns this activity as a MessageReactionActivity object; or None, if this is not that type of activity.
:return: This activity as a message reaction activity; or None.
"""
return self if self.__is_activity(ActivityTypes.message_reaction) else None
def as_message_update_activity(self):
"""
Returns this activity as an MessageUpdateActivity object; or None, if this is not that type of activity.
:returns: This activity as a message update request; or None.
"""
return self if self.__is_activity(ActivityTypes.message_update) else None
def as_suggestion_activity(self):
"""
Returns this activity as a SuggestionActivity object; or None, if this is not that type of activity.
:returns: This activity as a suggestion activity; or None.
"""
return self if self.__is_activity(ActivityTypes.suggestion) else None
def as_trace_activity(self):
"""
Returns this activity as a TraceActivity object; or None, if this is not that type of activity.
:returns: This activity as a trace activity; or None.
"""
return self if self.__is_activity(ActivityTypes.trace) else None
def as_typing_activity(self):
"""
Returns this activity as a TypingActivity object; or null, if this is not that type of activity.
:returns: This activity as a typing activity; or null.
"""
return self if self.__is_activity(ActivityTypes.typing) else None
@staticmethod
def create_contact_relation_update_activity():
"""
Creates an instance of the :class:`Activity` class as a ContactRelationUpdateActivity object.
:returns: The new contact relation update activity.
"""
return Activity(type=ActivityTypes.contact_relation_update)
@staticmethod
def create_conversation_update_activity():
"""
Creates an instance of the :class:`Activity` class as a ConversationUpdateActivity object.
:returns: The new conversation update activity.
"""
return Activity(type=ActivityTypes.conversation_update)
@staticmethod
def create_end_of_conversation_activity():
"""
Creates an instance of the :class:`Activity` class as an EndOfConversationActivity object.
:returns: The new end of conversation activity.
"""
return Activity(type=ActivityTypes.end_of_conversation)
@staticmethod
def create_event_activity():
"""
Creates an instance of the :class:`Activity` class as an EventActivity object.
:returns: The new event activity.
"""
return Activity(type=ActivityTypes.event)
@staticmethod
def create_handoff_activity():
"""
Creates an instance of the :class:`Activity` class as a HandoffActivity object.
:returns: The new handoff activity.
"""
return Activity(type=ActivityTypes.handoff)
@staticmethod
def create_invoke_activity():
"""
Creates an instance of the :class:`Activity` class as an InvokeActivity object.
:returns: The new invoke activity.
"""
return Activity(type=ActivityTypes.invoke)
@staticmethod
def create_message_activity():
"""
Creates an instance of the :class:`Activity` class as a MessageActivity object.
:returns: The new message activity.
"""
return Activity(type=ActivityTypes.message)
def create_reply(self, text: str | None = None, locale: str | None = None):
"""
Creates a new message activity as a response to this activity.
:param text: The text of the reply.
:param locale: The language code for the text.
:returns: The new message activity.
.. remarks::
The new activity sets up routing information based on this activity.
"""
return pick_model(
Activity,
type=ActivityTypes.message,
timestamp=datetime.now(timezone.utc),
from_property=SkipNone(
ChannelAccount.pick_properties(self.recipient, ["id", "name"])
),
recipient=SkipNone(
ChannelAccount.pick_properties(self.from_property, ["id", "name"])
),
reply_to_id=(
SkipNone(self.id)
if type != ActivityTypes.conversation_update
or self.channel_id not in ["directline", "webchat"]
else None
),
service_url=self.service_url,
channel_id=self.channel_id,
conversation=SkipNone(
ConversationAccount.pick_properties(
self.conversation, ["is_group", "id", "name"]
)
),
text=text if text else "",
locale=locale if locale else SkipNone(self.locale),
attachments=[],
entities=[],
)
def create_trace(
self,
name: str,
value: object | None = None,
value_type: str | None = None,
label: str | None = None,
):
"""
Creates a new trace activity based on this activity.
:param name: The name of the trace operation to create.
:param value: Optional, the content for this trace operation.
:param value_type: Optional, identifier for the format of the value. Default is the name of type of the value.
:param label: Optional, a descriptive label for this trace operation.
:returns: The new trace activity.
"""
if not value_type and value is not None:
value_type = type(value).__name__
return cast(
Activity,
pick_model(
Activity,
type=ActivityTypes.trace,
timestamp=datetime.now(timezone.utc),
from_property=SkipNone(
ChannelAccount.pick_properties(self.recipient, ["id", "name"])
),
recipient=SkipNone(
ChannelAccount.pick_properties(self.from_property, ["id", "name"])
),
reply_to_id=(
SkipNone(self.id) # preserve unset
if type != ActivityTypes.conversation_update
or self.channel_id not in ["directline", "webchat"]
else None
),
service_url=self.service_url,
channel_id=self.channel_id,
conversation=SkipNone(
ConversationAccount.pick_properties(
self.conversation, ["is_group", "id", "name"]
)
),
name=SkipNone(name),
label=SkipNone(label),
value_type=SkipNone(value_type),
value=SkipNone(value),
),
).as_trace_activity()
@staticmethod
def create_trace_activity(
name: str,
value: object | None = None,
value_type: str | None = None,
label: str | None = None,
):
"""
Creates an instance of the :class:`Activity` class as a TraceActivity object.
:param name: The name of the trace operation to create.
:param value: Optional, the content for this trace operation.
:param value_type: Optional, identifier for the format of the value. Default is the name of type of the value.
:param label: Optional, a descriptive label for this trace operation.
:returns: The new trace activity.
"""
if not value_type and value is not None:
value_type = type(value).__name__
return pick_model(
Activity,
type=ActivityTypes.trace,
name=name,
label=SkipNone(label),
value_type=SkipNone(value_type),
value=SkipNone(value),
)
@staticmethod
def create_typing_activity() -> "Activity":
"""
Creates an instance of the :class:`Activity` class as a TypingActivity object.
:returns: The new typing activity.
"""
return Activity(type=ActivityTypes.typing)
def get_conversation_reference(self) -> ConversationReference:
"""
Creates a ConversationReference based on this activity.
:returns: A conversation reference for the conversation that contains this activity.
"""
return cast(
ConversationReference,
pick_model(
ConversationReference,
activity_id=(
SkipNone(self.id)
if self.type != ActivityTypes.conversation_update
or self.channel_id not in ["directline", "webchat"]
else None
),
user=copy(self.from_property),
agent=copy(self.recipient),
conversation=copy(self.conversation),
channel_id=self.channel_id,
locale=self.locale,
service_url=self.service_url,
),
)
def get_product_info_entity(self) -> ProductInfo | None:
if not self.entities:
return None
target = EntityTypes.PRODUCT_INFO.lower()
# validated entities can be Entity, and that prevents us from
# making assumptions about the casing of the 'type' attribute
return next(filter(lambda e: e.type.lower() == target, self.entities), None)
def get_mentions(self) -> list[Mention]:
"""
Resolves the mentions from the entities of this activity.
:returns: The array of mentions; or an empty array, if none are found.
.. remarks::
This method is defined on the :class:`Activity` class, but is only intended for use with a message activity,
where the activity Activity.Type is set to ActivityTypes.Message.
"""
if not self.entities:
return []
return [x for x in self.entities if x.type.lower() == EntityTypes.MENTION]
def get_reply_conversation_reference(
self, reply: ResourceResponse
) -> ConversationReference:
"""
Create a ConversationReference based on this Activity's Conversation info and the ResourceResponse from sending an activity.
:param reply: ResourceResponse returned from send_activity.
:return: A ConversationReference that can be stored and used later to delete or update the activity.
"""
reference = self.get_conversation_reference()
reference.activity_id = reply.id
return reference
def has_content(self) -> bool:
"""
Indicates whether this activity has content.
:returns: True, if this activity has any content to send; otherwise, false.
.. remarks::
This method is defined on the :class:`Activity` class, but is only intended for use with a message activity,
where the activity Activity.Type is set to ActivityTypes.Message.
"""
if self.text and self.text.strip():
return True
if self.summary and self.summary.strip():
return True
if self.attachments and len(self.attachments) > 0:
return True
if self.channel_data:
return True
return False
def is_from_streaming_connection(self) -> bool:
"""
Determine if the Activity was sent via an Http/Https connection or Streaming.
This can be determined by looking at the service_url property:
(1) All channels that send messages via http/https are not streaming.
(2) Channels that send messages via streaming have a ServiceUrl that does not begin with http/https.
:returns: True if the Activity originated from a streaming connection.
"""
if self.service_url:
return not self.service_url.lower().startswith("http")
return False
def __is_activity(self, activity_type: str) -> bool:
"""
Indicates whether this activity is of a specified activity type.
:param activity_type: The activity type to check for.
:return: True if this activity is of the specified activity type; otherwise, False.
"""
if self.type is None:
return False
type_attribute = f"ActivityTypes.{str(self.type)}".lower()
activity_type = str(activity_type).lower()
result = type_attribute.startswith(activity_type)
if result:
result = len(type_attribute) == len(activity_type)
if not result:
result = (
len(type_attribute) > len(activity_type)
and type_attribute[len(activity_type)] == "/"
)
return result
def add_ai_metadata(
self,
citations: list[ClientCitation] | None = None,
usage_info: SensitivityUsageInfo | None = None,
) -> None:
"""
Adds AI entity to an activity to indicate AI-generated content.
Args:
activity: The activity to modify
citations: Optional list of citations
usage_info: Optional sensitivity usage information
"""
if citations:
ai_entity = AIEntity(
id="",
additional_type=["AIGeneratedContent"],
citation=citations,
usage_info=usage_info,
)
if self.entities is None:
self.entities = []
self.entities.append(ai_entity)
def is_agentic_request(self) -> bool:
return self.recipient and self.recipient.role in [
RoleTypes.agentic_identity,
RoleTypes.agentic_user,
]
def get_agentic_instance_id(self) -> str | None:
"""Gets the agent instance ID from the context if it's an agentic request."""
if not self.is_agentic_request() or not self.recipient:
return None
return self.recipient.agentic_app_id
def get_agentic_user(self) -> str | None:
"""Gets the agentic user (agenticUserId) from the context if it's an agentic request."""
if not self.is_agentic_request() or not self.recipient:
return None
return self.recipient.agentic_user_id
def get_agentic_tenant_id(self) -> str | None:
"""Gets the agentic tenant ID from the context if it's an agentic request."""
if self.is_agentic_request():
if self.recipient and self.recipient.tenant_id:
return self.recipient.tenant_id
if self.conversation and self.conversation.tenant_id:
return self.conversation.tenant_id
return None