diff --git a/src/backend/app_config.py b/src/backend/app_config.py index 17771fed2..fe2b9f90c 100644 --- a/src/backend/app_config.py +++ b/src/backend/app_config.py @@ -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() diff --git a/src/backend/app_kernel.py b/src/backend/app_kernel.py index 8eb811965..e0e81abd1 100644 --- a/src/backend/app_kernel.py +++ b/src/backend/app_kernel.py @@ -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 @@ -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: @@ -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): """ @@ -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", { @@ -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") @@ -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 diff --git a/src/backend/kernel_tools/hr_tools.py b/src/backend/kernel_tools/hr_tools.py index 9951c0a1a..fc106373e 100644 --- a/src/backend/kernel_tools/hr_tools.py +++ b/src/backend/kernel_tools/hr_tools.py @@ -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}" ) diff --git a/src/backend/kernel_tools/product_tools.py b/src/backend/kernel_tools/product_tools.py index b5c119b76..e3d98e030 100644 --- a/src/backend/kernel_tools/product_tools.py +++ b/src/backend/kernel_tools/product_tools.py @@ -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}" ) diff --git a/src/backend/models/messages_kernel.py b/src/backend/models/messages_kernel.py index ac10f8e25..533af6aa3 100644 --- a/src/backend/models/messages_kernel.py +++ b/src/backend/models/messages_kernel.py @@ -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.""" diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 5cac25b2f..872e5b154 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -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 diff --git a/src/backend/test_utils_date_fixed.py b/src/backend/test_utils_date_fixed.py new file mode 100644 index 000000000..62eb8fc67 --- /dev/null +++ b/src/backend/test_utils_date_fixed.py @@ -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() diff --git a/src/backend/tests/test_utils_date_enhanced.py b/src/backend/tests/test_utils_date_enhanced.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/frontend/src/api/apiClient.tsx b/src/frontend/src/api/apiClient.tsx index 8d574fb18..88bc4d606 100644 --- a/src/frontend/src/api/apiClient.tsx +++ b/src/frontend/src/api/apiClient.tsx @@ -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(); @@ -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); @@ -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) { diff --git a/src/frontend/src/api/apiService.tsx b/src/frontend/src/api/apiService.tsx index 1b11ab621..27f35b065 100644 --- a/src/frontend/src/api/apiService.tsx +++ b/src/frontend/src/api/apiService.tsx @@ -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 @@ -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 diff --git a/src/frontend/src/api/config.tsx b/src/frontend/src/api/config.tsx index bf99d97f7..5c8fa23e6 100644 --- a/src/frontend/src/api/config.tsx +++ b/src/frontend/src/api/config.tsx @@ -51,8 +51,6 @@ export function getConfigData() { export async function getUserInfo(): Promise { 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." @@ -60,7 +58,6 @@ export async function getUserInfo(): Promise { 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 || "", @@ -71,7 +68,6 @@ export async function getUserInfo(): Promise { 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; diff --git a/src/frontend/src/components/content/HomeInput.tsx b/src/frontend/src/components/content/HomeInput.tsx index 4e2c140de..15ca5566c 100644 --- a/src/frontend/src/components/content/HomeInput.tsx +++ b/src/frontend/src/components/content/HomeInput.tsx @@ -69,14 +69,12 @@ const HomeInput: React.FC = ({ dismissToast(id); navigate(`/plan/${response.plan_id}`); } else { - console.log("Invalid plan:", response.status); showToast("Failed to create plan", "error"); dismissToast(id); } - } catch (error) { - console.log("Failed to create plan:", error); + } catch (error:any) { dismissToast(id); - showToast("Something went wrong", "error"); + showToast(JSON.parse(error?.message)?.detail, "error"); } finally { setInput(""); setSubmitting(false); diff --git a/src/frontend/src/index.tsx b/src/frontend/src/index.tsx index 0ece07e2e..a47ac3767 100644 --- a/src/frontend/src/index.tsx +++ b/src/frontend/src/index.tsx @@ -6,6 +6,7 @@ import reportWebVitals from './reportWebVitals'; import { FluentProvider, teamsLightTheme, teamsDarkTheme } from "@fluentui/react-components"; import { setEnvData, setApiUrl, config as defaultConfig, toBoolean, getUserInfo, setUserInfoGlobal } from './api/config'; import { UserInfo } from './models'; +import { apiService } from './api'; const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); const AppWrapper = () => { @@ -22,7 +23,7 @@ const AppWrapper = () => { window.appConfig = config; setEnvData(config); setApiUrl(config.API_URL); - + const browserLanguage = await apiService.sendUserBrowserLanguage(); try { const response = await fetch('/config'); let config = defaultConfig; @@ -46,7 +47,7 @@ const AppWrapper = () => { setIsUserInfoLoaded(true); } }; - + initConfig(); // Call the async function inside useEffect }, []); // Effect to listen for changes in the user's preferred color scheme diff --git a/src/frontend/src/pages/PlanPage.tsx b/src/frontend/src/pages/PlanPage.tsx index 84067f239..e469ff4bb 100644 --- a/src/frontend/src/pages/PlanPage.tsx +++ b/src/frontend/src/pages/PlanPage.tsx @@ -124,7 +124,6 @@ const PlanPage: React.FC = () => { } catch (error) { dismissToast(id); showToast("Failed to submit clarification", "error"); - console.log("Failed to submit clarification:", error); } finally { setInput(""); setSubmitting(false); @@ -150,7 +149,6 @@ const PlanPage: React.FC = () => { } catch (error) { dismissToast(id); showToast(`Failed to ${approve ? "approve" : "reject"} step`, "error"); - console.log(`Failed to ${approve ? "approve" : "reject"} step:`, error); } finally { setProcessingSubtaskId(null); setSubmitting(false); diff --git a/src/frontend/src/services/TaskService.tsx b/src/frontend/src/services/TaskService.tsx index d0c62ce0b..6178289c3 100644 --- a/src/frontend/src/services/TaskService.tsx +++ b/src/frontend/src/services/TaskService.tsx @@ -190,7 +190,7 @@ export class TaskService { if (error?.response?.data?.message) { message = error.response.data.message; } else if (error?.message) { - message = error.message; + message = error.message?.detail ? error.message.detail : error.message; } // Throw a new error with a user-friendly message throw new Error(message);