11import json
22import os
3+ import logging
4+ import traceback
5+ import uuid
36from typing import List , Dict , Any , Generator
47from azure .ai .inference import ChatCompletionsClient
58from azure .core .credentials import AzureKeyCredential
@@ -34,6 +37,12 @@ class LocalAgentProcessor:
3437 def __init__ (self , agent_id : str , domain : str ):
3538 self .agent_id = agent_id
3639 self .domain = domain
40+ self .logger = logging .getLogger ("local_agent_processor" )
41+ self ._debug = os .getenv ("A2A_DEBUG" , "" ).lower () in {"1" , "true" , "yes" }
42+ self ._last_error_id : str | None = None
43+ self ._last_error_detail : str | None = None
44+ if self ._debug :
45+ self .logger .setLevel (logging .DEBUG )
3746
3847 # Initialize GPT client (shared across all agents)
3948 endpoint = (
@@ -58,6 +67,13 @@ def __init__(self, agent_id: str, domain: str):
5867 self .use_gpt = False
5968 self .client = None
6069 self .model = deployment
70+ self ._inference_endpoint : str | None = None
71+ self ._using_key_auth : bool = False
72+
73+ # Default to managed identity / Entra ID auth in Azure.
74+ # Only use key-based auth when explicitly enabled.
75+ self ._prefer_aad : bool = os .getenv ("A2A_PREFER_AAD" , "true" ).lower () in {"1" , "true" , "yes" }
76+ self ._allow_key_auth : bool = os .getenv ("A2A_USE_KEY_AUTH" , "" ).lower () in {"1" , "true" , "yes" }
6177
6278 if endpoint and deployment :
6379 # Convert endpoint to Foundry format if needed
@@ -67,21 +83,67 @@ def __init__(self, agent_id: str, domain: str):
6783 if not foundry_endpoint .endswith ('/models' ):
6884 foundry_endpoint = f"{ foundry_endpoint .rstrip ('/' )} /models"
6985
86+ self ._inference_endpoint = foundry_endpoint
87+
7088 try :
71- # Prefer key auth if present; otherwise use token-based auth (Managed Identity in cloud).
72- if api_key :
89+ # Prefer token-based auth (Managed Identity in cloud).
90+ # Keys are often disabled (disableLocalAuth) and should be opt-in.
91+ if api_key and self ._allow_key_auth and not self ._prefer_aad :
7392 credential = AzureKeyCredential (api_key )
93+ self ._using_key_auth = True
7494 else :
7595 credential = get_inference_credential (
7696 api_key = None ,
7797 default_credential = get_default_credential (),
7898 endpoint = foundry_endpoint ,
7999 )
100+ self ._using_key_auth = False
80101 self .client = ChatCompletionsClient (endpoint = foundry_endpoint , credential = credential )
81102 self .use_gpt = True
82103 except Exception :
104+ self .logger .exception ("Failed to initialize ChatCompletionsClient (endpoint=%s, deployment=%s)" , foundry_endpoint , deployment )
83105 self .use_gpt = False
84106
107+ def _format_exception_detail (self , error_id : str , exc : Exception ) -> str :
108+ """Format a detailed, UI-safe error message for troubleshooting."""
109+ parts : list [str ] = []
110+ parts .append (f"error_id={ error_id } " )
111+ parts .append (f"agent_domain={ self .domain } " )
112+ parts .append (f"model={ self .model } " )
113+ parts .append (f"endpoint={ self ._inference_endpoint } " )
114+ parts .append (f"auth_mode={ 'key' if self ._using_key_auth else 'aad' } " )
115+
116+ # Helpful identity context when running on Azure
117+ azure_client_id = os .getenv ("AZURE_CLIENT_ID" )
118+ if azure_client_id :
119+ parts .append (f"AZURE_CLIENT_ID={ azure_client_id } " )
120+
121+ parts .append (f"exception_type={ type (exc ).__name__ } " )
122+ parts .append (f"exception={ str (exc )} " )
123+
124+ # Try to extract HTTP response details if present
125+ response = getattr (exc , "response" , None )
126+ if response is not None :
127+ status_code = getattr (response , "status_code" , None )
128+ if status_code is not None :
129+ parts .append (f"http_status={ status_code } " )
130+
131+ headers = getattr (response , "headers" , None ) or {}
132+ for header_name in ("x-ms-request-id" , "x-ms-client-request-id" , "x-ms-correlation-request-id" ):
133+ header_value = headers .get (header_name )
134+ if header_value :
135+ parts .append (f"{ header_name } ={ header_value } " )
136+
137+ status_code = getattr (exc , "status_code" , None )
138+ if status_code is not None and "http_status=" not in "\n " .join (parts ):
139+ parts .append (f"http_status={ status_code } " )
140+
141+ # Include traceback only in debug mode
142+ if self ._debug :
143+ parts .append ("traceback=\n " + traceback .format_exc ())
144+
145+ return "\n " .join (parts )
146+
85147 def _call_gpt (self , user_message : str , conversation_history : List [Dict [str , str ]] | None = None , additional_context : Dict [str , Any ] | None = None ) -> str :
86148 """Call GPT with domain-specific system prompt."""
87149 if not self .use_gpt :
@@ -115,10 +177,55 @@ def _call_gpt(self, user_message: str, conversation_history: List[Dict[str, str]
115177 temperature = 0.7 ,
116178 max_tokens = 500
117179 )
118-
180+ self ._last_error_id = None
181+ self ._last_error_detail = None
119182 return response .choices [0 ].message .content
120183 except Exception as e :
121- return f"I'm having trouble connecting right now. Error: { str (e )[:100 ]} "
184+ # If we attempted key auth and the resource has local auth disabled,
185+ # transparently retry once with Entra ID (managed identity) auth.
186+ if self ._using_key_auth and self ._inference_endpoint :
187+ try :
188+ error_code = getattr (e , "error" , None )
189+ if hasattr (e , "response" ) and getattr (getattr (e , "response" , None ), "status_code" , None ) == 403 :
190+ # Heuristic: the common case we see is AuthenticationTypeDisabled.
191+ msg = str (e ) or ""
192+ if "AuthenticationTypeDisabled" in msg or "Key based authentication is disabled" in msg :
193+ self .logger .warning (
194+ "Key auth disabled for inference endpoint; retrying with AAD (domain=%s, endpoint=%s)" ,
195+ self .domain ,
196+ self ._inference_endpoint ,
197+ )
198+ aad_cred = get_inference_credential (
199+ api_key = None ,
200+ default_credential = get_default_credential (),
201+ endpoint = self ._inference_endpoint ,
202+ )
203+ self .client = ChatCompletionsClient (endpoint = self ._inference_endpoint , credential = aad_cred )
204+ self ._using_key_auth = False
205+ retry = self .client .complete (
206+ messages = messages ,
207+ model = self .model ,
208+ temperature = 0.7 ,
209+ max_tokens = 500 ,
210+ )
211+ self ._last_error_id = None
212+ self ._last_error_detail = None
213+ return retry .choices [0 ].message .content
214+ except Exception :
215+ # Fall through to normal error handling
216+ pass
217+
218+ error_id = uuid .uuid4 ().hex
219+ self ._last_error_id = error_id
220+ self ._last_error_detail = self ._format_exception_detail (error_id , e )
221+ self .logger .exception (
222+ "GPT call failed (error_id=%s, domain=%s, endpoint=%s, model=%s)" ,
223+ error_id ,
224+ self .domain ,
225+ self ._inference_endpoint ,
226+ self .model ,
227+ )
228+ return f"I'm having trouble connecting right now. (error_id={ error_id } )"
122229
123230 def _interior_design (self , user_message : str , conversation_history : List [Dict [str , str ]] | None = None , additional_context : Dict [str , Any ] | None = None ) -> Dict [str , Any ]:
124231 # Check if this is an image generation request
@@ -154,12 +261,20 @@ def _interior_design(self, user_message: str, conversation_history: List[Dict[st
154261
155262 def _inventory (self , user_message : str , conversation_history : List [Dict [str , str ]] | None = None , additional_context : Dict [str , Any ] | None = None ) -> Dict [str , Any ]:
156263 answer = self ._call_gpt (user_message , conversation_history , additional_context )
157- return {"answer" : answer }
264+ result : Dict [str , Any ] = {"answer" : answer }
265+ if self ._last_error_detail :
266+ result ["error" ] = self ._last_error_detail
267+ result ["error_id" ] = self ._last_error_id
268+ return result
158269
159270 def _customer_loyalty (self , customer_id : str | None , conversation_history : List [Dict [str , str ]] | None = None , additional_context : Dict [str , Any ] | None = None ) -> Dict [str , Any ]:
160271 user_message = f"Check loyalty benefits for customer { customer_id or 'current customer' } "
161272 answer = self ._call_gpt (user_message , conversation_history , additional_context )
162- return {"answer" : answer , "discount_percentage" : "10" }
273+ result : Dict [str , Any ] = {"answer" : answer , "discount_percentage" : "10" }
274+ if self ._last_error_detail :
275+ result ["error" ] = self ._last_error_detail
276+ result ["error_id" ] = self ._last_error_id
277+ return result
163278
164279 def _cart_management (self , user_message : str , conversation_history : List [Dict [str , str ]] | None = None , additional_context : Dict [str , Any ] | None = None ) -> Dict [str , Any ]:
165280 cart = additional_context .get ("cart" , []) if additional_context else []
@@ -176,11 +291,19 @@ def _cart_management(self, user_message: str, conversation_history: List[Dict[st
176291
177292 # Get GPT response about the cart action
178293 answer = self ._call_gpt (user_message , conversation_history , {"cart" : cart })
179- return {"answer" : answer , "cart" : cart }
294+ result : Dict [str , Any ] = {"answer" : answer , "cart" : cart }
295+ if self ._last_error_detail :
296+ result ["error" ] = self ._last_error_detail
297+ result ["error_id" ] = self ._last_error_id
298+ return result
180299
181300 def _cora (self , user_message : str , conversation_history : List [Dict [str , str ]] | None = None , additional_context : Dict [str , Any ] | None = None ) -> Dict [str , Any ]:
182301 answer = self ._call_gpt (user_message , conversation_history , additional_context )
183- return {"answer" : answer }
302+ result : Dict [str , Any ] = {"answer" : answer }
303+ if self ._last_error_detail :
304+ result ["error" ] = self ._last_error_detail
305+ result ["error_id" ] = self ._last_error_id
306+ return result
184307
185308 def run_conversation_with_text_stream (
186309 self ,
0 commit comments