1- """Agent template for building foundry agents with Azure AI Search, Bing , and MCP plugins (agent_framework version)."""
1+ """Agent template for building Foundry agents with Azure AI Search, optional MCP tool , and Code Interpreter (agent_framework version)."""
22
33import logging
44from typing import List , Optional
55
66from azure .ai .agents .models import Agent , AzureAISearchTool , CodeInterpreterToolDefinition
77from agent_framework .azure import AzureAIAgentClient
8- from agent_framework import ChatMessage , Role , ChatOptions , HostedMCPTool # HostedMCPTool for MCP plugin mapping
8+ from agent_framework import ChatMessage , Role , ChatOptions , HostedMCPTool
99
10- from v3 .magentic_agents .common .lifecycle import AzureAgentBase
11- from v3 .magentic_agents .models .agent_models import MCPConfig , SearchConfig
12- from v3 .config .agent_registry import agent_registry
10+ from af .magentic_agents .common .lifecycle import AzureAgentBase
11+ from af .magentic_agents .models .agent_models import MCPConfig , SearchConfig
12+ from af .config .agent_registry import agent_registry
1313
14- # exception too broad warning
14+ # Broad exception flag
1515# pylint: disable=w0718
1616
1717
1818class FoundryAgentTemplate (AzureAgentBase ):
19- """Agent that uses Azure AI Search (RAG) and optional MCP tools via agent_framework."""
19+ """Agent that uses Azure AI Search (RAG) and optional MCP tool via agent_framework."""
2020
2121 def __init__ (
2222 self ,
@@ -26,7 +26,6 @@ def __init__(
2626 model_deployment_name : str ,
2727 enable_code_interpreter : bool = False ,
2828 mcp_config : MCPConfig | None = None ,
29- # bing_config: BingConfig | None = None,
3029 search_config : SearchConfig | None = None ,
3130 ) -> None :
3231 super ().__init__ (mcp = mcp_config )
@@ -35,85 +34,91 @@ def __init__(
3534 self .agent_instructions = agent_instructions
3635 self .model_deployment_name = model_deployment_name
3736 self .enable_code_interpreter = enable_code_interpreter
38- # self.bing = bing_config
3937 self .mcp = mcp_config
4038 self .search = search_config
39+
4140 self ._search_connection = None
42- self ._bing_connection = None
4341 self .logger = logging .getLogger (__name__ )
4442
45- if self .model_deployment_name in ["o3" , "o4-mini" ]:
46- raise ValueError (
47- "The current version of Foundry agents does not support reasoning models."
48- )
43+ if self .model_deployment_name in {"o3" , "o4-mini" }:
44+ raise ValueError ("Foundry agents do not support reasoning models in this implementation." )
4945
46+ # -------------------------
47+ # Tool construction helpers
48+ # -------------------------
5049 async def _make_azure_search_tool (self ) -> Optional [AzureAISearchTool ]:
51- """Create Azure AI Search tool for RAG capabilities ."""
52- if not all ([ self .client , self .search and self .search .connection_name , self . search and self .search .index_name ] ):
53- self .logger .info ("Azure AI Search tool not enabled" )
50+ """Create Azure AI Search tool ( RAG capability) ."""
51+ if not ( self .client and self .search and self .search .connection_name and self .search .index_name ):
52+ self .logger .info ("Azure AI Search tool not enabled (missing config or client). " )
5453 return None
5554
5655 try :
57- self ._search_connection = await self .client .connections .get (
58- name = self .search .connection_name
59- )
56+ self ._search_connection = await self .client .connections .get (name = self .search .connection_name )
6057 self .logger .info ("Found Azure AI Search connection: %s" , self ._search_connection .id )
6158
62- search_tool = AzureAISearchTool (
59+ return AzureAISearchTool (
6360 index_connection_id = self ._search_connection .id ,
6461 index_name = self .search .index_name ,
6562 )
66- self .logger .info ("Azure AI Search tool created for index: %s" , self .search .index_name )
67- return search_tool
68-
6963 except Exception as ex :
7064 self .logger .error (
71- "Azure AI Search tool creation failed: %s | Connection name: %s | Index name: %s | "
72- "Ensure the connection exists in Azure AI Foundry portal." ,
65+ "Azure AI Search tool creation failed: %s | connection=%s | index=%s" ,
7366 ex ,
7467 getattr (self .search , "connection_name" , None ),
7568 getattr (self .search , "index_name" , None ),
7669 )
7770 return None
7871
7972 async def _collect_tools_and_resources (self ) -> tuple [List , dict ]:
80- """Collect all available tools and tool_resources to embed in persistent agent definition."""
73+ """Collect tool definitions + tool_resources for agent definition creation ."""
8174 tools : List = []
8275 tool_resources : dict = {}
8376
77+ # Search tool
8478 if self .search and self .search .connection_name and self .search .index_name :
8579 search_tool = await self ._make_azure_search_tool ()
8680 if search_tool :
8781 tools .extend (search_tool .definitions )
8882 tool_resources = search_tool .resources
8983 self .logger .info (
90- "Added Azure AI Search tools: %d tool definitions" , len (search_tool .definitions )
84+ "Added %d Azure AI Search tool definitions." ,
85+ len (search_tool .definitions ),
9186 )
9287 else :
93- self .logger .error ("Azure AI Search tool not configured properly" )
88+ self .logger .warning ("Azure AI Search tool not configured properly. " )
9489
90+ # Code Interpreter
9591 if self .enable_code_interpreter :
9692 try :
9793 tools .append (CodeInterpreterToolDefinition ())
98- self .logger .info ("Added Code Interpreter tool" )
94+ self .logger .info ("Added Code Interpreter tool definition. " )
9995 except ImportError as ie :
100- self .logger .error ("Code Interpreter tool requires additional dependencies : %s" , ie )
96+ self .logger .error ("Code Interpreter dependency missing : %s" , ie )
10197
102- self .logger .info ("Total tools configured in definition : %d" , len (tools ))
98+ self .logger .info ("Total tool definitions collected : %d" , len (tools ))
10399 return tools , tool_resources
104100
101+ # -------------------------
102+ # Agent lifecycle override
103+ # -------------------------
105104 async def _after_open (self ) -> None :
106- """Build or reuse the Azure AI agent definition; create agent_framework client ."""
105+ """Create or reuse Azure AI agent definition and wrap with AzureAIAgentClient ."""
107106 definition = await self ._get_azure_ai_agent_definition (self .agent_name )
108107
109108 if definition is not None :
110- connection_compatible = await self ._check_connection_compatibility (definition )
111- if not connection_compatible :
112- await self .client .agents .delete_agent (definition .id )
113- self .logger .info (
114- "Existing agent '%s' used incompatible connection. Creating new definition." ,
115- self .agent_name ,
116- )
109+ if not await self ._check_connection_compatibility (definition ):
110+ try :
111+ await self .client .agents .delete_agent (definition .id )
112+ self .logger .info (
113+ "Deleted incompatible existing agent '%s'; will recreate with new connection settings." ,
114+ self .agent_name ,
115+ )
116+ except Exception as ex :
117+ self .logger .warning (
118+ "Failed deleting incompatible agent '%s': %s (will still recreate)." ,
119+ self .agent_name ,
120+ ex ,
121+ )
117122 definition = None
118123
119124 if definition is None :
@@ -126,138 +131,128 @@ async def _after_open(self) -> None:
126131 tools = tools ,
127132 tool_resources = tool_resources ,
128133 )
134+ self .logger .info ("Created new Azure AI agent definition '%s'" , self .agent_name )
129135
136+ # Instantiate persistent AzureAIAgentClient bound to existing agent_id
130137 try :
131- # Wrap existing agent definition with agent_framework client (persistent agent mode)
132138 self ._agent = AzureAIAgentClient (
133139 project_client = self .client ,
134140 agent_id = str (definition .id ),
135141 agent_name = self .agent_name ,
136- thread_id = None , # created dynamically if omitted during invocation
137142 )
138143 except Exception as ex :
139144 self .logger .error ("Failed to initialize AzureAIAgentClient: %s" , ex )
140145 raise
141146
142- # Register with global registry
147+ # Register agent globally
143148 try :
144149 agent_registry .register_agent (self )
145- self .logger .info ("📝 Registered agent '%s' with global registry" , self .agent_name )
146- except Exception as registry_error :
147- self .logger .warning (
148- "⚠️ Failed to register agent '%s' with registry: %s" , self .agent_name , registry_error
149- )
150-
151- async def fetch_run_details (self , thread_id : str , run_id : str ) -> None :
152- """Fetch and log run details on failure for diagnostics."""
153- try :
154- run = await self .client .agents .runs .get (thread = thread_id , run = run_id )
155- self .logger .error (
156- "Run failure details | status=%s | id=%s | last_error=%s | usage=%s" ,
157- getattr (run , "status" , None ),
158- run_id ,
159- getattr (run , "last_error" , None ),
160- getattr (run , "usage" , None ),
161- )
162- except Exception as ex :
163- self .logger .error ("Could not fetch run details: %s" , ex )
150+ self .logger .info ("Registered agent '%s' in global registry." , self .agent_name )
151+ except Exception as reg_ex :
152+ self .logger .warning ("Could not register agent '%s': %s" , self .agent_name , reg_ex )
164153
154+ # -------------------------
155+ # Definition compatibility
156+ # -------------------------
165157 async def _check_connection_compatibility (self , existing_definition : Agent ) -> bool :
166- """Ensure existing agent definition's Azure AI Search connection matches current configuration ."""
158+ """Verify existing Azure AI Search connection matches current config ."""
167159 try :
168- if not self .search or not self .search .connection_name :
169- self .logger .info ("No search configuration provided; treating existing definition as compatible ." )
160+ if not ( self .search and self .search .connection_name ) :
161+ self .logger .info ("No search config provided; assuming compatibility ." )
170162 return True
171163
172- if not getattr (existing_definition , "tool_resources" , None ):
173- self .logger .info ("Existing definition lacks tool resources." )
174- return not self .search .connection_name
175-
176- azure_ai_search_resources = existing_definition .tool_resources .get ("azure_ai_search" , {})
177- if not azure_ai_search_resources :
178- self .logger .info ("Existing definition has no Azure AI Search resources." )
164+ tool_resources = getattr (existing_definition , "tool_resources" , None )
165+ if not tool_resources :
166+ self .logger .info ("Existing agent has no tool resources; incompatible with search requirement." )
179167 return False
180168
181- indexes = azure_ai_search_resources .get ("indexes" , [])
169+ azure_search = tool_resources .get ("azure_ai_search" , {})
170+ indexes = azure_search .get ("indexes" , [])
182171 if not indexes :
183- self .logger .info ("Existing definition search resources contain no indexes." )
172+ self .logger .info ("Existing agent has no Azure AI Search indexes; incompatible ." )
184173 return False
185174
186- existing_connection_id = indexes [0 ].get ("index_connection_id" )
187- if not existing_connection_id :
188- self .logger .info ("Existing definition missing connection ID ." )
175+ existing_conn_id = indexes [0 ].get ("index_connection_id" )
176+ if not existing_conn_id :
177+ self .logger .info ("Existing agent missing index_connection_id; incompatible ." )
189178 return False
190179
191180 current_connection = await self .client .connections .get (name = self .search .connection_name )
192- current_connection_id = current_connection .id
193- compatible = existing_connection_id == current_connection_id
194-
195- if compatible :
196- self .logger .info ("Connection compatible: %s" , existing_connection_id )
181+ same = existing_conn_id == current_connection .id
182+ if same :
183+ self .logger .info ("Search connection compatible: %s" , existing_conn_id )
197184 else :
198185 self .logger .info (
199- "Connection mismatch: existing %s vs current %s" ,
200- existing_connection_id ,
201- current_connection_id ,
186+ "Search connection mismatch: existing= %s current= %s" ,
187+ existing_conn_id ,
188+ current_connection . id ,
202189 )
203- return compatible
190+ return same
204191 except Exception as ex :
205- self .logger .error ("Error checking connection compatibility: %s" , ex )
192+ self .logger .error ("Error during connection compatibility check : %s" , ex )
206193 return False
207194
208195 async def _get_azure_ai_agent_definition (self , agent_name : str ) -> Agent | None :
209- """Retrieve an existing Azure AI Agent definition by name if present ."""
196+ """Return existing agent definition by name or None ."""
210197 try :
211- agent_id = None
212- agent_list = self .client .agents .list_agents ()
213- async for agent in agent_list :
198+ async for agent in self .client .agents .list_agents ():
214199 if agent .name == agent_name :
215- agent_id = agent .id
216- break
217- if agent_id is not None :
218- self .logger .info ("Found existing agent definition with ID %s" , agent_id )
219- return await self .client .agents .get_agent (agent_id )
200+ self .logger .info ("Found existing agent '%s' (id=%s)." , agent_name , agent .id )
201+ return await self .client .agents .get_agent (agent .id )
220202 return None
221203 except Exception as e :
222204 if "ResourceNotFound" in str (e ) or "404" in str (e ):
223- self .logger .info ("Agent '%s' not found; will create new definition ." , agent_name )
205+ self .logger .info ("Agent '%s' not found; will create new." , agent_name )
224206 else :
225207 self .logger .warning (
226- "Unexpected error retrieving agent '%s': %s. Proceeding to create new definition ." ,
208+ "Unexpected error listing agent '%s': %s; will attempt creation ." ,
227209 agent_name ,
228210 e ,
229211 )
230212 return None
231213
214+ # -------------------------
215+ # Diagnostics helper
216+ # -------------------------
217+ async def fetch_run_details (self , thread_id : str , run_id : str ) -> None :
218+ """Log run diagnostics for a failed run."""
219+ try :
220+ run = await self .client .agents .runs .get (thread = thread_id , run = run_id )
221+ self .logger .error (
222+ "Run failure | status=%s | id=%s | last_error=%s | usage=%s" ,
223+ getattr (run , "status" , None ),
224+ run_id ,
225+ getattr (run , "last_error" , None ),
226+ getattr (run , "usage" , None ),
227+ )
228+ except Exception as ex :
229+ self .logger .error ("Failed fetching run details (thread=%s run=%s): %s" , thread_id , run_id , ex )
230+
231+ # -------------------------
232+ # Invocation (streaming)
233+ # -------------------------
232234 async def invoke (self , prompt : str ):
233235 """
234236 Stream model output for a prompt.
235237
236- Yields agent_framework ChatResponseUpdate objects:
237- - update.text for incremental text
238- - update.contents for tool calls / usage events
238+ Yields ChatResponseUpdate objects:
239+ - update.text for incremental text
240+ - update.contents for tool calls / usage events
239241 """
240- if not hasattr ( self , "_agent" ) or self ._agent is None :
242+ if not self ._agent :
241243 raise RuntimeError ("Agent client not initialized; call open() first." )
242244
243245 messages = [ChatMessage (role = Role .USER , text = prompt )]
244246
245247 tools = []
246- # Map MCP plugin (if any) to HostedMCPTool for runtime tool calling
247- if self .mcp_plugin :
248- # Minimal HostedMCPTool; advanced mapping (approval modes, categories) can be added later.
249- tools .append (
250- HostedMCPTool (
251- name = self .mcp_plugin .name ,
252- server_label = self .mcp_plugin .name .replace (" " , "_" ),
253- description = getattr (self .mcp_plugin , "description" , "" ),
254- )
255- )
248+ # Use mcp_tool prepared in AzureAgentBase
249+ if self .mcp_tool and isinstance (self .mcp_tool , HostedMCPTool ):
250+ tools .append (self .mcp_tool )
256251
257252 chat_options = ChatOptions (
258253 model_id = self .model_deployment_name ,
259254 tools = tools if tools else None ,
260- tool_choice = "auto" ,
255+ tool_choice = "auto" if tools else "none" ,
261256 allow_multiple_tool_calls = True ,
262257 temperature = 0.7 ,
263258 )
@@ -270,24 +265,25 @@ async def invoke(self, prompt: str):
270265 yield update
271266
272267
268+ # -------------------------
269+ # Factory
270+ # -------------------------
273271async def create_foundry_agent (
274272 agent_name : str ,
275273 agent_description : str ,
276274 agent_instructions : str ,
277275 model_deployment_name : str ,
278- mcp_config : MCPConfig ,
279- # bing_config: BingConfig,
280- search_config : SearchConfig ,
276+ mcp_config : MCPConfig | None ,
277+ search_config : SearchConfig | None ,
281278) -> FoundryAgentTemplate :
282- """Factory function to create and open a FoundryAgentTemplate (agent_framework version)."""
279+ """Factory to create and open a FoundryAgentTemplate (agent_framework version)."""
283280 agent = FoundryAgentTemplate (
284281 agent_name = agent_name ,
285282 agent_description = agent_description ,
286283 agent_instructions = agent_instructions ,
287284 model_deployment_name = model_deployment_name ,
288285 enable_code_interpreter = True ,
289286 mcp_config = mcp_config ,
290- # bing_config=bing_config,
291287 search_config = search_config ,
292288 )
293289 await agent .open ()
0 commit comments