Skip to content

Commit a7f509f

Browse files
committed
Refactor FoundryAgent to use new tool interfaces
Replaces Azure AI Search and code interpreter tool logic with HostedFileSearchTool and HostedCodeInterpreterTool. Updates lifecycle to use async MCP tool preparation and simplifies tool/resource collection. Removes legacy compatibility and diagnostics code, streamlining agent initialization and invocation for improved maintainability.
1 parent 60cba77 commit a7f509f

File tree

3 files changed

+59
-221
lines changed

3 files changed

+59
-221
lines changed

src/backend/af/magentic_agents/common/lifecycle.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
ToolMode,
2222
ToolProtocol,
2323
)
24-
from agent_framework import HostedMCPTool
24+
from agent_framework import MCPStreamableHTTPTool
2525

2626
from af.magentic_agents.models.agent_models import MCPConfig
2727
from af.config.agent_registry import agent_registry
@@ -86,17 +86,18 @@ async def _after_open(self) -> None:
8686
"""Subclasses must build self._agent here."""
8787
raise NotImplementedError
8888

89-
def _prepare_mcp_tool(self) -> None:
89+
async def _prepare_mcp_tool(self) -> None:
9090
"""Translate MCPConfig to a HostedMCPTool (agent_framework construct)."""
9191
if not self.mcp_cfg:
9292
return
9393
try:
94-
self.mcp_tool = HostedMCPTool(
94+
mcp_tool = MCPStreamableHTTPTool(
9595
name=self.mcp_cfg.name,
9696
description=self.mcp_cfg.description,
97-
server_label=self.mcp_cfg.name.replace(" ", "_"),
98-
url="", # URL will be resolved via MCPConfig in HostedMCPTool
97+
url=self.mcp_cfg.url
9998
)
99+
await self._stack.enter_async_context(mcp_tool)
100+
self.mcp_tool = mcp_tool # Store for later use
100101
except Exception: # noqa: BLE001
101102
self.mcp_tool = None
102103

@@ -145,7 +146,7 @@ async def open(self) -> "AzureAgentBase":
145146
await self._stack.enter_async_context(self.client)
146147

147148
# Prepare MCP
148-
self._prepare_mcp_tool()
149+
await self._prepare_mcp_tool()
149150

150151
# Let subclass build agent client
151152
await self._after_open()

src/backend/af/magentic_agents/foundry_agent.py

Lines changed: 52 additions & 214 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,18 @@
33
import logging
44
from typing import List, Optional
55

6-
from azure.ai.agents.models import (
7-
Agent,
8-
AzureAISearchTool,
9-
CodeInterpreterToolDefinition,
10-
)
11-
126
from agent_framework import (
7+
ChatAgent,
138
ChatMessage,
149
Role,
15-
ChatOptions,
16-
HostedMCPTool,
17-
AggregateContextProvider,
18-
ChatAgent,
19-
ChatClientProtocol,
20-
ChatMessageStoreProtocol,
21-
ContextProvider,
22-
Middleware,
23-
ToolMode,
24-
ToolProtocol,
10+
HostedFileSearchTool,
11+
HostedVectorStoreContent,
12+
HostedCodeInterpreterTool,
2513
)
2614
from af.magentic_agents.common.lifecycle import AzureAgentBase
2715
from af.magentic_agents.models.agent_models import MCPConfig, SearchConfig
2816
from af.config.agent_registry import agent_registry
2917

30-
# Broad exception flag
31-
# pylint: disable=w0718
32-
3318

3419
class FoundryAgentTemplate(AzureAgentBase):
3520
"""Agent that uses Azure AI Search (RAG) and optional MCP tool via agent_framework."""
@@ -44,253 +29,106 @@ def __init__(
4429
mcp_config: MCPConfig | None = None,
4530
search_config: SearchConfig | None = None,
4631
) -> None:
47-
super().__init__(mcp=mcp_config)
32+
super().__init__(mcp=mcp_config, model_deployment_name=model_deployment_name)
4833
self.agent_name = agent_name
4934
self.agent_description = agent_description
5035
self.agent_instructions = agent_instructions
51-
self.model_deployment_name = model_deployment_name
5236
self.enable_code_interpreter = enable_code_interpreter
53-
self.mcp = mcp_config
5437
self.search = search_config
55-
56-
self._search_connection = None
5738
self.logger = logging.getLogger(__name__)
5839

59-
if self.model_deployment_name in {"o3", "o4-mini"}:
60-
raise ValueError(
61-
"Foundry agents do not support reasoning models in this implementation."
62-
)
63-
6440
# -------------------------
6541
# Tool construction helpers
6642
# -------------------------
67-
async def _make_azure_search_tool(self) -> Optional[AzureAISearchTool]:
68-
"""Create Azure AI Search tool (RAG capability)."""
69-
if not (
70-
self.client
71-
and self.search
72-
and self.search.connection_name
73-
and self.search.index_name
74-
):
75-
self.logger.info(
76-
"Azure AI Search tool not enabled (missing config or client)."
77-
)
43+
async def _make_file_search_tool(self) -> Optional[HostedFileSearchTool]:
44+
"""Create File Search tool (RAG capability) using vector stores."""
45+
if not self.search or not self.search.vector_store_id:
46+
self.logger.info("File search tool not enabled (missing vector_store_id).")
7847
return None
7948

8049
try:
81-
self._search_connection = await self.client.connections.get(
82-
name=self.search.connection_name
83-
)
84-
self.logger.info(
85-
"Found Azure AI Search connection: %s", self._search_connection.id
86-
)
87-
88-
return AzureAISearchTool(
89-
index_connection_id=self._search_connection.id,
90-
index_name=self.search.index_name,
50+
# HostedFileSearchTool uses vector stores, not direct Azure AI Search indexes
51+
file_search_tool = HostedFileSearchTool(
52+
inputs=[HostedVectorStoreContent(vector_store_id=self.search.vector_store_id)],
53+
max_results=self.search.max_results if hasattr(self.search, 'max_results') else None,
54+
description="Search through indexed documents"
9155
)
56+
self.logger.info("Created HostedFileSearchTool with vector store: %s", self.search.vector_store_id)
57+
return file_search_tool
9258
except Exception as ex:
93-
self.logger.error(
94-
"Azure AI Search tool creation failed: %s | connection=%s | index=%s",
95-
ex,
96-
getattr(self.search, "connection_name", None),
97-
getattr(self.search, "index_name", None),
98-
)
59+
self.logger.error("File search tool creation failed: %s", ex)
9960
return None
10061

101-
async def _collect_tools_and_resources(self) -> tuple[List, dict]:
102-
"""Collect tool definitions + tool_resources for agent definition creation."""
62+
async def _collect_tools(self) -> List:
63+
"""Collect tool definitions for ChatAgent."""
10364
tools: List = []
104-
tool_resources: dict = {}
10565

106-
# Search tool
107-
if self.search and self.search.connection_name and self.search.index_name:
108-
search_tool = await self._make_azure_search_tool()
66+
# File Search tool (RAG)
67+
if self.search:
68+
search_tool = await self._make_file_search_tool()
10969
if search_tool:
110-
tools.extend(search_tool.definitions)
111-
tool_resources = search_tool.resources
112-
self.logger.info(
113-
"Added %d Azure AI Search tool definitions.",
114-
len(search_tool.definitions),
115-
)
116-
else:
117-
self.logger.warning("Azure AI Search tool not configured properly.")
70+
tools.append(search_tool)
71+
self.logger.info("Added File Search tool.")
11872

11973
# Code Interpreter
12074
if self.enable_code_interpreter:
12175
try:
122-
tools.append(CodeInterpreterToolDefinition())
123-
self.logger.info("Added Code Interpreter tool definition.")
124-
except ImportError as ie:
125-
self.logger.error("Code Interpreter dependency missing: %s", ie)
76+
code_tool = HostedCodeInterpreterTool()
77+
tools.append(code_tool)
78+
self.logger.info("Added Code Interpreter tool.")
79+
except Exception as ie:
80+
self.logger.error("Code Interpreter tool creation failed: %s", ie)
81+
82+
# MCP Tool (from base class)
83+
if self.mcp_tool:
84+
tools.append(self.mcp_tool)
85+
self.logger.info("Added MCP tool: %s", self.mcp_tool.name)
12686

127-
self.logger.info("Total tool definitions collected: %d", len(tools))
128-
return tools, tool_resources
87+
self.logger.info("Total tools collected: %d", len(tools))
88+
return tools
12989

13090
# -------------------------
13191
# Agent lifecycle override
13292
# -------------------------
13393
async def _after_open(self) -> None:
134-
# Instantiate persistent AzureAIAgentClient bound to existing agent_id
94+
"""Initialize ChatAgent after connections are established."""
13595
try:
136-
# AzureAIAgentClient(
137-
# project_client=self.client,
138-
# agent_id=str(definition.id),
139-
# agent_name=self.agent_name,
140-
# )
141-
tools, tool_resources = await self._collect_tools_and_resources()
96+
tools = await self._collect_tools()
97+
14298
self._agent = ChatAgent(
14399
chat_client=self.client,
144-
instructions=self.agent_description + " " + self.agent_instructions,
100+
instructions=self.agent_instructions,
145101
name=self.agent_name,
146102
description=self.agent_description,
147103
tools=tools if tools else None,
148104
tool_choice="auto" if tools else "none",
149-
allow_multiple_tool_calls=True,
150105
temperature=0.7,
106+
model_id=self.model_deployment_name,
151107
)
152-
108+
109+
self.logger.info("Initialized ChatAgent '%s'", self.agent_name)
153110
except Exception as ex:
154-
self.logger.error("Failed to initialize AzureAIAgentClient: %s", ex)
111+
self.logger.error("Failed to initialize ChatAgent: %s", ex)
155112
raise
156113

157114
# Register agent globally
158115
try:
159116
agent_registry.register_agent(self)
160-
self.logger.info(
161-
"Registered agent '%s' in global registry.", self.agent_name
162-
)
117+
self.logger.info("Registered agent '%s' in global registry.", self.agent_name)
163118
except Exception as reg_ex:
164-
self.logger.warning(
165-
"Could not register agent '%s': %s", self.agent_name, reg_ex
166-
)
167-
168-
# -------------------------
169-
# Definition compatibility
170-
# -------------------------
171-
async def _check_connection_compatibility(self, existing_definition: Agent) -> bool:
172-
"""Verify existing Azure AI Search connection matches current config."""
173-
try:
174-
if not (self.search and self.search.connection_name):
175-
self.logger.info("No search config provided; assuming compatibility.")
176-
return True
177-
178-
tool_resources = getattr(existing_definition, "tool_resources", None)
179-
if not tool_resources:
180-
self.logger.info(
181-
"Existing agent has no tool resources; incompatible with search requirement."
182-
)
183-
return False
184-
185-
azure_search = tool_resources.get("azure_ai_search", {})
186-
indexes = azure_search.get("indexes", [])
187-
if not indexes:
188-
self.logger.info(
189-
"Existing agent has no Azure AI Search indexes; incompatible."
190-
)
191-
return False
192-
193-
existing_conn_id = indexes[0].get("index_connection_id")
194-
if not existing_conn_id:
195-
self.logger.info(
196-
"Existing agent missing index_connection_id; incompatible."
197-
)
198-
return False
199-
200-
current_connection = await self.client.connections.get(
201-
name=self.search.connection_name
202-
)
203-
same = existing_conn_id == current_connection.id
204-
if same:
205-
self.logger.info("Search connection compatible: %s", existing_conn_id)
206-
else:
207-
self.logger.info(
208-
"Search connection mismatch: existing=%s current=%s",
209-
existing_conn_id,
210-
current_connection.id,
211-
)
212-
return same
213-
except Exception as ex:
214-
self.logger.error("Error during connection compatibility check: %s", ex)
215-
return False
216-
217-
async def _get_azure_ai_agent_definition(self, agent_name: str) -> Agent | None:
218-
"""Return existing agent definition by name or None."""
219-
try:
220-
async for agent in self.client.agents.list_agents():
221-
if agent.name == agent_name:
222-
self.logger.info(
223-
"Found existing agent '%s' (id=%s).", agent_name, agent.id
224-
)
225-
return await self.client.agents.get_agent(agent.id)
226-
return None
227-
except Exception as e:
228-
if "ResourceNotFound" in str(e) or "404" in str(e):
229-
self.logger.info("Agent '%s' not found; will create new.", agent_name)
230-
else:
231-
self.logger.warning(
232-
"Unexpected error listing agent '%s': %s; will attempt creation.",
233-
agent_name,
234-
e,
235-
)
236-
return None
237-
238-
# -------------------------
239-
# Diagnostics helper
240-
# -------------------------
241-
async def fetch_run_details(self, thread_id: str, run_id: str) -> None:
242-
"""Log run diagnostics for a failed run."""
243-
try:
244-
run = await self.client.agents.runs.get(thread=thread_id, run=run_id)
245-
self.logger.error(
246-
"Run failure | status=%s | id=%s | last_error=%s | usage=%s",
247-
getattr(run, "status", None),
248-
run_id,
249-
getattr(run, "last_error", None),
250-
getattr(run, "usage", None),
251-
)
252-
except Exception as ex:
253-
self.logger.error(
254-
"Failed fetching run details (thread=%s run=%s): %s",
255-
thread_id,
256-
run_id,
257-
ex,
258-
)
119+
self.logger.warning("Could not register agent '%s': %s", self.agent_name, reg_ex)
259120

260121
# -------------------------
261122
# Invocation (streaming)
262123
# -------------------------
263124
async def invoke(self, prompt: str):
264-
"""
265-
Stream model output for a prompt.
266-
267-
Yields ChatResponseUpdate objects:
268-
- update.text for incremental text
269-
- update.contents for tool calls / usage events
270-
"""
125+
"""Stream model output for a prompt."""
271126
if not self._agent:
272-
raise RuntimeError("Agent client not initialized; call open() first.")
127+
raise RuntimeError("Agent not initialized; call open() first.")
273128

274129
messages = [ChatMessage(role=Role.USER, text=prompt)]
275-
276-
tools = []
277-
# Use mcp_tool prepared in AzureAgentBase
278-
if self.mcp_tool and isinstance(self.mcp_tool, HostedMCPTool):
279-
tools.append(self.mcp_tool)
280-
281-
chat_options = ChatOptions(
282-
model_id=self.model_deployment_name,
283-
tools=tools if tools else None,
284-
tool_choice="auto" if tools else "none",
285-
allow_multiple_tool_calls=True,
286-
temperature=0.7,
287-
)
288-
289-
async for update in self._agent.run_stream(
290-
messages=messages,
291-
# chat_options=chat_options,
292-
# instructions=self.agent_instructions,
293-
):
130+
131+
async for update in self._agent.run_stream(messages=messages):
294132
yield update
295133

296134

@@ -305,7 +143,7 @@ async def create_foundry_agent(
305143
mcp_config: MCPConfig | None,
306144
search_config: SearchConfig | None,
307145
) -> FoundryAgentTemplate:
308-
"""Factory to create and open a FoundryAgentTemplate (agent_framework version)."""
146+
"""Factory to create and open a FoundryAgentTemplate."""
309147
agent = FoundryAgentTemplate(
310148
agent_name=agent_name,
311149
agent_description=agent_description,
@@ -316,4 +154,4 @@ async def create_foundry_agent(
316154
search_config=search_config,
317155
)
318156
await agent.open()
319-
return agent
157+
return agent

0 commit comments

Comments
 (0)