diff --git a/docs/design.md b/docs/design.md index 9f107784..fea18970 100644 --- a/docs/design.md +++ b/docs/design.md @@ -81,12 +81,14 @@ The foundation for distributed tracing in agent applications. Built on OpenTelem | Class | Purpose | |-------|---------| -| `InvokeAgentDetails` | Agent endpoint, session ID, and invocation metadata | +| `InvokeAgentScopeDetails` | Agent endpoint and invocation metadata | | `AgentDetails` | Agent identification and metadata | -| `TenantDetails` | Tenant identification for multi-tenant scenarios | +| `UserDetails` | Human caller identification (user ID, email, name, IP) | +| `CallerDetails` | Wrapper for user details and/or caller agent details | +| `SpanDetails` | Parent context, timing, and span kind for custom spans | | `InferenceCallDetails` | Model name, tokens, provider information | | `ToolCallDetails` | Tool name, arguments, endpoint | -| `Request` | Execution context and correlation ID | +| `Request` | Content, correlation ID, and conversation ID | **Usage Example:** @@ -94,8 +96,8 @@ The foundation for distributed tracing in agent applications. Built on OpenTelem from microsoft_agents_a365.observability.core import ( configure, InvokeAgentScope, - InvokeAgentDetails, - TenantDetails, + InvokeAgentScopeDetails, + AgentDetails, Request, BaggageBuilder, ) @@ -112,9 +114,9 @@ configure( with BaggageBuilder().tenant_id(tenant_id).agent_id(agent_id).build(): # Trace agent invocation with InvokeAgentScope.start( - invoke_agent_details=InvokeAgentDetails(...), - tenant_details=TenantDetails(...), - request=Request(...) + request=Request(content="Hello"), + invoke_scope_details=InvokeAgentScopeDetails(...), + agent_details=AgentDetails(...), ) as scope: # Agent logic here scope.record_response("result") diff --git a/libraries/microsoft-agents-a365-observability-core/CHANGELOG.md b/libraries/microsoft-agents-a365-observability-core/CHANGELOG.md new file mode 100644 index 00000000..8de2f04c --- /dev/null +++ b/libraries/microsoft-agents-a365-observability-core/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog — microsoft-agents-a365-observability-core + +All notable changes to this package will be documented in this file. + +## [Unreleased] + +### Breaking Changes + +- **`InvokeAgentDetails` renamed to `InvokeAgentScopeDetails`** — Now contains only scope-level config (`endpoint`). Agent identity (`AgentDetails`) is a separate parameter. `session_id` moved to `Request`. +- **`InvokeAgentScope.start()`**: New signature `start(request, invoke_scope_details, agent_details, caller_details?, span_details?)`. `request` is required. +- **`InferenceScope.start()`**: New signature `start(request, details, agent_details, user_details?, span_details?)`. `request` is required. +- **`ExecuteToolScope.start()`**: New signature `start(request, details, agent_details, user_details?, span_details?)`. Same pattern as `InferenceScope`. +- **`OutputScope.start()`**: New signature `start(request, response, agent_details, user_details?, span_details?)`. Same pattern. +- **`CallerDetails` renamed to `UserDetails`** — Fields renamed: `caller_id` → `user_id`, `caller_upn` → `user_email`, `caller_name` → `user_name`, `caller_client_ip` → `user_client_ip`. +- **`CallerDetails` is now a composite wrapper** — Groups `user_details: UserDetails` and `caller_agent_details: AgentDetails` for A2A scenarios. +- **`TenantDetails` removed** — `tenant_id` is now on `AgentDetails.tenant_id`. Removed from all scope `start()` methods. +- **`ExecutionType` enum removed** — Removed from `Request`. `GEN_AI_EXECUTION_TYPE_KEY` constant also removed. +- **`AgentDetails` fields renamed** — `agent_auid` → `agentic_user_id`, `agent_upn` → `agentic_user_email`. `conversation_id` moved to `Request`. +- **`Request` model updated** — Removed `execution_type`. Added `conversation_id`. `content` is now optional. +- **`BaggageBuilder` methods renamed** — `agent_upn()` → `agentic_user_email()`, `agent_auid()` → `agentic_user_id()`, `caller_id()` → `user_id()`, `caller_name()` → `user_name()`, `caller_upn()` → `user_email()`, `caller_client_ip()` → `user_client_ip()`. + +### Added + +- **`SpanDetails`** — Groups `span_kind`, `parent_context`, `start_time`, `end_time` for scope construction. +- **`UserDetails`** — Human caller identity with `user_id`, `user_email`, `user_name`, `user_client_ip`. +- **`CallerDetails`** (new wrapper) — Groups `user_details` and `caller_agent_details` for A2A scenarios. +- **`InvokeAgentScopeDetails`** — Scope-level config with `endpoint` only. +- **`Request.conversation_id`** — Conversation ID field on the unified `Request` model. +- **`ERROR_TYPE_CANCELLED`** constant — `"TaskCanceledException"`, used by `record_cancellation()`. +- **`OutputScope`** now exported from `microsoft_agents_a365.observability.core`. diff --git a/libraries/microsoft-agents-a365-observability-core/docs/design.md b/libraries/microsoft-agents-a365-observability-core/docs/design.md index a02ba87e..cc1944ab 100644 --- a/libraries/microsoft-agents-a365-observability-core/docs/design.md +++ b/libraries/microsoft-agents-a365-observability-core/docs/design.md @@ -65,10 +65,10 @@ configure( class OpenTelemetryScope: """Base class for OpenTelemetry tracing scopes.""" - def __init__(self, kind, operation_name, activity_name, agent_details, tenant_details): + def __init__(self, kind, operation_name, activity_name, agent_details): # Creates span with given parameters # Sets common attributes (gen_ai.system, operation name) - # Sets agent/tenant details as span attributes + # Sets agent details as span attributes def __enter__(self): # Makes span active in current context @@ -96,19 +96,15 @@ Traces agent invocation operations (entry point for agent requests): ```python from microsoft_agents_a365.observability.core import ( InvokeAgentScope, - InvokeAgentDetails, - TenantDetails, + InvokeAgentScopeDetails, AgentDetails, + Request, ) with InvokeAgentScope.start( - invoke_agent_details=InvokeAgentDetails( - endpoint=parsed_url, - session_id="session-123", - details=AgentDetails(agent_id="agent-456", agent_name="MyAgent") - ), - tenant_details=TenantDetails(tenant_id="tenant-789"), - request=Request(content="Hello", execution_type=ExecutionType.CHAT), + request=Request(content="Hello"), + invoke_scope_details=InvokeAgentScopeDetails(endpoint=parsed_url), + agent_details=AgentDetails(agent_id="agent-456", agent_name="MyAgent"), ) as scope: # Agent processing scope.record_response("Agent response") @@ -116,9 +112,7 @@ with InvokeAgentScope.start( **Span attributes recorded:** - Server address and port -- Session ID - Execution source metadata -- Execution type - Input/output messages - Caller details (if provided) @@ -127,15 +121,15 @@ with InvokeAgentScope.start( Traces LLM/AI model inference calls: ```python -from microsoft_agents_a365.observability.core import InferenceScope, InferenceCallDetails +from microsoft_agents_a365.observability.core import InferenceScope, InferenceCallDetails, Request with InferenceScope.start( - inference_call_details=InferenceCallDetails( + request=Request(content="Hello"), + details=InferenceCallDetails( model_name="gpt-4", provider="openai" ), agent_details=agent_details, - tenant_details=tenant_details, ) as scope: # LLM call scope.record_input_tokens(100) @@ -148,15 +142,15 @@ with InferenceScope.start( Traces tool execution operations: ```python -from microsoft_agents_a365.observability.core import ExecuteToolScope, ToolCallDetails +from microsoft_agents_a365.observability.core import ExecuteToolScope, ToolCallDetails, Request with ExecuteToolScope.start( - tool_call_details=ToolCallDetails( + request=Request(content="search for weather"), + details=ToolCallDetails( tool_name="search", tool_arguments={"query": "weather"} ), agent_details=agent_details, - tenant_details=tenant_details, ) as scope: # Tool execution scope.record_response("Tool result") @@ -174,7 +168,7 @@ with BaggageBuilder() \ .tenant_id("tenant-123") \ .agent_id("agent-456") \ .correlation_id("corr-789") \ - .caller_id("user-abc") \ + .user_id("user-abc") \ .session_id("session-xyz") \ .build(): # All child spans inherit this baggage @@ -195,9 +189,11 @@ with BaggageBuilder.set_request_context( | `tenant_id(value)` | `tenant_id` | | `agent_id(value)` | `gen_ai.agent.id` | | `agent_auid(value)` | `gen_ai.agent.auid` | -| `agent_upn(value)` | `gen_ai.agent.upn` | +| `agent_email(value)` | `gen_ai.agent.upn` | | `correlation_id(value)` | `correlation_id` | -| `caller_id(value)` | `gen_ai.caller.id` | +| `user_id(value)` | `gen_ai.caller.id` | +| `user_name(value)` | `gen_ai.caller.name` | +| `user_email(value)` | `gen_ai.caller.upn` | | `session_id(value)` | `session_id` | | `conversation_id(value)` | `gen_ai.conversation.id` | | `channel_name(value)` | `gen_ai.execution.source.name` | @@ -246,14 +242,12 @@ options = Agent365ExporterOptions( ## Data Classes -### InvokeAgentDetails +### InvokeAgentScopeDetails ```python @dataclass -class InvokeAgentDetails: +class InvokeAgentScopeDetails: endpoint: ParseResult | None # Parsed URL of the agent endpoint - session_id: str | None # Session identifier - details: AgentDetails # Agent metadata ``` ### AgentDetails @@ -265,20 +259,42 @@ class AgentDetails: agent_name: str | None agent_description: str | None agent_auid: str | None # Agent unique identifier - agent_upn: str | None # User principal name + agent_email: str | None # Agent email address agent_blueprint_id: str | None agent_type: AgentType | None tenant_id: str | None - conversation_id: str | None icon_uri: str | None ``` -### TenantDetails +### UserDetails ```python @dataclass -class TenantDetails: - tenant_id: str | None +class UserDetails: + user_id: str | None + user_email: str | None + user_name: str | None + caller_client_ip: str | None +``` + +### CallerDetails + +```python +@dataclass +class CallerDetails: + user_details: UserDetails | None + caller_agent_details: AgentDetails | None +``` + +### SpanDetails + +```python +@dataclass +class SpanDetails: + parent_context: Context | None + start_time: int | None + end_time: int | None + span_kind: SpanKind | None ``` ### InferenceCallDetails @@ -358,14 +374,15 @@ microsoft_agents_a365/observability/core/ ├── invoke_agent_scope.py # Agent invocation tracing ├── inference_scope.py # LLM inference tracing ├── execute_tool_scope.py # Tool execution tracing +├── output_scope.py # Output tracing ├── agent_details.py # AgentDetails dataclass -├── tenant_details.py # TenantDetails dataclass -├── invoke_agent_details.py # InvokeAgentDetails dataclass +├── invoke_agent_scope_details.py # InvokeAgentScopeDetails dataclass +├── user_details.py # UserDetails dataclass +├── span_details.py # SpanDetails dataclass ├── inference_call_details.py # InferenceCallDetails dataclass ├── tool_call_details.py # ToolCallDetails dataclass ├── request.py # Request dataclass ├── source_metadata.py # SourceMetadata dataclass -├── execution_type.py # ExecutionType enum ├── inference_operation_type.py # InferenceOperationType enum ├── tool_type.py # ToolType enum ├── constants.py # Attribute key constants diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/__init__.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/__init__.py index 95cc4737..ec2bdbc2 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/__init__.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/__init__.py @@ -11,7 +11,6 @@ is_configured, ) from .execute_tool_scope import ExecuteToolScope -from .execution_type import ExecutionType from .exporters.agent365_exporter_options import Agent365ExporterOptions from .exporters.enriched_span import EnrichedReadableSpan from .exporters.enriching_span_processor import ( @@ -23,13 +22,16 @@ from .inference_call_details import InferenceCallDetails, ServiceEndpoint from .inference_operation_type import InferenceOperationType from .inference_scope import InferenceScope -from .invoke_agent_details import InvokeAgentDetails +from .invoke_agent_details import InvokeAgentScopeDetails from .invoke_agent_scope import InvokeAgentScope from .middleware.baggage_builder import BaggageBuilder +from .models.caller_details import CallerDetails +from .models.user_details import UserDetails from .opentelemetry_scope import OpenTelemetryScope from .request import Request from .channel import Channel -from .tenant_details import TenantDetails +from .span_details import SpanDetails +from .spans_scopes.output_scope import OutputScope from .tool_call_details import ToolCallDetails from .tool_type import ToolType from .trace_processor.span_processor import SpanProcessor @@ -57,26 +59,26 @@ "ExecuteToolScope", "InvokeAgentScope", "InferenceScope", + "OutputScope", # Middleware "BaggageBuilder", # Data classes - "InvokeAgentDetails", + "InvokeAgentScopeDetails", "AgentDetails", - "TenantDetails", + "CallerDetails", + "UserDetails", "ToolCallDetails", "Channel", "Request", + "SpanDetails", "InferenceCallDetails", "ServiceEndpoint", # Enums - "ExecutionType", "InferenceOperationType", "ToolType", # Utility functions "extract_context_from_headers", "get_traceparent", - # Constants - # all constants from constants.py are exported via * ] # This is a namespace package diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/agent_details.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/agent_details.py index 00433085..6cdc9087 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/agent_details.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/agent_details.py @@ -18,11 +18,11 @@ class AgentDetails: agent_description: Optional[str] = None """A description of the AI agent's purpose or capabilities.""" - agent_auid: Optional[str] = None + agentic_user_id: Optional[str] = None """Agentic User ID for the agent.""" - agent_upn: Optional[str] = None - """User Principal Name (UPN) for the agentic user.""" + agentic_user_email: Optional[str] = None + """Email address for the agentic user.""" agent_blueprint_id: Optional[str] = None """Blueprint/Application ID for the agent.""" @@ -33,9 +33,6 @@ class AgentDetails: tenant_id: Optional[str] = None """Tenant ID for the agent.""" - conversation_id: Optional[str] = None - """Optional conversation ID for compatibility.""" - icon_uri: Optional[str] = None """Optional icon URI for the agent.""" diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/constants.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/constants.py index f4730427..b400c848 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/constants.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/constants.py @@ -87,8 +87,10 @@ GEN_AI_AGENT_EMAIL_KEY = "microsoft.agent.user.email" GEN_AI_AGENT_BLUEPRINT_ID_KEY = "microsoft.a365.agent.blueprint.id" +# Error type constants +ERROR_TYPE_CANCELLED = "TaskCanceledException" + # Execution context dimensions -GEN_AI_EXECUTION_TYPE_KEY = "gen_ai.execution.type" GEN_AI_EXECUTION_PAYLOAD_KEY = "gen_ai.execution.payload" # Channel dimensions diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/execute_tool_scope.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/execute_tool_scope.py index e21f412c..e2e5df8e 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/execute_tool_scope.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/execute_tool_scope.py @@ -1,9 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from datetime import datetime - -from opentelemetry.context import Context from opentelemetry.trace import SpanKind from .agent_details import AgentDetails @@ -11,6 +8,8 @@ CHANNEL_LINK_KEY, CHANNEL_NAME_KEY, EXECUTE_TOOL_OPERATION_NAME, + GEN_AI_CALLER_CLIENT_IP_KEY, + GEN_AI_CONVERSATION_ID_KEY, GEN_AI_TOOL_ARGS_KEY, GEN_AI_TOOL_CALL_ID_KEY, GEN_AI_TOOL_DESCRIPTION_KEY, @@ -18,11 +17,16 @@ GEN_AI_TOOL_TYPE_KEY, SERVER_ADDRESS_KEY, SERVER_PORT_KEY, + USER_EMAIL_KEY, + USER_ID_KEY, + USER_NAME_KEY, ) +from .models.user_details import UserDetails from .opentelemetry_scope import OpenTelemetryScope from .request import Request -from .tenant_details import TenantDetails +from .span_details import SpanDetails from .tool_call_details import ToolCallDetails +from .utils import validate_and_normalize_ip class ExecuteToolScope(OpenTelemetryScope): @@ -30,88 +34,71 @@ class ExecuteToolScope(OpenTelemetryScope): @staticmethod def start( + request: Request, details: ToolCallDetails, agent_details: AgentDetails, - tenant_details: TenantDetails, - request: Request | None = None, - parent_context: Context | None = None, - start_time: datetime | None = None, - end_time: datetime | None = None, - span_kind: SpanKind | None = None, + user_details: UserDetails | None = None, + span_details: SpanDetails | None = None, ) -> "ExecuteToolScope": """Creates and starts a new scope for tool execution tracing. Args: + request: Request details for the tool execution details: The details of the tool call agent_details: The details of the agent making the call - tenant_details: The details of the tenant - request: Optional request details for additional context - parent_context: Optional OpenTelemetry Context used to link this span to an - upstream operation. Use ``extract_context_from_headers()`` to convert a - Context from HTTP headers containing W3C traceparent. - start_time: Optional explicit start time as a datetime object. Useful when - recording a tool call after execution has already completed. - end_time: Optional explicit end time as a datetime object. When provided, - the span will use this timestamp when disposed instead of the - current wall-clock time. - span_kind: Optional span kind override. Defaults to ``SpanKind.INTERNAL``. - Use ``SpanKind.CLIENT`` when the tool calls an external service. + user_details: Optional human user details + span_details: Optional span configuration (parent context, timing, kind) Returns: A new ExecuteToolScope instance """ return ExecuteToolScope( + request, details, agent_details, - tenant_details, - request, - parent_context, - start_time, - end_time, - span_kind, + user_details, + span_details, ) def __init__( self, + request: Request, details: ToolCallDetails, agent_details: AgentDetails, - tenant_details: TenantDetails, - request: Request | None = None, - parent_context: Context | None = None, - start_time: datetime | None = None, - end_time: datetime | None = None, - span_kind: SpanKind | None = None, + user_details: UserDetails | None = None, + span_details: SpanDetails | None = None, ): """Initialize the tool execution scope. Args: + request: Request details for the tool execution details: The details of the tool call agent_details: The details of the agent making the call - tenant_details: The details of the tenant - request: Optional request details for additional context - parent_context: Optional OpenTelemetry Context used to link this span to an - upstream operation. Use ``extract_context_from_headers()`` to convert a - Context from HTTP headers containing W3C traceparent. - start_time: Optional explicit start time as a datetime object. Useful when - recording a tool call after execution has already completed. - end_time: Optional explicit end time as a datetime object. When provided, - the span will use this timestamp when disposed instead of the - current wall-clock time. - span_kind: Optional span kind override. Defaults to ``SpanKind.INTERNAL``. - Use ``SpanKind.CLIENT`` when the tool calls an external service. + user_details: Optional human user details + span_details: Optional span configuration (parent context, timing, kind) """ + kind = SpanKind.INTERNAL + parent_context = None + start_time = None + end_time = None + if span_details is not None: + if span_details.span_kind is not None: + kind = span_details.span_kind + parent_context = span_details.parent_context + start_time = span_details.start_time + end_time = span_details.end_time + super().__init__( - kind=span_kind if span_kind is not None else SpanKind.INTERNAL, + kind=kind, operation_name=EXECUTE_TOOL_OPERATION_NAME, activity_name=f"{EXECUTE_TOOL_OPERATION_NAME} {details.tool_name}", agent_details=agent_details, - tenant_details=tenant_details, parent_context=parent_context, start_time=start_time, end_time=end_time, ) - # Extract details using deconstruction-like approach + # Extract details tool_name = details.tool_name arguments = details.arguments tool_call_id = details.tool_call_id @@ -124,6 +111,7 @@ def __init__( self.set_tag_maybe(GEN_AI_TOOL_TYPE_KEY, tool_type) self.set_tag_maybe(GEN_AI_TOOL_CALL_ID_KEY, tool_call_id) self.set_tag_maybe(GEN_AI_TOOL_DESCRIPTION_KEY, description) + self.set_tag_maybe(GEN_AI_CONVERSATION_ID_KEY, request.conversation_id) if endpoint: self.set_tag_maybe(SERVER_ADDRESS_KEY, endpoint.hostname) @@ -131,10 +119,20 @@ def __init__( self.set_tag_maybe(SERVER_PORT_KEY, endpoint.port) # Set request metadata if provided - if request and request.channel: + if request.channel: self.set_tag_maybe(CHANNEL_NAME_KEY, request.channel.name) self.set_tag_maybe(CHANNEL_LINK_KEY, request.channel.link) + # Set user details if provided + if user_details: + self.set_tag_maybe(USER_ID_KEY, user_details.user_id) + self.set_tag_maybe(USER_EMAIL_KEY, user_details.user_email) + self.set_tag_maybe(USER_NAME_KEY, user_details.user_name) + self.set_tag_maybe( + GEN_AI_CALLER_CLIENT_IP_KEY, + validate_and_normalize_ip(user_details.user_client_ip), + ) + def record_response(self, response: str) -> None: """Records response information for telemetry tracking. diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/inference_scope.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/inference_scope.py index 10774a0a..193a0d71 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/inference_scope.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/inference_scope.py @@ -1,16 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from datetime import datetime from typing import List -from opentelemetry.context import Context - from .agent_details import AgentDetails from .constants import ( CHANNEL_LINK_KEY, CHANNEL_NAME_KEY, GEN_AI_AGENT_THOUGHT_PROCESS_KEY, + GEN_AI_CONVERSATION_ID_KEY, GEN_AI_INPUT_MESSAGES_KEY, GEN_AI_OPERATION_NAME_KEY, GEN_AI_OUTPUT_MESSAGES_KEY, @@ -21,12 +19,17 @@ GEN_AI_USAGE_OUTPUT_TOKENS_KEY, SERVER_ADDRESS_KEY, SERVER_PORT_KEY, + USER_EMAIL_KEY, + USER_ID_KEY, + USER_NAME_KEY, + GEN_AI_CALLER_CLIENT_IP_KEY, ) from .inference_call_details import InferenceCallDetails +from .models.user_details import UserDetails from .opentelemetry_scope import OpenTelemetryScope from .request import Request -from .tenant_details import TenantDetails -from .utils import safe_json_dumps +from .span_details import SpanDetails +from .utils import safe_json_dumps, validate_and_normalize_ip class InferenceScope(OpenTelemetryScope): @@ -34,71 +37,64 @@ class InferenceScope(OpenTelemetryScope): @staticmethod def start( + request: Request, details: InferenceCallDetails, agent_details: AgentDetails, - tenant_details: TenantDetails, - request: Request | None = None, - parent_context: Context | None = None, - start_time: datetime | None = None, - end_time: datetime | None = None, + user_details: UserDetails | None = None, + span_details: SpanDetails | None = None, ) -> "InferenceScope": """Creates and starts a new scope for inference tracing. Args: + request: Request details for the inference details: The details of the inference call agent_details: The details of the agent making the call - tenant_details: The details of the tenant - request: Optional request details for additional context - parent_context: Optional OpenTelemetry Context used to link this span to an - upstream operation. Use ``extract_context_from_headers()`` to convert a - Context from HTTP headers containing W3C traceparent. - start_time: Optional explicit start time as a datetime object. - end_time: Optional explicit end time as a datetime object. + user_details: Optional human user details + span_details: Optional span configuration (parent context, timing) Returns: A new InferenceScope instance """ - return InferenceScope( - details, agent_details, tenant_details, request, parent_context, start_time, end_time - ) + return InferenceScope(request, details, agent_details, user_details, span_details) def __init__( self, + request: Request, details: InferenceCallDetails, agent_details: AgentDetails, - tenant_details: TenantDetails, - request: Request | None = None, - parent_context: Context | None = None, - start_time: datetime | None = None, - end_time: datetime | None = None, + user_details: UserDetails | None = None, + span_details: SpanDetails | None = None, ): """Initialize the inference scope. Args: + request: Request details for the inference details: The details of the inference call agent_details: The details of the agent making the call - tenant_details: The details of the tenant - request: Optional request details for additional context - parent_context: Optional OpenTelemetry Context used to link this span to an - upstream operation. Use ``extract_context_from_headers()`` to convert a - Context from HTTP headers containing W3C traceparent. - start_time: Optional explicit start time as a datetime object. - end_time: Optional explicit end time as a datetime object. + user_details: Optional human user details + span_details: Optional span configuration (parent context, timing) """ + parent_context = None + start_time = None + end_time = None + if span_details is not None: + parent_context = span_details.parent_context + start_time = span_details.start_time + end_time = span_details.end_time super().__init__( kind="Client", operation_name=details.operationName.value, activity_name=f"{details.operationName.value} {details.model}", agent_details=agent_details, - tenant_details=tenant_details, parent_context=parent_context, start_time=start_time, end_time=end_time, ) - if request: + if request.content: self.set_tag_maybe(GEN_AI_INPUT_MESSAGES_KEY, request.content) + self.set_tag_maybe(GEN_AI_CONVERSATION_ID_KEY, request.conversation_id) self.set_tag_maybe(GEN_AI_OPERATION_NAME_KEY, details.operationName.value) self.set_tag_maybe(GEN_AI_REQUEST_MODEL_KEY, details.model) @@ -124,10 +120,20 @@ def __init__( self.set_tag_maybe(SERVER_PORT_KEY, str(details.endpoint.port)) # Set request metadata if provided - if request and request.channel: + if request.channel: self.set_tag_maybe(CHANNEL_NAME_KEY, request.channel.name) self.set_tag_maybe(CHANNEL_LINK_KEY, request.channel.link) + # Set user details if provided + if user_details: + self.set_tag_maybe(USER_ID_KEY, user_details.user_id) + self.set_tag_maybe(USER_EMAIL_KEY, user_details.user_email) + self.set_tag_maybe(USER_NAME_KEY, user_details.user_name) + self.set_tag_maybe( + GEN_AI_CALLER_CLIENT_IP_KEY, + validate_and_normalize_ip(user_details.user_client_ip), + ) + def record_input_messages(self, messages: List[str]) -> None: """Records the input messages for telemetry tracking. diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/invoke_agent_details.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/invoke_agent_details.py index 72b3d8e0..33f99540 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/invoke_agent_details.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/invoke_agent_details.py @@ -1,18 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Data class for invoke agent details. +# Data class for invoke agent scope details. from dataclasses import dataclass from urllib.parse import ParseResult -from .agent_details import AgentDetails - @dataclass -class InvokeAgentDetails: - """Details for agent invocation tracing.""" +class InvokeAgentScopeDetails: + """Scope-level configuration for agent invocation tracing.""" - details: AgentDetails endpoint: ParseResult | None = None - session_id: str | None = None diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/invoke_agent_scope.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/invoke_agent_scope.py index 2620c412..0e69fa81 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/invoke_agent_scope.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/invoke_agent_scope.py @@ -4,9 +4,7 @@ # Invoke agent scope for tracing agent invocation. import logging -from datetime import datetime -from opentelemetry.context import Context from opentelemetry.trace import SpanKind from .agent_details import AgentDetails @@ -20,7 +18,7 @@ GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY, GEN_AI_CALLER_AGENT_USER_ID_KEY, GEN_AI_CALLER_CLIENT_IP_KEY, - GEN_AI_EXECUTION_TYPE_KEY, + GEN_AI_CONVERSATION_ID_KEY, GEN_AI_INPUT_MESSAGES_KEY, GEN_AI_OUTPUT_MESSAGES_KEY, INVOKE_AGENT_OPERATION_NAME, @@ -31,11 +29,11 @@ USER_ID_KEY, USER_NAME_KEY, ) -from .invoke_agent_details import InvokeAgentDetails +from .invoke_agent_details import InvokeAgentScopeDetails from .models.caller_details import CallerDetails from .opentelemetry_scope import OpenTelemetryScope from .request import Request -from .tenant_details import TenantDetails +from .span_details import SpanDetails from .utils import safe_json_dumps, validate_and_normalize_ip logger = logging.getLogger(__name__) @@ -46,142 +44,123 @@ class InvokeAgentScope(OpenTelemetryScope): @staticmethod def start( - invoke_agent_details: InvokeAgentDetails, - tenant_details: TenantDetails, - request: Request | None = None, - caller_agent_details: AgentDetails | None = None, + request: Request, + scope_details: InvokeAgentScopeDetails, + agent_details: AgentDetails, caller_details: CallerDetails | None = None, - parent_context: Context | None = None, - start_time: datetime | None = None, - end_time: datetime | None = None, - span_kind: SpanKind | None = None, + span_details: SpanDetails | None = None, ) -> "InvokeAgentScope": """Create and start a new scope for agent invocation tracing. Args: - invoke_agent_details: The details of the agent invocation including endpoint, - agent information, and session context - tenant_details: The details of the tenant - request: Optional request details for additional context - caller_agent_details: Optional details of the caller agent - caller_details: Optional details of the non-agentic caller - parent_context: Optional OpenTelemetry Context used to link this span to an - upstream operation. Use ``extract_context_from_headers()`` to convert a - Context from HTTP headers containing W3C traceparent. - start_time: Optional explicit start time as a datetime object. - end_time: Optional explicit end time as a datetime object. - span_kind: Optional span kind override. Defaults to ``SpanKind.CLIENT``. - Use ``SpanKind.SERVER`` when the agent is receiving an inbound request. + request: Request details for the invocation + scope_details: Scope-level configuration (endpoint) + agent_details: The details of the agent being invoked + caller_details: Optional composite caller details (human user and/or + calling agent for A2A scenarios) + span_details: Optional span configuration (parent context, timing, kind) Returns: A new InvokeAgentScope instance """ return InvokeAgentScope( - invoke_agent_details, - tenant_details, request, - caller_agent_details, + scope_details, + agent_details, caller_details, - parent_context, - start_time, - end_time, - span_kind, + span_details, ) def __init__( self, - invoke_agent_details: InvokeAgentDetails, - tenant_details: TenantDetails, - request: Request | None = None, - caller_agent_details: AgentDetails | None = None, + request: Request, + scope_details: InvokeAgentScopeDetails, + agent_details: AgentDetails, caller_details: CallerDetails | None = None, - parent_context: Context | None = None, - start_time: datetime | None = None, - end_time: datetime | None = None, - span_kind: SpanKind | None = None, + span_details: SpanDetails | None = None, ): """Initialize the agent invocation scope. Args: - invoke_agent_details: The details of the agent invocation - tenant_details: The details of the tenant - request: Optional request details for additional context - caller_agent_details: Optional details of the caller agent - caller_details: Optional details of the non-agentic caller - parent_context: Optional OpenTelemetry Context used to link this span to an - upstream operation. Use ``extract_context_from_headers()`` to convert a - Context from HTTP headers containing W3C traceparent. - start_time: Optional explicit start time as a datetime object. - end_time: Optional explicit end time as a datetime object. - span_kind: Optional span kind override. Defaults to ``SpanKind.CLIENT``. - Use ``SpanKind.SERVER`` when the agent is receiving an inbound request. + request: Request details for the invocation + scope_details: Scope-level configuration (endpoint) + agent_details: The details of the agent being invoked + caller_details: Optional composite caller details (human user and/or + calling agent for A2A scenarios) + span_details: Optional span configuration (parent context, timing, kind) """ activity_name = INVOKE_AGENT_OPERATION_NAME - if invoke_agent_details.details.agent_name: - activity_name = ( - f"{INVOKE_AGENT_OPERATION_NAME} {invoke_agent_details.details.agent_name}" - ) + if agent_details.agent_name: + activity_name = f"{INVOKE_AGENT_OPERATION_NAME} {agent_details.agent_name}" + + kind = SpanKind.CLIENT + parent_context = None + start_time = None + end_time = None + if span_details is not None: + if span_details.span_kind is not None: + kind = span_details.span_kind + parent_context = span_details.parent_context + start_time = span_details.start_time + end_time = span_details.end_time super().__init__( - kind=span_kind if span_kind is not None else SpanKind.CLIENT, + kind=kind, operation_name=INVOKE_AGENT_OPERATION_NAME, activity_name=activity_name, - agent_details=invoke_agent_details.details, - tenant_details=tenant_details, + agent_details=agent_details, parent_context=parent_context, start_time=start_time, end_time=end_time, ) - endpoint, _, session_id = ( - invoke_agent_details.endpoint, - invoke_agent_details.details, - invoke_agent_details.session_id, - ) + self.set_tag_maybe(SESSION_ID_KEY, request.session_id) + self.set_tag_maybe(GEN_AI_CONVERSATION_ID_KEY, request.conversation_id) - self.set_tag_maybe(SESSION_ID_KEY, session_id) + endpoint = scope_details.endpoint if endpoint: self.set_tag_maybe(SERVER_ADDRESS_KEY, endpoint.hostname) - - # Only record port if it is different from 443 if endpoint.port and endpoint.port != 443: self.set_tag_maybe(SERVER_PORT_KEY, endpoint.port) - # Set request metadata if provided - if request: - if request.channel: - self.set_tag_maybe(CHANNEL_NAME_KEY, request.channel.name) - self.set_tag_maybe(CHANNEL_LINK_KEY, request.channel.link) - - self.set_tag_maybe( - GEN_AI_EXECUTION_TYPE_KEY, - request.execution_type.value if request.execution_type else None, - ) + # Set request metadata + if request.channel: + self.set_tag_maybe(CHANNEL_NAME_KEY, request.channel.name) + self.set_tag_maybe(CHANNEL_LINK_KEY, request.channel.link) + if request.content: self.set_tag_maybe(GEN_AI_INPUT_MESSAGES_KEY, safe_json_dumps([request.content])) # Set caller details tags if caller_details: - self.set_tag_maybe(USER_ID_KEY, caller_details.caller_id) - self.set_tag_maybe(USER_EMAIL_KEY, caller_details.caller_upn) - self.set_tag_maybe(USER_NAME_KEY, caller_details.caller_name) - # Validate and set caller client IP - self.set_tag_maybe( - GEN_AI_CALLER_CLIENT_IP_KEY, - validate_and_normalize_ip(caller_details.caller_client_ip), - ) - - # Set caller agent details tags - if caller_agent_details: - self.set_tag_maybe(GEN_AI_CALLER_AGENT_NAME_KEY, caller_agent_details.agent_name) - self.set_tag_maybe(GEN_AI_CALLER_AGENT_ID_KEY, caller_agent_details.agent_id) - self.set_tag_maybe( - GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY, caller_agent_details.agent_blueprint_id - ) - self.set_tag_maybe(GEN_AI_CALLER_AGENT_USER_ID_KEY, caller_agent_details.agent_auid) - self.set_tag_maybe(GEN_AI_CALLER_AGENT_EMAIL_KEY, caller_agent_details.agent_upn) - self.set_tag_maybe( - GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY, caller_agent_details.agent_platform_id - ) + user_details = caller_details.user_details + if user_details: + self.set_tag_maybe(USER_ID_KEY, user_details.user_id) + self.set_tag_maybe(USER_EMAIL_KEY, user_details.user_email) + self.set_tag_maybe(USER_NAME_KEY, user_details.user_name) + self.set_tag_maybe( + GEN_AI_CALLER_CLIENT_IP_KEY, + validate_and_normalize_ip(user_details.user_client_ip), + ) + + # Set caller agent details tags + caller_agent_details = caller_details.caller_agent_details + if caller_agent_details: + self.set_tag_maybe(GEN_AI_CALLER_AGENT_NAME_KEY, caller_agent_details.agent_name) + self.set_tag_maybe(GEN_AI_CALLER_AGENT_ID_KEY, caller_agent_details.agent_id) + self.set_tag_maybe( + GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY, + caller_agent_details.agent_blueprint_id, + ) + self.set_tag_maybe( + GEN_AI_CALLER_AGENT_USER_ID_KEY, caller_agent_details.agentic_user_id + ) + self.set_tag_maybe( + GEN_AI_CALLER_AGENT_EMAIL_KEY, caller_agent_details.agentic_user_email + ) + self.set_tag_maybe( + GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY, + caller_agent_details.agent_platform_id, + ) def record_response(self, response: str) -> None: """Record response information for telemetry tracking. diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/middleware/baggage_builder.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/middleware/baggage_builder.py index f788e539..b61411f4 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/middleware/baggage_builder.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/middleware/baggage_builder.py @@ -94,11 +94,11 @@ def agent_id(self, value: str | None) -> "BaggageBuilder": self._set(GEN_AI_AGENT_ID_KEY, value) return self - def agent_auid(self, value: str | None) -> "BaggageBuilder": - """Set the agent AUID baggage value. + def agentic_user_id(self, value: str | None) -> "BaggageBuilder": + """Set the agentic user ID baggage value. Args: - value: The agent AUID + value: The agentic user ID Returns: Self for method chaining @@ -106,11 +106,11 @@ def agent_auid(self, value: str | None) -> "BaggageBuilder": self._set(GEN_AI_AGENT_AUID_KEY, value) return self - def agent_upn(self, value: str | None) -> "BaggageBuilder": - """Set the agent UPN baggage value. + def agentic_user_email(self, value: str | None) -> "BaggageBuilder": + """Set the agentic user email baggage value. Args: - value: The agent UPN + value: The agentic user email Returns: Self for method chaining @@ -130,11 +130,11 @@ def agent_blueprint_id(self, value: str | None) -> "BaggageBuilder": self._set(GEN_AI_AGENT_BLUEPRINT_ID_KEY, value) return self - def caller_id(self, value: str | None) -> "BaggageBuilder": - """Set the caller ID baggage value. + def user_id(self, value: str | None) -> "BaggageBuilder": + """Set the user ID baggage value. Args: - value: The caller ID + value: The user ID Returns: Self for method chaining @@ -152,18 +152,18 @@ def agent_description(self, value: str | None) -> "BaggageBuilder": self._set(GEN_AI_AGENT_DESCRIPTION_KEY, value) return self - def caller_name(self, value: str | None) -> "BaggageBuilder": - """Set the caller name baggage value.""" + def user_name(self, value: str | None) -> "BaggageBuilder": + """Set the user name baggage value.""" self._set(USER_NAME_KEY, value) return self - def caller_upn(self, value: str | None) -> "BaggageBuilder": - """Set the caller UPN baggage value.""" + def user_email(self, value: str | None) -> "BaggageBuilder": + """Set the user email baggage value.""" self._set(USER_EMAIL_KEY, value) return self - def caller_client_ip(self, value: str | None) -> "BaggageBuilder": - """Set the caller client IP baggage value.""" + def user_client_ip(self, value: str | None) -> "BaggageBuilder": + """Set the user client IP baggage value.""" self._set(GEN_AI_CALLER_CLIENT_IP_KEY, validate_and_normalize_ip(value)) return self diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/models/caller_details.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/models/caller_details.py index eee3fc9d..2fb79e7c 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/models/caller_details.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/models/caller_details.py @@ -2,21 +2,23 @@ # Licensed under the MIT License. from dataclasses import dataclass -from typing import Optional +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from ..agent_details import AgentDetails + +from .user_details import UserDetails @dataclass class CallerDetails: - """Details about the caller that invoked an agent.""" - - caller_id: Optional[str] = None - """The unique identifier for the caller.""" + """Composite caller details for agent-to-agent (A2A) scenarios. - caller_upn: Optional[str] = None - """The User Principal Name (UPN) of the caller.""" + Groups the human caller identity and the calling agent identity together. + """ - caller_name: Optional[str] = None - """The human-readable name of the caller.""" + user_details: Optional[UserDetails] = None + """Details about the human user in the call chain.""" - caller_client_ip: Optional[str] = None - """The client IP address of the caller.""" + caller_agent_details: Optional["AgentDetails"] = None + """Details about the calling agent in A2A scenarios.""" diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/models/user_details.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/models/user_details.py new file mode 100644 index 00000000..93f71a0f --- /dev/null +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/models/user_details.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class UserDetails: + """Details about the human user that invoked an agent.""" + + user_id: Optional[str] = None + """The unique identifier for the user.""" + + user_email: Optional[str] = None + """The email address of the user.""" + + user_name: Optional[str] = None + """The human-readable name of the user.""" + + user_client_ip: Optional[str] = None + """The client IP address of the user.""" diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/opentelemetry_scope.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/opentelemetry_scope.py index 5839982d..1691300d 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/opentelemetry_scope.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/opentelemetry_scope.py @@ -24,6 +24,7 @@ from .constants import ( ENABLE_A365_OBSERVABILITY, ENABLE_OBSERVABILITY, + ERROR_TYPE_CANCELLED, ERROR_TYPE_KEY, GEN_AI_AGENT_AUID_KEY, GEN_AI_AGENT_BLUEPRINT_ID_KEY, @@ -32,7 +33,6 @@ GEN_AI_AGENT_ID_KEY, GEN_AI_AGENT_NAME_KEY, GEN_AI_AGENT_PLATFORM_ID_KEY, - GEN_AI_CONVERSATION_ID_KEY, GEN_AI_ICON_URI_KEY, GEN_AI_OPERATION_NAME_KEY, GEN_AI_OUTPUT_MESSAGES_KEY, @@ -49,7 +49,6 @@ if TYPE_CHECKING: from .agent_details import AgentDetails - from .tenant_details import TenantDetails # Create logger for this module - inherits from 'microsoft_agents_a365.observability.core' logger = logging.getLogger(__name__) @@ -98,7 +97,6 @@ def __init__( operation_name: str, activity_name: str, agent_details: "AgentDetails | None" = None, - tenant_details: "TenantDetails | None" = None, parent_context: Context | None = None, start_time: datetime | None = None, end_time: datetime | None = None, @@ -112,7 +110,6 @@ def __init__( operation_name: The name of the operation being traced activity_name: The name of the activity for display purposes agent_details: Optional agent details - tenant_details: Optional tenant details parent_context: Optional OpenTelemetry Context used to link this span to an upstream operation. Use ``extract_context_from_headers()`` to extract a Context from HTTP headers containing W3C traceparent. @@ -186,8 +183,8 @@ def __init__( self.set_tag_maybe( GEN_AI_AGENT_DESCRIPTION_KEY, agent_details.agent_description ) - self.set_tag_maybe(GEN_AI_AGENT_AUID_KEY, agent_details.agent_auid) - self.set_tag_maybe(GEN_AI_AGENT_EMAIL_KEY, agent_details.agent_upn) + self.set_tag_maybe(GEN_AI_AGENT_AUID_KEY, agent_details.agentic_user_id) + self.set_tag_maybe(GEN_AI_AGENT_EMAIL_KEY, agent_details.agentic_user_email) self.set_tag_maybe( GEN_AI_AGENT_BLUEPRINT_ID_KEY, agent_details.agent_blueprint_id ) @@ -195,15 +192,10 @@ def __init__( GEN_AI_AGENT_PLATFORM_ID_KEY, agent_details.agent_platform_id ) self.set_tag_maybe(TENANT_ID_KEY, agent_details.tenant_id) - self.set_tag_maybe(GEN_AI_CONVERSATION_ID_KEY, agent_details.conversation_id) self.set_tag_maybe(GEN_AI_ICON_URI_KEY, agent_details.icon_uri) # Set provider name dynamically from agent details self.set_tag_maybe(GEN_AI_PROVIDER_NAME_KEY, agent_details.provider_name) - # Set tenant details if provided - if tenant_details: - self.set_tag_maybe(TENANT_ID_KEY, str(tenant_details.tenant_id)) - def record_error(self, exception: Exception) -> None: """Record an error in the span. @@ -229,7 +221,7 @@ def record_response(self, response: str) -> None: def record_cancellation(self) -> None: """Record task cancellation.""" if self._span and self._is_telemetry_enabled(): - self._error_type = "TaskCanceledException" + self._error_type = ERROR_TYPE_CANCELLED self._span.set_attribute(ERROR_TYPE_KEY, self._error_type) self._span.set_status(Status(StatusCode.ERROR, "Task was cancelled")) diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/request.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/request.py index 5201cf0f..1523fbab 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/request.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/request.py @@ -5,7 +5,6 @@ from dataclasses import dataclass -from .execution_type import ExecutionType from .channel import Channel @@ -13,7 +12,7 @@ class Request: """Request details for agent execution.""" - content: str - execution_type: ExecutionType + content: str | None = None session_id: str | None = None channel: Channel | None = None + conversation_id: str | None = None diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/span_details.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/span_details.py new file mode 100644 index 00000000..bb913108 --- /dev/null +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/span_details.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from dataclasses import dataclass +from datetime import datetime + +from opentelemetry.context import Context +from opentelemetry.trace import SpanKind + + +@dataclass +class SpanDetails: + """Groups span configuration for scope construction.""" + + span_kind: SpanKind | None = None + """Optional span kind override.""" + + parent_context: Context | None = None + """Optional OpenTelemetry Context used to link this span to an upstream operation.""" + + start_time: datetime | None = None + """Optional explicit start time as a datetime object.""" + + end_time: datetime | None = None + """Optional explicit end time as a datetime object.""" diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/spans_scopes/output_scope.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/spans_scopes/output_scope.py index 06689967..f4836787 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/spans_scopes/output_scope.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/spans_scopes/output_scope.py @@ -1,16 +1,21 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from datetime import datetime - -from opentelemetry.context import Context - from ..agent_details import AgentDetails -from ..constants import GEN_AI_OUTPUT_MESSAGES_KEY +from ..constants import ( + GEN_AI_CALLER_CLIENT_IP_KEY, + GEN_AI_CONVERSATION_ID_KEY, + GEN_AI_OUTPUT_MESSAGES_KEY, + USER_EMAIL_KEY, + USER_ID_KEY, + USER_NAME_KEY, +) from ..models.response import Response +from ..models.user_details import UserDetails from ..opentelemetry_scope import OpenTelemetryScope -from ..tenant_details import TenantDetails -from ..utils import safe_json_dumps +from ..request import Request +from ..span_details import SpanDetails +from ..utils import safe_json_dumps, validate_and_normalize_ip OUTPUT_OPERATION_NAME = "output_messages" @@ -22,70 +27,79 @@ class OutputScope(OpenTelemetryScope): @staticmethod def start( - agent_details: AgentDetails, - tenant_details: TenantDetails, + request: Request, response: Response, - parent_context: Context | None = None, - start_time: datetime | None = None, - end_time: datetime | None = None, + agent_details: AgentDetails, + user_details: UserDetails | None = None, + span_details: SpanDetails | None = None, ) -> "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 + request: Request details for the output response: The response details from the agent - parent_context: Optional OpenTelemetry Context used to link this span to an - upstream operation. Use ``extract_context_from_headers()`` to convert a - Context from HTTP headers containing W3C traceparent. - start_time: Optional explicit start time as a datetime object. - end_time: Optional explicit end time as a datetime object. + agent_details: The details of the agent + user_details: Optional human user details + span_details: Optional span configuration (parent context, timing) Returns: A new OutputScope instance """ - return OutputScope( - agent_details, tenant_details, response, parent_context, start_time, end_time - ) + return OutputScope(request, response, agent_details, user_details, span_details) def __init__( self, - agent_details: AgentDetails, - tenant_details: TenantDetails, + request: Request, response: Response, - parent_context: Context | None = None, - start_time: datetime | None = None, - end_time: datetime | None = None, + agent_details: AgentDetails, + user_details: UserDetails | None = None, + span_details: SpanDetails | None = None, ): """Initialize the output scope. Args: - agent_details: The details of the agent - tenant_details: The details of the tenant + request: Request details for the output response: The response details from the agent - parent_context: Optional OpenTelemetry Context used to link this span to an - upstream operation. Use ``extract_context_from_headers()`` to convert a - Context from HTTP headers containing W3C traceparent. - start_time: Optional explicit start time as a datetime object. - end_time: Optional explicit end time as a datetime object. + agent_details: The details of the agent + user_details: Optional human user details + span_details: Optional span configuration (parent context, timing) """ + parent_context = None + start_time = None + end_time = None + if span_details is not None: + parent_context = span_details.parent_context + start_time = span_details.start_time + end_time = span_details.end_time + 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, parent_context=parent_context, start_time=start_time, end_time=end_time, ) + self.set_tag_maybe(GEN_AI_CONVERSATION_ID_KEY, request.conversation_id) + # Initialize accumulated messages list self._output_messages: list[str] = list(response.messages) # Set response messages self.set_tag_maybe(GEN_AI_OUTPUT_MESSAGES_KEY, safe_json_dumps(self._output_messages)) + # Set user details if provided + if user_details: + self.set_tag_maybe(USER_ID_KEY, user_details.user_id) + self.set_tag_maybe(USER_EMAIL_KEY, user_details.user_email) + self.set_tag_maybe(USER_NAME_KEY, user_details.user_name) + self.set_tag_maybe( + GEN_AI_CALLER_CLIENT_IP_KEY, + validate_and_normalize_ip(user_details.user_client_ip), + ) + def record_output_messages(self, messages: list[str]) -> None: """Records the output messages for telemetry tracking. diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/trace_processor/util.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/trace_processor/util.py index 8e5a9472..45b95b56 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/trace_processor/util.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/trace_processor/util.py @@ -39,5 +39,4 @@ consts.GEN_AI_CALLER_AGENT_EMAIL_KEY, # microsoft.a365.caller.agent.user.email consts.GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY, # microsoft.a365.caller.agent.blueprint.id consts.GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY, # microsoft.a365.caller.agent.platform.id - consts.GEN_AI_EXECUTION_TYPE_KEY, # gen_ai.execution.type ] diff --git a/libraries/microsoft-agents-a365-observability-extensions-langchain/CHANGELOG.md b/libraries/microsoft-agents-a365-observability-extensions-langchain/CHANGELOG.md new file mode 100644 index 00000000..1452eaa5 --- /dev/null +++ b/libraries/microsoft-agents-a365-observability-extensions-langchain/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog — microsoft-agents-a365-observability-extensions-langchain + +All notable changes to this package will be documented in this file. + +## [Unreleased] + +### Changed + +- Removed `set_execution_type()` function and all `ExecutionType` / `GEN_AI_EXECUTION_TYPE_KEY` usage. diff --git a/libraries/microsoft-agents-a365-observability-extensions-langchain/microsoft_agents_a365/observability/extensions/langchain/tracer.py b/libraries/microsoft-agents-a365-observability-extensions-langchain/microsoft_agents_a365/observability/extensions/langchain/tracer.py index b5cb3dcd..992f479a 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-langchain/microsoft_agents_a365/observability/extensions/langchain/tracer.py +++ b/libraries/microsoft-agents-a365-observability-extensions-langchain/microsoft_agents_a365/observability/extensions/langchain/tracer.py @@ -49,7 +49,6 @@ model_name, output_messages, prompts, - set_execution_type, token_counts, tools, ) @@ -253,7 +252,6 @@ def _update_span(span: Span, run: Run) -> None: dict( flatten( chain( - set_execution_type(), add_operation_type(run), invoke_agent_input_message(run.inputs), invoke_agent_output_message(run.outputs), diff --git a/libraries/microsoft-agents-a365-observability-extensions-langchain/microsoft_agents_a365/observability/extensions/langchain/utils.py b/libraries/microsoft-agents-a365-observability-extensions-langchain/microsoft_agents_a365/observability/extensions/langchain/utils.py index 72fe66d2..256496ad 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-langchain/microsoft_agents_a365/observability/extensions/langchain/utils.py +++ b/libraries/microsoft-agents-a365-observability-extensions-langchain/microsoft_agents_a365/observability/extensions/langchain/utils.py @@ -10,7 +10,6 @@ from langchain_core.tracers.schemas import Run from microsoft_agents_a365.observability.core.constants import ( EXECUTE_TOOL_OPERATION_NAME, - GEN_AI_EXECUTION_TYPE_KEY, GEN_AI_INPUT_MESSAGES_KEY, GEN_AI_OPERATION_NAME_KEY, GEN_AI_OUTPUT_MESSAGES_KEY, @@ -30,7 +29,6 @@ INVOKE_AGENT_OPERATION_NAME, SESSION_ID_KEY, ) -from microsoft_agents_a365.observability.core.execution_type import ExecutionType from microsoft_agents_a365.observability.core.inference_operation_type import InferenceOperationType from microsoft_agents_a365.observability.core.utils import ( get_first_value, @@ -693,8 +691,3 @@ def invoke_agent_output_message( if content and isinstance(content, str) and content.strip(): yield GEN_AI_OUTPUT_MESSAGES_KEY, content return - - -def set_execution_type() -> Iterator[tuple[str, str]]: - """Yields the execution type as human_to_agent.""" - yield GEN_AI_EXECUTION_TYPE_KEY, ExecutionType.HUMAN_TO_AGENT.value diff --git a/libraries/microsoft-agents-a365-observability-extensions-openai/CHANGELOG.md b/libraries/microsoft-agents-a365-observability-extensions-openai/CHANGELOG.md new file mode 100644 index 00000000..e1d6093c --- /dev/null +++ b/libraries/microsoft-agents-a365-observability-extensions-openai/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog — microsoft-agents-a365-observability-extensions-openai + +All notable changes to this package will be documented in this file. + +## [Unreleased] + +### Changed + +- Removed `ExecutionType` usage from `OpenAIAgentsTraceProcessor`. Spans no longer set `gen_ai.execution.type`. diff --git a/libraries/microsoft-agents-a365-observability-extensions-openai/microsoft_agents_a365/observability/extensions/openai/trace_processor.py b/libraries/microsoft-agents-a365-observability-extensions-openai/microsoft_agents_a365/observability/extensions/openai/trace_processor.py index 2df88190..1a53ca60 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-openai/microsoft_agents_a365/observability/extensions/openai/trace_processor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-openai/microsoft_agents_a365/observability/extensions/openai/trace_processor.py @@ -22,7 +22,6 @@ from microsoft_agents_a365.observability.core.constants import ( CUSTOM_PARENT_SPAN_ID_KEY, EXECUTE_TOOL_OPERATION_NAME, - GEN_AI_EXECUTION_TYPE_KEY, GEN_AI_INPUT_MESSAGES_KEY, GEN_AI_OPERATION_NAME_KEY, GEN_AI_OUTPUT_MESSAGES_KEY, @@ -32,7 +31,6 @@ GEN_AI_TOOL_TYPE_KEY, INVOKE_AGENT_OPERATION_NAME, ) -from microsoft_agents_a365.observability.core.execution_type import ExecutionType from microsoft_agents_a365.observability.core.utils import as_utc_nano, safe_json_dumps from opentelemetry import trace as ot_trace from opentelemetry.context import attach, detach @@ -247,7 +245,6 @@ def on_span_end(self, span: Span[Any]) -> None: while len(self._reverse_handoffs_dict) > self._MAX_HANDOFFS_IN_FLIGHT: self._reverse_handoffs_dict.popitem(last=False) elif isinstance(data, AgentSpanData): - otel_span.set_attribute(GEN_AI_EXECUTION_TYPE_KEY, ExecutionType.HUMAN_TO_AGENT.value) # Lookup the parent node if exists key = f"{data.name}:{span.trace_id}" if parent_node := self._reverse_handoffs_dict.pop(key, None): diff --git a/libraries/microsoft-agents-a365-observability-extensions-semantickernel/CHANGELOG.md b/libraries/microsoft-agents-a365-observability-extensions-semantickernel/CHANGELOG.md new file mode 100644 index 00000000..6336360a --- /dev/null +++ b/libraries/microsoft-agents-a365-observability-extensions-semantickernel/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog — microsoft-agents-a365-observability-extensions-semantickernel + +All notable changes to this package will be documented in this file. + +## [Unreleased] + +### Changed + +- Removed `ExecutionType` and `INVOKE_AGENT_OPERATION_NAME` usage from `SpanProcessor`. Removed dead code block for invoke agent spans. diff --git a/libraries/microsoft-agents-a365-observability-extensions-semantickernel/microsoft_agents_a365/observability/extensions/semantickernel/span_processor.py b/libraries/microsoft-agents-a365-observability-extensions-semantickernel/microsoft_agents_a365/observability/extensions/semantickernel/span_processor.py index 6763f71d..63c6860f 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-semantickernel/microsoft_agents_a365/observability/extensions/semantickernel/span_processor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-semantickernel/microsoft_agents_a365/observability/extensions/semantickernel/span_processor.py @@ -2,11 +2,8 @@ # Licensed under the MIT License. from microsoft_agents_a365.observability.core.constants import ( - GEN_AI_EXECUTION_TYPE_KEY, GEN_AI_OPERATION_NAME_KEY, - INVOKE_AGENT_OPERATION_NAME, ) -from microsoft_agents_a365.observability.core.execution_type import ExecutionType from microsoft_agents_a365.observability.core.inference_operation_type import InferenceOperationType from microsoft_agents_a365.observability.core.utils import extract_model_name from opentelemetry import context as context_api @@ -41,11 +38,6 @@ def on_start(self, span: Span, parent_context: context_api.Context | None) -> No model_name = extract_model_name(span.name) span.update_name(f"{InferenceOperationType.CHAT.value.lower()} {model_name}") - if span.name.startswith(INVOKE_AGENT_OPERATION_NAME): - span.set_attribute( - GEN_AI_EXECUTION_TYPE_KEY, ExecutionType.HUMAN_TO_AGENT.value.lower() - ) - def on_end(self, span: ReadableSpan) -> None: """ Called when a span ends. diff --git a/libraries/microsoft-agents-a365-observability-hosting/CHANGELOG.md b/libraries/microsoft-agents-a365-observability-hosting/CHANGELOG.md new file mode 100644 index 00000000..456a642a --- /dev/null +++ b/libraries/microsoft-agents-a365-observability-hosting/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog — microsoft-agents-a365-observability-hosting + +All notable changes to this package will be documented in this file. + +## [Unreleased] + +### Changed + +- **`OutputLoggingMiddleware`** — Updated to use new scope APIs (`Request`, `SpanDetails`, `UserDetails`). Removed `TenantDetails` and `ExecutionType` dependencies. Middleware no longer gates on tenant presence. +- **`scope_helpers/utils.py`** — Removed `get_execution_type_pair()`. +- **`populate_baggage.py`** / **`populate_invoke_agent_scope.py`** — Removed execution type population. diff --git a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/middleware/output_logging_middleware.py b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/middleware/output_logging_middleware.py index 1820e3e8..57587a40 100644 --- a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/middleware/output_logging_middleware.py +++ b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/middleware/output_logging_middleware.py @@ -14,22 +14,14 @@ from microsoft_agents_a365.observability.core.constants import ( CHANNEL_LINK_KEY, CHANNEL_NAME_KEY, - GEN_AI_CONVERSATION_ID_KEY, - GEN_AI_EXECUTION_TYPE_KEY, - USER_EMAIL_KEY, - USER_ID_KEY, - USER_NAME_KEY, ) -from microsoft_agents_a365.observability.core.models.caller_details import CallerDetails from microsoft_agents_a365.observability.core.models.response import Response +from microsoft_agents_a365.observability.core.models.user_details import UserDetails +from microsoft_agents_a365.observability.core.request import Request +from microsoft_agents_a365.observability.core.span_details import SpanDetails from microsoft_agents_a365.observability.core.spans_scopes.output_scope import OutputScope -from microsoft_agents_a365.observability.core.tenant_details import TenantDetails from microsoft_agents_a365.observability.core.utils import extract_context_from_headers -from ..scope_helpers.utils import ( - get_execution_type_pair, -) - logger = logging.getLogger(__name__) # TurnState key for the parent trace context (W3C traceparent string). @@ -52,28 +44,21 @@ def _derive_agent_details(context: TurnContext) -> AgentDetails | None: return AgentDetails( agent_id=activity.get_agentic_instance_id() or "", agent_name=getattr(recipient, "name", None), - agent_auid=getattr(recipient, "aad_object_id", None), - agent_upn=activity.get_agentic_user(), + agentic_user_id=getattr(recipient, "aad_object_id", None), + agentic_user_email=activity.get_agentic_user(), agent_description=getattr(recipient, "role", None), tenant_id=getattr(recipient, "tenant_id", None), ) -def _derive_tenant_details(context: TurnContext) -> TenantDetails | None: - """Derive tenant details from the activity recipient.""" - tenant_id = getattr(getattr(context.activity, "recipient", None), "tenant_id", None) - return TenantDetails(tenant_id=tenant_id) if tenant_id else None - - -def _derive_caller_details(context: TurnContext) -> CallerDetails | None: - """Derive caller identity details from the activity from property.""" +def _derive_user_details(context: TurnContext) -> UserDetails | None: + """Derive user identity details from the activity from property.""" frm = getattr(context.activity, "from_property", None) if not frm: return None - return CallerDetails( - caller_id=getattr(frm, "aad_object_id", None), - caller_upn=getattr(frm, "agentic_user_id", None), - caller_name=getattr(frm, "name", None), + return UserDetails( + user_id=getattr(frm, "aad_object_id", None), + user_name=getattr(frm, "name", None), ) @@ -99,14 +84,6 @@ def _derive_channel( return {"name": channel_name, "link": sub_channel} -def _derive_execution_type(context: TurnContext) -> str | None: - """Derive execution type from the activity.""" - pairs = list(get_execution_type_pair(context.activity)) - if pairs: - return pairs[0][1] - return None - - class OutputLoggingMiddleware: """Middleware that creates :class:`OutputScope` spans for outgoing messages. @@ -123,26 +100,22 @@ async def on_turn( logic: Callable[[TurnContext], Awaitable], ) -> None: agent_details = _derive_agent_details(context) - tenant_details = _derive_tenant_details(context) - if not agent_details or not tenant_details: + if not agent_details: await logic() return - caller_details = _derive_caller_details(context) + user_details = _derive_user_details(context) conversation_id = _derive_conversation_id(context) channel = _derive_channel(context) - execution_type = _derive_execution_type(context) context.on_send_activities( self._create_send_handler( context, agent_details, - tenant_details, - caller_details, + user_details, conversation_id, channel, - execution_type, ) ) @@ -152,11 +125,9 @@ def _create_send_handler( self, turn_context: TurnContext, agent_details: AgentDetails, - tenant_details: TenantDetails, - caller_details: CallerDetails | None, + user_details: UserDetails | None, conversation_id: str | None, channel: dict[str, str | None], - execution_type: str | None, ) -> Callable: """Create a send handler that wraps outgoing messages in OutputScope spans. @@ -187,24 +158,24 @@ async def handler( A365_PARENT_TRACEPARENT_KEY, ) + request = Request( + conversation_id=conversation_id, + ) + + span_details = SpanDetails(parent_context=parent_context) if parent_context else None + output_scope = OutputScope.start( - agent_details=agent_details, - tenant_details=tenant_details, + request=request, response=Response(messages=messages), - parent_context=parent_context, + agent_details=agent_details, + user_details=user_details, + span_details=span_details, ) # Set additional attributes on the scope - output_scope.set_tag_maybe(GEN_AI_CONVERSATION_ID_KEY, conversation_id) - output_scope.set_tag_maybe(GEN_AI_EXECUTION_TYPE_KEY, execution_type) output_scope.set_tag_maybe(CHANNEL_NAME_KEY, channel.get("name")) output_scope.set_tag_maybe(CHANNEL_LINK_KEY, channel.get("link")) - if caller_details: - output_scope.set_tag_maybe(USER_ID_KEY, caller_details.caller_id) - output_scope.set_tag_maybe(USER_EMAIL_KEY, caller_details.caller_upn) - output_scope.set_tag_maybe(USER_NAME_KEY, caller_details.caller_name) - try: await send_next() except Exception as error: diff --git a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/populate_baggage.py b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/populate_baggage.py index 3fe3ecc1..f54d39b3 100644 --- a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/populate_baggage.py +++ b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/populate_baggage.py @@ -13,7 +13,6 @@ get_caller_pairs, get_channel_pairs, get_conversation_pairs, - get_execution_type_pair, get_target_agent_pairs, get_tenant_id_pair, ) @@ -24,7 +23,6 @@ def _iter_all_pairs(turn_context: TurnContext) -> Iterator[tuple[str, Any]]: if not activity: return yield from get_caller_pairs(activity) - yield from get_execution_type_pair(activity) yield from get_target_agent_pairs(activity) yield from get_tenant_id_pair(activity) yield from get_channel_pairs(activity) diff --git a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/populate_invoke_agent_scope.py b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/populate_invoke_agent_scope.py index e6fc6ac6..cea776d7 100644 --- a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/populate_invoke_agent_scope.py +++ b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/populate_invoke_agent_scope.py @@ -11,7 +11,6 @@ get_caller_pairs, get_channel_pairs, get_conversation_pairs, - get_execution_type_pair, get_target_agent_pairs, get_tenant_id_pair, ) @@ -36,7 +35,6 @@ def populate(scope: InvokeAgentScope, turn_context: TurnContext) -> InvokeAgentS activity = turn_context.activity scope.record_attributes(get_caller_pairs(activity)) - scope.record_attributes(get_execution_type_pair(activity)) scope.record_attributes(get_target_agent_pairs(activity)) scope.record_attributes(get_tenant_id_pair(activity)) scope.record_attributes(get_channel_pairs(activity)) diff --git a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py index 570e8572..737f57f0 100644 --- a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py +++ b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py @@ -14,13 +14,11 @@ GEN_AI_AGENT_NAME_KEY, GEN_AI_CONVERSATION_ID_KEY, GEN_AI_CONVERSATION_ITEM_LINK_KEY, - GEN_AI_EXECUTION_TYPE_KEY, TENANT_ID_KEY, USER_EMAIL_KEY, USER_ID_KEY, USER_NAME_KEY, ) -from microsoft_agents_a365.observability.core.execution_type import ExecutionType AGENT_ROLE = "agenticUser" @@ -43,19 +41,6 @@ def get_caller_pairs(activity: Activity) -> Iterator[tuple[str, Any]]: yield USER_EMAIL_KEY, frm.agentic_user_id -def get_execution_type_pair(activity: Activity) -> Iterator[tuple[str, Any]]: - frm = activity.from_property - rec = activity.recipient - is_agentic_caller = _is_agentic(frm) - is_agentic_recipient = _is_agentic(rec) - exec_type = ( - ExecutionType.AGENT_TO_AGENT.value - if (is_agentic_caller and is_agentic_recipient) - else ExecutionType.HUMAN_TO_AGENT.value - ) - yield GEN_AI_EXECUTION_TYPE_KEY, exec_type - - def get_target_agent_pairs(activity: Activity) -> Iterator[tuple[str, Any]]: rec = activity.recipient if not rec: diff --git a/tests/observability/core/test_baggage_builder.py b/tests/observability/core/test_baggage_builder.py index 1e5dd2b3..77002742 100644 --- a/tests/observability/core/test_baggage_builder.py +++ b/tests/observability/core/test_baggage_builder.py @@ -83,11 +83,11 @@ def test_all_baggage_keys(self): BaggageBuilder() .tenant_id("tenant-1") .agent_id("agent-1") - .agent_auid("auid-1") - .agent_upn("upn-1") + .agentic_user_id("auid-1") + .agentic_user_email("upn-1") .agent_blueprint_id("blueprint-1") - .caller_id("caller-1") - .caller_client_ip("192.168.1.100") + .user_id("caller-1") + .user_client_ip("192.168.1.100") .build() ): current_baggage = baggage.get_all() @@ -153,10 +153,10 @@ def test_baggage_reset_after_scope_exit(self): BaggageBuilder() .tenant_id("test-tenant") .agent_id("test-agent") - .agent_auid("test-auid") - .agent_upn("test-upn") + .agentic_user_id("test-auid") + .agentic_user_email("test-upn") .agent_blueprint_id("test-blueprint") - .caller_id("test-caller") + .user_id("test-caller") .build() ): # Inside scope - verify all baggage values are set @@ -271,29 +271,29 @@ def test_channel_links_method(self): "https://teams.microsoft.com/channel/123", ) - def test_caller_client_ip_method(self): - """Test caller_client_ip method sets client IP baggage with validation.""" + def test_user_client_ip_method(self): + """Test user_client_ip method sets client IP baggage with validation.""" # Should exist and be callable - self.assertTrue(hasattr(self.builder, "caller_client_ip")) - self.assertTrue(callable(self.builder.caller_client_ip)) + self.assertTrue(hasattr(self.builder, "user_client_ip")) + self.assertTrue(callable(self.builder.user_client_ip)) # Test valid IPv4 address - with BaggageBuilder().caller_client_ip("192.168.1.100").build(): + with BaggageBuilder().user_client_ip("192.168.1.100").build(): current_baggage = baggage.get_all() self.assertEqual(current_baggage.get(GEN_AI_CALLER_CLIENT_IP_KEY), "192.168.1.100") # Test valid IPv6 address - with BaggageBuilder().caller_client_ip("2001:db8::1").build(): + with BaggageBuilder().user_client_ip("2001:db8::1").build(): current_baggage = baggage.get_all() self.assertEqual(current_baggage.get(GEN_AI_CALLER_CLIENT_IP_KEY), "2001:db8::1") # Test None value (should not set baggage) - with BaggageBuilder().caller_client_ip(None).build(): + with BaggageBuilder().user_client_ip(None).build(): current_baggage = baggage.get_all() self.assertIsNone(current_baggage.get(GEN_AI_CALLER_CLIENT_IP_KEY)) # Test invalid IP address (should be handled gracefully now) - with BaggageBuilder().caller_client_ip("not.an.ip.address").build(): + with BaggageBuilder().user_client_ip("not.an.ip.address").build(): current_baggage = baggage.get_all() # Should be None due to proper exception handling self.assertIsNone(current_baggage.get(GEN_AI_CALLER_CLIENT_IP_KEY)) diff --git a/tests/observability/core/test_custom_start_end_time.py b/tests/observability/core/test_custom_start_end_time.py index 080e4caf..5a74420f 100644 --- a/tests/observability/core/test_custom_start_end_time.py +++ b/tests/observability/core/test_custom_start_end_time.py @@ -14,7 +14,8 @@ from microsoft_agents_a365.observability.core import ( AgentDetails, ExecuteToolScope, - TenantDetails, + Request, + SpanDetails, ToolCallDetails, configure, get_tracer_provider, @@ -38,7 +39,6 @@ def setUpClass(cls): service_namespace="test-namespace", ) # Create test data - cls.tenant_details = TenantDetails(tenant_id="12345678-1234-5678-1234-567812345678") cls.agent_details = AgentDetails( agent_id="test-agent-123", agent_name="Test Agent", @@ -85,11 +85,10 @@ def test_custom_start_and_end_time_with_datetime(self): custom_end = datetime(2023, 11, 14, 22, 13, 25, tzinfo=UTC) # 5 seconds later scope = ExecuteToolScope.start( + Request(), self.tool_details, self.agent_details, - self.tenant_details, - start_time=custom_start, - end_time=custom_end, + span_details=SpanDetails(start_time=custom_start, end_time=custom_end), ) scope.dispose() @@ -111,11 +110,10 @@ def test_set_end_time_overrides_end_time(self): later_end = datetime(2023, 11, 14, 22, 13, 48, tzinfo=UTC) # 8 seconds after start scope = ExecuteToolScope.start( + Request(), self.tool_details, self.agent_details, - self.tenant_details, - start_time=custom_start, - end_time=initial_end, + span_details=SpanDetails(start_time=custom_start, end_time=initial_end), ) # Override the end time scope.set_end_time(later_end) @@ -131,9 +129,9 @@ def test_wall_clock_time_used_when_no_custom_times(self): """Test that wall-clock time is used when no custom times are provided.""" before = time.time_ns() scope = ExecuteToolScope.start( + Request(), self.tool_details, self.agent_details, - self.tenant_details, ) scope.dispose() after = time.time_ns() @@ -153,10 +151,10 @@ def test_only_start_time_provided(self): custom_start = datetime(2023, 11, 14, 22, 13, 20, tzinfo=UTC) scope = ExecuteToolScope.start( + Request(), self.tool_details, self.agent_details, - self.tenant_details, - start_time=custom_start, + span_details=SpanDetails(start_time=custom_start), ) scope.dispose() diff --git a/tests/observability/core/test_execute_tool_scope.py b/tests/observability/core/test_execute_tool_scope.py index f857299b..169d72d6 100644 --- a/tests/observability/core/test_execute_tool_scope.py +++ b/tests/observability/core/test_execute_tool_scope.py @@ -11,9 +11,8 @@ AgentDetails, Channel, ExecuteToolScope, - ExecutionType, Request, - TenantDetails, + SpanDetails, ToolCallDetails, configure, extract_context_from_headers, @@ -44,7 +43,6 @@ def setUpClass(cls): service_namespace="test-namespace", ) # Create test data - cls.tenant_details = TenantDetails(tenant_id="12345678-1234-5678-1234-567812345678") cls.agent_details = AgentDetails( agent_id="test-agent-123", agent_name="Test Agent", @@ -83,7 +81,7 @@ def tearDown(self): def test_record_response_method_exists(self): """Test that record_response method exists on ExecuteToolScope.""" - scope = ExecuteToolScope.start(self.tool_details, self.agent_details, self.tenant_details) + scope = ExecuteToolScope.start(Request(), self.tool_details, self.agent_details) if scope is not None: # Test that the method exists @@ -95,14 +93,11 @@ def test_request_metadata_set_on_span(self): """Test that request source metadata is set on span attributes.""" request = Request( content="Execute tool with request metadata", - execution_type=ExecutionType.AGENT_TO_AGENT, session_id="session-xyz", channel=Channel(name="Channel 1", link="Link to channel"), ) - scope = ExecuteToolScope.start( - self.tool_details, self.agent_details, self.tenant_details, request - ) + scope = ExecuteToolScope.start(request, self.tool_details, self.agent_details) if scope is not None: scope.dispose() @@ -143,10 +138,10 @@ def test_execute_tool_scope_with_parent_context(self): parent_context = extract_context_from_headers({"traceparent": traceparent}) with ExecuteToolScope.start( + Request(), self.tool_details, self.agent_details, - self.tenant_details, - parent_context=parent_context, + span_details=SpanDetails(parent_context=parent_context), ): pass @@ -167,7 +162,7 @@ def test_execute_tool_scope_with_parent_context(self): def test_span_kind_defaults_to_internal(self): """Test that ExecuteToolScope defaults to SpanKind.INTERNAL.""" - scope = ExecuteToolScope.start(self.tool_details, self.agent_details, self.tenant_details) + scope = ExecuteToolScope.start(Request(), self.tool_details, self.agent_details) scope.dispose() finished_spans = self.span_exporter.get_finished_spans() @@ -177,7 +172,10 @@ def test_span_kind_defaults_to_internal(self): def test_span_kind_override_to_client(self): """Test that ExecuteToolScope accepts SpanKind.CLIENT override.""" scope = ExecuteToolScope.start( - self.tool_details, self.agent_details, self.tenant_details, span_kind=SpanKind.CLIENT + Request(), + self.tool_details, + self.agent_details, + span_details=SpanDetails(span_kind=SpanKind.CLIENT), ) scope.dispose() diff --git a/tests/observability/core/test_inference_scope.py b/tests/observability/core/test_inference_scope.py index 0ff414ca..5722c845 100644 --- a/tests/observability/core/test_inference_scope.py +++ b/tests/observability/core/test_inference_scope.py @@ -9,12 +9,11 @@ import pytest from microsoft_agents_a365.observability.core import ( Channel, - ExecutionType, InferenceCallDetails, InferenceOperationType, InferenceScope, Request, - TenantDetails, + SpanDetails, configure, extract_context_from_headers, get_tracer_provider, @@ -43,9 +42,8 @@ def setUpClass(cls): service_name="test-inference-service", service_namespace="test-namespace", ) - # Create test agent and tenant details + # Create test agent details cls.agent_details = AgentDetails(agent_id="test-inference-agent") - cls.tenant_details = TenantDetails(tenant_id="12345678-1234-5678-1234-567812345678") def setUp(self): super().setUp() @@ -119,7 +117,7 @@ def test_inference_scope_start_method(self): providerName="openai", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) # Scope might be None if telemetry is disabled if scope is not None: @@ -139,11 +137,10 @@ def test_inference_scope_with_request(self): request = Request( content="What is the weather like?", - execution_type=ExecutionType.EVENT_TO_AGENT, session_id="test-session-123", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details, request) + scope = InferenceScope.start(request, details, self.agent_details) # Test that scope can be created with request if scope is not None: @@ -159,12 +156,11 @@ def test_request_metadata_set_on_span(self): request = Request( content="Inference request with source metadata", - execution_type=ExecutionType.AGENT_TO_AGENT, session_id="session-meta", channel=Channel(name="Channel 1", link="Link to channel"), ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details, request) + scope = InferenceScope.start(request, details, self.agent_details) if scope is not None: scope.dispose() @@ -205,7 +201,7 @@ def test_inference_scope_context_manager(self): outputTokens=50, ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) if scope is not None: # Test context manager usage @@ -230,7 +226,7 @@ def test_inference_scope_dispose(self): providerName="openai", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) if scope is not None: # Test manual dispose @@ -246,7 +242,7 @@ def test_record_input_messages(self): providerName="openai", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) if scope is not None: # Test recording input messages @@ -263,7 +259,7 @@ def test_record_output_messages(self): providerName="openai", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) if scope is not None: # Test recording output messages @@ -280,7 +276,7 @@ def test_record_input_tokens(self): providerName="openai", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) if scope is not None: # Test recording input tokens @@ -296,7 +292,7 @@ def test_record_output_tokens(self): providerName="openai", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) if scope is not None: # Test recording output tokens @@ -312,7 +308,7 @@ def test_record_finish_reasons(self): providerName="openai", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) if scope is not None: # Test recording finish reasons @@ -329,7 +325,7 @@ def test_record_thought_process(self): providerName="openai", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) if scope is not None: # Test recording thought process @@ -354,7 +350,10 @@ def test_inference_scope_with_parent_context(self): parent_context = extract_context_from_headers({"traceparent": traceparent}) with InferenceScope.start( - details, self.agent_details, self.tenant_details, parent_context=parent_context + Request(), + details, + self.agent_details, + span_details=SpanDetails(parent_context=parent_context), ): pass diff --git a/tests/observability/core/test_invoke_agent_scope.py b/tests/observability/core/test_invoke_agent_scope.py index dcd34a33..d27a39a1 100644 --- a/tests/observability/core/test_invoke_agent_scope.py +++ b/tests/observability/core/test_invoke_agent_scope.py @@ -10,12 +10,13 @@ import pytest from microsoft_agents_a365.observability.core import ( AgentDetails, + CallerDetails, Channel, - ExecutionType, - InvokeAgentDetails, InvokeAgentScope, + InvokeAgentScopeDetails, Request, - TenantDetails, + SpanDetails, + UserDetails, configure, get_tracer_provider, ) @@ -23,10 +24,8 @@ from microsoft_agents_a365.observability.core.constants import ( CHANNEL_LINK_KEY, CHANNEL_NAME_KEY, - GEN_AI_EXECUTION_TYPE_KEY, GEN_AI_INPUT_MESSAGES_KEY, ) -from microsoft_agents_a365.observability.core.models.caller_details import CallerDetails from microsoft_agents_a365.observability.core.opentelemetry_scope import OpenTelemetryScope from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter @@ -47,16 +46,13 @@ def setUpClass(cls): service_namespace="test-namespace", ) # Create test data - cls.tenant_details = TenantDetails(tenant_id="12345678-1234-5678-1234-567812345678") cls.agent_details = AgentDetails( agent_id="test-agent-123", agent_name="Test Agent", agent_description="A test agent for invoke scope testing", ) - cls.invoke_details = InvokeAgentDetails( + cls.invoke_scope_details = InvokeAgentScopeDetails( endpoint=urlparse("https://example.com/agent"), - details=cls.agent_details, - session_id="session-123", ) # Create channel for requests @@ -68,17 +64,20 @@ def setUpClass(cls): # Create a comprehensive request object cls.test_request = Request( content="Process customer inquiry about order status", - execution_type=ExecutionType.AGENT_TO_AGENT, session_id="session-abc123", channel=cls.channel, + conversation_id="conv-abc123", ) # Create caller details (non-agentic caller) + cls.user_details = UserDetails( + user_id="user-123", + user_email="user@contoso.com", + user_name="John Doe", + user_client_ip="192.168.1.100", + ) cls.caller_details = CallerDetails( - caller_id="user-123", - caller_upn="user@contoso.com", - caller_name="John Doe", - caller_client_ip="192.168.1.100", + user_details=cls.user_details, ) # Create caller agent details (agentic caller) @@ -87,8 +86,8 @@ def setUpClass(cls): agent_name="Caller Agent", agent_description="The agent that initiated this request", agent_blueprint_id="blueprint-456", - agent_auid="auid-123", - agent_upn="agent@contoso.com", + agentic_user_id="auid-123", + agentic_user_email="agent@contoso.com", tenant_id="tenant-789", agent_platform_id="platform-123", ) @@ -120,7 +119,7 @@ def tearDown(self): def test_record_response_method_exists(self): """Test that record_response method exists on InvokeAgentScope.""" - scope = InvokeAgentScope.start(self.invoke_details, self.tenant_details) + scope = InvokeAgentScope.start(Request(), self.invoke_scope_details, self.agent_details) if scope is not None: # Test that the method exists @@ -130,7 +129,7 @@ def test_record_response_method_exists(self): def test_record_input_messages_method_exists(self): """Test that record_input_messages method exists on InvokeAgentScope.""" - scope = InvokeAgentScope.start(self.invoke_details, self.tenant_details) + scope = InvokeAgentScope.start(Request(), self.invoke_scope_details, self.agent_details) if scope is not None: # Test that the method exists @@ -140,7 +139,7 @@ def test_record_input_messages_method_exists(self): def test_record_output_messages_method_exists(self): """Test that record_output_messages method exists on InvokeAgentScope.""" - scope = InvokeAgentScope.start(self.invoke_details, self.tenant_details) + scope = InvokeAgentScope.start(Request(), self.invoke_scope_details, self.agent_details) if scope is not None: # Test that the method exists @@ -152,9 +151,9 @@ def test_request_attributes_set_on_span(self): """Test that request parameters from mock data are available on span attributes.""" # Create scope with request scope = InvokeAgentScope.start( - invoke_agent_details=self.invoke_details, - tenant_details=self.tenant_details, - request=self.test_request, + self.test_request, + self.invoke_scope_details, + self.agent_details, ) if scope is not None: @@ -183,13 +182,6 @@ def test_request_attributes_set_on_span(self): self.channel.link, # From cls.channel.link ) - # Check execution type from mock data - if GEN_AI_EXECUTION_TYPE_KEY in span_attributes: - self.assertEqual( - span_attributes[GEN_AI_EXECUTION_TYPE_KEY], - self.test_request.execution_type.value, # From cls.test_request.execution_type - ) - # Check input messages contain request content from mock data if GEN_AI_INPUT_MESSAGES_KEY in span_attributes: input_messages = span_attributes[GEN_AI_INPUT_MESSAGES_KEY] @@ -202,9 +194,9 @@ def test_invoke_agent_scope_span_kind(self): """Test that InvokeAgentScope creates spans with the correct SpanKind.""" # Create scope scope = InvokeAgentScope.start( - invoke_agent_details=self.invoke_details, - tenant_details=self.tenant_details, - request=self.test_request, + self.test_request, + self.invoke_scope_details, + self.agent_details, ) if scope is not None: @@ -224,10 +216,10 @@ def test_invoke_agent_scope_span_kind(self): # Test SERVER span kind override scope_server = InvokeAgentScope.start( - invoke_agent_details=self.invoke_details, - tenant_details=self.tenant_details, - request=self.test_request, - span_kind=SpanKind.SERVER, + self.test_request, + self.invoke_scope_details, + self.agent_details, + span_details=SpanDetails(span_kind=SpanKind.SERVER), ) if scope_server is not None: diff --git a/tests/observability/core/test_output_scope.py b/tests/observability/core/test_output_scope.py index 64a519c2..fe6ba164 100644 --- a/tests/observability/core/test_output_scope.py +++ b/tests/observability/core/test_output_scope.py @@ -9,7 +9,8 @@ import pytest from microsoft_agents_a365.observability.core import ( AgentDetails, - TenantDetails, + Request, + SpanDetails, configure, extract_context_from_headers, get_tracer_provider, @@ -36,7 +37,6 @@ def setUpClass(cls): service_namespace="test-namespace", ) - cls.tenant_details = TenantDetails(tenant_id="12345678-1234-5678-1234-567812345678") cls.agent_details = AgentDetails( agent_id="test-agent-123", agent_name="Test Agent", @@ -76,7 +76,7 @@ def test_output_scope_creates_span_with_messages(self): """Test OutputScope creates span with output messages attribute.""" response = Response(messages=["First message", "Second message"]) - with OutputScope.start(self.agent_details, self.tenant_details, response): + with OutputScope.start(Request(), response, self.agent_details): pass span, attributes = self._get_last_span() @@ -95,7 +95,7 @@ def test_record_output_messages_appends(self): """Test record_output_messages appends to accumulated messages.""" response = Response(messages=["Initial"]) - with OutputScope.start(self.agent_details, self.tenant_details, response) as scope: + with OutputScope.start(Request(), response, self.agent_details) as scope: scope.record_output_messages(["Appended 1"]) scope.record_output_messages(["Appended 2", "Appended 3"]) @@ -119,7 +119,10 @@ def test_output_scope_with_parent_context(self): parent_context = extract_context_from_headers({"traceparent": traceparent}) with OutputScope.start( - self.agent_details, self.tenant_details, response, parent_context=parent_context + Request(), + response, + self.agent_details, + span_details=SpanDetails(parent_context=parent_context), ): pass @@ -139,7 +142,7 @@ def test_output_scope_dispose(self): """Test OutputScope dispose method ends the span.""" response = Response(messages=["Test"]) - scope = OutputScope.start(self.agent_details, self.tenant_details, response) + scope = OutputScope.start(Request(), response, self.agent_details) self.assertIsNotNone(scope) scope.dispose() diff --git a/tests/observability/core/test_output_scope_bounded.py b/tests/observability/core/test_output_scope_bounded.py index deaa4fae..93718684 100644 --- a/tests/observability/core/test_output_scope_bounded.py +++ b/tests/observability/core/test_output_scope_bounded.py @@ -19,15 +19,11 @@ def _make_scope(self, initial_messages: list[str] | None = None) -> OutputScope: agent_details.agent_name = "Test Agent" agent_details.agent_description = None agent_details.platform_id = None - agent_details.conversation_id = None agent_details.icon_uri = None - agent_details.agent_auid = None - agent_details.agent_upn = None + agent_details.agentic_user_id = None + agent_details.agentic_user_email = None agent_details.agent_blueprint_id = None - tenant_details = MagicMock() - tenant_details.tenant_id = "test-tenant" - response = MagicMock() response.messages = initial_messages or ["hello"] diff --git a/tests/observability/core/test_record_attributes.py b/tests/observability/core/test_record_attributes.py index a43b67b2..2faff6d4 100644 --- a/tests/observability/core/test_record_attributes.py +++ b/tests/observability/core/test_record_attributes.py @@ -6,7 +6,7 @@ import unittest.mock from unittest.mock import Mock, patch -from microsoft_agents_a365.observability.core import AgentDetails, TenantDetails +from microsoft_agents_a365.observability.core import AgentDetails from microsoft_agents_a365.observability.core.config import _telemetry_manager from microsoft_agents_a365.observability.core.opentelemetry_scope import OpenTelemetryScope from opentelemetry import trace @@ -77,14 +77,12 @@ def test_record_attributes_with_dict(self): agent_details = AgentDetails( agent_id="test-agent", agent_name="Test Agent", agent_description="A test agent" ) - tenant_details = TenantDetails(tenant_id="test-tenant") with OpenTelemetryScope( kind="Internal", operation_name="test_operation", activity_name="test_activity", agent_details=agent_details, - tenant_details=tenant_details, ) as scope: # Record custom attributes using a dictionary attributes = { diff --git a/tests/observability/core/test_trace_context_propagation.py b/tests/observability/core/test_trace_context_propagation.py index eb17dda8..28027ea5 100644 --- a/tests/observability/core/test_trace_context_propagation.py +++ b/tests/observability/core/test_trace_context_propagation.py @@ -14,9 +14,10 @@ InferenceCallDetails, InferenceOperationType, InferenceScope, - InvokeAgentDetails, InvokeAgentScope, - TenantDetails, + InvokeAgentScopeDetails, + Request, + SpanDetails, ToolCallDetails, configure, extract_context_from_headers, @@ -41,7 +42,6 @@ def setUpClass(cls): service_namespace="test-namespace", ) - cls.tenant_details = TenantDetails(tenant_id="12345678-1234-5678-1234-567812345678") cls.agent_details = AgentDetails( agent_id="test-agent-123", agent_name="Test Agent", @@ -76,7 +76,7 @@ def test_inject_context_to_headers_returns_headers(self): providerName="openai", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) if scope is not None: headers = scope.inject_context_to_headers() @@ -103,7 +103,7 @@ def test_inject_context_to_headers_contains_span_id(self): providerName="openai", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) if scope is not None: headers = scope.inject_context_to_headers() @@ -129,7 +129,7 @@ def test_get_context_returns_context_object(self): providerName="openai", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) if scope is not None: ctx = scope.get_context() @@ -148,7 +148,7 @@ def test_context_propagation_via_inject_extract(self): providerName="openai", ) - parent_scope = InferenceScope.start(parent_details, self.agent_details, self.tenant_details) + parent_scope = InferenceScope.start(Request(), parent_details, self.agent_details) # Get injected headers from parent headers = parent_scope.inject_context_to_headers() @@ -165,10 +165,10 @@ def test_context_propagation_via_inject_extract(self): ) child_scope = ExecuteToolScope.start( + Request(), tool_details, self.agent_details, - self.tenant_details, - parent_context=parent_context, + span_details=SpanDetails(parent_context=parent_context), ) child_scope.dispose() @@ -203,7 +203,7 @@ def test_inject_headers_for_http_propagation(self): providerName="openai", ) - scope = InferenceScope.start(details, self.agent_details, self.tenant_details) + scope = InferenceScope.start(Request(), details, self.agent_details) if scope is not None: headers = scope.inject_context_to_headers() @@ -220,7 +220,10 @@ def test_inject_headers_for_http_propagation(self): ) child_scope = InferenceScope.start( - child_details, self.agent_details, self.tenant_details, parent_context=parent_ctx + Request(), + child_details, + self.agent_details, + span_details=SpanDetails(parent_context=parent_ctx), ) if child_scope is not None: child_scope.dispose() @@ -246,14 +249,15 @@ def test_invoke_agent_scope_with_parent_context(self): parent_context = extract_context_from_headers({"traceparent": traceparent}) - invoke_details = InvokeAgentDetails( + invoke_scope_details = InvokeAgentScopeDetails( endpoint=urlparse("https://example.com/agent"), - details=self.agent_details, - session_id="session-123", ) with InvokeAgentScope.start( - invoke_details, self.tenant_details, parent_context=parent_context + Request(), + invoke_scope_details, + self.agent_details, + span_details=SpanDetails(parent_context=parent_context), ) as scope: headers = scope.inject_context_to_headers() self.assertIn("traceparent", headers) diff --git a/tests/observability/extensions/openai/integration/test_openai_trace_processor.py b/tests/observability/extensions/openai/integration/test_openai_trace_processor.py index 19d73cb2..a9a412e6 100644 --- a/tests/observability/extensions/openai/integration/test_openai_trace_processor.py +++ b/tests/observability/extensions/openai/integration/test_openai_trace_processor.py @@ -8,7 +8,6 @@ from microsoft_agents_a365.observability.core.constants import ( GEN_AI_AGENT_ID_KEY, GEN_AI_AGENT_NAME_KEY, - GEN_AI_EXECUTION_TYPE_KEY, GEN_AI_INPUT_MESSAGES_KEY, GEN_AI_OPERATION_NAME_KEY, GEN_AI_OUTPUT_MESSAGES_KEY, @@ -232,14 +231,14 @@ async def run_agent(): BaggageBuilder() .agent_id("test-agent-id") .agent_name("TestAgent") - .agent_auid("test-agent-auid") - .agent_upn("test-agent@test.com") + .agentic_user_id("test-agent-auid") + .agentic_user_email("test-agent@test.com") .agent_blueprint_id("test-blueprint-id") .tenant_id("test-tenant-id") - .caller_id("test-caller-id") - .caller_name("Test Caller") - .caller_upn("test-caller@test.com") - .caller_client_ip("127.0.0.1") + .user_id("test-caller-id") + .user_name("Test Caller") + .user_email("test-caller@test.com") + .user_client_ip("127.0.0.1") .conversation_id("test-conversation-id") .channel_name("test-channel") .build() @@ -269,7 +268,6 @@ async def run_agent(): GEN_AI_OPERATION_NAME_KEY, # "gen_ai.operation.name" - Set by SDK GEN_AI_AGENT_ID_KEY, # "gen_ai.agent.id" GEN_AI_AGENT_NAME_KEY, # "gen_ai.agent.name" - GEN_AI_EXECUTION_TYPE_KEY, # "gen_ai.execution.type" GEN_AI_INPUT_MESSAGES_KEY, # "gen_ai.input.messages" GEN_AI_OUTPUT_MESSAGES_KEY, # "gen_ai.output.messages" ] diff --git a/tests/observability/hosting/middleware/test_output_logging_middleware.py b/tests/observability/hosting/middleware/test_output_logging_middleware.py index 3ec3749f..fc1619b4 100644 --- a/tests/observability/hosting/middleware/test_output_logging_middleware.py +++ b/tests/observability/hosting/middleware/test_output_logging_middleware.py @@ -99,7 +99,7 @@ async def logic(): @pytest.mark.asyncio async def test_output_logging_passes_through_without_tenant(): - """Should pass through without registering handlers if no tenant id.""" + """Should still register handlers even if no tenant id — tenant is optional.""" middleware = OutputLoggingMiddleware() ctx = _make_turn_context(recipient_tenant_id=None) @@ -112,7 +112,8 @@ async def logic(): await middleware.on_turn(ctx, logic) assert logic_called is True - assert len(ctx._on_send_activities) == 0 + # Handlers should still be registered — tenant_id is optional now + assert len(ctx._on_send_activities) == 1 @pytest.mark.asyncio @@ -188,9 +189,10 @@ async def test_send_handler_uses_parent_span_from_turn_state(): await handler(ctx, activities, send_next) call_kwargs = mock_output_scope_cls.start.call_args - # parent_context should be set (extracted from traceparent header) - assert "parent_context" in call_kwargs.kwargs - assert call_kwargs.kwargs["parent_context"] is not None + # span_details should be set with parent_context (extracted from traceparent header) + assert "span_details" in call_kwargs.kwargs + assert call_kwargs.kwargs["span_details"] is not None + assert call_kwargs.kwargs["span_details"].parent_context is not None @pytest.mark.asyncio diff --git a/tests/observability/hosting/scope_helpers/test_populate_invoke_agent_scope.py b/tests/observability/hosting/scope_helpers/test_populate_invoke_agent_scope.py index bc38cde0..95b8e04a 100644 --- a/tests/observability/hosting/scope_helpers/test_populate_invoke_agent_scope.py +++ b/tests/observability/hosting/scope_helpers/test_populate_invoke_agent_scope.py @@ -11,13 +11,12 @@ from microsoft_agents_a365.observability.core.constants import ( CHANNEL_NAME_KEY, GEN_AI_CONVERSATION_ID_KEY, - GEN_AI_EXECUTION_TYPE_KEY, GEN_AI_INPUT_MESSAGES_KEY, USER_ID_KEY, ) -from microsoft_agents_a365.observability.core.invoke_agent_details import InvokeAgentDetails +from microsoft_agents_a365.observability.core.invoke_agent_details import InvokeAgentScopeDetails from microsoft_agents_a365.observability.core.invoke_agent_scope import InvokeAgentScope -from microsoft_agents_a365.observability.core.tenant_details import TenantDetails +from microsoft_agents_a365.observability.core.request import Request from microsoft_agents_a365.observability.hosting.scope_helpers.populate_invoke_agent_scope import ( populate, ) @@ -44,11 +43,9 @@ def enable_telemetry(): def test_populate(): """Test populate populates scope from turn context.""" # Create real InvokeAgentScope with minimal required parameters - invoke_agent_details = InvokeAgentDetails( - details=AgentDetails(agent_id="test-agent", agent_name="Test Agent") - ) - tenant_details = TenantDetails(tenant_id="test-tenant") - scope = InvokeAgentScope(invoke_agent_details, tenant_details) + invoke_scope_details = InvokeAgentScopeDetails() + agent_details = AgentDetails(agent_id="test-agent", agent_name="Test Agent") + scope = InvokeAgentScope(Request(), invoke_scope_details, agent_details) # Create real Activity and TurnContext activity = Activity( @@ -76,9 +73,6 @@ def test_populate(): assert USER_ID_KEY in attributes assert attributes[USER_ID_KEY] == "caller-aad-id" - # Check execution type - assert GEN_AI_EXECUTION_TYPE_KEY in attributes - # Check execution source assert CHANNEL_NAME_KEY in attributes assert attributes[CHANNEL_NAME_KEY] == "test-channel" diff --git a/tests/observability/hosting/scope_helpers/test_scope_helper_utils.py b/tests/observability/hosting/scope_helpers/test_scope_helper_utils.py index bedaf497..60e4327f 100644 --- a/tests/observability/hosting/scope_helpers/test_scope_helper_utils.py +++ b/tests/observability/hosting/scope_helpers/test_scope_helper_utils.py @@ -12,18 +12,15 @@ GEN_AI_AGENT_NAME_KEY, GEN_AI_CONVERSATION_ID_KEY, GEN_AI_CONVERSATION_ITEM_LINK_KEY, - GEN_AI_EXECUTION_TYPE_KEY, TENANT_ID_KEY, USER_EMAIL_KEY, USER_ID_KEY, USER_NAME_KEY, ) -from microsoft_agents_a365.observability.core.execution_type import ExecutionType from microsoft_agents_a365.observability.hosting.scope_helpers.utils import ( get_caller_pairs, get_channel_pairs, get_conversation_pairs, - get_execution_type_pair, get_target_agent_pairs, get_tenant_id_pair, ) @@ -46,17 +43,6 @@ def test_get_caller_pairs(): assert (USER_EMAIL_KEY, "caller-upn") in result -def test_get_execution_type_pair(): - """Test get_execution_type_pair determines execution type correctly.""" - from_account = ChannelAccount(role="agenticUser") - recipient = ChannelAccount(role="agenticUser") - activity = Activity(type="message", from_property=from_account, recipient=recipient) - - result = list(get_execution_type_pair(activity)) - - assert (GEN_AI_EXECUTION_TYPE_KEY, ExecutionType.AGENT_TO_AGENT.value) in result - - def test_get_target_agent_pairs(): """Test get_target_agent_pairs extracts target agent information.""" recipient = ChannelAccount( diff --git a/tests/usage_example.py b/tests/usage_example.py index 34e21072..ea6537b6 100644 --- a/tests/usage_example.py +++ b/tests/usage_example.py @@ -4,8 +4,6 @@ import os from urllib.parse import urlparse -from microsoft_agents_a365.observability.core.tenant_details import TenantDetails - def main(): """Demonstrate the aligned Microsoft Agent 365 Python SDK functionality.""" @@ -17,9 +15,8 @@ def main(): from microsoft_agents_a365.observability.core import ( AgentDetails, Channel, - ExecutionType, - InvokeAgentDetails, InvokeAgentScope, + InvokeAgentScopeDetails, Request, configure, ) @@ -43,10 +40,9 @@ def main(): # Create a rich request object Request( content="Process customer inquiry about order status", - execution_type=ExecutionType.AGENT_TO_AGENT, session_id="session-abc123", channel=channel, - payload="Customer ID: 12345, Order ID: 67890", + conversation_id="conv-12345", ) # Note: ExecuteAgentScope has been removed from the SDK @@ -54,45 +50,42 @@ def main(): print(" 🔄 Tool execution example (ExecuteAgentScope no longer available)") # Example tool usage that would typically be inside an agent execution context - # Note: This would require proper agent_details and tenant_details in real usage + # Note: This would require proper agent_details in real usage print(" 🔧 Tool execution would be used within agent contexts") print(" ✅ SDK functionality demonstrated (ExecuteAgentScope removed)") # Example 2: Agent-to-Agent Invocation with Enhanced Details print("\n📞 Example 2: Agent-to-Agent Invocation") - tenant_details = TenantDetails(tenant_id="12345678-1234-5678-1234-567812345678") - # Create detailed agent information (aligned with .NET SDK AgentDetails) target_agent_details = AgentDetails( agent_id="inventory-agent-999", agent_name="Inventory Agent", agent_description="Handles inventory queries and updates", - conversation_id="conv-xyz789", - icon_uri="https://example.com/inventory-agent-icon.png", # New icon_uri field + icon_uri="https://example.com/inventory-agent-icon.png", ) - # Create invoke agent details (aligned with .NET SDK InvokeAgentDetails) - invoke_details = InvokeAgentDetails( + # Create invoke agent scope details (aligned with .NET SDK) + invoke_scope_details = InvokeAgentScopeDetails( endpoint=urlparse("https://agents.company.com:8080/inventory"), - details=target_agent_details, - session_id="session-abc123", # New session_id field ) # Create request for the invocation invoke_request = Request( content="Check inventory for product SKU: ABC-123", - execution_type=ExecutionType.AGENT_TO_AGENT, session_id="session-abc123", channel=channel, + conversation_id="conv-xyz789", ) # Use InvokeAgentScope with enhanced details (like .NET SDK) - with InvokeAgentScope.start(invoke_details, tenant_details, invoke_request): + with InvokeAgentScope.start(invoke_request, invoke_scope_details, target_agent_details): print(" 📡 Agent invocation started with full agent details and session context") print(f" 📊 Target: {target_agent_details.agent_name} ({target_agent_details.agent_id})") - print(f" 🌐 Endpoint: {invoke_details.endpoint.hostname}:{invoke_details.endpoint.port}") - print(f" 🆔 Session: {invoke_details.session_id}") + print( + f" 🌐 Endpoint: " + f"{invoke_scope_details.endpoint.hostname}:{invoke_scope_details.endpoint.port}" + ) print(f" 🎨 Icon: {target_agent_details.icon_uri}") print(" ✅ Agent invocation completed with comprehensive telemetry") @@ -104,14 +97,14 @@ def main(): print(" ✅ ExecuteAgentScope has been removed from the SDK") # Tool execution still works but requires proper context in real usage - print(" ✅ Tool execution API available (requires agent/tenant context)") + print(" ✅ Tool execution API available (requires agent context)") print("\n🎯 Key Alignments with .NET SDK:") print(" ✅ AgentDetails now includes icon_uri") - print(" ✅ InvokeAgentDetails now includes session_id") + print(" ✅ InvokeAgentScopeDetails for scope configuration") print(" ✅ ExecuteAgentScope has been removed from Python SDK") print(" ✅ Constants aligned: gen_ai.agent.id, session.id, gen_ai.agent365.icon_uri") - print(" ✅ New classes: Channel, Request, ExecutionType") + print(" ✅ New classes: Channel, Request, SpanDetails, UserDetails, CallerDetails") print(" ✅ Baggage propagation from parent to child spans") print(" ✅ Backward compatibility maintained")