99 from azure .ai .projects import AIProjectClient # type: ignore
1010 from azure .identity import DefaultAzureCredential # type: ignore
1111 from services .azure_auth import get_default_credential , get_inference_credential # type: ignore
12+ try :
13+ # Preferred runtime client for threads/messages/runs
14+ from azure .ai .agents import AgentsClient # type: ignore
15+ except Exception :
16+ AgentsClient = None # type: ignore
1217 _REMOTE_AVAILABLE = True
1318except Exception :
1419 _REMOTE_AVAILABLE = False
@@ -113,6 +118,7 @@ def __init__(self, agent_id: str, project_endpoint: str = None):
113118 project_endpoint: Optional project endpoint (reads from env if not provided)
114119 """
115120 self .agent_id = agent_id
121+ self ._runtime_agent_id = agent_id
116122
117123 raw_endpoint = (
118124 project_endpoint
@@ -139,6 +145,142 @@ def __init__(self, agent_id: str, project_endpoint: str = None):
139145
140146 self .project_endpoint = full_project_endpoint
141147 self .client = AIProjectClient (endpoint = self .project_endpoint , credential = get_default_credential ())
148+
149+ # Best-effort: resolve the underlying OpenAI-style assistant id (asst_...)
150+ # when the configured id is a friendly/name-based id.
151+ self ._runtime_agent_id = self ._maybe_resolve_assistant_id (self ._runtime_agent_id )
152+
153+ # Some azure-ai-projects builds expose only agent-management operations on .agents.
154+ # In that case, use azure-ai-agents AgentsClient for thread/message/run operations.
155+ self ._agents_api = None
156+ try :
157+ if (
158+ hasattr (self .client , "agents" )
159+ and hasattr (self .client .agents , "threads" )
160+ and hasattr (self .client .agents .threads , "create" )
161+ and hasattr (self .client .agents , "messages" )
162+ and hasattr (self .client .agents .messages , "create" )
163+ and hasattr (self .client .agents , "runs" )
164+ and hasattr (self .client .agents .runs , "create_and_process" )
165+ ):
166+ self ._agents_api = self .client .agents
167+ except Exception :
168+ self ._agents_api = None
169+
170+ if self ._agents_api is None :
171+ if AgentsClient is None :
172+ raise ValueError (
173+ "Remote agent support unavailable: this SDK build doesn't expose threads on AIProjectClient.agents "
174+ "and azure-ai-agents is not installed."
175+ )
176+ # AgentsClient expects the project endpoint (per Microsoft docs snippets).
177+ self ._agents_api = AgentsClient (endpoint = self .project_endpoint , credential = get_default_credential ())
178+
179+ def _maybe_resolve_assistant_id (self , configured_id : str ) -> str :
180+ if not configured_id :
181+ return configured_id
182+ # Local simulation stays untouched.
183+ if configured_id .startswith ("asst_local_" ):
184+ return configured_id
185+ # Already an assistant id.
186+ if configured_id .startswith ("asst" ):
187+ return configured_id
188+
189+ try :
190+ agents = getattr (self .client , "agents" , None )
191+ if not agents or not hasattr (agents , "list" ):
192+ return configured_id
193+ for agent in agents .list ():
194+ agent_id = getattr (agent , "id" , None )
195+ agent_name = getattr (agent , "name" , None )
196+ if configured_id not in {agent_id , agent_name }:
197+ continue
198+ for attr in (
199+ "assistant_id" ,
200+ "assistantId" ,
201+ "openai_assistant_id" ,
202+ "openaiAssistantId" ,
203+ "assistantID" ,
204+ ):
205+ value = getattr (agent , attr , None )
206+ if isinstance (value , str ) and value .startswith ("asst" ):
207+ return value
208+ # Some SDKs only populate `id` with an assistant id.
209+ if isinstance (agent_id , str ) and agent_id .startswith ("asst" ):
210+ return agent_id
211+ except Exception :
212+ # Best-effort only; keep configured value.
213+ return configured_id
214+
215+ return configured_id
216+
217+ @staticmethod
218+ def _get_obj_id (obj : Any ) -> str | None :
219+ if obj is None :
220+ return None
221+ # SDK models can be rich objects or MutableMapping
222+ if hasattr (obj , "id" ):
223+ return getattr (obj , "id" )
224+ if isinstance (obj , dict ):
225+ return obj .get ("id" )
226+ return None
227+
228+ def _create_thread (self ):
229+ agents = self ._agents_api
230+ if hasattr (agents , "threads" ) and hasattr (agents .threads , "create" ):
231+ return agents .threads .create ()
232+ if hasattr (agents , "create_thread" ):
233+ return agents .create_thread ()
234+ raise AttributeError ("No supported thread creation method on agents client" )
235+
236+ def _delete_thread (self , thread_id : str ) -> None :
237+ agents = self ._agents_api
238+ if hasattr (agents , "threads" ) and hasattr (agents .threads , "delete" ):
239+ agents .threads .delete (thread_id )
240+ return
241+ if hasattr (agents , "delete_thread" ):
242+ agents .delete_thread (thread_id )
243+ return
244+
245+ def _create_message (self , thread_id : str , role : str , content : str ) -> None :
246+ agents = self ._agents_api
247+ if hasattr (agents , "messages" ) and hasattr (agents .messages , "create" ):
248+ agents .messages .create (thread_id = thread_id , role = role , content = content )
249+ return
250+ if hasattr (agents , "create_message" ):
251+ agents .create_message (thread_id = thread_id , role = role , content = content )
252+ return
253+ raise AttributeError ("No supported message creation method on agents client" )
254+
255+ def _run_and_process (self , thread_id : str ):
256+ agents = self ._agents_api
257+ runtime_id = self ._runtime_agent_id
258+ # Preferred (azure-ai-agents style)
259+ if hasattr (agents , "runs" ) and hasattr (agents .runs , "create_and_process" ):
260+ # Different SDK builds use either `agent_id` or `assistant_id`.
261+ try :
262+ return agents .runs .create_and_process (thread_id = thread_id , agent_id = runtime_id )
263+ except TypeError :
264+ return agents .runs .create_and_process (thread_id = thread_id , assistant_id = runtime_id )
265+ # Older helper naming
266+ if hasattr (agents , "create_and_process_run" ):
267+ # This helper is typically OpenAI-assistants shaped.
268+ return agents .create_and_process_run (thread_id = thread_id , assistant_id = runtime_id )
269+ # Some clients expose a one-shot convenience
270+ if hasattr (agents , "create_thread_and_process_run" ):
271+ try :
272+ return agents .create_thread_and_process_run (agent_id = runtime_id )
273+ except TypeError :
274+ return agents .create_thread_and_process_run (assistant_id = runtime_id )
275+ raise AttributeError ("No supported run method on agents client" )
276+
277+ def _list_messages (self , thread_id : str ):
278+ agents = self ._agents_api
279+ if hasattr (agents , "messages" ) and hasattr (agents .messages , "list" ):
280+ return agents .messages .list (thread_id = thread_id )
281+ if hasattr (agents , "list_messages" ):
282+ return agents .list_messages (thread_id = thread_id )
283+ raise AttributeError ("No supported message listing method on agents client" )
142284
143285 def run_conversation_with_text_stream (
144286 self ,
@@ -157,40 +299,56 @@ def run_conversation_with_text_stream(
157299 Yields:
158300 Chunks of the agent's response
159301 """
302+ thread_id : str | None = None
160303 try :
161304 # Create a thread for this conversation
162- thread = self .client .agents .create_thread ()
305+ thread = self ._create_thread ()
306+ thread_id = self ._get_obj_id (thread )
307+ if not thread_id :
308+ raise RuntimeError ("Agent thread creation returned no id" )
163309
164310 # Build the message content
165311 message_content = user_message
166312 if additional_context :
167313 message_content = f"Context: { json .dumps (additional_context )} \n \n User: { user_message } "
168314
169315 # Add message to thread
170- self .client .agents .create_message (
171- thread_id = thread .id ,
172- role = "user" ,
173- content = message_content
174- )
316+ self ._create_message (thread_id = thread_id , role = "user" , content = message_content )
175317
176318 # Run the agent
177- run = self .client .agents .create_and_process_run (
178- thread_id = thread .id ,
179- assistant_id = self .agent_id
180- )
319+ self ._run_and_process (thread_id = thread_id )
181320
182321 # Get messages
183- messages = self .client . agents . list_messages (thread_id = thread . id )
322+ messages = self ._list_messages (thread_id = thread_id )
184323
185324 # Find the assistant's response
186325 for message in messages :
187326 if message .role == "assistant" :
188- for content in message .content :
189- if hasattr (content , 'text' ):
327+ # Message content can be a list of blocks or a mapping
328+ contents = getattr (message , "content" , None )
329+ if isinstance (message , dict ) and contents is None :
330+ contents = message .get ("content" )
331+ if not contents :
332+ continue
333+ for content in contents :
334+ # SDK content blocks commonly expose .text.value
335+ if hasattr (content , "text" ) and hasattr (content .text , "value" ):
190336 yield content .text .value
191-
192- # Clean up
193- self .client .agents .delete_thread (thread .id )
337+ elif isinstance (content , dict ):
338+ text = content .get ("text" )
339+ if isinstance (text , dict ) and isinstance (text .get ("value" ), str ):
340+ yield text ["value" ]
341+ elif isinstance (text , str ):
342+ yield text
343+ elif isinstance (content , str ):
344+ yield content
194345
195346 except Exception as e :
196347 yield f"Error communicating with agent: { str (e )} "
348+ finally :
349+ if thread_id :
350+ try :
351+ self ._delete_thread (thread_id )
352+ except Exception :
353+ # Best-effort cleanup; ignore failures
354+ pass
0 commit comments