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
16 changes: 16 additions & 0 deletions src/backend/app_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,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()
102 changes: 100 additions & 2 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,6 +85,89 @@
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):
"""
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 @@ -638,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
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
7 changes: 4 additions & 3 deletions src/backend/kernel_tools/product_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,26 @@
import json
from typing import get_type_hints
from utils_date import format_date_for_user
from app_config import config


class ProductTools:
"""Define Product Agent functions (tools)"""

agent_name = AgentType.PRODUCT.value
selecetd_language = config.get_user_local_browser_language()

@staticmethod
@kernel_function(
description="Add an extras pack/new product to the mobile plan for the customer. For example, adding a roaming plan to their service."
description="Add an extras pack/new product to the mobile plan for the customer. For example, adding a roaming plan to their service. 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"
)
async def add_mobile_extras_pack(new_extras_pack_name: str, start_date: str) -> str:
"""Add an extras pack/new product to the mobile plan for the customer. For example, adding a roaming plan to their service. The arguments should include the new_extras_pack_name and the start_date as strings. You must provide the exact plan name, as found using the get_product_info() function."""
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."
formatted_date = format_date_for_user(start_date)
analysis = (
f"# Request to Add Extras Pack to Mobile Plan\n"
f"## New Plan:\n{new_extras_pack_name}\n"
f"## Start Date:\n{formatted_date}\n\n"
f"## Start Date:\n{start_date}\n\n"
f"These changes have been completed and should be reflected in your app in 5-10 minutes."
f"\n\n{formatting_instructions}"
)
Expand Down
4 changes: 4 additions & 0 deletions src/backend/models/messages_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@ class InputTask(KernelBaseModel):
description: str # Initial goal


class UserLanguage(KernelBaseModel):
language: str


class ApprovalRequest(KernelBaseModel):
"""Message sent to HumanAgent to request approval for a step."""

Expand Down
3 changes: 3 additions & 0 deletions src/backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ azure-ai-evaluation

opentelemetry-exporter-otlp-proto-grpc

# Date and internationalization
babel>=2.9.0

# Testing tools
pytest>=8.2,<9 # Compatible version for pytest-asyncio
pytest-asyncio==0.24.0
Expand Down
54 changes: 54 additions & 0 deletions src/backend/test_utils_date_fixed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Quick test for the fixed utils_date.py functionality
"""

import os
from datetime import datetime
from utils_date import format_date_for_user


def test_date_formatting():
"""Test the date formatting function with various inputs"""

# Set up different language environments
test_cases = [
('en-US', '2025-07-29', 'US English'),
('en-IN', '2025-07-29', 'Indian English'),
('en-GB', '2025-07-29', 'British English'),
('fr-FR', '2025-07-29', 'French'),
('de-DE', '2025-07-29', 'German'),
]

print("Testing date formatting with different locales:")
print("=" * 50)

for locale, date_str, description in test_cases:
os.environ['USER_LOCAL_BROWSER_LANGUAGE'] = locale
try:
result = format_date_for_user(date_str)
print(f"{description} ({locale}): {result}")
except Exception as e:
print(f"{description} ({locale}): ERROR - {e}")

print("\n" + "=" * 50)
print("Testing with datetime object:")

# Test with datetime object
os.environ['USER_LOCAL_BROWSER_LANGUAGE'] = 'en-US'
dt = datetime(2025, 7, 29, 14, 30, 0)
result = format_date_for_user(dt)
print(f"Datetime object: {result}")

print("\nTesting error handling:")
print("=" * 30)

# Test error handling
try:
result = format_date_for_user('invalid-date-string')
print(f"Invalid date: {result}")
except Exception as e:
print(f"Invalid date: ERROR - {e}")


if __name__ == "__main__":
test_date_formatting()
Empty file.
6 changes: 0 additions & 6 deletions src/frontend/src/api/apiClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,8 @@ const fetchWithAuth = async (url: string, method: string = "GET", body: BodyInit
try {
const apiUrl = getApiUrl();
const finalUrl = `${apiUrl}${url}`;
console.log('Final URL:', finalUrl);
console.log('Request Options:', options);
// Log the request details
const response = await fetch(finalUrl, options);
console.log('response', response);

if (!response.ok) {
const errorText = await response.text();
Expand All @@ -58,8 +55,6 @@ const fetchWithAuth = async (url: string, method: string = "GET", body: BodyInit

const isJson = response.headers.get('content-type')?.includes('application/json');
const responseData = isJson ? await response.json() : null;

console.log('Response JSON:', responseData);
return responseData;
} catch (error) {
console.info('API Error:', (error as Error).message);
Expand Down Expand Up @@ -87,7 +82,6 @@ const fetchWithoutAuth = async (url: string, method: string = "POST", body: Body
const errorText = await response.text();
throw new Error(errorText || 'Login failed');
}
console.log('response', response);
const isJson = response.headers.get('content-type')?.includes('application/json');
return isJson ? await response.json() : null;
} catch (error) {
Expand Down
15 changes: 14 additions & 1 deletion src/frontend/src/api/apiService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ const API_ENDPOINTS = {
APPROVE_STEPS: '/approve_step_or_steps',
HUMAN_CLARIFICATION: '/human_clarification_on_plan',
AGENT_MESSAGES: '/agent_messages',
MESSAGES: '/messages'
MESSAGES: '/messages',
USER_BROWSER_LANGUAGE: '/user_browser_language'
};

// Simple cache implementation
Expand Down Expand Up @@ -500,6 +501,18 @@ export class APIService {

return Math.round((completedSteps / plan.steps.length) * 100);
}

/**
* Send the user's browser language to the backend
* @returns Promise with response object
*/
async sendUserBrowserLanguage(): Promise<{ status: string }> {
const language = navigator.language || navigator.languages[0] || 'en';
const response = await apiClient.post(API_ENDPOINTS.USER_BROWSER_LANGUAGE, {
language
});
return response;
}
}

// Export a singleton instance
Expand Down
4 changes: 0 additions & 4 deletions src/frontend/src/api/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,13 @@ export function getConfigData() {
export async function getUserInfo(): Promise<UserInfo> {
try {
const response = await fetch("/.auth/me");
console.log("Fetching user info from: ", "/.auth/me");
console.log("Response ", response);
if (!response.ok) {
console.log(
"No identity provider found. Access to chat will be blocked."
);
return {} as UserInfo;
}
const payload = await response.json();
console.log("User info payload: ", payload[0]);
const userInfo: UserInfo = {
access_token: payload[0].access_token || "",
expires_on: payload[0].expires_on || "",
Expand All @@ -71,7 +68,6 @@ export async function getUserInfo(): Promise<UserInfo> {
user_first_last_name: payload[0].user_claims?.find((claim: claim) => claim.typ === 'name')?.val || "",
user_id: payload[0].user_claims?.find((claim: claim) => claim.typ === 'http://schemas.microsoft.com/identity/claims/objectidentifier')?.val || '',
};
console.log("User info: ", userInfo);
return userInfo;
} catch (e) {
return {} as UserInfo;
Expand Down
Loading
Loading