Skip to content

Commit f94c27d

Browse files
committed
Refactor Foundry agent and factory imports, improve logic
Updated import paths from 'v3' to 'af' for consistency and modularity. Improved error handling, logging, and tool/resource collection logic in FoundryAgentTemplate. Cleaned up unused Bing references and clarified code interpreter and MCP tool handling. These changes enhance maintainability and agent lifecycle management.
1 parent 982dcb4 commit f94c27d

File tree

2 files changed

+118
-122
lines changed

2 files changed

+118
-122
lines changed

src/backend/af/magentic_agents/foundry_agent.py

Lines changed: 114 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
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

33
import logging
44
from typing import List, Optional
55

66
from azure.ai.agents.models import Agent, AzureAISearchTool, CodeInterpreterToolDefinition
77
from 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

1818
class 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+
# -------------------------
273271
async 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

Comments
 (0)