-
Notifications
You must be signed in to change notification settings - Fork 7
WIP | Add support for input and output scopes #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
|
|
||
| from dataclasses import dataclass | ||
|
|
||
|
|
||
| @dataclass | ||
| class Response: | ||
| """Response details from agent execution.""" | ||
|
|
||
| messages: list[str] | ||
| """The list of response messages.""" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,80 @@ | ||||||||||||||||||||||||||||||||||||
| # Copyright (c) Microsoft Corporation. | ||||||||||||||||||||||||||||||||||||
| # Licensed under the MIT License. | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+2
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| from ..agent_details import AgentDetails | ||||||||||||||||||||||||||||||||||||
| from ..constants import ( | ||||||||||||||||||||||||||||||||||||
| GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, | ||||||||||||||||||||||||||||||||||||
| GEN_AI_EXECUTION_SOURCE_NAME_KEY, | ||||||||||||||||||||||||||||||||||||
| GEN_AI_EXECUTION_TYPE_KEY, | ||||||||||||||||||||||||||||||||||||
| GEN_AI_INPUT_MESSAGES_KEY, | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| from ..opentelemetry_scope import OpenTelemetryScope | ||||||||||||||||||||||||||||||||||||
| from ..request import Request | ||||||||||||||||||||||||||||||||||||
| from ..tenant_details import TenantDetails | ||||||||||||||||||||||||||||||||||||
| from ..utils import safe_json_dumps | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| INPUT_OPERATION_NAME = "input_messages" | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| class InputScope(OpenTelemetryScope): | ||||||||||||||||||||||||||||||||||||
| """Provides OpenTelemetry tracing scope for input messages.""" | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||||||||||||||
| def start( | ||||||||||||||||||||||||||||||||||||
| agent_details: AgentDetails, | ||||||||||||||||||||||||||||||||||||
| tenant_details: TenantDetails, | ||||||||||||||||||||||||||||||||||||
| request: Request, | ||||||||||||||||||||||||||||||||||||
| ) -> "InputScope": | ||||||||||||||||||||||||||||||||||||
| """Creates and starts a new scope for input tracing. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||
| agent_details: The details of the agent | ||||||||||||||||||||||||||||||||||||
| tenant_details: The details of the tenant | ||||||||||||||||||||||||||||||||||||
| request: The request details which invokes the agent | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||
| A new InputScope instance | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
| return InputScope(agent_details, tenant_details, request) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| def __init__( | ||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||
| agent_details: AgentDetails, | ||||||||||||||||||||||||||||||||||||
| tenant_details: TenantDetails, | ||||||||||||||||||||||||||||||||||||
| request: Request, | ||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||
| """Initialize the input scope. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||
| agent_details: The details of the agent | ||||||||||||||||||||||||||||||||||||
| tenant_details: The details of the tenant | ||||||||||||||||||||||||||||||||||||
| request: The request details which invokes the agent | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
| super().__init__( | ||||||||||||||||||||||||||||||||||||
| kind="Client", | ||||||||||||||||||||||||||||||||||||
| operation_name=INPUT_OPERATION_NAME, | ||||||||||||||||||||||||||||||||||||
| activity_name=(f"{INPUT_OPERATION_NAME} {agent_details.agent_id}"), | ||||||||||||||||||||||||||||||||||||
| agent_details=agent_details, | ||||||||||||||||||||||||||||||||||||
| tenant_details=tenant_details, | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Set request metadata | ||||||||||||||||||||||||||||||||||||
| if request.source_metadata: | ||||||||||||||||||||||||||||||||||||
| self.set_tag_maybe(GEN_AI_EXECUTION_SOURCE_NAME_KEY, request.source_metadata.name) | ||||||||||||||||||||||||||||||||||||
| self.set_tag_maybe( | ||||||||||||||||||||||||||||||||||||
| GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, request.source_metadata.description | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| self.set_tag_maybe( | ||||||||||||||||||||||||||||||||||||
| GEN_AI_EXECUTION_TYPE_KEY, | ||||||||||||||||||||||||||||||||||||
| request.execution_type.value if request.execution_type else None, | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| self.set_tag_maybe(GEN_AI_INPUT_MESSAGES_KEY, safe_json_dumps([request.content])) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| def record_input_messages(self, messages: list[str]) -> None: | ||||||||||||||||||||||||||||||||||||
| """Records the input messages for telemetry tracking. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||
| messages: List of input messages | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+75
to
+78
|
||||||||||||||||||||||||||||||||||||
| """Records the input messages for telemetry tracking. | |
| Args: | |
| messages: List of input messages | |
| """Records or updates the input messages for telemetry tracking. | |
| This method is intended for scenarios where the full set of input messages is | |
| not yet available when the :class:`InputScope` is created, or when the initial | |
| request content needs to be updated (for example, after normalization, | |
| aggregation across multiple turns, or other preprocessing). | |
| By default, the constructor records the initial input messages based on | |
| ``request.content``. Calling this method will overwrite the previously | |
| recorded ``GEN_AI_INPUT_MESSAGES_KEY`` value with the provided ``messages``. | |
| Args: | |
| messages: The complete list of input messages to associate with this scope. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
|
Comment on lines
+1
to
+2
|
||
|
|
||
| from ..agent_details import AgentDetails | ||
| from ..constants import GEN_AI_OUTPUT_MESSAGES_KEY | ||
| from ..models.response import Response | ||
| from ..opentelemetry_scope import OpenTelemetryScope | ||
| from ..tenant_details import TenantDetails | ||
| from ..utils import safe_json_dumps | ||
|
|
||
| OUTPUT_OPERATION_NAME = "output_messages" | ||
|
|
||
|
|
||
| class OutputScope(OpenTelemetryScope): | ||
| """Provides OpenTelemetry tracing scope for output messages.""" | ||
|
|
||
| @staticmethod | ||
| def start( | ||
| agent_details: AgentDetails, | ||
| tenant_details: TenantDetails, | ||
| response: Response, | ||
| ) -> "OutputScope": | ||
| """Creates and starts a new scope for output tracing. | ||
|
|
||
| Args: | ||
| agent_details: The details of the agent | ||
| tenant_details: The details of the tenant | ||
| response: The response details from the agent | ||
|
|
||
| Returns: | ||
| A new OutputScope instance | ||
| """ | ||
| return OutputScope(agent_details, tenant_details, response) | ||
|
|
||
| def __init__( | ||
| self, | ||
| agent_details: AgentDetails, | ||
| tenant_details: TenantDetails, | ||
| response: Response, | ||
| ): | ||
| """Initialize the output scope. | ||
|
|
||
| Args: | ||
| agent_details: The details of the agent | ||
| tenant_details: The details of the tenant | ||
| response: The response details from the agent | ||
| """ | ||
| super().__init__( | ||
| kind="Client", | ||
| operation_name=OUTPUT_OPERATION_NAME, | ||
| activity_name=(f"{OUTPUT_OPERATION_NAME} {agent_details.agent_id}"), | ||
| agent_details=agent_details, | ||
| tenant_details=tenant_details, | ||
| ) | ||
|
|
||
| # Set response messages | ||
| self.set_tag_maybe(GEN_AI_OUTPUT_MESSAGES_KEY, safe_json_dumps(response.messages)) | ||
|
|
||
| def record_output_messages(self, messages: list[str]) -> None: | ||
| """Records the output messages for telemetry tracking. | ||
|
|
||
| Args: | ||
| messages: List of output messages | ||
| """ | ||
| self.set_tag_maybe(GEN_AI_OUTPUT_MESSAGES_KEY, safe_json_dumps(messages)) | ||
|
Comment on lines
+59
to
+65
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,134 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Copyright (c) Microsoft Corporation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Licensed under the MIT License. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+2
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from collections.abc import Awaitable, Callable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from microsoft_agents.activity import Activity | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from microsoft_agents.hosting.core.middleware_set import Middleware, TurnContext | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from microsoft_agents_a365.observability.core.agent_details import AgentDetails | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from microsoft_agents_a365.observability.core.execution_type import ExecutionType | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from microsoft_agents_a365.observability.core.models.response import Response | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from microsoft_agents_a365.observability.core.request import Request | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from microsoft_agents_a365.observability.core.spans_scopes.input_scope import InputScope | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from microsoft_agents_a365.observability.core.spans_scopes.output_scope import OutputScope | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from microsoft_agents_a365.observability.core.tenant_details import TenantDetails | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class MessageLoggingMiddleware(Middleware): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Lightweight middleware for logging input and output messages. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Lightweight middleware for logging input and output messages. | |
| Lightweight middleware that logs input and output messages and creates | |
| OpenTelemetry spans for these messages. | |
| The middleware starts an ``InputScope`` for the turn when a user message | |
| is present and uses an ``OutputScope`` for outgoing bot messages. This | |
| behavior affects observability infrastructure and trace collection, in | |
| addition to providing message logging. |
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstring lacks an example of how to use this class within the middleware registration pattern. Given the complex interaction with InputScope and OutputScope, and the unusual pattern of manually calling enter and exit, adding a usage example would significantly help developers understand the expected behavior.
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method manually calls enter and exit on the InputScope context manager, which is an unusual pattern and potentially error-prone. The standard pattern would be to use a 'with' statement. However, if manual control is required due to the async execution flow, this should be documented with a comment explaining why the standard context manager pattern cannot be used here.
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The agent_id is set to "unknown" when activity.recipient is None, but the agent_name in the same case is set to None. This inconsistency could lead to confusion in logs and telemetry. Consider either setting both to "unknown" for consistency, or both to None to indicate missing data.
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is code duplication in the creation of AgentDetails and TenantDetails objects. The same logic for extracting these details from the activity appears in both _create_input_scope (lines 72-82) and _create_send_handler (lines 105-122). This duplication makes the code harder to maintain and increases the risk of inconsistencies. Consider extracting this into a helper method like _extract_agent_details(activity) and _extract_tenant_details(activity).
| # Collect all outgoing message texts | |
| messages = [activity.text for activity in activities if activity.text] | |
| if messages: | |
| # Create OutputScope as a child of the current InputScope | |
| agent_details = AgentDetails( | |
| agent_id=incoming_activity.recipient.id | |
| if incoming_activity.recipient | |
| else "unknown", | |
| agent_name=incoming_activity.recipient.name | |
| if incoming_activity.recipient | |
| else None, | |
| conversation_id=incoming_activity.conversation.id | |
| if incoming_activity.conversation | |
| else None, | |
| ) | |
| tenant_details = TenantDetails( | |
| tenant_id=incoming_activity.conversation.tenant_id | |
| if incoming_activity.conversation | |
| and hasattr(incoming_activity.conversation, "tenant_id") | |
| else "unknown" | |
| ) | |
| def _extract_agent_details(activity: Activity) -> AgentDetails: | |
| return AgentDetails( | |
| agent_id=activity.recipient.id if activity.recipient else "unknown", | |
| agent_name=activity.recipient.name if activity.recipient else None, | |
| conversation_id=activity.conversation.id | |
| if activity.conversation | |
| else None, | |
| ) | |
| def _extract_tenant_details(activity: Activity) -> TenantDetails: | |
| return TenantDetails( | |
| tenant_id=activity.conversation.tenant_id | |
| if activity.conversation and hasattr(activity.conversation, "tenant_id") | |
| else "unknown" | |
| ) | |
| # Collect all outgoing message texts | |
| messages = [activity.text for activity in activities if activity.text] | |
| if messages: | |
| # Create OutputScope as a child of the current InputScope | |
| agent_details = _extract_agent_details(incoming_activity) | |
| tenant_details = _extract_tenant_details(incoming_activity) |
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The emoji usage in log messages ('📥' and '📤') may not render correctly in all logging environments and could cause issues with log parsing tools or text-based terminals. Consider using standard text prefixes like '[INPUT]' and '[OUTPUT]' instead, or make the emoji usage configurable.
| self.logger.info(f"📤 Bot output message: {message}") | |
| self.logger.info(f"[OUTPUT] Bot output message: {message}") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
|
Comment on lines
+1
to
+2
|
||
|
|
||
|
|
||
| from microsoft_agents.hosting.aiohttp import CloudAdapter | ||
|
|
||
| from .message_logging_middleware import MessageLoggingMiddleware | ||
|
|
||
|
|
||
| class ObservabilityMiddlewareRegistrar: | ||
| """ | ||
| Registrar for configuring and registering observability middleware. | ||
|
|
||
| Usage: | ||
| # Quick start with defaults | ||
| ObservabilityMiddlewareRegistrar().with_message_logging().apply(adapter) | ||
|
|
||
| """ | ||
|
|
||
| def __init__(self): | ||
| """Initialize the registrar.""" | ||
| self._middleware_configs: list = [] | ||
|
|
||
| def with_message_logging( | ||
| self, | ||
| log_user_messages: bool = True, | ||
| log_bot_messages: bool = True, | ||
| ) -> "ObservabilityMiddlewareRegistrar": | ||
| """Configure message logging middleware. | ||
|
|
||
| Args: | ||
| log_user_messages: Whether to log user messages (default: True) | ||
| log_bot_messages: Whether to log bot messages (default: True) | ||
|
|
||
| Returns: | ||
| The registrar instance for chaining | ||
| """ | ||
| self._middleware_configs.append( | ||
| lambda: MessageLoggingMiddleware( | ||
| log_user_messages=log_user_messages, | ||
| log_bot_messages=log_bot_messages, | ||
| ) | ||
| ) | ||
| return self | ||
|
|
||
| def apply(self, adapter: CloudAdapter) -> None: | ||
| """Apply all configured middleware to the adapter. | ||
|
|
||
| Args: | ||
| adapter: CloudAdapter to register middleware with | ||
| """ | ||
| for create_middleware in self._middleware_configs: | ||
| middleware = create_middleware() | ||
| adapter.use(middleware) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Should we name this Content to keep it similar to Request?