Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions application/single_app/semantic_kernel_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
from semantic_kernel_plugins.plugin_loader import discover_plugins
from semantic_kernel_plugins.openapi_plugin_factory import OpenApiPluginFactory
import app_settings_cache
try:
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
except ImportError:
DefaultAzureCredential = None
get_bearer_token_provider = None



Expand Down Expand Up @@ -758,7 +763,9 @@ def load_single_agent_for_kernel(kernel, agent_cfg, settings, context_obj, redis

log_event(f"[SK Loader] Agent config resolved for {agent_cfg.get('name')} - endpoint: {bool(agent_config.get('endpoint'))}, key: {bool(agent_config.get('key'))}, deployment: {agent_config.get('deployment')}, max_completion_tokens: {agent_config.get('max_completion_tokens')}", level=logging.INFO)

if AzureChatCompletion and agent_config["endpoint"] and agent_config["key"] and agent_config["deployment"]:
auth_type = settings.get('azure_openai_gpt_authentication_type', '')
use_managed_identity = (auth_type == 'managed_identity') and not apim_enabled and not agent_config.get("key")
Comment thread
vivche marked this conversation as resolved.
Outdated
if AzureChatCompletion and agent_config["endpoint"] and (agent_config["key"] or use_managed_identity) and agent_config["deployment"]:
Comment thread
vivche marked this conversation as resolved.
Comment thread
vivche marked this conversation as resolved.
print(f"[SK Loader] Azure config valid for {agent_config['name']}, creating chat service...")
if apim_enabled:
log_event(
Expand All @@ -779,6 +786,25 @@ def load_single_agent_for_kernel(kernel, agent_cfg, settings, context_obj, redis
api_version=agent_config["api_version"],
# default_headers={"Ocp-Apim-Subscription-Key": agent_config["key"]}
)
elif use_managed_identity:
log_event(
f"[SK Loader] Initializing Managed Identity AzureChatCompletion for agent: {agent_config['name']} ({mode_label})",
{
"aoai_endpoint": agent_config["endpoint"],
"aoai_deployment": agent_config["deployment"],
"agent_name": agent_config["name"]
},
level=logging.INFO
)
_scope = "https://cognitiveservices.azure.us/.default" if ".azure.us" in (agent_config.get("endpoint") or "") else "https://cognitiveservices.azure.com/.default"
Comment thread
vivche marked this conversation as resolved.
Outdated
_token_provider = get_bearer_token_provider(DefaultAzureCredential(), _scope)
Comment thread
vivche marked this conversation as resolved.
Outdated
chat_service = AzureChatCompletion(
service_id=service_id,
deployment_name=agent_config["deployment"],
endpoint=agent_config["endpoint"],
ad_token_provider=_token_provider,
api_version=agent_config["api_version"],
)
else:
log_event(
f"[SK Loader] Initializing GPT Direct AzureChatCompletion for agent: {agent_config['name']} ({mode_label})",
Expand Down Expand Up @@ -1521,7 +1547,10 @@ def load_semantic_kernel(kernel: Kernel, settings):
agent_config = resolve_agent_config(agent_cfg, settings)
chat_service = None
service_id = f"aoai-chat-{agent_config['name'].replace(' ', '').lower()}"
if AzureChatCompletion and agent_config["endpoint"] and agent_config["key"] and agent_config["deployment"]:
_ma_auth_type = settings.get('azure_openai_gpt_authentication_type', '')
_ma_apim_enabled = settings.get("enable_gpt_apim", False)
_ma_use_mi = (_ma_auth_type == 'managed_identity') and not _ma_apim_enabled and not agent_config.get("key")
Comment thread
vivche marked this conversation as resolved.
Outdated
if AzureChatCompletion and agent_config["endpoint"] and (agent_config["key"] or _ma_use_mi) and agent_config["deployment"]:
try:
try:
chat_service = kernel.get_service(service_id=service_id)
Expand All @@ -1548,6 +1577,16 @@ def load_semantic_kernel(kernel: Kernel, settings):
api_version=agent_config["api_version"],
# default_headers={"Ocp-Apim-Subscription-Key": agent_config["key"]}
)
elif _ma_use_mi:
_scope = "https://cognitiveservices.azure.us/.default" if ".azure.us" in (agent_config.get("endpoint") or "") else "https://cognitiveservices.azure.com/.default"
_token_provider = get_bearer_token_provider(DefaultAzureCredential(), _scope)
chat_service = AzureChatCompletion(
service_id=service_id,
deployment_name=agent_config["deployment"],
endpoint=agent_config["endpoint"],
ad_token_provider=_token_provider,
api_version=agent_config["api_version"],
)
else:
chat_service = AzureChatCompletion(
service_id=service_id,
Expand Down Expand Up @@ -1631,7 +1670,10 @@ def load_semantic_kernel(kernel: Kernel, settings):
orchestrator_config = resolve_agent_config(orchestrator_cfg, settings)
service_id = f"aoai-chat-{orchestrator_config['name']}"
chat_service = None
if AzureChatCompletion and orchestrator_config["endpoint"] and orchestrator_config["key"] and orchestrator_config["deployment"]:
_orch_auth_type = settings.get('azure_openai_gpt_authentication_type', '')
_orch_apim_enabled = settings.get("enable_gpt_apim", False)
_orch_use_mi = (_orch_auth_type == 'managed_identity') and not _orch_apim_enabled and not orchestrator_config.get("key")
Comment thread
vivche marked this conversation as resolved.
Outdated
if AzureChatCompletion and orchestrator_config["endpoint"] and (orchestrator_config["key"] or _orch_use_mi) and orchestrator_config["deployment"]:
try:
chat_service = kernel.get_service(service_id=service_id)
except Exception:
Expand All @@ -1657,6 +1699,16 @@ def load_semantic_kernel(kernel: Kernel, settings):
api_version=orchestrator_config["api_version"],
# default_headers={"Ocp-Apim-Subscription-Key": orchestrator_config["key"]}
)
elif _orch_use_mi:
_scope = "https://cognitiveservices.azure.us/.default" if ".azure.us" in (orchestrator_config.get("endpoint") or "") else "https://cognitiveservices.azure.com/.default"
_token_provider = get_bearer_token_provider(DefaultAzureCredential(), _scope)
chat_service = AzureChatCompletion(
service_id=service_id,
deployment_name=orchestrator_config["deployment"],
endpoint=orchestrator_config["endpoint"],
ad_token_provider=_token_provider,
api_version=orchestrator_config["api_version"],
)
else:
chat_service = AzureChatCompletion(
service_id=service_id,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Agent Managed Identity SK Loader Fix

**Fixed/Implemented in version:** **0.238.025** (matches `config.py` `app.config['VERSION']`)
Comment thread
vivche marked this conversation as resolved.
Outdated
**GitHub Issue:** [#769 — Agents fail silently when using Managed Identity authentication](https://github.com/microsoft/simplechat/issues/769)

## Issue Description

When using **Azure Managed Identity (MI)** for Azure OpenAI authentication, agents configured through
the **Model & Connection** page (Step 2 of the agent wizard) failed silently — the agent never loaded,
fell back to plain GPT-4.1 with no tools or instructions, and fabricated responses instead of calling
real APIs (e.g., ServiceNow).

## Root Cause Analysis

### How Agent Config Is Resolved

`resolve_agent_config()` in `semantic_kernel_loader.py` (~line 107) figures out which endpoint/key/
deployment to use for an agent by running through a **decision tree** (~line 291):

```
# 1. User APIM enabled and any user APIM values set → use user APIM
# 2. User APIM enabled but empty, global APIM enabled → use global APIM
# 3. Agent GPT config is FULLY filled → use agent GPT config
# 4. Agent GPT config is PARTIALLY filled, global APIM off → merge agent GPT with global GPT
# 5. Global APIM enabled → use global APIM
# 6. Fallback → use global GPT config entirely
```

### The Failure

When an agent is configured with only the deployment name set (endpoint and key left blank), the
decision tree hits **case 4** — it merges the agent's partial config with global settings:

```
Agent-level: endpoint='', key='', deployment='gpt-4.1', api_version=''
```

After merge with global settings:
- `endpoint` = global endpoint ✓
- `deployment` = `'gpt-4.1'` ✓
- `key` = global key = **`None`** ✗ (MI auth — no API key is stored in settings)

The gate condition at ~line 768 then fails:

```python
if AzureChatCompletion and agent_config["endpoint"] and agent_config["key"] and agent_config["deployment"]:
```

`agent_config["key"]` is `None` → **condition is False** → falls into the `else` block:

```
[SK Loader] Azure config INVALID for servicenow_support_agent:
- AzureChatCompletion available: True
- endpoint: True
- key: False ← THIS IS THE FAILURE
- deployment: True
```

Returns `None, None` → no agent loaded → chat uses plain GPT-4.1 with no tools/instructions
→ GPT fabricates responses instead of calling the actual API.

## Files Modified

| File | Lines Changed |
|------|--------------|
| `application/single_app/semantic_kernel_loader.py` | ~43-47, ~767-768, ~810-829, ~1530-1532, ~1548-1558, ~1636-1638, ~1655-1665 |
| `application/single_app/config.py` | VERSION bump |

## Fix

### 1. Added Azure Identity imports (~line 43)

```python
try:
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
except ImportError:
DefaultAzureCredential = None
get_bearer_token_provider = None
```

### 2. Added MI detection before each gate (~line 767)

At each of the 3 `AzureChatCompletion` creation sites (single agent, multi-agent specialist,
multi-agent orchestrator):

```python
auth_type = settings.get('azure_openai_gpt_authentication_type', '')
use_managed_identity = (auth_type == 'managed_identity') and not apim_enabled and not agent_config.get("key")
```

`use_managed_identity` is `True` when ALL of:
- Global auth type is `managed_identity`
- APIM is not enabled (APIM uses subscription keys, not MI)
- No API key is present (if a key exists, use it directly)

### 3. Updated gate condition to accept MI (~line 768)

Before:
```python
if AzureChatCompletion and agent_config["endpoint"] and agent_config["key"] and agent_config["deployment"]:
```

After:
```python
if AzureChatCompletion and agent_config["endpoint"] and (agent_config["key"] or use_managed_identity) and agent_config["deployment"]:
```

### 4. Added MI branch for AzureChatCompletion creation (~line 789)

Between the existing APIM branch and direct-key branch, a new `elif use_managed_identity:` block:

```python
elif use_managed_identity:
# Detect gov vs commercial cloud from endpoint URL
_scope = "https://cognitiveservices.azure.us/.default" if ".azure.us" in (agent_config.get("endpoint") or "") else "https://cognitiveservices.azure.com/.default"
_token_provider = get_bearer_token_provider(DefaultAzureCredential(), _scope)
chat_service = AzureChatCompletion(
service_id=service_id,
deployment_name=agent_config["deployment"],
endpoint=agent_config["endpoint"],
ad_token_provider=_token_provider, # ← MI token, not api_key
api_version=agent_config["api_version"],
)
```

The scope is auto-detected: endpoints containing `.azure.us` use the Azure Government scope;
all others use the commercial Azure scope.

## Auth Flow After Fix

```
User sends message
→ SK Loader resolves agent config (case 4: merge agent partial + global GPT)
→ endpoint = global endpoint, key = None (MI), deployment = 'gpt-4.1'
→ use_managed_identity = True (auth_type='managed_identity', key=None, APIM=off)
→ Gate passes: (agent_config["key"] or use_managed_identity) = True
→ AzureChatCompletion created with ad_token_provider (DefaultAzureCredential)
→ Agent loads with full instructions + ServiceNow tools (OpenAPI plugin)
→ Agent calls queryAssets → OpenAPI plugin injects Bearer token → ServiceNow returns real data
→ Real results displayed (no fabrication)
```