Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ class SuggestedActions(AgentsModel):
:type actions: list[~microsoft_agents.activity.CardAction]
"""

to: list[NonEmptyString]
to: list[NonEmptyString] = []
Comment thread
rodrigobr-msft marked this conversation as resolved.
actions: list[CardAction]
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@

AGENTIC_TOKEN = "agents.authorization.agentic_token"
AZURE_BOT_TOKEN = "agents.authorization.azure_bot_token"
AZURE_BOT_SIGN_OUT = "agents.authorization.azure_bot_sign_out"
AZURE_BOT_SIGN_IN = "agents.authorization.azure_bot_sign_in"
AZURE_BOT_SIGN_OUT = "agents.authorization.azure_bot_signout"
AZURE_BOT_SIGN_IN = "agents.authorization.azure_bot_signin"
Comment thread
rodrigobr-msft marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ def _callback(self, span: Span, duration: float, error: Exception | None) -> Non
attributes.CONVERSATION_ID: get_conversation_id(
self._turn_context.activity
),
attributes.SUCCESS: error is None,
}
if error is None:
metrics.turn_count.add(1, attributes=attrs)
metrics.turn_duration.record(duration, attributes=attrs)
else:
metrics.turn_error_count.add(1, attributes=attrs)
metrics.turn_duration.record(duration, attributes=attrs)

def _get_attributes(self) -> AttributeMap:
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

# Spans

SPAN_GET_ACCESS_TOKEN = "agents.auth.getAccessToken"
SPAN_ACQUIRE_TOKEN_ON_BEHALF_OF = "agents.auth.acquireTokenOnBehalfOf"
SPAN_GET_AGENTIC_INSTANCE_TOKEN = "agents.auth.getAgenticInstanceToken"
SPAN_GET_AGENTIC_USER_TOKEN = "agents.auth.getAgenticUserToken"
SPAN_GET_ACCESS_TOKEN = "agents.authentication.get_access_token"
SPAN_ACQUIRE_TOKEN_ON_BEHALF_OF = "agents.authentication.acquire_token_on_behalf_of"
SPAN_GET_AGENTIC_INSTANCE_TOKEN = "agents.authentication.get_agentic_instance_token"
SPAN_GET_AGENTIC_USER_TOKEN = "agents.authentication.get_agentic_user_token"

# Metrics

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,21 +333,21 @@ async def update_activity(
conversation_id = self._normalize_conversation_id(conversation_id)
url = f"v3/conversations/{conversation_id}/activities/{activity_id}"

logger.info(
"Updating activity: %s in conversation: %s. Activity type is %s",
activity_id,
conversation_id,
body.type,
)
async with self.client.put(
url,
json=body.model_dump(by_alias=True, exclude_unset=True),
) as response:
if response.status >= 300:
logger.error(
"Error updating activity: %s", response.status, stack_info=True
)
response.raise_for_status()
logger.info(
"Updating activity: %s in conversation: %s. Activity type is %s",
activity_id,
conversation_id,
body.type,
)
async with self.client.put(
url,
json=body.model_dump(by_alias=True, exclude_unset=True),
) as response:
if response.status >= 300:
logger.error(
"Error updating activity: %s", response.status, stack_info=True
)
response.raise_for_status()

data = await response.json()
return ResourceResponse.model_validate(data)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

SPAN_REPLY_TO_ACTIVITY = "agents.connector.replyToActivity"
SPAN_SEND_TO_CONVERSATION = "agents.connector.sendToConversation"
SPAN_UPDATE_ACTIVITY = "agents.connector.updateActivity"
SPAN_DELETE_ACTIVITY = "agents.connector.deleteActivity"
SPAN_CREATE_CONVERSATION = "agents.connector.createConversation"
SPAN_GET_CONVERSATIONS = "agents.connector.getConversations"
SPAN_GET_CONVERSATION_MEMBERS = "agents.connector.getConversationMembers"
SPAN_UPLOAD_ATTACHMENT = "agents.connector.uploadAttachment"
SPAN_GET_ATTACHMENT = "agents.connector.getAttachment"
SPAN_GET_ATTACHMENT_INFO = "agents.connector.getAttachmentInfo"
SPAN_REPLY_TO_ACTIVITY = "agents.connector.reply_to_activity"
SPAN_SEND_TO_CONVERSATION = "agents.connector.send_to_conversation"
SPAN_UPDATE_ACTIVITY = "agents.connector.update_activity"
SPAN_DELETE_ACTIVITY = "agents.connector.delete_activity"
SPAN_CREATE_CONVERSATION = "agents.connector.create_conversation"
SPAN_GET_CONVERSATIONS = "agents.connector.get_conversations"
SPAN_GET_CONVERSATION_MEMBERS = "agents.connector.get_conversation_members"
SPAN_UPLOAD_ATTACHMENT = "agents.connector.upload_attachment"
SPAN_GET_ATTACHMENT = "agents.connector.get_attachment"
SPAN_GET_ATTACHMENT_INFO = "agents.connector.get_attachment_info"

SPAN_GET_USER_TOKEN = "agents.user_token_client.get_user_token"
SPAN_SIGN_OUT = "agents.user_token_client.sign_out"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,11 @@ def _get_attributes(self) -> dict[str, str]:

NOTE: a dict is the annotated return type to allow child classes to add additional attributes.
"""
attr_dict = {}
if self._connection_name is not None:
attr_dict[attributes.CONNECTION_NAME] = self._connection_name
if self._user_id is not None:
attr_dict[attributes.USER_ID] = self._user_id
if self._channel_id is not None:
attr_dict[attributes.ACTIVITY_CHANNEL_ID] = self._channel_id
return attr_dict
return {
attributes.CONNECTION_NAME: self._connection_name,
attributes.USER_ID: self._user_id,
attributes.ACTIVITY_CHANNEL_ID: self._channel_id,
}
Comment thread
rodrigobr-msft marked this conversation as resolved.


class GetUserToken(_UserTokenClientSpanWrapper):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,11 @@ async def process_request(
activity.type == "invoke"
or activity.delivery_mode == DeliveryModes.expect_replies
):
# Invoke and ExpectReplies cannot be performed async
return HttpResponseFactory.json(
invoke_response.body, invoke_response.status
)
with spans.AdapterWriteResponse(activity):
# Invoke and ExpectReplies cannot be performed async
return HttpResponseFactory.json(
invoke_response.body, invoke_response.status
)

return HttpResponseFactory.accepted()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,11 @@ def get_value(

if not value and default_value_factory is not None:
# If the value is None and a factory is provided, call the factory to get a default value
return default_value_factory()
default = default_value_factory()
# Store the default value in the cache so modifications to it are tracked
if self._cached_state is not None:
self._cached_state.state[property_name] = default
return default
Comment thread
rodrigobr-msft marked this conversation as resolved.

if target_cls and value:
# Attempt to deserialize the value if it is not None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,24 @@
class _StorageSpanWrapper(SimpleSpanWrapper):
"""Base SpanWrapper for spans related to storage operations. This is meant to be a base class for spans related to storage operations, such as retrieving or saving state, and can be used to share common functionality and attributes related to storage operations."""

def __init__(self, span_name: str, *, key_count: int):
def __init__(self, span_name: str, operation_name: str, *, key_count: int):
"""Initializes the _StorageSpanWrapper span."""
super().__init__(span_name)
self._operation_name = operation_name
self._key_count = key_count

def _callback(self, span: Span, duration: float, error: Exception | None) -> None:
"""Callback function that is called when the span is ended. This is used to record metrics for the storage operation based on the outcome of the span."""
metrics.storage_operation_duration.record(
duration,
attributes={
attributes.STORAGE_OPERATION: self._span_name,
attributes.STORAGE_OPERATION: self._operation_name,
},
)
metrics.storage_operation_total.add(
1,
attributes={
attributes.STORAGE_OPERATION: self._span_name,
attributes.STORAGE_OPERATION: self._operation_name,
},
)

Expand All @@ -42,6 +43,7 @@ def _get_attributes(self) -> dict[str, str | int]:
"""
return {
attributes.KEY_COUNT: self._key_count,
attributes.STORAGE_OPERATION: self._operation_name,
}


Expand All @@ -50,20 +52,20 @@ class StorageRead(_StorageSpanWrapper):

def __init__(self, key_count: int):
"""Initializes the StorageRead span."""
super().__init__(constants.SPAN_STORAGE_READ, key_count=key_count)
super().__init__(constants.SPAN_STORAGE_READ, "read", key_count=key_count)


class StorageWrite(_StorageSpanWrapper):
"""Span for writing to storage."""

def __init__(self, key_count: int):
"""Initializes the StorageWrite span."""
super().__init__(constants.SPAN_STORAGE_WRITE, key_count=key_count)
super().__init__(constants.SPAN_STORAGE_WRITE, "write", key_count=key_count)


class StorageDelete(_StorageSpanWrapper):
"""Span for deleting from storage."""

def __init__(self, key_count: int):
"""Initializes the StorageDelete span."""
super().__init__(constants.SPAN_STORAGE_DELETE, key_count=key_count)
super().__init__(constants.SPAN_STORAGE_DELETE, "delete", key_count=key_count)
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
SPAN_CONTINUE_CONVERSATION = "agents.adapter.continue_conversation"
SPAN_CREATE_CONNECTOR_CLIENT = "agents.adapter.create_connector_client"
SPAN_CREATE_USER_TOKEN_CLIENT = "agents.adapter.create_user_token_client"
SPAN_WRITE_RESPONSE = "agents.adapter.write_response"

METRIC_ADAPTER_PROCESS_DURATION = "agents.adapter.process.duration"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,28 @@ def _get_attributes(self) -> AttributeMap:
attributes.AUTH_SCOPES: format_scopes(self._scopes),
attributes.IS_AGENTIC: self._is_agentic_request,
}


class AdapterWriteResponse(SimpleSpanWrapper):
"""Span for writing an InvokeResponse in the adapter. This captures the handling of expectReplies, invoke, and streaming"""

def __init__(self, activity: Activity):
"""Initializes the AdapterWriteResponse span."""
super().__init__(constants.SPAN_WRITE_RESPONSE)
self._activity = activity

def _callback(self, span: Span, duration: float, error: Exception | None) -> None:
metrics.activities_sent.add(
1,
attributes={
attributes.ACTIVITY_TYPE: self._activity.type,
attributes.ACTIVITY_CHANNEL_ID: self._activity.channel_id
or attributes.UNKNOWN,
},
)

def _get_attributes(self) -> AttributeMap:
"""Returns a dictionary of attributes to set on the span when it is started. This includes attributes related to the activities being sent."""
return {
attributes.CONVERSATION_ID: get_conversation_id(self._activity),
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
HTTP_METHOD = "http.method"
HTTP_STATUS_CODE = "http.status_code"

IS_AGENTIC = "is_agentic_request"
IS_AGENTIC = "activity.is_agentic_request"

KEY_COUNT = "storage.keys.count"

Expand All @@ -39,6 +39,7 @@

SERVICE_URL = "service_url"
STORAGE_OPERATION = "storage.operation"
SUCCESS = "success"

TOKEN_SERVICE_ENDPOINT = "agents.token_service.endpoint"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,6 @@ def meter(self) -> Meter:
"""Returns the OpenTelemetry meter instance for recording metrics"""
return self._meter

def _extract_attributes_from_context(
self, turn_context: TurnContextProtocol
) -> dict:
"""Helper method to extract common attributes from the TurnContext for span and metric recording"""

# This can be expanded to extract common attributes for spans and metrics from the context
attributes = {}
attributes["activity.type"] = turn_context.activity.type
attributes["agent.is_agentic"] = turn_context.activity.is_agentic_request()
if turn_context.activity.from_property:
attributes["from.id"] = turn_context.activity.from_property.id
if turn_context.activity.recipient:
attributes["recipient.id"] = turn_context.activity.recipient.id
if turn_context.activity.conversation:
attributes["conversation.id"] = turn_context.activity.conversation.id
attributes["channel_id"] = turn_context.activity.channel_id
attributes["message.text.length"] = (
len(turn_context.activity.text) if turn_context.activity.text else 0
)
return attributes

@contextmanager
def start_as_current_span(
self,
Expand All @@ -74,7 +53,9 @@ def start_as_current_span(
:return: An iterator that yields the started span, which will be ended when the context manager exits
"""

with self._tracer.start_as_current_span(span_name) as span:
with self._tracer.start_as_current_span(
span_name, record_exception=False, set_status_on_exception=False
) as span:

start = time.time()
exception: Exception | None = None
Expand All @@ -92,7 +73,6 @@ def start_as_current_span(
duration = (end - start) * 1000 # milliseconds

if success:
span.add_event(f"{span_name} completed", {"duration_ms": duration})
span.set_status(trace.Status(trace.StatusCode.OK))
if callback:
callback(span, duration, None)
Expand All @@ -101,6 +81,7 @@ def start_as_current_span(
callback(span, duration, exception)

span.set_status(trace.Status(trace.StatusCode.ERROR))
span.record_exception(exception)


agents_telemetry = _AgentsTelemetry()
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

SPAN_TURN_SEND_ACTIVITY = "agents.turn.send_activity"
SPAN_TURN_SEND_ACTIVITIES = "agents.turn.send_activities"
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
from . import constants


class TurnContextSendActivity(SimpleSpanWrapper):
"""Span wrapper for sending an activity within a turn context."""
class TurnContextSendActivities(SimpleSpanWrapper):
"""Span wrapper for sending activities within a turn context."""

def __init__(self, turn_context: TurnContextProtocol):
super().__init__(constants.SPAN_TURN_SEND_ACTIVITY)
super().__init__(constants.SPAN_TURN_SEND_ACTIVITIES)
self._turn_context = turn_context

def _get_attributes(self) -> AttributeMap:
Expand Down
Loading
Loading