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 @@ -124,6 +124,25 @@ def services(self):
"""
return self._services

@property
def streaming_response(self):
"""
Gets a StreamingResponse instance for this turn context.
This allows for streaming partial responses to the client.
"""
# Use lazy import to avoid circular dependency
if not hasattr(self, "_streaming_response"):
try:
from microsoft.agents.hosting.aiohttp.app.streaming import (
StreamingResponse,
)

self._streaming_response = StreamingResponse(self)
except ImportError:
# If the hosting library isn't available, return None
self._streaming_response = None
return self._streaming_response

def get(self, key: str) -> object:
if not key or not isinstance(key, str):
raise TypeError('"key" must be a valid string.')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@
ConversationsResult,
PagedMembersResult,
)
from microsoft.agents.authorization import (
AccessTokenProviderBase,
)
from microsoft.agents.connector import ConnectorClientBase
from ..attachments_base import AttachmentsBase
from ..conversations_base import ConversationsBase
Expand Down Expand Up @@ -175,17 +172,20 @@ async def reply_to_activity(

async with self.client.post(
url,
json=body.model_dump(by_alias=True, exclude_unset=True, mode="json"),
json=body.model_dump(
by_alias=True, exclude_unset=True, exclude_none=True, mode="json"
),
) as response:
result = await response.json() if response.content_length else {}

if response.status >= 400:
logger.error(f"Error replying to activity: {response.status}")
logger.error(f"Error replying to activity: {result or response.status}")
response.raise_for_status()

data = await response.json() if response.content_length else {}
logger.info(
f"Reply to conversation/activity: {data.get('id')}, {activity_id}"
f"Reply to conversation/activity: {result.get('id')}, {activity_id}"
)
return ResourceResponse.model_validate(data)
return ResourceResponse.model_validate(result)

async def send_to_conversation(
self, conversation_id: str, body: Activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@
from .conversations_result import ConversationsResult
from .expected_replies import ExpectedReplies
from .entity import Entity
from .ai_entity import (
AIEntity,
ClientCitation,
ClientCitationAppearance,
ClientCitationImage,
ClientCitationIconName,
SensitivityUsageInfo,
SensitivityPattern,
add_ai_to_activity,
)
from .error import Error
from .error_response import ErrorResponse
from .fact import Fact
Expand Down Expand Up @@ -109,6 +119,14 @@
"ConversationsResult",
"ExpectedReplies",
"Entity",
"AIEntity",
"ClientCitation",
"ClientCitationAppearance",
"ClientCitationImage",
"ClientCitationIconName",
"SensitivityUsageInfo",
"SensitivityPattern",
"add_ai_to_activity",
"Error",
"ErrorResponse",
"Fact",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from copy import copy
from datetime import datetime, timezone
from typing import Optional
from pydantic import Field
from pydantic import Field, SerializeAsAny
from .activity_types import ActivityTypes
from .channel_account import ChannelAccount
from .conversation_account import ConversationAccount
Expand Down Expand Up @@ -145,7 +145,7 @@ class Activity(AgentsModel):
summary: NonEmptyString = None
suggested_actions: SuggestedActions = None
attachments: list[Attachment] = None
entities: list[Entity] = None
entities: list[SerializeAsAny[Entity]] = None
channel_data: object = None
action: NonEmptyString = None
reply_to_id: NonEmptyString = None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from enum import Enum
from typing import List, Optional, Union, Literal
from dataclasses import dataclass

from .agents_model import AgentsModel
from .activity import Activity
from .entity import Entity


class ClientCitationIconName(str, Enum):
"""Enumeration of supported citation icon names."""

MICROSOFT_WORD = "Microsoft Word"
MICROSOFT_EXCEL = "Microsoft Excel"
MICROSOFT_POWERPOINT = "Microsoft PowerPoint"
MICROSOFT_ONENOTE = "Microsoft OneNote"
MICROSOFT_SHAREPOINT = "Microsoft SharePoint"
MICROSOFT_VISIO = "Microsoft Visio"
MICROSOFT_LOOP = "Microsoft Loop"
MICROSOFT_WHITEBOARD = "Microsoft Whiteboard"
ADOBE_ILLUSTRATOR = "Adobe Illustrator"
ADOBE_PHOTOSHOP = "Adobe Photoshop"
ADOBE_INDESIGN = "Adobe InDesign"
ADOBE_FLASH = "Adobe Flash"
SKETCH = "Sketch"
SOURCE_CODE = "Source Code"
IMAGE = "Image"
GIF = "GIF"
VIDEO = "Video"
SOUND = "Sound"
ZIP = "ZIP"
TEXT = "Text"
PDF = "PDF"


class ClientCitationImage(AgentsModel):
"""Information about the citation's icon."""

type: str = "ImageObject"
name: str = ""


class SensitivityPattern(AgentsModel):
"""Pattern information for sensitivity usage info."""

type: str = "DefinedTerm"
in_defined_term_set: str = ""
name: str = ""
term_code: str = ""


class SensitivityUsageInfo(AgentsModel):
"""
Sensitivity usage info for content sent to the user.
This is used to provide information about the content to the user.
"""

type: str = "https://schema.org/Message"
schema_type: str = "CreativeWork"
description: Optional[str] = None
name: str = ""
position: Optional[int] = None
pattern: Optional[SensitivityPattern] = None


class ClientCitationAppearance(AgentsModel):
"""Appearance information for a client citation."""

type: str = "DigitalDocument"
name: str = ""
text: Optional[str] = None
url: Optional[str] = None
abstract: str = ""
encoding_format: Optional[str] = None
image: Optional[ClientCitationImage] = None
keywords: Optional[List[str]] = None
usage_info: Optional[SensitivityUsageInfo] = None


class ClientCitation(AgentsModel):
"""
Represents a Teams client citation to be included in a message.
See Bot messages with AI-generated content for more details.
https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bot-messages-ai-generated-content?tabs=before%2Cbotmessage
"""

type: str = "Claim"
position: int = 0
appearance: Optional[ClientCitationAppearance] = None

def __post_init__(self):
if self.appearance is None:
self.appearance = ClientCitationAppearance()


class AIEntity(Entity):
"""Entity indicating AI-generated content."""

type: str = "https://schema.org/Message"
schema_type: str = "Message"
context: str = "https://schema.org"
id: str = ""
additional_type: Optional[List[str]] = None
citation: Optional[List[ClientCitation]] = None
usage_info: Optional[SensitivityUsageInfo] = None

def __post_init__(self):
if self.additional_type is None:
self.additional_type = ["AIGeneratedContent"]


def add_ai_to_activity(
activity: Activity,
citations: Optional[List[ClientCitation]] = None,
usage_info: Optional[SensitivityUsageInfo] = 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(
type="https://schema.org/Message",
schema_type="Message",
context="https://schema.org",
id="",
additional_type=["AIGeneratedContent"],
citation=citations,
usage_info=usage_info,
)

if activity.entities is None:
activity.entities = []

activity.entities.append(ai_entity)
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ class DeliveryModes(str, Enum):
notification = "notification"
expect_replies = "expectReplies"
ephemeral = "ephemeral"
stream = "stream"
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from typing import Any
from typing import Any, Optional

from pydantic import model_serializer, model_validator
from .agents_model import AgentsModel, ConfigDict
from pydantic.alias_generators import to_camel, to_snake
from ._type_aliases import NonEmptyString


Expand All @@ -18,3 +21,18 @@ class Entity(AgentsModel):
def additional_properties(self) -> dict[str, Any]:
"""Returns the set of properties that are not None."""
return self.model_extra

@model_validator(mode="before")
@classmethod
def to_snake_for_all(cls, data):
ret = {to_snake(k): v for k, v in data.items()}
return ret

@model_serializer(mode="plain")
def to_camel_for_all(self, config):
if config.by_alias:
new_data = {}
for k, v in self:
new_data[to_camel(k)] = v
return new_data
return {k: v for k, v in self}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
jwt_authorization_middleware,
jwt_authorization_decorator,
)
from .app.streaming import (
Citation,
CitationUtil,
StreamingResponse,
)

__all__ = [
"start_agent_process",
Expand All @@ -14,4 +19,7 @@
"jwt_authorization_middleware",
"jwt_authorization_decorator",
"channel_service_route_table",
"Citation",
"CitationUtil",
"StreamingResponse",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from .streaming import (
Citation,
CitationUtil,
StreamingResponse,
)

__all__ = [
"Citation",
"CitationUtil",
"StreamingResponse",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from .citation import Citation
from .citation_util import CitationUtil
from .streaming_response import StreamingResponse

__all__ = [
"Citation",
"CitationUtil",
"StreamingResponse",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from typing import Optional
from dataclasses import dataclass


@dataclass
class Citation:
"""Citations returned by the model."""

content: str
"""The content of the citation."""

title: Optional[str] = None
"""The title of the citation."""

url: Optional[str] = None
"""The URL of the citation."""

filepath: Optional[str] = None
"""The filepath of the document."""
Loading