Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,10 @@ module containerApp 'br/public:avm/res/app/container-app:0.14.2' = if (container
name: 'AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME'
value: aiFoundryAiServicesModelDeployment.name
}
{
name: 'APP_ENV'
value: 'Prod'
}
]
}
]
Expand Down Expand Up @@ -1087,6 +1091,7 @@ module webSite 'br/public:avm/res/web/site:0.15.1' = if (webSiteEnabled) {
WEBSITES_CONTAINER_START_TIME_LIMIT: '1800' // 30 minutes, adjust as needed
BACKEND_API_URL: 'https://${containerApp.outputs.fqdn}'
AUTH_ENABLED: 'false'
APP_ENV: 'Prod'
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/backend/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
APPLICATIONINSIGHTS_CONNECTION_STRING=
AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME=gpt-4o
AZURE_AI_AGENT_ENDPOINT=
APP_ENV="dev"

BACKEND_API_URL=http://localhost:8000
FRONTEND_SITE_NAME=http://127.0.0.1:3000
41 changes: 20 additions & 21 deletions src/backend/app_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from azure.ai.projects.aio import AIProjectClient
from azure.cosmos.aio import CosmosClient
from azure.identity import DefaultAzureCredential
from helpers.azure_credential_utils import get_azure_credential
from dotenv import load_dotenv
from semantic_kernel.kernel import Kernel

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

def get_azure_credentials(self):
"""Get Azure credentials using DefaultAzureCredential.

Returns:
DefaultAzureCredential instance for Azure authentication
"""
# Cache the credentials object
if self._azure_credentials is not None:
return self._azure_credentials

try:
self._azure_credentials = DefaultAzureCredential()
return self._azure_credentials
except Exception as exc:
logging.warning("Failed to create DefaultAzureCredential: %s", exc)
return None

def get_cosmos_database_client(self):
"""Get a Cosmos DB client for the configured database.

Expand All @@ -132,7 +115,7 @@ def get_cosmos_database_client(self):
try:
if self._cosmos_client is None:
self._cosmos_client = CosmosClient(
self.COSMOSDB_ENDPOINT, credential=self.get_azure_credentials()
self.COSMOSDB_ENDPOINT, credential=get_azure_credential()
)

if self._cosmos_database is None:
Expand Down Expand Up @@ -169,10 +152,10 @@ def get_ai_project_client(self):
return self._ai_project_client

try:
credential = self.get_azure_credentials()
credential = get_azure_credential()
if credential is None:
raise RuntimeError(
"Unable to acquire Azure credentials; ensure DefaultAzureCredential is configured"
"Unable to acquire Azure credentials; ensure Managed Identity is configured"
)

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

def get_user_local_browser_language(self) -> str:
"""Get the user's local browser language from environment variables.

Returns:
The user's local browser language or 'en-US' if not set
"""
return self._get_optional("USER_LOCAL_BROWSER_LANGUAGE", "en-US")

def set_user_local_browser_language(self, language: str):
"""Set the user's local browser language in environment variables.

Args:
language: The language code to set (e.g., 'en-US')
"""
os.environ["USER_LOCAL_BROWSER_LANGUAGE"] = language


# Create a global instance of AppConfig
config = AppConfig()
116 changes: 113 additions & 3 deletions src/backend/app_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from auth.auth_utils import get_authenticated_user_details

# Azure monitoring
import re
from dateutil import parser
from azure.monitor.opentelemetry import configure_azure_monitor
from config_kernel import Config
from event_utils import track_event_if_configured
Expand All @@ -29,11 +31,13 @@
InputTask,
PlanWithSteps,
Step,
UserLanguage
)

# Updated import for KernelArguments
from utils_kernel import initialize_runtime_and_context, rai_success


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


def format_dates_in_messages(messages, target_locale="en-US"):
"""
Format dates in agent messages according to the specified locale.

Args:
messages: List of message objects or string content
target_locale: Target locale for date formatting (default: en-US)

Returns:
Formatted messages with dates converted to target locale format
"""
# Define target format patterns per locale
locale_date_formats = {
"en-IN": "%d %b %Y", # 30 Jul 2025
"en-US": "%b %d, %Y", # Jul 30, 2025
}

output_format = locale_date_formats.get(target_locale, "%d %b %Y")
# Match both "Jul 30, 2025, 12:00:00 AM" and "30 Jul 2025"
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])?)'

def convert_date(match):
date_str = match.group(0)
try:
dt = parser.parse(date_str)
return dt.strftime(output_format)
except Exception:
return date_str # Leave it unchanged if parsing fails

# Process messages
if isinstance(messages, list):
formatted_messages = []
for message in messages:
if hasattr(message, 'content') and message.content:
# Create a copy of the message with formatted content
formatted_message = message.model_copy() if hasattr(message, 'model_copy') else message
if hasattr(formatted_message, 'content'):
formatted_message.content = re.sub(date_pattern, convert_date, formatted_message.content)
formatted_messages.append(formatted_message)
else:
formatted_messages.append(message)
return formatted_messages
elif isinstance(messages, str):
return re.sub(date_pattern, convert_date, messages)
else:
return messages


@app.post("/api/user_browser_language")
async def user_browser_language_endpoint(
user_language: UserLanguage,
request: Request
):
"""
Receive the user's browser language.

---
tags:
- User
parameters:
- name: language
in: query
type: string
required: true
description: The user's browser language
responses:
200:
description: Language received successfully
schema:
type: object
properties:
status:
type: string
description: Confirmation message
"""
config.set_user_local_browser_language(user_language.language)

# Log the received language for the user
logging.info(f"Received browser language '{user_language}' for user ")

return {"status": "Language received successfully"}


@app.post("/api/input_task")
async def input_task_endpoint(input_task: InputTask, request: Request):
"""
Receive the initial input task from the user.
"""
# Fix 1: Properly await the async rai_success function
if not await rai_success(input_task.description):
if not await rai_success(input_task.description, True):
print("RAI failed")

track_event_if_configured(
Expand Down Expand Up @@ -177,6 +264,13 @@ async def input_task_endpoint(input_task: InputTask, request: Request):
}

except Exception as e:
# Extract clean error message for rate limit errors
error_msg = str(e)
if "Rate limit is exceeded" in error_msg:
match = re.search(r"Rate limit is exceeded\. Try again in (\d+) seconds?\.", error_msg)
if match:
error_msg = f"Rate limit is exceeded. Try again in {match.group(1)} seconds."

track_event_if_configured(
"InputTaskError",
{
Expand All @@ -185,7 +279,7 @@ async def input_task_endpoint(input_task: InputTask, request: Request):
"error": str(e),
},
)
raise HTTPException(status_code=400, detail=f"Error creating plan: {e}")
raise HTTPException(status_code=400, detail=f"Error creating plan: {error_msg}") from e


@app.post("/api/human_feedback")
Expand Down Expand Up @@ -351,6 +445,18 @@ async def human_clarification_endpoint(
400:
description: Missing or invalid user information
"""
if not await rai_success(human_clarification.human_clarification, False):
print("RAI failed")
track_event_if_configured(
"RAI failed",
{
"status": "Clarification is not received",
"description": human_clarification.human_clarification,
"session_id": human_clarification.session_id,
},
)
raise HTTPException(status_code=400, detail="Invalida Clarification")

authenticated_user = get_authenticated_user_details(request_headers=request.headers)
user_id = authenticated_user["user_principal_id"]
if not user_id:
Expand Down Expand Up @@ -626,7 +732,11 @@ async def get_plans(

plan_with_steps = PlanWithSteps(**plan.model_dump(), steps=steps)
plan_with_steps.update_step_counts()
return [plan_with_steps, messages]

# Format dates in messages according to locale
formatted_messages = format_dates_in_messages(messages, config.get_user_local_browser_language())

return [plan_with_steps, formatted_messages]

all_plans = await memory_store.get_all_plans()
# Fetch steps for all plans concurrently
Expand Down
3 changes: 2 additions & 1 deletion src/backend/config_kernel.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Import AppConfig from app_config
from app_config import config
from helpers.azure_credential_utils import get_azure_credential


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

@staticmethod
def GetCosmosDatabaseClient():
Expand Down
4 changes: 2 additions & 2 deletions src/backend/context/cosmos_memory_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from azure.cosmos.partition_key import PartitionKey
from azure.cosmos.aio import CosmosClient
from azure.identity import DefaultAzureCredential
from helpers.azure_credential_utils import get_azure_credential
from semantic_kernel.memory.memory_record import MemoryRecord
from semantic_kernel.memory.memory_store_base import MemoryStoreBase
from semantic_kernel.contents import ChatMessageContent, ChatHistory, AuthorRole
Expand Down Expand Up @@ -73,7 +73,7 @@ async def initialize(self):
if not self._database:
# Create Cosmos client
cosmos_client = CosmosClient(
self._cosmos_endpoint, credential=DefaultAzureCredential()
self._cosmos_endpoint, credential=get_azure_credential()
)
self._database = cosmos_client.get_database_client(
self._cosmos_database
Expand Down
41 changes: 41 additions & 0 deletions src/backend/helpers/azure_credential_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import os
from azure.identity import ManagedIdentityCredential, DefaultAzureCredential
from azure.identity.aio import ManagedIdentityCredential as AioManagedIdentityCredential, DefaultAzureCredential as AioDefaultAzureCredential


async def get_azure_credential_async(client_id=None):
"""
Returns an Azure credential asynchronously based on the application environment.

If the environment is 'dev', it uses AioDefaultAzureCredential.
Otherwise, it uses AioManagedIdentityCredential.

Args:
client_id (str, optional): The client ID for the Managed Identity Credential.

Returns:
Credential object: Either AioDefaultAzureCredential or AioManagedIdentityCredential.
"""
if os.getenv("APP_ENV", "prod").lower() == 'dev':
return AioDefaultAzureCredential() # CodeQL [SM05139] Okay use of DefaultAzureCredential as it is only used in development
else:
return AioManagedIdentityCredential(client_id=client_id)


def get_azure_credential(client_id=None):
"""
Returns an Azure credential based on the application environment.

If the environment is 'dev', it uses DefaultAzureCredential.
Otherwise, it uses ManagedIdentityCredential.

Args:
client_id (str, optional): The client ID for the Managed Identity Credential.

Returns:
Credential object: Either DefaultAzureCredential or ManagedIdentityCredential.
"""
if os.getenv("APP_ENV", "prod").lower() == 'dev':
return DefaultAzureCredential() # CodeQL [SM05139] Okay use of DefaultAzureCredential as it is only used in development
else:
return ManagedIdentityCredential(client_id=client_id)
9 changes: 4 additions & 5 deletions src/backend/kernel_tools/hr_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,25 @@
from models.messages_kernel import AgentType
import json
from typing import get_type_hints
from utils_date import format_date_for_user
from app_config import config


class HrTools:
# Define HR tools (functions)
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."
selecetd_language = config.get_user_local_browser_language()
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"
agent_name = AgentType.HR.value

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

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

Expand Down
Loading
Loading