Skip to content

Commit 3b8de8e

Browse files
Merge pull request microsoft#355 from microsoft/dev
feat: replacing DefaultAzureCredential with ManagedIdentityCredential, RAI check, API upgrades
2 parents cd1cf07 + 91ea5df commit 3b8de8e

24 files changed

+363
-69
lines changed

infra/main.bicep

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,10 @@ module containerApp 'br/public:avm/res/app/container-app:0.14.2' = if (container
10341034
name: 'AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME'
10351035
value: aiFoundryAiServicesModelDeployment.name
10361036
}
1037+
{
1038+
name: 'APP_ENV'
1039+
value: 'Prod'
1040+
}
10371041
]
10381042
}
10391043
]
@@ -1087,6 +1091,7 @@ module webSite 'br/public:avm/res/web/site:0.15.1' = if (webSiteEnabled) {
10871091
WEBSITES_CONTAINER_START_TIME_LIMIT: '1800' // 30 minutes, adjust as needed
10881092
BACKEND_API_URL: 'https://${containerApp.outputs.fqdn}'
10891093
AUTH_ENABLED: 'false'
1094+
APP_ENV: 'Prod'
10901095
}
10911096
}
10921097
}

src/backend/.env.sample

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
1616
APPLICATIONINSIGHTS_CONNECTION_STRING=
1717
AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME=gpt-4o
1818
AZURE_AI_AGENT_ENDPOINT=
19+
APP_ENV="dev"
1920

2021
BACKEND_API_URL=http://localhost:8000
2122
FRONTEND_SITE_NAME=http://127.0.0.1:3000

src/backend/app_config.py

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from azure.ai.projects.aio import AIProjectClient
77
from azure.cosmos.aio import CosmosClient
8-
from azure.identity import DefaultAzureCredential
8+
from helpers.azure_credential_utils import get_azure_credential
99
from dotenv import load_dotenv
1010
from semantic_kernel.kernel import Kernel
1111

@@ -106,23 +106,6 @@ def _get_bool(self, name: str) -> bool:
106106
"""
107107
return name in os.environ and os.environ[name].lower() in ["true", "1"]
108108

109-
def get_azure_credentials(self):
110-
"""Get Azure credentials using DefaultAzureCredential.
111-
112-
Returns:
113-
DefaultAzureCredential instance for Azure authentication
114-
"""
115-
# Cache the credentials object
116-
if self._azure_credentials is not None:
117-
return self._azure_credentials
118-
119-
try:
120-
self._azure_credentials = DefaultAzureCredential()
121-
return self._azure_credentials
122-
except Exception as exc:
123-
logging.warning("Failed to create DefaultAzureCredential: %s", exc)
124-
return None
125-
126109
def get_cosmos_database_client(self):
127110
"""Get a Cosmos DB client for the configured database.
128111
@@ -132,7 +115,7 @@ def get_cosmos_database_client(self):
132115
try:
133116
if self._cosmos_client is None:
134117
self._cosmos_client = CosmosClient(
135-
self.COSMOSDB_ENDPOINT, credential=self.get_azure_credentials()
118+
self.COSMOSDB_ENDPOINT, credential=get_azure_credential()
136119
)
137120

138121
if self._cosmos_database is None:
@@ -169,10 +152,10 @@ def get_ai_project_client(self):
169152
return self._ai_project_client
170153

171154
try:
172-
credential = self.get_azure_credentials()
155+
credential = get_azure_credential()
173156
if credential is None:
174157
raise RuntimeError(
175-
"Unable to acquire Azure credentials; ensure DefaultAzureCredential is configured"
158+
"Unable to acquire Azure credentials; ensure Managed Identity is configured"
176159
)
177160

178161
endpoint = self.AZURE_AI_AGENT_ENDPOINT
@@ -183,6 +166,22 @@ def get_ai_project_client(self):
183166
logging.error("Failed to create AIProjectClient: %s", exc)
184167
raise
185168

169+
def get_user_local_browser_language(self) -> str:
170+
"""Get the user's local browser language from environment variables.
171+
172+
Returns:
173+
The user's local browser language or 'en-US' if not set
174+
"""
175+
return self._get_optional("USER_LOCAL_BROWSER_LANGUAGE", "en-US")
176+
177+
def set_user_local_browser_language(self, language: str):
178+
"""Set the user's local browser language in environment variables.
179+
180+
Args:
181+
language: The language code to set (e.g., 'en-US')
182+
"""
183+
os.environ["USER_LOCAL_BROWSER_LANGUAGE"] = language
184+
186185

187186
# Create a global instance of AppConfig
188187
config = AppConfig()

src/backend/app_kernel.py

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from auth.auth_utils import get_authenticated_user_details
1111

1212
# Azure monitoring
13+
import re
14+
from dateutil import parser
1315
from azure.monitor.opentelemetry import configure_azure_monitor
1416
from config_kernel import Config
1517
from event_utils import track_event_if_configured
@@ -29,11 +31,13 @@
2931
InputTask,
3032
PlanWithSteps,
3133
Step,
34+
UserLanguage
3235
)
3336

3437
# Updated import for KernelArguments
3538
from utils_kernel import initialize_runtime_and_context, rai_success
3639

40+
3741
# Check if the Application Insights Instrumentation Key is set in the environment variables
3842
connection_string = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
3943
if connection_string:
@@ -81,13 +85,96 @@
8185
logging.info("Added health check middleware")
8286

8387

88+
def format_dates_in_messages(messages, target_locale="en-US"):
89+
"""
90+
Format dates in agent messages according to the specified locale.
91+
92+
Args:
93+
messages: List of message objects or string content
94+
target_locale: Target locale for date formatting (default: en-US)
95+
96+
Returns:
97+
Formatted messages with dates converted to target locale format
98+
"""
99+
# Define target format patterns per locale
100+
locale_date_formats = {
101+
"en-IN": "%d %b %Y", # 30 Jul 2025
102+
"en-US": "%b %d, %Y", # Jul 30, 2025
103+
}
104+
105+
output_format = locale_date_formats.get(target_locale, "%d %b %Y")
106+
# Match both "Jul 30, 2025, 12:00:00 AM" and "30 Jul 2025"
107+
date_pattern = r'(\d{1,2} [A-Za-z]{3,9} \d{4}|[A-Za-z]{3,9} \d{1,2}, \d{4}(, \d{1,2}:\d{2}:\d{2} ?[APap][Mm])?)'
108+
109+
def convert_date(match):
110+
date_str = match.group(0)
111+
try:
112+
dt = parser.parse(date_str)
113+
return dt.strftime(output_format)
114+
except Exception:
115+
return date_str # Leave it unchanged if parsing fails
116+
117+
# Process messages
118+
if isinstance(messages, list):
119+
formatted_messages = []
120+
for message in messages:
121+
if hasattr(message, 'content') and message.content:
122+
# Create a copy of the message with formatted content
123+
formatted_message = message.model_copy() if hasattr(message, 'model_copy') else message
124+
if hasattr(formatted_message, 'content'):
125+
formatted_message.content = re.sub(date_pattern, convert_date, formatted_message.content)
126+
formatted_messages.append(formatted_message)
127+
else:
128+
formatted_messages.append(message)
129+
return formatted_messages
130+
elif isinstance(messages, str):
131+
return re.sub(date_pattern, convert_date, messages)
132+
else:
133+
return messages
134+
135+
136+
@app.post("/api/user_browser_language")
137+
async def user_browser_language_endpoint(
138+
user_language: UserLanguage,
139+
request: Request
140+
):
141+
"""
142+
Receive the user's browser language.
143+
144+
---
145+
tags:
146+
- User
147+
parameters:
148+
- name: language
149+
in: query
150+
type: string
151+
required: true
152+
description: The user's browser language
153+
responses:
154+
200:
155+
description: Language received successfully
156+
schema:
157+
type: object
158+
properties:
159+
status:
160+
type: string
161+
description: Confirmation message
162+
"""
163+
config.set_user_local_browser_language(user_language.language)
164+
165+
# Log the received language for the user
166+
logging.info(f"Received browser language '{user_language}' for user ")
167+
168+
return {"status": "Language received successfully"}
169+
170+
84171
@app.post("/api/input_task")
85172
async def input_task_endpoint(input_task: InputTask, request: Request):
86173
"""
87174
Receive the initial input task from the user.
88175
"""
89176
# Fix 1: Properly await the async rai_success function
90-
if not await rai_success(input_task.description):
177+
if not await rai_success(input_task.description, True):
91178
print("RAI failed")
92179

93180
track_event_if_configured(
@@ -177,6 +264,13 @@ async def input_task_endpoint(input_task: InputTask, request: Request):
177264
}
178265

179266
except Exception as e:
267+
# Extract clean error message for rate limit errors
268+
error_msg = str(e)
269+
if "Rate limit is exceeded" in error_msg:
270+
match = re.search(r"Rate limit is exceeded\. Try again in (\d+) seconds?\.", error_msg)
271+
if match:
272+
error_msg = f"Rate limit is exceeded. Try again in {match.group(1)} seconds."
273+
180274
track_event_if_configured(
181275
"InputTaskError",
182276
{
@@ -185,7 +279,7 @@ async def input_task_endpoint(input_task: InputTask, request: Request):
185279
"error": str(e),
186280
},
187281
)
188-
raise HTTPException(status_code=400, detail=f"Error creating plan: {e}")
282+
raise HTTPException(status_code=400, detail=f"Error creating plan: {error_msg}") from e
189283

190284

191285
@app.post("/api/human_feedback")
@@ -351,6 +445,18 @@ async def human_clarification_endpoint(
351445
400:
352446
description: Missing or invalid user information
353447
"""
448+
if not await rai_success(human_clarification.human_clarification, False):
449+
print("RAI failed")
450+
track_event_if_configured(
451+
"RAI failed",
452+
{
453+
"status": "Clarification is not received",
454+
"description": human_clarification.human_clarification,
455+
"session_id": human_clarification.session_id,
456+
},
457+
)
458+
raise HTTPException(status_code=400, detail="Invalida Clarification")
459+
354460
authenticated_user = get_authenticated_user_details(request_headers=request.headers)
355461
user_id = authenticated_user["user_principal_id"]
356462
if not user_id:
@@ -626,7 +732,11 @@ async def get_plans(
626732

627733
plan_with_steps = PlanWithSteps(**plan.model_dump(), steps=steps)
628734
plan_with_steps.update_step_counts()
629-
return [plan_with_steps, messages]
735+
736+
# Format dates in messages according to locale
737+
formatted_messages = format_dates_in_messages(messages, config.get_user_local_browser_language())
738+
739+
return [plan_with_steps, formatted_messages]
630740

631741
all_plans = await memory_store.get_all_plans()
632742
# Fetch steps for all plans concurrently

src/backend/config_kernel.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Import AppConfig from app_config
22
from app_config import config
3+
from helpers.azure_credential_utils import get_azure_credential
34

45

56
# This file is left as a lightweight wrapper around AppConfig for backward compatibility
@@ -31,7 +32,7 @@ class Config:
3132
@staticmethod
3233
def GetAzureCredentials():
3334
"""Get Azure credentials using the AppConfig implementation."""
34-
return config.get_azure_credentials()
35+
return get_azure_credential()
3536

3637
@staticmethod
3738
def GetCosmosDatabaseClient():

src/backend/context/cosmos_memory_kernel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from azure.cosmos.partition_key import PartitionKey
1212
from azure.cosmos.aio import CosmosClient
13-
from azure.identity import DefaultAzureCredential
13+
from helpers.azure_credential_utils import get_azure_credential
1414
from semantic_kernel.memory.memory_record import MemoryRecord
1515
from semantic_kernel.memory.memory_store_base import MemoryStoreBase
1616
from semantic_kernel.contents import ChatMessageContent, ChatHistory, AuthorRole
@@ -73,7 +73,7 @@ async def initialize(self):
7373
if not self._database:
7474
# Create Cosmos client
7575
cosmos_client = CosmosClient(
76-
self._cosmos_endpoint, credential=DefaultAzureCredential()
76+
self._cosmos_endpoint, credential=get_azure_credential()
7777
)
7878
self._database = cosmos_client.get_database_client(
7979
self._cosmos_database
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import os
2+
from azure.identity import ManagedIdentityCredential, DefaultAzureCredential
3+
from azure.identity.aio import ManagedIdentityCredential as AioManagedIdentityCredential, DefaultAzureCredential as AioDefaultAzureCredential
4+
5+
6+
async def get_azure_credential_async(client_id=None):
7+
"""
8+
Returns an Azure credential asynchronously based on the application environment.
9+
10+
If the environment is 'dev', it uses AioDefaultAzureCredential.
11+
Otherwise, it uses AioManagedIdentityCredential.
12+
13+
Args:
14+
client_id (str, optional): The client ID for the Managed Identity Credential.
15+
16+
Returns:
17+
Credential object: Either AioDefaultAzureCredential or AioManagedIdentityCredential.
18+
"""
19+
if os.getenv("APP_ENV", "prod").lower() == 'dev':
20+
return AioDefaultAzureCredential() # CodeQL [SM05139] Okay use of DefaultAzureCredential as it is only used in development
21+
else:
22+
return AioManagedIdentityCredential(client_id=client_id)
23+
24+
25+
def get_azure_credential(client_id=None):
26+
"""
27+
Returns an Azure credential based on the application environment.
28+
29+
If the environment is 'dev', it uses DefaultAzureCredential.
30+
Otherwise, it uses ManagedIdentityCredential.
31+
32+
Args:
33+
client_id (str, optional): The client ID for the Managed Identity Credential.
34+
35+
Returns:
36+
Credential object: Either DefaultAzureCredential or ManagedIdentityCredential.
37+
"""
38+
if os.getenv("APP_ENV", "prod").lower() == 'dev':
39+
return DefaultAzureCredential() # CodeQL [SM05139] Okay use of DefaultAzureCredential as it is only used in development
40+
else:
41+
return ManagedIdentityCredential(client_id=client_id)

src/backend/kernel_tools/hr_tools.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,25 @@
55
from models.messages_kernel import AgentType
66
import json
77
from typing import get_type_hints
8-
from utils_date import format_date_for_user
8+
from app_config import config
99

1010

1111
class HrTools:
1212
# Define HR tools (functions)
13-
formatting_instructions = "Instructions: returning the output of this function call verbatim to the user in markdown. Then write AGENT SUMMARY: and then include a summary of what you did."
13+
selecetd_language = config.get_user_local_browser_language()
14+
formatting_instructions = "Instructions: returning the output of this function call verbatim to the user in markdown. Then write AGENT SUMMARY: and then include a summary of what you did. Convert all date strings in the following text to short date format with 3-letter month (MMM) in the {selecetd_language} locale (e.g., en-US, en-IN), remove time, and replace original dates with the formatted ones"
1415
agent_name = AgentType.HR.value
1516

1617
@staticmethod
1718
@kernel_function(description="Schedule an orientation session for a new employee.")
1819
async def schedule_orientation_session(employee_name: str, date: str) -> str:
19-
formatted_date = format_date_for_user(date)
2020

2121
return (
2222
f"##### Orientation Session Scheduled\n"
2323
f"**Employee Name:** {employee_name}\n"
24-
f"**Date:** {formatted_date}\n\n"
24+
f"**Date:** {date}\n\n"
2525
f"Your orientation session has been successfully scheduled. "
2626
f"Please mark your calendar and be prepared for an informative session.\n"
27-
f"AGENT SUMMARY: I scheduled the orientation session for {employee_name} on {formatted_date}, as part of her onboarding process.\n"
2827
f"{HrTools.formatting_instructions}"
2928
)
3029

0 commit comments

Comments
 (0)