From f41c3b42ff8e4418b6426a46b8ecad6a05a0369d Mon Sep 17 00:00:00 2001 From: Joan Bency Date: Mon, 12 Jan 2026 10:28:01 +0000 Subject: [PATCH 1/9] Updated Claude Python to use A365 oservability SDK and MCP tooling --- python/claude/sample-agent/.env.template | 22 +- python/claude/sample-agent/agent.py | 273 +++++++++--------- .../claude/sample-agent/host_agent_server.py | 139 ++++----- .../sample-agent/observability_helpers.py | 164 ----------- python/claude/sample-agent/pyproject.toml | 17 +- 5 files changed, 198 insertions(+), 417 deletions(-) delete mode 100644 python/claude/sample-agent/observability_helpers.py diff --git a/python/claude/sample-agent/.env.template b/python/claude/sample-agent/.env.template index cb1e4474..b0ba21aa 100644 --- a/python/claude/sample-agent/.env.template +++ b/python/claude/sample-agent/.env.template @@ -37,15 +37,11 @@ ENV_ID= # AUTHENTICATION OPTIONS # ============================================================================= -# Use agentic authentication (optional, defaults to false) -# Set to "true" to use agentic authentication with M365 Agents SDK -USE_AGENTIC_AUTH=false - # Bearer token (required if not using client credentials) # Use for development/testing without full app registration BEARER_TOKEN=your_bearer_token_here -# Agentic authentication scope (required if USE_AGENTIC_AUTH=true) +# Agentic authentication scope (required for agentic authentication) # Example: https://api.powerplatform.com/.default AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default @@ -98,23 +94,15 @@ PORT=3978 LOG_LEVEL=INFO # ============================================================================= -# OBSERVABILITY CONFIGURATION (Optional) +# OBSERVABILITY CONFIGURATION # ============================================================================= -# Enable observability tracing (set to true to track agent operations) -ENABLE_OBSERVABILITY=true - -# Service name for observability -OBSERVABILITY_SERVICE_NAME=claude-agent +# Service name for observability (required) +OBSERVABILITY_SERVICE_NAME=claude-sample-agent -# Service namespace for observability +# Service namespace for observability (required) OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples -# Enable Agent 365 Observability Exporter (optional, defaults to false) -# Set to "true" to export telemetry to Agent 365 backend -# When false, uses ConsoleSpanExporter for local debugging -ENABLE_A365_OBSERVABILITY_EXPORTER=false - # Python environment (influences target cluster/category) # Options: development, production PYTHON_ENVIRONMENT=development diff --git a/python/claude/sample-agent/agent.py b/python/claude/sample-agent/agent.py index d3cfd714..20285332 100644 --- a/python/claude/sample-agent/agent.py +++ b/python/claude/sample-agent/agent.py @@ -48,29 +48,23 @@ # Microsoft Agents SDK from local_authentication_options import LocalAuthenticationOptions from microsoft_agents.hosting.core import Authorization, TurnContext +from token_cache import get_cached_agentic_token -# Notifications -from microsoft_agents_a365.notifications.agent_notification import NotificationTypes +# Observability Components +from microsoft_agents_a365.observability.core.config import configure -# Observability (optional - only imported if enabled) +# MCP Tooling (optional - Claude Agent SDK has built-in tools) try: - from microsoft_agents_a365.observability.core import ( - InferenceScope, - InvokeAgentDetails, - InvokeAgentScope, - ) - from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder - from microsoft_agents_a365.observability.core.tool_type import ToolType - from observability_helpers import ( - create_agent_details, - create_tenant_details, - create_request_details, - create_inference_details, + from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import ( + McpToolServerConfigurationService, ) - OBSERVABILITY_AVAILABLE = True + MCP_AVAILABLE = True except ImportError: - OBSERVABILITY_AVAILABLE = False - logger.debug("Observability packages not installed - tracing disabled") + MCP_AVAILABLE = False + logger.debug("MCP tooling packages not installed - using Claude built-in tools only") + +# Notifications +from microsoft_agents_a365.notifications.agent_notification import NotificationTypes # @@ -87,9 +81,18 @@ def __init__(self): """Initialize the Claude agent.""" self.logger = logging.getLogger(self.__class__.__name__) + # Initialize observability + self._setup_observability() + # Initialize authentication options self.auth_options = LocalAuthenticationOptions.from_environment() + # Initialize MCP services if available + if MCP_AVAILABLE: + self._initialize_mcp_services() + else: + logger.info("MCP tooling not available - using Claude built-in tools") + # Create Claude client self._create_client() @@ -133,6 +136,93 @@ def _create_client(self): # + # ========================================================================= + # OBSERVABILITY CONFIGURATION + # ========================================================================= + # + + def token_resolver(self, agent_id: str, tenant_id: str) -> str | None: + """ + Token resolver function for Agent 365 Observability exporter. + + Uses the cached agentic token obtained from AGENT_APP.auth.get_token(context, auth_handler_name). + This is the only valid authentication method for this context. + """ + + try: + logger.info(f"Token resolver called for agent_id: {agent_id}, tenant_id: {tenant_id}") + + # Use cached agentic token from agent authentication + cached_token = get_cached_agentic_token(tenant_id, agent_id) + if cached_token: + logger.info("Using cached agentic token from agent authentication") + return cached_token + else: + logger.warning( + f"No cached agentic token found for agent_id: {agent_id}, tenant_id: {tenant_id}" + ) + return None + + except Exception as e: + logger.error(f"Error resolving token for agent {agent_id}, tenant {tenant_id}: {e}") + return None + + def _setup_observability(self): + """ + Configure Microsoft Agent 365 observability (simplified pattern) + + This follows the same pattern as the reference examples: + - semantic_kernel: configure() + SemanticKernelInstrumentor().instrument() + - openai_agents: configure() + OpenAIAgentsTraceInstrumentor().instrument() + """ + try: + # Step 1: Configure Agent 365 Observability with service information + status = configure( + service_name=os.getenv("OBSERVABILITY_SERVICE_NAME", "claude-sample-agent"), + service_namespace=os.getenv("OBSERVABILITY_SERVICE_NAMESPACE", "agent365-samples"), + token_resolver=self.token_resolver, + ) + + if not status: + logger.warning("⚠️ Agent 365 Observability configuration failed") + return + + logger.info("✅ Agent 365 Observability configured successfully") + + # Note: Claude Agent SDK doesn't have automatic instrumentation yet + # Manual scopes will be used in process_user_message + + except Exception as e: + logger.error(f"❌ Error setting up observability: {e}") + + # + + # ========================================================================= + # MCP SERVER SETUP (OPTIONAL) + # ========================================================================= + # + + def _initialize_mcp_services(self): + """ + Initialize MCP services for tool server configuration. + + Note: Claude Agent SDK has built-in tools (WebSearch, Read, Write, WebFetch). + MCP tooling provides additional enterprise tools for M365 integration. + """ + try: + # Create configuration service for MCP tool servers + self.config_service = McpToolServerConfigurationService() + logger.info("✅ MCP tool configuration service initialized") + + # Note: Claude Agent SDK doesn't support dynamic tool registration like OpenAI + # MCP tools would need to be called separately if needed + logger.info("ℹ️ Claude uses built-in tools - MCP available for future extension") + + except Exception as e: + logger.warning(f"⚠️ Failed to initialize MCP services: {e}") + + # + # ========================================================================= # INITIALIZATION AND MESSAGE PROCESSING # ========================================================================= @@ -148,95 +238,13 @@ async def initialize(self): raise async def process_user_message( - self, message: str, auth: Authorization, context: TurnContext + self, message: str, auth: Authorization, auth_handler_name: str, context: TurnContext ) -> str: """Process user message using the Claude Agent SDK with observability tracing""" - # Check if observability is enabled - enable_observability = os.getenv("ENABLE_OBSERVABILITY", "false").lower() in ("true", "1", "yes") - - # Create observability objects if available and enabled - invoke_scope = None - baggage_context = None - if OBSERVABILITY_AVAILABLE and enable_observability: - try: - agent_details = create_agent_details(context) - tenant_details = create_tenant_details(context) - - # Get session ID from conversation - session_id = None - if context and context.activity and context.activity.conversation: - session_id = context.activity.conversation.id - - # Create invoke details - invoke_details = InvokeAgentDetails( - details=agent_details, - session_id=session_id, - ) - request_details = create_request_details(message, session_id) - - # Build baggage context - # Extract tenant_id and agent_id from context - tenant_id = None - agent_id = None - if context and context.activity: - if hasattr(context.activity, 'recipient'): - tenant_id = getattr(context.activity.recipient, 'tenant_id', None) - agent_id = getattr(context.activity.recipient, 'agentic_app_id', None) - - # Build and start baggage context - baggage_context = BaggageBuilder().tenant_id(tenant_id).agent_id(agent_id).build() - baggage_context.__enter__() - - invoke_scope = InvokeAgentScope.start( - invoke_agent_details=invoke_details, - tenant_details=tenant_details, - request=request_details, - ) - invoke_scope.__enter__() - - logger.debug("✅ Observability scope started") - except Exception as e: - logger.warning(f"Failed to start observability scope: {e}") - invoke_scope = None - baggage_context = None - try: logger.info(f"📨 Processing message: {message[:100]}...") - # Track tokens for observability - total_input_tokens = 0 - total_output_tokens = 0 - total_thinking_tokens = 0 - - # Create inference scope if observability enabled - inference_scope = None - if OBSERVABILITY_AVAILABLE and enable_observability: - try: - agent_details = create_agent_details(context) - tenant_details = create_tenant_details(context) - session_id = context.activity.conversation.id if context and context.activity and context.activity.conversation else None - - inference_details = create_inference_details( - model=self.claude_options.model, - input_tokens=0, # Will update after response - output_tokens=0, - ) - request_details = create_request_details(message, session_id) - - # Correct API: details, agent_details, tenant_details, request - inference_scope = InferenceScope.start( - details=inference_details, - agent_details=agent_details, - tenant_details=tenant_details, - request=request_details, - ) - inference_scope.__enter__() - logger.debug("✅ Inference scope started") - except Exception as e: - logger.warning(f"Failed to start inference scope: {e}") - inference_scope = None - # Create a new client for this conversation # Claude SDK uses async context manager async with ClaudeSDKClient(self.claude_options) as client: @@ -254,20 +262,13 @@ async def process_user_message( # Collect thinking (Claude's reasoning) if isinstance(block, ThinkingBlock): thinking_parts.append(f"💭 {block.thinking}") - # Track thinking tokens - total_thinking_tokens += len(block.thinking.split()) logger.info(f"💭 Claude thinking: {block.thinking[:100]}...") # Collect actual response text elif isinstance(block, TextBlock): response_parts.append(block.text) - # Track output tokens - total_output_tokens += len(block.text.split()) logger.info(f"💬 Claude response: {block.text[:100]}...") - # Track input tokens (approximate) - total_input_tokens = len(message.split()) - # Combine thinking and response full_response = "" @@ -283,42 +284,30 @@ async def process_user_message( else: full_response += "I couldn't process your request at this time." - # Close inference scope with token counts - if inference_scope: - try: - # Update inference details with actual token counts - # Note: These are approximate counts based on word splitting - logger.info(f"📊 Tokens - Input: {total_input_tokens}, Output: {total_output_tokens}, Thinking: {total_thinking_tokens}") - inference_scope.__exit__(None, None, None) - except Exception as e: - logger.warning(f"Failed to close inference scope: {e}") - - # Close invoke scope successfully - if invoke_scope: - try: - invoke_scope.__exit__(None, None, None) - if baggage_context is not None: - baggage_context.__exit__(None, None, None) - except Exception as e: - logger.warning(f"Failed to close invoke scope: {e}") - return full_response except Exception as e: logger.error(f"Error processing message: {e}") logger.exception("Full error details:") - - # Record error in scopes - if invoke_scope: - try: - invoke_scope.record_error(e) - invoke_scope.__exit__(type(e), e, e.__traceback__) - if baggage_context is not None: - baggage_context.__exit__(None, None, None) - except Exception as cleanup_error: - logger.warning(f"Failed to clean up invoke/baggage context after error: {cleanup_error}") - return f"Sorry, I encountered an error: {str(e)}" + async for msg in client.receive_response(): + if isinstance(msg, AssistantMessage): + for block in msg.content: + # Collect thinking (Claude's reasoning) + if isinstance(block, ThinkingBlock): + thinking_parts.append(f"💭 {block.thinking}") + # Track thinking tokens + total_thinking_tokens += len(block.thinking.split()) + logger.info(f"💭 Claude thinking: {block.thinking[:100]}...") + + # Collect actual response text + elif isinstance(block, TextBlock): + response_parts.append(block.text) + # Track output tokens + total_output_tokens += len(block.text.split()) + logger.info(f"💬 Claude response: {block.text[:100]}...") + + # @@ -328,7 +317,7 @@ async def process_user_message( # async def handle_agent_notification_activity( - self, notification_activity, auth: Authorization, context: TurnContext + self, notification_activity, auth: Authorization, auth_handler_name: str, context: TurnContext ) -> str: """ Handle agent notification activities (email, Word mentions, etc.) @@ -359,7 +348,7 @@ async def handle_agent_notification_activity( logger.info(f"📧 Processing email notification") # Process with Claude - response = await self.process_user_message(message, auth, context) + response = await self.process_user_message(message, auth, auth_handler_name, context) return response or "Email notification processed." # Handle Word Comment Notifications @@ -383,7 +372,7 @@ async def handle_agent_notification_activity( ) # Process with Claude - response = await self.process_user_message(message, auth, context) + response = await self.process_user_message(message, auth, auth_handler_name, context) return response or "Word notification processed." # Generic notification handling @@ -406,7 +395,7 @@ async def handle_agent_notification_activity( logger.info(f"📨 Processing generic notification: {notification_type}") # Process with Claude - response = await self.process_user_message(notification_message, auth, context) + response = await self.process_user_message(notification_message, auth, auth_handler_name, context) return response or "Notification processed successfully." except Exception as e: diff --git a/python/claude/sample-agent/host_agent_server.py b/python/claude/sample-agent/host_agent_server.py index fb7b4a06..35f8bace 100644 --- a/python/claude/sample-agent/host_agent_server.py +++ b/python/claude/sample-agent/host_agent_server.py @@ -52,15 +52,11 @@ ChannelId, ) from microsoft_agents_a365.notifications import EmailResponse, NotificationTypes - -# Observability imports (optional) -try: - from microsoft_agents_a365.observability.core.config import configure as configure_observability - from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder - from token_cache import get_cached_agentic_token, cache_agentic_token - OBSERVABILITY_AVAILABLE = True -except ImportError: - OBSERVABILITY_AVAILABLE = False +from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder +from microsoft_agents_a365.runtime.environment_utils import ( + get_observability_authentication_scope, +) +from token_cache import cache_agentic_token # Load configuration load_dotenv() @@ -83,6 +79,8 @@ def __init__(self, agent_class: type[AgentInterface], *agent_args, **agent_kwarg if not check_agent_inheritance(agent_class): raise TypeError(f"Agent class {agent_class.__name__} must inherit from AgentInterface") + self.auth_handler_name = "AGENTIC" + self.agent_class = agent_class self.agent_args = agent_args self.agent_kwargs = agent_kwargs @@ -127,41 +125,58 @@ async def help_handler(context: TurnContext, _: TurnState): self.agent_app.conversation_update("membersAdded")(help_handler) self.agent_app.message("/help")(help_handler) - @self.agent_app.activity("message") + handler = [self.auth_handler_name] + @self.agent_app.activity("message", auth_handlers=handler) async def on_message(context: TurnContext, _: TurnState): """Handle all messages with the hosted agent""" try: - # Ensure the agent is available - if not self.agent_instance: - error_msg = "❌ Sorry, the agent is not available." - logger.error(error_msg) - await context.send_activity(error_msg) - return + tenant_id = context.activity.recipient.tenant_id + agent_id = context.activity.recipient.agentic_app_id + with BaggageBuilder().tenant_id(tenant_id).agent_id(agent_id).build(): + # Ensure the agent is available + if not self.agent_instance: + error_msg = "❌ Sorry, the agent is not available." + logger.error(error_msg) + await context.send_activity(error_msg) + return + + exaau_token = await self.agent_app.auth.exchange_token( + context, + scopes=get_observability_authentication_scope(), + auth_handler_id=self.auth_handler_name, + ) - user_message = context.activity.text or "" - logger.info(f"📨 Processing message: '{user_message}'") + # Cache the agentic token for Agent 365 Observability exporter use + cache_agentic_token( + tenant_id, + agent_id, + exaau_token.token, + ) - # Skip empty messages - if not user_message.strip(): - return + user_message = context.activity.text or "" + logger.info(f"📨 Processing message: '{user_message}'") - # Skip messages that are handled by other decorators (like /help) - if user_message.strip() == "/help": - return + # Skip empty messages + if not user_message.strip(): + return - # Process with the hosted agent - logger.info(f"🤖 Processing with {self.agent_class.__name__}...") - response = await self.agent_instance.process_user_message( - user_message, self.agent_app.auth, context - ) + # Skip messages that are handled by other decorators (like /help) + if user_message.strip() == "/help": + return - # Send response back - logger.info( - f"📤 Sending response: '{response[:100] if len(response) > 100 else response}'" - ) - await context.send_activity(response) + # Process with the hosted agent + logger.info(f"🤖 Processing with {self.agent_class.__name__}...") + response = await self.agent_instance.process_user_message( + user_message, self.agent_app.auth, self.auth_handler_name, context + ) - logger.info("✅ Response sent successfully to client") + # Send response back + logger.info( + f"📤 Sending response: '{response[:100] if len(response) > 100 else response}'" + ) + await context.send_activity(response) + + logger.info("✅ Response sent successfully to client") except Exception as e: error_msg = f"Sorry, I encountered an error: {str(e)}" @@ -184,12 +199,7 @@ async def handle_notification_common( return tenant_id, agent_id = result - if OBSERVABILITY_AVAILABLE: - with BaggageBuilder().tenant_id(tenant_id).agent_id(agent_id).build(): - await self._handle_notification_with_agent( - context, notification_activity - ) - else: + with BaggageBuilder().tenant_id(tenant_id).agent_id(agent_id).build(): await self._handle_notification_with_agent( context, notification_activity ) @@ -297,17 +307,11 @@ async def _setup_observability_token( tenant_id: Tenant identifier agent_id: Agent identifier """ - if not OBSERVABILITY_AVAILABLE: - return - try: - from microsoft_agents_a365.runtime.environment_utils import ( - get_observability_authentication_scope, - ) - exaau_token = await self.agent_app.auth.exchange_token( context, scopes=get_observability_authentication_scope(), + auth_handler_id=self.auth_handler_name, ) cache_agentic_token(tenant_id, agent_id, exaau_token.token) logger.debug(f"✅ Cached observability token for {tenant_id}:{agent_id}") @@ -479,43 +483,8 @@ def create_and_run_host(agent_class: type[AgentInterface], *agent_args, **agent_ if not check_agent_inheritance(agent_class): raise TypeError(f"Agent class {agent_class.__name__} must inherit from AgentInterface") - # Configure observability if available and enabled - if OBSERVABILITY_AVAILABLE: - enable_observability = os.getenv("ENABLE_OBSERVABILITY", "false").lower() in ("true", "1", "yes") - if enable_observability: - service_name = os.getenv("OBSERVABILITY_SERVICE_NAME", "generic-agent-host") - service_namespace = os.getenv("OBSERVABILITY_SERVICE_NAMESPACE", "agent365") - - # Token resolver for Agent365 exporter (optional) - def token_resolver(agent_id: str, tenant_id: str) -> str | None: - """Resolve authentication token for observability exporter""" - try: - logger.debug(f"Token resolver called for agent_id: {agent_id}, tenant_id: {tenant_id}") - # Use cached agentic token if available - cached_token = get_cached_agentic_token(tenant_id, agent_id) - if cached_token: - logger.debug("Using cached agentic token for observability") - return cached_token - logger.debug("No cached token available for observability") - return None - except Exception as e: - logger.warning(f"Error resolving token for observability: {e}") - return None - - try: - configure_observability( - service_name=service_name, - service_namespace=service_namespace, - token_resolver=token_resolver, - cluster_category=os.getenv("PYTHON_ENVIRONMENT", "development"), - ) - logger.info(f"✅ Observability configured: {service_name} ({service_namespace})") - except Exception as e: - logger.warning(f"⚠️ Failed to configure observability: {e}") - else: - logger.info("ℹ️ Observability disabled (ENABLE_OBSERVABILITY=false)") - else: - logger.debug("ℹ️ Observability packages not available") + # Token resolver for Agent365 exporter + # Note: Actual observability configuration is done in agent.py _setup_observability() # Create the host host = GenericAgentHost(agent_class, *agent_args, **agent_kwargs) diff --git a/python/claude/sample-agent/observability_helpers.py b/python/claude/sample-agent/observability_helpers.py deleted file mode 100644 index 5dace7e3..00000000 --- a/python/claude/sample-agent/observability_helpers.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -""" -Observability helper utilities for Claude Agent. -Creates observability objects for tracing agent invocations, tool execution, and inference. -""" - -import logging -from os import environ -from typing import Optional - -from microsoft_agents.hosting.core import TurnContext -from microsoft_agents_a365.observability.core.agent_details import AgentDetails -from microsoft_agents_a365.observability.core.execution_type import ExecutionType -from microsoft_agents_a365.observability.core.inference_call_details import InferenceCallDetails -from microsoft_agents_a365.observability.core.inference_operation_type import InferenceOperationType -from microsoft_agents_a365.observability.core.request import Request -from microsoft_agents_a365.observability.core.tenant_details import TenantDetails -from microsoft_agents_a365.observability.core.tool_call_details import ToolCallDetails -from microsoft_agents_a365.observability.core.tool_type import ToolType - -logger = logging.getLogger(__name__) - - -def create_agent_details(context: Optional[TurnContext] = None) -> AgentDetails: - """ - Create agent details for observability. - - Args: - context: Optional TurnContext from M365 SDK - - Returns: - AgentDetails instance with agent information - """ - agent_id = environ.get("AGENT_ID", "claude-agent") - conversation_id = None - - if context and context.activity: - # Extract agent ID from recipient if available - if hasattr(context.activity, "recipient") and hasattr(context.activity.recipient, "agentic_app_id"): - agent_id = context.activity.recipient.agentic_app_id or agent_id - - # Extract conversation ID - if hasattr(context.activity, "conversation") and context.activity.conversation: - conversation_id = context.activity.conversation.id - - return AgentDetails( - agent_id=agent_id, - conversation_id=conversation_id, - agent_name=environ.get("OBSERVABILITY_SERVICE_NAME", "Claude Agent"), - agent_description="AI agent powered by Anthropic Claude Agent SDK with extended thinking", - ) - - -def create_tenant_details(context: Optional[TurnContext] = None) -> TenantDetails: - """ - Create tenant details for observability. - - Args: - context: Optional TurnContext from M365 SDK - - Returns: - TenantDetails instance with tenant information - """ - tenant_id = "default-tenant" - - if context and context.activity and hasattr(context.activity, "recipient"): - # Extract tenant ID from activity recipient - if hasattr(context.activity.recipient, "tenant_id") and context.activity.recipient.tenant_id: - tenant_id = context.activity.recipient.tenant_id - logger.debug(f"Extracted tenant from recipient: {tenant_id}") - - # Fall back to environment variable - if tenant_id == "default-tenant": - tenant_id = environ.get("TENANT_ID", "default-tenant") - logger.debug(f"Using tenant ID from environment: {tenant_id}") - - return TenantDetails(tenant_id=tenant_id) - - -def create_request_details( - user_message: str, - session_id: Optional[str] = None, - execution_type: ExecutionType = ExecutionType.HUMAN_TO_AGENT -) -> Request: - """ - Create request details for observability. - - Args: - user_message: The user's input message - session_id: Optional session/conversation ID - execution_type: Type of execution (default: HUMAN_TO_AGENT) - - Returns: - Request instance with request information - """ - return Request( - content=user_message, - execution_type=execution_type, - session_id=session_id, - ) - - -def create_inference_details( - model: str, - input_tokens: int = 0, - output_tokens: int = 0, - thinking_tokens: int = 0, - finish_reasons: Optional[list[str]] = None, - response_id: Optional[str] = None -) -> InferenceCallDetails: - """ - Create inference call details for observability. - - Args: - model: Claude model name (e.g., "claude-sonnet-4-20250514") - input_tokens: Number of input tokens - output_tokens: Number of output tokens - thinking_tokens: Number of extended thinking tokens (unique to Claude) - finish_reasons: List of finish reasons (e.g., ["end_turn"]) - response_id: Optional response ID from Claude - - Returns: - InferenceCallDetails instance with inference information - """ - return InferenceCallDetails( - operationName=InferenceOperationType.CHAT, - model=model, - providerName="anthropic-claude", - inputTokens=input_tokens, - outputTokens=output_tokens + thinking_tokens, # Total output includes thinking - finishReasons=finish_reasons or ["end_turn"], - responseId=response_id, - ) - - -def create_tool_call_details( - tool_name: str, - tool_type: ToolType, - tool_call_id: Optional[str] = None, - arguments: Optional[str] = None, - description: Optional[str] = None -) -> ToolCallDetails: - """ - Create tool call details for observability. - - Args: - tool_name: Name of the tool (e.g., "WebSearch", "Read", "Write") - tool_type: Type of tool (ToolType.FUNCTION) - tool_call_id: Optional unique ID for this tool call - arguments: Optional tool arguments as string - description: Optional tool description - - Returns: - ToolCallDetails instance with tool information - """ - return ToolCallDetails( - tool_name=tool_name, - tool_type=tool_type, - tool_call_id=tool_call_id, - arguments=arguments, - description=description or f"Execute {tool_name} tool", - ) diff --git a/python/claude/sample-agent/pyproject.toml b/python/claude/sample-agent/pyproject.toml index 72046edb..a934ac4f 100644 --- a/python/claude/sample-agent/pyproject.toml +++ b/python/claude/sample-agent/pyproject.toml @@ -1,7 +1,10 @@ [project] name = "sample-claude-agent" version = "0.1.0" -description = "Sample Claude Agent using Microsoft Agent 365 SDK" +description = "Sample Claude Agent using Microsoft Agent 365 SDK with MCP tooling support" +authors = [ + { name = "Microsoft", email = "support@microsoft.com" } +] dependencies = [ # Claude Agent SDK "claude-agent-sdk>=0.1.0", @@ -12,12 +15,6 @@ dependencies = [ "microsoft-agents-authentication-msal", "microsoft-agents-activity", - # Agent 365 Observability - "microsoft-agents-a365-observability-core>=0.1.0", - - # Agent 365 Notifications (Email, Word @mentions, etc.) - "microsoft_agents_a365_notifications >= 0.1.0", - # Core dependencies "python-dotenv", "aiohttp", @@ -25,8 +22,10 @@ dependencies = [ # Additional utilities "typing-extensions>=4.0.0", - # Local packages from local index - # - Update package versions to match your built wheels + # Microsoft Agent 365 SDK packages + "microsoft_agents_a365_tooling >= 0.1.0", + "microsoft_agents_a365_observability_core >= 0.1.0", + "microsoft_agents_a365_notifications >= 0.1.0", "microsoft_agents_a365_runtime >= 0.1.0", ] requires-python = ">=3.11" From 8bcfbc912d010290bf156f6d1ecc899cab72e8b7 Mon Sep 17 00:00:00 2001 From: Joan Bency Date: Mon, 12 Jan 2026 16:45:08 +0000 Subject: [PATCH 2/9] Removed duplicate code --- python/claude/sample-agent/agent.py | 22 +++------------------- python/claude/sample-agent/pyproject.toml | 1 + 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/python/claude/sample-agent/agent.py b/python/claude/sample-agent/agent.py index 20285332..895df553 100644 --- a/python/claude/sample-agent/agent.py +++ b/python/claude/sample-agent/agent.py @@ -214,9 +214,9 @@ def _initialize_mcp_services(self): self.config_service = McpToolServerConfigurationService() logger.info("✅ MCP tool configuration service initialized") - # Note: Claude Agent SDK doesn't support dynamic tool registration like OpenAI - # MCP tools would need to be called separately if needed - logger.info("ℹ️ Claude uses built-in tools - MCP available for future extension") + print("MCP AVAILABLE:", MCP_AVAILABLE) + print("Configured MCP services:", getattr(self, "config_service", None)) + except Exception as e: logger.warning(f"⚠️ Failed to initialize MCP services: {e}") @@ -290,22 +290,6 @@ async def process_user_message( logger.error(f"Error processing message: {e}") logger.exception("Full error details:") return f"Sorry, I encountered an error: {str(e)}" - async for msg in client.receive_response(): - if isinstance(msg, AssistantMessage): - for block in msg.content: - # Collect thinking (Claude's reasoning) - if isinstance(block, ThinkingBlock): - thinking_parts.append(f"💭 {block.thinking}") - # Track thinking tokens - total_thinking_tokens += len(block.thinking.split()) - logger.info(f"💭 Claude thinking: {block.thinking[:100]}...") - - # Collect actual response text - elif isinstance(block, TextBlock): - response_parts.append(block.text) - # Track output tokens - total_output_tokens += len(block.text.split()) - logger.info(f"💬 Claude response: {block.text[:100]}...") diff --git a/python/claude/sample-agent/pyproject.toml b/python/claude/sample-agent/pyproject.toml index a934ac4f..083e115f 100644 --- a/python/claude/sample-agent/pyproject.toml +++ b/python/claude/sample-agent/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "microsoft_agents_a365_observability_core >= 0.1.0", "microsoft_agents_a365_notifications >= 0.1.0", "microsoft_agents_a365_runtime >= 0.1.0", + "wrapt>=2.0.1" ] requires-python = ">=3.11" From 889390563ae3d2f51f0b3015e7fe7790ecff3701 Mon Sep 17 00:00:00 2001 From: Joan Bency Date: Mon, 12 Jan 2026 16:57:48 +0000 Subject: [PATCH 3/9] Updation for resolving comments --- python/claude/sample-agent/.env.template | 5 +++++ python/claude/sample-agent/host_agent_server.py | 10 ++++++---- python/claude/sample-agent/pyproject.toml | 8 ++++---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/python/claude/sample-agent/.env.template b/python/claude/sample-agent/.env.template index b0ba21aa..058e7218 100644 --- a/python/claude/sample-agent/.env.template +++ b/python/claude/sample-agent/.env.template @@ -37,6 +37,11 @@ ENV_ID= # AUTHENTICATION OPTIONS # ============================================================================= +# Auth handler name (optional, defaults to "AGENTIC") +# The name of the authentication handler to use for token exchange +# This should match the handler name configured in your agent authentication settings +AGENT_AUTH_HANDLER_NAME=AGENTIC + # Bearer token (required if not using client credentials) # Use for development/testing without full app registration BEARER_TOKEN=your_bearer_token_here diff --git a/python/claude/sample-agent/host_agent_server.py b/python/claude/sample-agent/host_agent_server.py index 35f8bace..f4827d49 100644 --- a/python/claude/sample-agent/host_agent_server.py +++ b/python/claude/sample-agent/host_agent_server.py @@ -66,20 +66,21 @@ class GenericAgentHost: """Generic host that can host any agent implementing the AgentInterface""" - def __init__(self, agent_class: type[AgentInterface], *agent_args, **agent_kwargs): + def __init__(self, agent_class: type[AgentInterface], *agent_args, auth_handler_name: str | None = None, **agent_kwargs): """ Initialize the generic host with an agent class and its initialization parameters. Args: agent_class: The agent class to instantiate (must implement AgentInterface) *agent_args: Positional arguments to pass to the agent constructor + auth_handler_name: Name of the auth handler to use (defaults to AGENT_AUTH_HANDLER_NAME env var or "AGENTIC") **agent_kwargs: Keyword arguments to pass to the agent constructor """ # Check that the agent inherits from AgentInterface if not check_agent_inheritance(agent_class): raise TypeError(f"Agent class {agent_class.__name__} must inherit from AgentInterface") - self.auth_handler_name = "AGENTIC" + self.auth_handler_name = auth_handler_name or os.getenv("AGENT_AUTH_HANDLER_NAME", "AGENTIC") self.agent_class = agent_class self.agent_args = agent_args @@ -469,13 +470,14 @@ async def cleanup(self): logger.error(f"Error during agent cleanup: {e}") -def create_and_run_host(agent_class: type[AgentInterface], *agent_args, **agent_kwargs): +def create_and_run_host(agent_class: type[AgentInterface], *agent_args, auth_handler_name: str | None = None, **agent_kwargs): """ Convenience function to create and run a generic agent host. Args: agent_class: The agent class to host (must implement AgentInterface) *agent_args: Positional arguments to pass to the agent constructor + auth_handler_name: Name of the auth handler to use (defaults to AGENT_AUTH_HANDLER_NAME env var or "AGENTIC") **agent_kwargs: Keyword arguments to pass to the agent constructor """ try: @@ -487,7 +489,7 @@ def create_and_run_host(agent_class: type[AgentInterface], *agent_args, **agent_ # Note: Actual observability configuration is done in agent.py _setup_observability() # Create the host - host = GenericAgentHost(agent_class, *agent_args, **agent_kwargs) + host = GenericAgentHost(agent_class, *agent_args, auth_handler_name=auth_handler_name, **agent_kwargs) # Create authentication configuration auth_config = host.create_auth_configuration() diff --git a/python/claude/sample-agent/pyproject.toml b/python/claude/sample-agent/pyproject.toml index 083e115f..a269c0c8 100644 --- a/python/claude/sample-agent/pyproject.toml +++ b/python/claude/sample-agent/pyproject.toml @@ -23,10 +23,10 @@ dependencies = [ "typing-extensions>=4.0.0", # Microsoft Agent 365 SDK packages - "microsoft_agents_a365_tooling >= 0.1.0", - "microsoft_agents_a365_observability_core >= 0.1.0", - "microsoft_agents_a365_notifications >= 0.1.0", - "microsoft_agents_a365_runtime >= 0.1.0", + "microsoft_agents_a365_tooling>=0.1.0", + "microsoft_agents_a365_observability_core>=0.1.0", + "microsoft_agents_a365_notifications>=0.1.0", + "microsoft_agents_a365_runtime>=0.1.0", "wrapt>=2.0.1" ] requires-python = ">=3.11" From 5a2d3d63935631aa5cfd2dd0932a6d6d2377cf7e Mon Sep 17 00:00:00 2001 From: Joan Bency Date: Mon, 12 Jan 2026 18:49:03 +0000 Subject: [PATCH 4/9] Minor changes where flag was added back --- python/claude/sample-agent/.env.template | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/claude/sample-agent/.env.template b/python/claude/sample-agent/.env.template index 058e7218..d3d43e65 100644 --- a/python/claude/sample-agent/.env.template +++ b/python/claude/sample-agent/.env.template @@ -108,6 +108,11 @@ OBSERVABILITY_SERVICE_NAME=claude-sample-agent # Service namespace for observability (required) OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples +# Enable Agent 365 Observability Exporter (optional, defaults to false) +# Set to "true" to export telemetry to Agent 365 backend for production monitoring +# When false, uses ConsoleSpanExporter for local debugging (logs to console only) +ENABLE_A365_OBSERVABILITY_EXPORTER=false + # Python environment (influences target cluster/category) # Options: development, production PYTHON_ENVIRONMENT=development From c8cdba8e8050a5bbcebde28ad04f2167dbb833f8 Mon Sep 17 00:00:00 2001 From: Joan Bency Date: Mon, 12 Jan 2026 19:16:10 +0000 Subject: [PATCH 5/9] Shared the console logs for invoke_agent, inference and execute tool --- python/claude/sample-agent/agent.py | 101 ++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/python/claude/sample-agent/agent.py b/python/claude/sample-agent/agent.py index 895df553..33974503 100644 --- a/python/claude/sample-agent/agent.py +++ b/python/claude/sample-agent/agent.py @@ -52,6 +52,17 @@ # Observability Components from microsoft_agents_a365.observability.core.config import configure +from microsoft_agents_a365.observability.core import ( + InvokeAgentScope, + InvokeAgentDetails, + InferenceScope, + InferenceCallDetails, + InferenceOperationType, + AgentDetails, + TenantDetails, + Request, + ExecutionType, +) # MCP Tooling (optional - Claude Agent SDK has built-in tools) try: @@ -242,8 +253,63 @@ async def process_user_message( ) -> str: """Process user message using the Claude Agent SDK with observability tracing""" + # Extract context details for observability + tenant_id = context.activity.recipient.tenant_id if context.activity.recipient else None + agent_id = context.activity.recipient.agentic_app_id if context.activity.recipient else None + conversation_id = context.activity.conversation.id if context.activity.conversation else None + + # Create observability scopes + invoke_scope = None + inference_scope = None + try: logger.info(f"📨 Processing message: {message[:100]}...") + + # Create InvokeAgentScope for tracking agent invocation + agent_details = AgentDetails( + agent_id=agent_id or os.getenv("AGENT_ID", "claude-agent"), + conversation_id=conversation_id, + agent_name=os.getenv("OBSERVABILITY_SERVICE_NAME", "Claude Agent"), + agent_description="AI agent powered by Anthropic Claude Agent SDK", + ) + + tenant_details = TenantDetails(tenant_id=tenant_id or "default-tenant") + + request = Request( + content=message, + execution_type=ExecutionType.HUMAN_TO_AGENT, + session_id=conversation_id, + ) + + invoke_details = InvokeAgentDetails( + details=agent_details, + session_id=conversation_id, + ) + + invoke_scope = InvokeAgentScope.start( + invoke_agent_details=invoke_details, + tenant_details=tenant_details, + request=request, + ) + + # Record input message + if hasattr(invoke_scope, 'record_input_messages'): + invoke_scope.record_input_messages([message]) + + # Create InferenceScope for tracking LLM call + inference_details = InferenceCallDetails( + operationName=InferenceOperationType.CHAT, + model=self.claude_options.model, + providerName="anthropic-claude", + finishReasons=["end_turn"], + ) + + inference_scope = InferenceScope.start( + details=inference_details, + agent_details=agent_details, + tenant_details=tenant_details, + request=request, + ) # Create a new client for this conversation # Claude SDK uses async context manager @@ -284,11 +350,46 @@ async def process_user_message( else: full_response += "I couldn't process your request at this time." + # Record output message + if invoke_scope and hasattr(invoke_scope, 'record_output_messages'): + invoke_scope.record_output_messages([full_response]) + + # Record finish reason + if inference_scope and hasattr(inference_scope, 'record_finish_reasons'): + inference_scope.record_finish_reasons(["end_turn"]) + + # Close scopes successfully + if inference_scope: + inference_scope.__exit__(None, None, None) + if invoke_scope: + invoke_scope.__exit__(None, None, None) + + logger.info("✅ Observability scopes closed successfully") + return full_response except Exception as e: logger.error(f"Error processing message: {e}") logger.exception("Full error details:") + + # Record error in scopes + if invoke_scope and hasattr(invoke_scope, 'record_error'): + invoke_scope.record_error(e) + if inference_scope and hasattr(inference_scope, 'record_error'): + inference_scope.record_error(e) + + # Close scopes with error + if inference_scope: + try: + inference_scope.__exit__(type(e), e, e.__traceback__) + except Exception: + pass + if invoke_scope: + try: + invoke_scope.__exit__(type(e), e, e.__traceback__) + except Exception: + pass + return f"Sorry, I encountered an error: {str(e)}" From ae6e26c609e94f9f55f81e0601250690ddb986bb Mon Sep 17 00:00:00 2001 From: Joan Bency Date: Wed, 14 Jan 2026 19:26:23 +0000 Subject: [PATCH 6/9] Completed observability for invoke and inference --- python/claude/sample-agent/.env.template | 8 + python/claude/sample-agent/agent.py | 341 +++++++++--------- .../claude/sample-agent/host_agent_server.py | 28 +- 3 files changed, 207 insertions(+), 170 deletions(-) diff --git a/python/claude/sample-agent/.env.template b/python/claude/sample-agent/.env.template index d3d43e65..d6a29af8 100644 --- a/python/claude/sample-agent/.env.template +++ b/python/claude/sample-agent/.env.template @@ -108,6 +108,14 @@ OBSERVABILITY_SERVICE_NAME=claude-sample-agent # Service namespace for observability (required) OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples +# Agent observability attributes (optional but recommended) +# These populate the gen_ai.agent.* attributes in traces +AGENT_UPN=your_agent_upn@domain.com +AGENT_AUID=your_agent_auid + + +ENABLE_OBSERVABILITY=true + # Enable Agent 365 Observability Exporter (optional, defaults to false) # Set to "true" to export telemetry to Agent 365 backend for production monitoring # When false, uses ConsoleSpanExporter for local debugging (logs to console only) diff --git a/python/claude/sample-agent/agent.py b/python/claude/sample-agent/agent.py index 33974503..ef9b15f5 100644 --- a/python/claude/sample-agent/agent.py +++ b/python/claude/sample-agent/agent.py @@ -10,14 +10,15 @@ Features: - Claude Agent SDK with extended thinking capability - Microsoft 365 Agents SDK hosting and authentication -- Simplified observability setup +- Complete observability with BaggageBuilder - Conversation continuity across turns - Comprehensive error handling and cleanup """ -import asyncio import logging import os +import json +import uuid from dotenv import load_dotenv @@ -51,6 +52,8 @@ from token_cache import get_cached_agentic_token # Observability Components +from opentelemetry import trace +from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter from microsoft_agents_a365.observability.core.config import configure from microsoft_agents_a365.observability.core import ( InvokeAgentScope, @@ -62,17 +65,15 @@ TenantDetails, Request, ExecutionType, + ToolCallDetails, ) +from microsoft_agents_a365.observability.core.models.caller_details import CallerDetails +from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder +from microsoft_agents_a365.observability.core.execute_tool_scope import ExecuteToolScope -# MCP Tooling (optional - Claude Agent SDK has built-in tools) -try: - from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import ( - McpToolServerConfigurationService, - ) - MCP_AVAILABLE = True -except ImportError: - MCP_AVAILABLE = False - logger.debug("MCP tooling packages not installed - using Claude built-in tools only") +# MCP Tooling - not yet implemented for Claude SDK +# Claude has built-in tools: WebSearch, Read, Write, WebFetch +MCP_AVAILABLE = False # Notifications from microsoft_agents_a365.notifications.agent_notification import NotificationTypes @@ -98,17 +99,10 @@ def __init__(self): # Initialize authentication options self.auth_options = LocalAuthenticationOptions.from_environment() - # Initialize MCP services if available - if MCP_AVAILABLE: - self._initialize_mcp_services() - else: - logger.info("MCP tooling not available - using Claude built-in tools") - # Create Claude client self._create_client() - - # Claude client instance (will be set per conversation) - self.client: ClaudeSDKClient | None = None + + logger.info("Claude Agent uses built-in tools: WebSearch, Read, Write, WebFetch") # @@ -122,7 +116,6 @@ def _create_client(self): # Get model from environment or use default model = os.getenv("CLAUDE_MODEL", "claude-sonnet-4-20250514") - # Get API key api_key = os.getenv("ANTHROPIC_API_KEY") if not api_key: @@ -131,20 +124,14 @@ def _create_client(self): # Configure Claude options self.claude_options = ClaudeAgentOptions( model=model, - # Enable extended thinking for detailed reasoning max_thinking_tokens=1024, - # Allow web search and basic file operations allowed_tools=["WebSearch", "Read", "Write", "WebFetch"], - # Auto-accept edits for smoother operation permission_mode="acceptEdits", continue_conversation=True ) logger.info(f"✅ Claude Agent configured with model: {model}") - - - # # ========================================================================= @@ -159,7 +146,6 @@ def token_resolver(self, agent_id: str, tenant_id: str) -> str | None: Uses the cached agentic token obtained from AGENT_APP.auth.get_token(context, auth_handler_name). This is the only valid authentication method for this context. """ - try: logger.info(f"Token resolver called for agent_id: {agent_id}, tenant_id: {tenant_id}") @@ -180,14 +166,13 @@ def token_resolver(self, agent_id: str, tenant_id: str) -> str | None: def _setup_observability(self): """ - Configure Microsoft Agent 365 observability (simplified pattern) - - This follows the same pattern as the reference examples: - - semantic_kernel: configure() + SemanticKernelInstrumentor().instrument() - - openai_agents: configure() + OpenAIAgentsTraceInstrumentor().instrument() + Configure Microsoft Agent 365 observability + + Follows the pattern from official documentation: + - configure() with service information and token resolver + - Manual scopes for Claude Agent SDK (no auto-instrumentation yet) """ try: - # Step 1: Configure Agent 365 Observability with service information status = configure( service_name=os.getenv("OBSERVABILITY_SERVICE_NAME", "claude-sample-agent"), service_namespace=os.getenv("OBSERVABILITY_SERVICE_NAMESPACE", "agent365-samples"), @@ -198,41 +183,24 @@ def _setup_observability(self): logger.warning("⚠️ Agent 365 Observability configuration failed") return - logger.info("✅ Agent 365 Observability configured successfully") + # Add console exporter for debugging (shows spans in console) + try: + tracer_provider = trace.get_tracer_provider() + console_exporter = ConsoleSpanExporter() + console_processor = BatchSpanProcessor(console_exporter) + tracer_provider.add_span_processor(console_processor) + logger.info("✅ Console span exporter added for debugging") + except Exception as e: + logger.debug(f"Could not add console exporter: {e}") - # Note: Claude Agent SDK doesn't have automatic instrumentation yet - # Manual scopes will be used in process_user_message + logger.info("✅ Agent 365 Observability configured successfully") except Exception as e: logger.error(f"❌ Error setting up observability: {e}") # - # ========================================================================= - # MCP SERVER SETUP (OPTIONAL) - # ========================================================================= - # - - def _initialize_mcp_services(self): - """ - Initialize MCP services for tool server configuration. - - Note: Claude Agent SDK has built-in tools (WebSearch, Read, Write, WebFetch). - MCP tooling provides additional enterprise tools for M365 integration. - """ - try: - # Create configuration service for MCP tool servers - self.config_service = McpToolServerConfigurationService() - logger.info("✅ MCP tool configuration service initialized") - - print("MCP AVAILABLE:", MCP_AVAILABLE) - print("Configured MCP services:", getattr(self, "config_service", None)) - - - except Exception as e: - logger.warning(f"⚠️ Failed to initialize MCP services: {e}") - # # ========================================================================= # INITIALIZATION AND MESSAGE PROCESSING @@ -242,11 +210,9 @@ def _initialize_mcp_services(self): async def initialize(self): """Initialize the agent""" logger.info("Initializing Claude Agent...") - try: - logger.info("Claude Agent initialized successfully") - except Exception as e: - logger.error(f"Failed to initialize agent: {e}") - raise + logger.info("Claude Agent initialized successfully") + + async def process_user_message( self, message: str, auth: Authorization, auth_handler_name: str, context: TurnContext @@ -254,105 +220,156 @@ async def process_user_message( """Process user message using the Claude Agent SDK with observability tracing""" # Extract context details for observability - tenant_id = context.activity.recipient.tenant_id if context.activity.recipient else None - agent_id = context.activity.recipient.agentic_app_id if context.activity.recipient else None - conversation_id = context.activity.conversation.id if context.activity.conversation else None + activity = context.activity + tenant_id = activity.recipient.tenant_id if activity.recipient else None + agent_id = activity.recipient.agentic_app_id if activity.recipient else None + conversation_id = activity.conversation.id if activity.conversation else None - # Create observability scopes - invoke_scope = None - inference_scope = None + # Extract caller information + caller_id = activity.from_property.id if activity.from_property else None + caller_name = activity.from_property.name if activity.from_property else None + caller_aad_object_id = activity.from_property.aad_object_id if activity.from_property else None + channel_id = activity.channel_id or "unknown" try: logger.info(f"📨 Processing message: {message[:100]}...") - # Create InvokeAgentScope for tracking agent invocation - agent_details = AgentDetails( - agent_id=agent_id or os.getenv("AGENT_ID", "claude-agent"), - conversation_id=conversation_id, - agent_name=os.getenv("OBSERVABILITY_SERVICE_NAME", "Claude Agent"), - agent_description="AI agent powered by Anthropic Claude Agent SDK", - ) - - tenant_details = TenantDetails(tenant_id=tenant_id or "default-tenant") - - request = Request( - content=message, - execution_type=ExecutionType.HUMAN_TO_AGENT, - session_id=conversation_id, - ) - - invoke_details = InvokeAgentDetails( - details=agent_details, - session_id=conversation_id, - ) - - invoke_scope = InvokeAgentScope.start( - invoke_agent_details=invoke_details, - tenant_details=tenant_details, - request=request, - ) - - # Record input message - if hasattr(invoke_scope, 'record_input_messages'): - invoke_scope.record_input_messages([message]) - - # Create InferenceScope for tracking LLM call - inference_details = InferenceCallDetails( - operationName=InferenceOperationType.CHAT, - model=self.claude_options.model, - providerName="anthropic-claude", - finishReasons=["end_turn"], - ) - - inference_scope = InferenceScope.start( - details=inference_details, - agent_details=agent_details, - tenant_details=tenant_details, - request=request, - ) - - # Create a new client for this conversation - # Claude SDK uses async context manager - async with ClaudeSDKClient(self.claude_options) as client: - # Send the user message - await client.query(message) + # Use BaggageBuilder to set contextual information that flows through all spans + with ( + BaggageBuilder() + .tenant_id(tenant_id or "default-tenant") + .agent_id(agent_id or os.getenv("AGENT_ID", "claude-agent")) + .correlation_id(conversation_id or str(uuid.uuid4())) + .build() + ): + # Create AgentDetails with required fields per schema + agent_details = AgentDetails( + agent_id=agent_id or os.getenv("AGENT_ID", "claude-agent"), + conversation_id=conversation_id, + agent_name=os.getenv("OBSERVABILITY_SERVICE_NAME", "Claude Agent"), + agent_description="AI agent powered by Anthropic Claude Agent SDK", + tenant_id=tenant_id or "default-tenant", + # Required attributes for InvokeAgentScope + agent_upn=os.getenv("AGENT_UPN"), # gen_ai.agent.upn + agent_application_id=os.getenv("CLIENT_ID"), # gen_ai.agent.applicationid + agent_blueprint_id=os.getenv("CLIENT_ID") or os.getenv("AGENT_BLUEPRINT_ID"), + agent_auid=os.getenv("AGENT_AUID"), + ) - # Collect the response - response_parts = [] - thinking_parts = [] - # Receive and process messages - async for msg in client.receive_response(): - if isinstance(msg, AssistantMessage): - for block in msg.content: - # Collect thinking (Claude's reasoning) - if isinstance(block, ThinkingBlock): - thinking_parts.append(f"💭 {block.thinking}") - logger.info(f"💭 Claude thinking: {block.thinking[:100]}...") - - # Collect actual response text - elif isinstance(block, TextBlock): - response_parts.append(block.text) - logger.info(f"💬 Claude response: {block.text[:100]}...") + # Extract caller information (add UPN and IP if available) + caller = activity.from_property if activity and activity.from_property else None + caller_id = getattr(caller, "id", None) + caller_name = getattr(caller, "name", None) + caller_upn = ( + getattr(caller, "user_principal_name", None) + or getattr(caller, "upn", None) + ) + # Client IP may be set by hosting middleware. If you can’t read it directly, + # carry it via source_metadata so exporter can reflect it in attributes. + client_ip = getattr(activity, "caller_client_ip", None) - # Combine thinking and response - full_response = "" - # Add thinking if present (for transparency) - if thinking_parts: - full_response += "**Claude's Thinking:**\n" - full_response += "\n".join(thinking_parts) - full_response += "\n\n**Response:**\n" + # Create CallerDetails (don't include tenant_id per schema) + caller_details = CallerDetails( + caller_id=caller_id or "unknown-caller", + caller_upn=caller_upn or caller_name or "unknown-user", + caller_user_id=caller_aad_object_id or caller_id or "unknown-user-id", + ) - # Add the actual response - if response_parts: - full_response += "".join(response_parts) - else: - full_response += "I couldn't process your request at this time." - - # Record output message - if invoke_scope and hasattr(invoke_scope, 'record_output_messages'): - invoke_scope.record_output_messages([full_response]) + tenant_details = TenantDetails(tenant_id=tenant_id or "default-tenant") + + # Create Request without source_metadata (causes incorrect attributes) + request = Request( + content=message, + execution_type=ExecutionType.HUMAN_TO_AGENT, + session_id=conversation_id, + ) + + invoke_details = InvokeAgentDetails( + details=agent_details, + session_id=conversation_id, + ) + + # Use context manager pattern per documentation + with InvokeAgentScope.start( + invoke_agent_details=invoke_details, + tenant_details=tenant_details, + request=request, + caller_details=caller_details, + ) as invoke_scope: + # Record input message + if hasattr(invoke_scope, 'record_input_messages'): + invoke_scope.record_input_messages([message]) + + # Create InferenceScope for tracking LLM call + inference_details = InferenceCallDetails( + operationName=InferenceOperationType.CHAT, + model=self.claude_options.model, + providerName="Anthropic Claude", + ) + + with InferenceScope.start( + details=inference_details, + agent_details=agent_details, + tenant_details=tenant_details, + request=request, + ) as inference_scope: + # Create a new client for this conversation + async with ClaudeSDKClient(self.claude_options) as client: + # Send the user message + await client.query(message) + + # Collect the response + response_parts = [] + thinking_parts = [] + + # Receive and process messages + async for msg in client.receive_response(): + if isinstance(msg, AssistantMessage): + for block in msg.content: + if isinstance(block, ThinkingBlock): + thinking_parts.append(f"💭 {block.thinking}") + logger.info(f"💭 Claude thinking: {block.thinking[:100]}...") + elif isinstance(block, TextBlock): + response_parts.append(block.text) + logger.info(f"💬 Claude response: {block.text[:100]}...") + + # Combine thinking and response + full_response = "" + if thinking_parts: + full_response += "**Claude's Thinking:**\n" + full_response += "\n".join(thinking_parts) + full_response += "\n\n**Response:**\n" + + if response_parts: + full_response += "".join(response_parts) + else: + full_response += "I couldn't process your request at this time." + + # Capture usage statistics + usage = getattr(client, "last_usage", None) + if usage and hasattr(inference_scope, "record_input_tokens"): + try: + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + inference_scope.record_input_tokens(int(input_tokens)) + inference_scope.record_output_tokens(int(output_tokens)) + logger.info(f"📊 Tokens: {input_tokens} in, {output_tokens} out") + except Exception as e: + logger.debug(f"Could not record tokens: {e}") + + # Record finish reasons + if hasattr(inference_scope, 'record_finish_reasons'): + inference_scope.record_finish_reasons(["end_turn"]) + + # Record output messages on inference scope (gen_ai.output.messages) + if hasattr(inference_scope, 'record_output_messages'): + inference_scope.record_output_messages([full_response]) + + # Record output message on invoke scope (inside invoke scope, after inference scope closes) + if hasattr(invoke_scope, 'record_output_messages'): + invoke_scope.record_output_messages([full_response]) # Record finish reason if inference_scope and hasattr(inference_scope, 'record_finish_reasons'): @@ -389,11 +406,9 @@ async def process_user_message( invoke_scope.__exit__(type(e), e, e.__traceback__) except Exception: pass - + return f"Sorry, I encountered an error: {str(e)}" - - # # ========================================================================= @@ -427,12 +442,9 @@ async def handle_agent_notification_activity( email = notification_activity.email email_body = getattr(email, "html_body", "") or getattr(email, "body", "") - # Create message for Claude to process the email message = f"You have received the following email. Please follow any instructions in it.\n\n{email_body}" - logger.info(f"📧 Processing email notification") - # Process with Claude response = await self.process_user_message(message, auth, auth_handler_name, context) return response or "Email notification processed." @@ -447,8 +459,6 @@ async def handle_agent_notification_activity( logger.info(f"📄 Processing Word comment notification for doc {doc_id}") - # Note: Without MCP tools, we can't retrieve the actual Word document - # So we'll just process the comment text directly message = ( f"You have been mentioned in a Word document comment.\n" f"Document ID: {doc_id}\n" @@ -456,13 +466,11 @@ async def handle_agent_notification_activity( f"Please respond to this comment appropriately." ) - # Process with Claude response = await self.process_user_message(message, auth, auth_handler_name, context) return response or "Word notification processed." # Generic notification handling else: - # Log full activity structure for debugging logger.info(f"🔍 Full notification activity structure:") logger.info(f" Type: {notification_activity.activity.type}") logger.info(f" Name: {notification_activity.activity.name}") @@ -471,7 +479,6 @@ async def handle_agent_notification_activity( logger.info(f" Entities: {notification_activity.activity.entities}") logger.info(f" Channel ID: {notification_activity.activity.channel_id}") - # Try to get message from activity.text or activity.value notification_message = ( getattr(notification_activity.activity, 'text', None) or str(getattr(notification_activity.activity, 'value', None)) or @@ -479,7 +486,6 @@ async def handle_agent_notification_activity( ) logger.info(f"📨 Processing generic notification: {notification_type}") - # Process with Claude response = await self.process_user_message(notification_message, auth, auth_handler_name, context) return response or "Notification processed successfully." @@ -509,4 +515,3 @@ async def cleanup(self) -> None: logger.error(f"Error during cleanup: {e}") # - diff --git a/python/claude/sample-agent/host_agent_server.py b/python/claude/sample-agent/host_agent_server.py index f4827d49..834f74f0 100644 --- a/python/claude/sample-agent/host_agent_server.py +++ b/python/claude/sample-agent/host_agent_server.py @@ -10,6 +10,7 @@ import os import socket from os import environ +import uuid from aiohttp.web import Application, Request, Response, json_response, run_app from aiohttp.web_middlewares import middleware as web_middleware @@ -133,7 +134,30 @@ async def on_message(context: TurnContext, _: TurnState): try: tenant_id = context.activity.recipient.tenant_id agent_id = context.activity.recipient.agentic_app_id - with BaggageBuilder().tenant_id(tenant_id).agent_id(agent_id).build(): + + caller = getattr(context.activity, "from_property", None) or getattr(context.activity, "from", None) + caller_id = getattr(caller, "id", None) + caller_name = getattr(caller, "name", None) + caller_upn = getattr(caller, "userPrincipalName", None) or getattr(caller, "upn", None) + + conversation_id = context.activity.conversation.id if context.activity.conversation else None + correlation_id = str(uuid.uuid4()) + + with ( + BaggageBuilder() + .tenant_id(tenant_id) + .agent_id(agent_id) + .agent_name(self.agent_class.__name__) + .agent_description("An AI agent powered by Anthropic Claude") + .agent_upn(os.getenv("AGENT_UPN")) # optional if you have one + .caller_id(caller_id) + .caller_name(caller_name) + .caller_upn(caller_upn) + .conversation_id(conversation_id) + .conversation_item_link(os.getenv("CONVERSATION_ITEM_LINK")) # optional + .correlation_id(correlation_id) + .build() + ): # Ensure the agent is available if not self.agent_instance: error_msg = "❌ Sorry, the agent is not available." @@ -259,7 +283,7 @@ async def _handle_notification_with_agent( # Process the notification with the agent response = await self.agent_instance.handle_agent_notification_activity( - notification_activity, self.agent_app.auth, context + notification_activity, self.agent_app.auth, self.auth_handler_name, context ) # For email notifications, wrap response in EmailResponse entity From 8fe045849a6bdddfeba0f6e27945e0a8faa97ece Mon Sep 17 00:00:00 2001 From: Joan Bency Date: Wed, 14 Jan 2026 20:00:01 +0000 Subject: [PATCH 7/9] fix: Move observability init to module level before BaggageBuilder usage- Initialize SDK once at module load to prevent duplicate configurations- Add flag to track initialization state --- python/claude/sample-agent/agent.py | 145 ++++++++++++++-------------- 1 file changed, 74 insertions(+), 71 deletions(-) diff --git a/python/claude/sample-agent/agent.py b/python/claude/sample-agent/agent.py index ef9b15f5..5e0f94c3 100644 --- a/python/claude/sample-agent/agent.py +++ b/python/claude/sample-agent/agent.py @@ -80,6 +80,71 @@ # +# ============================================================================= +# OBSERVABILITY INITIALIZATION (Module-level, runs once) +# ============================================================================= +# + +# Flag to track if observability has been configured +_observability_configured = False + +def _initialize_observability_once(): + """Initialize observability SDK once at module level before any agent instances are created""" + global _observability_configured + + if _observability_configured: + logger.debug("Observability already configured, skipping") + return True + + def token_resolver(agent_id: str, tenant_id: str) -> str | None: + """Token resolver for Agent 365 Observability exporter""" + try: + logger.info(f"Token resolver called for agent_id: {agent_id}, tenant_id: {tenant_id}") + cached_token = get_cached_agentic_token(tenant_id, agent_id) + if cached_token: + logger.info("Using cached agentic token from agent authentication") + return cached_token + else: + logger.warning(f"No cached agentic token found for agent_id: {agent_id}, tenant_id: {tenant_id}") + return None + except Exception as e: + logger.error(f"Error resolving token for agent {agent_id}, tenant {tenant_id}: {e}") + return None + + try: + status = configure( + service_name=os.getenv("OBSERVABILITY_SERVICE_NAME", "claude-sample-agent"), + service_namespace=os.getenv("OBSERVABILITY_SERVICE_NAMESPACE", "agent365-samples"), + token_resolver=token_resolver, + ) + + if not status: + logger.warning("⚠️ Agent 365 Observability configuration failed") + return False + + # Add console exporter for debugging + try: + tracer_provider = trace.get_tracer_provider() + console_exporter = ConsoleSpanExporter() + console_processor = BatchSpanProcessor(console_exporter) + tracer_provider.add_span_processor(console_processor) + logger.info("✅ Console span exporter added for debugging") + except Exception as e: + logger.debug(f"Could not add console exporter: {e}") + + _observability_configured = True + logger.info("✅ Agent 365 Observability configured successfully") + return True + + except Exception as e: + logger.error(f"❌ Error setting up observability: {e}") + return False + +# Initialize observability immediately at module load time +_initialize_observability_once() + +# + class ClaudeAgent(AgentInterface): """Claude Agent integrated with Microsoft 365 Agents SDK""" @@ -93,8 +158,8 @@ def __init__(self): """Initialize the Claude agent.""" self.logger = logging.getLogger(self.__class__.__name__) - # Initialize observability - self._setup_observability() + # Observability is already configured at module level + # No need to configure again here # Initialize authentication options self.auth_options = LocalAuthenticationOptions.from_environment() @@ -134,71 +199,7 @@ def _create_client(self): # - # ========================================================================= - # OBSERVABILITY CONFIGURATION - # ========================================================================= - # - def token_resolver(self, agent_id: str, tenant_id: str) -> str | None: - """ - Token resolver function for Agent 365 Observability exporter. - - Uses the cached agentic token obtained from AGENT_APP.auth.get_token(context, auth_handler_name). - This is the only valid authentication method for this context. - """ - try: - logger.info(f"Token resolver called for agent_id: {agent_id}, tenant_id: {tenant_id}") - - # Use cached agentic token from agent authentication - cached_token = get_cached_agentic_token(tenant_id, agent_id) - if cached_token: - logger.info("Using cached agentic token from agent authentication") - return cached_token - else: - logger.warning( - f"No cached agentic token found for agent_id: {agent_id}, tenant_id: {tenant_id}" - ) - return None - - except Exception as e: - logger.error(f"Error resolving token for agent {agent_id}, tenant {tenant_id}: {e}") - return None - - def _setup_observability(self): - """ - Configure Microsoft Agent 365 observability - - Follows the pattern from official documentation: - - configure() with service information and token resolver - - Manual scopes for Claude Agent SDK (no auto-instrumentation yet) - """ - try: - status = configure( - service_name=os.getenv("OBSERVABILITY_SERVICE_NAME", "claude-sample-agent"), - service_namespace=os.getenv("OBSERVABILITY_SERVICE_NAMESPACE", "agent365-samples"), - token_resolver=self.token_resolver, - ) - - if not status: - logger.warning("⚠️ Agent 365 Observability configuration failed") - return - - # Add console exporter for debugging (shows spans in console) - try: - tracer_provider = trace.get_tracer_provider() - console_exporter = ConsoleSpanExporter() - console_processor = BatchSpanProcessor(console_exporter) - tracer_provider.add_span_processor(console_processor) - logger.info("✅ Console span exporter added for debugging") - except Exception as e: - logger.debug(f"Could not add console exporter: {e}") - - logger.info("✅ Agent 365 Observability configured successfully") - - except Exception as e: - logger.error(f"❌ Error setting up observability: {e}") - - # @@ -234,6 +235,10 @@ async def process_user_message( try: logger.info(f"📨 Processing message: {message[:100]}...") + # Verify observability is configured before using BaggageBuilder + if not _observability_configured: + logger.warning("⚠️ Observability not configured, spans may not be exported") + # Use BaggageBuilder to set contextual information that flows through all spans with ( BaggageBuilder() @@ -242,16 +247,14 @@ async def process_user_message( .correlation_id(conversation_id or str(uuid.uuid4())) .build() ): - # Create AgentDetails with required fields per schema + # Create AgentDetails with valid parameters only agent_details = AgentDetails( agent_id=agent_id or os.getenv("AGENT_ID", "claude-agent"), conversation_id=conversation_id, agent_name=os.getenv("OBSERVABILITY_SERVICE_NAME", "Claude Agent"), agent_description="AI agent powered by Anthropic Claude Agent SDK", tenant_id=tenant_id or "default-tenant", - # Required attributes for InvokeAgentScope - agent_upn=os.getenv("AGENT_UPN"), # gen_ai.agent.upn - agent_application_id=os.getenv("CLIENT_ID"), # gen_ai.agent.applicationid + agent_upn=os.getenv("AGENT_UPN"), agent_blueprint_id=os.getenv("CLIENT_ID") or os.getenv("AGENT_BLUEPRINT_ID"), agent_auid=os.getenv("AGENT_AUID"), ) @@ -406,7 +409,7 @@ async def process_user_message( invoke_scope.__exit__(type(e), e, e.__traceback__) except Exception: pass - + return f"Sorry, I encountered an error: {str(e)}" # From a570b1e63cddde612f1f1f8910c1d5ed42922b2e Mon Sep 17 00:00:00 2001 From: Joan Bency Date: Wed, 14 Jan 2026 22:52:45 +0000 Subject: [PATCH 8/9] Resolved comments --- python/claude/sample-agent/.env.template | 1 - python/claude/sample-agent/agent.py | 86 ++----------------- .../claude/sample-agent/host_agent_server.py | 40 ++++++++- 3 files changed, 47 insertions(+), 80 deletions(-) diff --git a/python/claude/sample-agent/.env.template b/python/claude/sample-agent/.env.template index d6a29af8..f2dbbfda 100644 --- a/python/claude/sample-agent/.env.template +++ b/python/claude/sample-agent/.env.template @@ -110,7 +110,6 @@ OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples # Agent observability attributes (optional but recommended) # These populate the gen_ai.agent.* attributes in traces -AGENT_UPN=your_agent_upn@domain.com AGENT_AUID=your_agent_auid diff --git a/python/claude/sample-agent/agent.py b/python/claude/sample-agent/agent.py index 5e0f94c3..d830f0f7 100644 --- a/python/claude/sample-agent/agent.py +++ b/python/claude/sample-agent/agent.py @@ -17,7 +17,6 @@ import logging import os -import json import uuid from dotenv import load_dotenv @@ -49,12 +48,8 @@ # Microsoft Agents SDK from local_authentication_options import LocalAuthenticationOptions from microsoft_agents.hosting.core import Authorization, TurnContext -from token_cache import get_cached_agentic_token # Observability Components -from opentelemetry import trace -from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter -from microsoft_agents_a365.observability.core.config import configure from microsoft_agents_a365.observability.core import ( InvokeAgentScope, InvokeAgentDetails, @@ -65,11 +60,12 @@ TenantDetails, Request, ExecutionType, - ToolCallDetails, ) from microsoft_agents_a365.observability.core.models.caller_details import CallerDetails from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder -from microsoft_agents_a365.observability.core.execute_tool_scope import ExecuteToolScope + +# Observability configuration (must be imported early) +from observability_config import is_observability_configured # MCP Tooling - not yet implemented for Claude SDK # Claude has built-in tools: WebSearch, Read, Write, WebFetch @@ -80,71 +76,6 @@ # -# ============================================================================= -# OBSERVABILITY INITIALIZATION (Module-level, runs once) -# ============================================================================= -# - -# Flag to track if observability has been configured -_observability_configured = False - -def _initialize_observability_once(): - """Initialize observability SDK once at module level before any agent instances are created""" - global _observability_configured - - if _observability_configured: - logger.debug("Observability already configured, skipping") - return True - - def token_resolver(agent_id: str, tenant_id: str) -> str | None: - """Token resolver for Agent 365 Observability exporter""" - try: - logger.info(f"Token resolver called for agent_id: {agent_id}, tenant_id: {tenant_id}") - cached_token = get_cached_agentic_token(tenant_id, agent_id) - if cached_token: - logger.info("Using cached agentic token from agent authentication") - return cached_token - else: - logger.warning(f"No cached agentic token found for agent_id: {agent_id}, tenant_id: {tenant_id}") - return None - except Exception as e: - logger.error(f"Error resolving token for agent {agent_id}, tenant {tenant_id}: {e}") - return None - - try: - status = configure( - service_name=os.getenv("OBSERVABILITY_SERVICE_NAME", "claude-sample-agent"), - service_namespace=os.getenv("OBSERVABILITY_SERVICE_NAMESPACE", "agent365-samples"), - token_resolver=token_resolver, - ) - - if not status: - logger.warning("⚠️ Agent 365 Observability configuration failed") - return False - - # Add console exporter for debugging - try: - tracer_provider = trace.get_tracer_provider() - console_exporter = ConsoleSpanExporter() - console_processor = BatchSpanProcessor(console_exporter) - tracer_provider.add_span_processor(console_processor) - logger.info("✅ Console span exporter added for debugging") - except Exception as e: - logger.debug(f"Could not add console exporter: {e}") - - _observability_configured = True - logger.info("✅ Agent 365 Observability configured successfully") - return True - - except Exception as e: - logger.error(f"❌ Error setting up observability: {e}") - return False - -# Initialize observability immediately at module load time -_initialize_observability_once() - -# - class ClaudeAgent(AgentInterface): """Claude Agent integrated with Microsoft 365 Agents SDK""" @@ -222,21 +153,22 @@ async def process_user_message( # Extract context details for observability activity = context.activity - tenant_id = activity.recipient.tenant_id if activity.recipient else None - agent_id = activity.recipient.agentic_app_id if activity.recipient else None + recipient = activity.recipient if activity.recipient else None + tenant_id = recipient.tenant_id if recipient else None + agent_id = recipient.agentic_app_id if recipient else None + agent_upn = getattr(recipient, "user_principal_name", None) or getattr(recipient, "upn", None) if recipient else None conversation_id = activity.conversation.id if activity.conversation else None # Extract caller information caller_id = activity.from_property.id if activity.from_property else None caller_name = activity.from_property.name if activity.from_property else None caller_aad_object_id = activity.from_property.aad_object_id if activity.from_property else None - channel_id = activity.channel_id or "unknown" try: logger.info(f"📨 Processing message: {message[:100]}...") # Verify observability is configured before using BaggageBuilder - if not _observability_configured: + if not is_observability_configured(): logger.warning("⚠️ Observability not configured, spans may not be exported") # Use BaggageBuilder to set contextual information that flows through all spans @@ -254,7 +186,7 @@ async def process_user_message( agent_name=os.getenv("OBSERVABILITY_SERVICE_NAME", "Claude Agent"), agent_description="AI agent powered by Anthropic Claude Agent SDK", tenant_id=tenant_id or "default-tenant", - agent_upn=os.getenv("AGENT_UPN"), + agent_upn=agent_upn, # Get from turn context recipient agent_blueprint_id=os.getenv("CLIENT_ID") or os.getenv("AGENT_BLUEPRINT_ID"), agent_auid=os.getenv("AGENT_AUID"), ) diff --git a/python/claude/sample-agent/host_agent_server.py b/python/claude/sample-agent/host_agent_server.py index 834f74f0..adf63873 100644 --- a/python/claude/sample-agent/host_agent_server.py +++ b/python/claude/sample-agent/host_agent_server.py @@ -109,6 +109,37 @@ def __init__(self, agent_class: type[AgentInterface], *agent_args, auth_handler_ # Setup message handlers self._setup_handlers() + + def _extract_conversation_item_link(self, activity): + # 1) Outlook / Word / Loop / SharePoint notifications + try: + link = activity.value.get("resource", {}).get("webUrl") + if link: + return link + except Exception: + pass + + # 2) Teams-based interactions + try: + for entity in activity.entities or []: + link = ( + entity.get("conversationItemLink") or + entity.get("link") + ) + if link: + return link + except Exception: + pass + + # 3) Teams channelData + try: + return activity.channel_data.get("clientInfo", {}).get("conversationItemLink") + except Exception: + pass + + return None + + def _setup_handlers(self): """Setup the Microsoft Agents SDK message handlers""" @@ -135,11 +166,16 @@ async def on_message(context: TurnContext, _: TurnState): tenant_id = context.activity.recipient.tenant_id agent_id = context.activity.recipient.agentic_app_id + # Get agent identity from recipient + recipient = context.activity.recipient + agent_upn = getattr(recipient, "user_principal_name", None) or getattr(recipient, "upn", None) + caller = getattr(context.activity, "from_property", None) or getattr(context.activity, "from", None) caller_id = getattr(caller, "id", None) caller_name = getattr(caller, "name", None) caller_upn = getattr(caller, "userPrincipalName", None) or getattr(caller, "upn", None) + conversation_item_link = self._extract_conversation_item_link(context.activity) conversation_id = context.activity.conversation.id if context.activity.conversation else None correlation_id = str(uuid.uuid4()) @@ -149,12 +185,12 @@ async def on_message(context: TurnContext, _: TurnState): .agent_id(agent_id) .agent_name(self.agent_class.__name__) .agent_description("An AI agent powered by Anthropic Claude") - .agent_upn(os.getenv("AGENT_UPN")) # optional if you have one + .agent_upn(agent_upn) # Get from turn context recipient .caller_id(caller_id) .caller_name(caller_name) .caller_upn(caller_upn) .conversation_id(conversation_id) - .conversation_item_link(os.getenv("CONVERSATION_ITEM_LINK")) # optional + .conversation_item_link(conversation_item_link) # optional .correlation_id(correlation_id) .build() ): From 86f6eb08306fac6b9b5ece5521e30369a03652fb Mon Sep 17 00:00:00 2001 From: Joan Bency Date: Thu, 15 Jan 2026 11:00:27 +0000 Subject: [PATCH 9/9] Added observability code to another file --- .../sample-agent/observability_config.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 python/claude/sample-agent/observability_config.py diff --git a/python/claude/sample-agent/observability_config.py b/python/claude/sample-agent/observability_config.py new file mode 100644 index 00000000..7c0ce83d --- /dev/null +++ b/python/claude/sample-agent/observability_config.py @@ -0,0 +1,75 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Observability Configuration Module + +Handles one-time initialization of Agent 365 Observability SDK. +This module should be imported early in the application lifecycle to ensure +observability is configured before any agents are instantiated. +""" + +import logging +import os + +from opentelemetry import trace +from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter +from microsoft_agents_a365.observability.core.config import configure +from token_cache import get_cached_agentic_token + +logger = logging.getLogger(__name__) + +# Flag to track if observability has been configured +_observability_configured = False + + +def _initialize_observability_once(): + """Initialize observability SDK once at module level before any agent instances are created""" + global _observability_configured + + if _observability_configured: + logger.debug("Observability already configured, skipping") + return True + + def token_resolver(agent_id: str, tenant_id: str) -> str | None: + """Token resolver for Agent 365 Observability exporter""" + try: + logger.info(f"Token resolver called for agent_id: {agent_id}, tenant_id: {tenant_id}") + cached_token = get_cached_agentic_token(tenant_id, agent_id) + if cached_token: + logger.info("Using cached agentic token from agent authentication") + return cached_token + else: + logger.warning(f"No cached agentic token found for agent_id: {agent_id}, tenant_id: {tenant_id}") + return None + except Exception as e: + logger.error(f"Error resolving token for agent {agent_id}, tenant {tenant_id}: {e}") + return None + + try: + status = configure( + service_name=os.getenv("OBSERVABILITY_SERVICE_NAME", "claude-sample-agent"), + service_namespace=os.getenv("OBSERVABILITY_SERVICE_NAMESPACE", "agent365-samples"), + token_resolver=token_resolver, + ) + + if not status: + logger.warning("⚠️ Agent 365 Observability configuration failed") + return False + + _observability_configured = True + logger.info("✅ Agent 365 Observability configured successfully") + return True + + except Exception as e: + logger.error(f"❌ Error setting up observability: {e}") + return False + + +def is_observability_configured() -> bool: + """Check if observability has been configured""" + return _observability_configured + + +# Initialize observability immediately at module load time +_initialize_observability_once()