Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
6 changes: 3 additions & 3 deletions .github/workflows/docker_image_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ jobs:
uses: Azure/docker-login@v2
with:
# Container registry username
username: ${{ secrets.MAIN_ACR_USERNAME }}
username: ${{ secrets.ACR_USERNAME }}
# Container registry password
password: ${{ secrets.MAIN_ACR_PASSWORD }}
password: ${{ secrets.ACR_PASSWORD }}
# Container registry server url
login-server: ${{ secrets.MAIN_ACR_LOGIN_SERVER }}
login-server: ${{ secrets.ACR_LOGIN_SERVER }}
- name: Normalize branch name for tag
run: |
REF="${GITHUB_REF_NAME}"
Expand Down
73 changes: 70 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,14 @@ 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")
and bool(DefaultAzureCredential)
)
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 +791,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 +1552,15 @@ 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")
and bool(DefaultAzureCredential)
)
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 +1587,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 +1680,15 @@ 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")
and bool(DefaultAzureCredential)
)
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 +1714,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)
```
Loading