diff --git a/dotnet/semantic-kernel/sample-agent/Agents/Agent365Agent.cs b/dotnet/semantic-kernel/sample-agent/Agents/Agent365Agent.cs index 3ca0569f..582cb73f 100644 --- a/dotnet/semantic-kernel/sample-agent/Agents/Agent365Agent.cs +++ b/dotnet/semantic-kernel/sample-agent/Agents/Agent365Agent.cs @@ -1,21 +1,21 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Agent365SemanticKernelSampleAgent.Plugins; -using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services; -using Microsoft.Agents.Builder; -using Microsoft.Agents.Builder.App.UserAuth; -using Microsoft.Extensions.Configuration; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Agent365SemanticKernelSampleAgent.Plugins; +using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services; +using Microsoft.Agents.Builder; +using Microsoft.Agents.Builder.App.UserAuth; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; namespace Agent365SemanticKernelSampleAgent.Agents; @@ -54,10 +54,28 @@ public static async Task CreateA365AgentWrapper(Kernel kernel, IS return _agent; } - public static bool TryGetBearerTokenForDevelopment(out string? bearerToken) - { - bearerToken = Environment.GetEnvironmentVariable("BEARER_TOKEN"); - return !string.IsNullOrEmpty(bearerToken); + public static bool TryGetBearerTokenForDevelopment(out string? bearerToken) + { + bearerToken = Environment.GetEnvironmentVariable("BEARER_TOKEN"); + return !string.IsNullOrEmpty(bearerToken); + } + + /// + /// Checks if graceful fallback to bare LLM mode is enabled when MCP tools fail to load. + /// This is only allowed in Development environment AND when SKIP_TOOLING_ON_ERRORS is explicitly set to "true". + /// + private static bool ShouldSkipToolingOnErrors() + { + var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? + Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? + "Production"; + + var skipToolingOnErrors = Environment.GetEnvironmentVariable("SKIP_TOOLING_ON_ERRORS"); + + // Only allow skipping tooling errors in Development mode AND when explicitly enabled + return environment.Equals("Development", StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrEmpty(skipToolingOnErrors) && + skipToolingOnErrors.Equals("true", StringComparison.OrdinalIgnoreCase); } /// @@ -79,17 +97,37 @@ public async Task InitializeAgent365Agent(Kernel kernel, IServiceProvider servic // Provide the tool service with necessary parameters to connect to A365 this._kernel.ImportPluginFromType(); - await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Loading tools..."); - - if (TryGetBearerTokenForDevelopment(out var bearerToken)) - { - // Development mode: Use bearer token from environment variable for simplified local testing - await toolService.AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext, bearerToken); - } - else - { - // Production mode: Use standard authentication flow (Client Credentials, Managed Identity, or Federated Credentials) - await toolService.AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext); + await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Loading tools..."); + + try + { + if (TryGetBearerTokenForDevelopment(out var bearerToken)) + { + // Development mode: Use bearer token from environment variable for simplified local testing + await toolService.AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext, bearerToken); + } + else + { + // Production mode: Use standard authentication flow (Client Credentials, Managed Identity, or Federated Credentials) + await toolService.AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext); + } + } + catch (Exception ex) + { + // Only allow graceful fallback in Development mode when SKIP_TOOLING_ON_ERRORS is explicitly enabled + if (ShouldSkipToolingOnErrors()) + { + // Graceful fallback: Log the error but continue without MCP tools + // This allows the agent to still respond to basic queries using only the LLM + System.Diagnostics.Debug.WriteLine($"Warning: Failed to load MCP tools: {ex.Message}"); + Console.WriteLine($"Warning: MCP tools unavailable - running in bare LLM mode. Error: {ex.Message}"); + await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Note: Some tools are not available. Running in basic mode."); + } + else + { + // In production or when SKIP_TOOLING_ON_ERRORS is not enabled, fail fast + throw; + } } } else diff --git a/dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs b/dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs index ddfbe39c..c206d839 100644 --- a/dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs +++ b/dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs @@ -122,6 +122,12 @@ await A365OtelWrapper.InvokeObservedAgentOperation( { await TeamsMessageActivityAsync(agent365Agent, turnContext, turnState, cancellationToken); } + else if (turnContext.Activity.ChannelId.Channel == Channels.Emulator || + turnContext.Activity.ChannelId.Channel == Channels.Test) + { + var response = await agent365Agent.InvokeAgentAsync(turnContext.Activity.Text, new ChatHistory(), turnContext); + await OutputResponseAsync(turnContext, turnState, response, cancellationToken); + } else { await turnContext.SendActivityAsync(MessageFactory.Text($"Sorry, I do not know how to respond to messages from channel '{turnContext.Activity.ChannelId}'."), cancellationToken); diff --git a/dotnet/semantic-kernel/sample-agent/ToolingManifest.json b/dotnet/semantic-kernel/sample-agent/ToolingManifest.json index fb64e3fe..7c94ba62 100644 --- a/dotnet/semantic-kernel/sample-agent/ToolingManifest.json +++ b/dotnet/semantic-kernel/sample-agent/ToolingManifest.json @@ -2,21 +2,6 @@ "mcpServers": [ { "mcpServerName": "mcp_MailTools" - }, - { - "mcpServerName": "mcp_CalendarTools" - }, - { - "mcpServerName": "OneDriveMCPServer" - }, - { - "mcpServerName": "mcp_NLWeb" - }, - { - "mcpServerName": "mcp_KnowledgeTools" - }, - { - "mcpServerName": "mcp_MeServer" } ] } \ No newline at end of file diff --git a/python/openai/sample-agent/agent.py b/python/openai/sample-agent/agent.py index c510c3f6..9cf2ec5e 100644 --- a/python/openai/sample-agent/agent.py +++ b/python/openai/sample-agent/agent.py @@ -65,6 +65,18 @@ class OpenAIAgentWithMCP(AgentInterface): # ========================================================================= # + @staticmethod + def should_skip_tooling_on_errors() -> bool: + """ + Checks if graceful fallback to bare LLM mode is enabled when MCP tools fail to load. + This is only allowed in Development environment AND when SKIP_TOOLING_ON_ERRORS is explicitly set to "true". + """ + environment = os.getenv("ENVIRONMENT", os.getenv("ASPNETCORE_ENVIRONMENT", "Production")) + skip_tooling_on_errors = os.getenv("SKIP_TOOLING_ON_ERRORS", "").lower() + + # Only allow skipping tooling errors in Development mode AND when explicitly enabled + return environment.lower() == "development" and skip_tooling_on_errors == "true" + def __init__(self, openai_api_key: str | None = None): self.openai_api_key = openai_api_key or os.getenv("OPENAI_API_KEY") if not self.openai_api_key and ( @@ -84,11 +96,15 @@ def __init__(self, openai_api_key: str | None = None): api_key=api_key, api_version="2025-01-01-preview", ) + # Use Azure deployment name for Azure OpenAI + model_name = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini") else: self.openai_client = AsyncOpenAI(api_key=self.openai_api_key) + # Use model name for OpenAI + model_name = os.getenv("OPENAI_MODEL", "gpt-4o-mini") self.model = OpenAIChatCompletionsModel( - model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"), openai_client=self.openai_client + model=model_name, openai_client=self.openai_client ) # Configure model settings (optional parameters) @@ -220,28 +236,52 @@ def _initialize_services(self): # return tool_service, auth_options async def setup_mcp_servers(self, auth: Authorization, auth_handler_name: str, context: TurnContext): - """Set up MCP server connections""" + """Set up MCP server connections based on authentication configuration. + + Authentication priority: + 1. Bearer token from config (BEARER_TOKEN) - for local development/testing + 2. Auth handler (auth_handler_name) - for production agentic auth + 3. No auth - gracefully skip MCP and run in bare LLM mode + + If MCP connection fails for any reason, the agent will gracefully fall back + to bare LLM mode without MCP tools. + """ try: - - use_agentic_auth = os.getenv("USE_AGENTIC_AUTH", "false").lower() == "true" - if use_agentic_auth: + # Priority 1: Bearer token provided in config (for local dev/testing) + if self.auth_options.bearer_token: + logger.info("🔑 Using bearer token from config for MCP servers") self.agent = await self.tool_service.add_tool_servers_to_agent( agent=self.agent, auth=auth, auth_handler_name=auth_handler_name, context=context, + auth_token=self.auth_options.bearer_token, ) - else: + # Priority 2: Auth handler configured (production agentic auth) + elif auth_handler_name: + logger.info(f"🔒 Using auth handler '{auth_handler_name}' for MCP servers") self.agent = await self.tool_service.add_tool_servers_to_agent( agent=self.agent, auth=auth, auth_handler_name=auth_handler_name, context=context, - auth_token=self.auth_options.bearer_token, ) + # Priority 3: No auth configured - skip MCP and run bare LLM + else: + logger.warning("⚠️ No authentication configured - running in bare LLM mode without MCP tools") + logger.info("💡 To enable MCP: provide BEARER_TOKEN or configure AUTH_HANDLER_NAME") + # Agent already initialized without MCP tools except Exception as e: - logger.error(f"Error setting up MCP servers: {e}") + # Only allow graceful fallback in Development mode when SKIP_TOOLING_ON_ERRORS is explicitly enabled + if self.should_skip_tooling_on_errors(): + logger.error(f"❌ Error setting up MCP servers: {e}") + logger.warning("⚠️ Falling back to bare LLM mode without MCP servers (SKIP_TOOLING_ON_ERRORS=true)") + # Agent continues with base LLM capabilities only + else: + # In production or when SKIP_TOOLING_ON_ERRORS is not enabled, fail fast + logger.error(f"❌ Error setting up MCP servers: {e}") + raise async def initialize(self): """Initialize the agent and MCP server connections""" diff --git a/python/openai/sample-agent/host_agent_server.py b/python/openai/sample-agent/host_agent_server.py index d124d980..4df50db0 100644 --- a/python/openai/sample-agent/host_agent_server.py +++ b/python/openai/sample-agent/host_agent_server.py @@ -68,7 +68,13 @@ 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" + # Auth handler name can be configured via environment + # Defaults to empty (no auth handler) - set AUTH_HANDLER_NAME=AGENTIC for production agentic auth + self.auth_handler_name = os.getenv("AUTH_HANDLER_NAME", "") or None + if self.auth_handler_name: + logger.info(f"🔐 Using auth handler: {self.auth_handler_name}") + else: + logger.info("🔓 No auth handler configured (AUTH_HANDLER_NAME not set)") self.agent_class = agent_class self.agent_args = agent_args @@ -110,8 +116,9 @@ async def help_handler(context: TurnContext, _: TurnState): self.agent_app.conversation_update("membersAdded")(help_handler) self.agent_app.message("/help")(help_handler) - handler = [self.auth_handler_name] - @self.agent_app.activity("message", auth_handlers=handler) + # Configure auth handlers - required for token exchange when auth_handler_name is set + handler_config = {"auth_handlers": [self.auth_handler_name]} if self.auth_handler_name else {} + @self.agent_app.activity("message", **handler_config) async def on_message(context: TurnContext, _: TurnState): """Handle all messages with the hosted agent""" try: @@ -125,18 +132,20 @@ async def on_message(context: TurnContext, _: TurnState): 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, - ) - - # Cache the agentic token for Agent 365 Observability exporter use - cache_agentic_token( - tenant_id, - agent_id, - exaau_token.token, - ) + # Exchange token for observability if auth handler is configured + if self.auth_handler_name: + exaau_token = await self.agent_app.auth.exchange_token( + context, + scopes=get_observability_authentication_scope(), + auth_handler_id=self.auth_handler_name, + ) + + # Cache the agentic token for Agent 365 Observability exporter use + cache_agentic_token( + tenant_id, + agent_id, + exaau_token.token, + ) user_message = context.activity.text or "" logger.info(f"📨 Processing message: '{user_message}'") @@ -208,7 +217,7 @@ def create_auth_configuration(self) -> AgentAuthConfiguration | None: if environ.get("BEARER_TOKEN"): logger.info( - "🔑 BEARER_TOKEN present but incomplete app registration; continuing in anonymous dev mode" + "🔑 BEARER_TOKEN present - will use for MCP server authentication" ) else: logger.warning("⚠️ No authentication env vars found; running anonymous") diff --git a/python/openai/sample-agent/local_authentication_options.py b/python/openai/sample-agent/local_authentication_options.py index a732a7d6..111753e4 100644 --- a/python/openai/sample-agent/local_authentication_options.py +++ b/python/openai/sample-agent/local_authentication_options.py @@ -58,11 +58,18 @@ def from_environment( LocalAuthenticationOptions instance with values from environment. """ # Load .env file (automatically searches current and parent directories) - load_dotenv() + load_dotenv(override=True) # Force reload to pick up changes bearer_token = os.getenv(token_var, "") print(f"🔧 Bearer Token: {'***' if bearer_token else 'NOT SET'}") + + # DEBUG: Print token details + if bearer_token: + print(f"🔍 DEBUG: Token loaded from env, length: {len(bearer_token)}") + print(f"🔍 DEBUG: Token first 50 chars: {bearer_token[:50]}...") + else: + print(f"⚠️ DEBUG: No BEARER_TOKEN found in environment!") return cls(bearer_token=bearer_token)