Skip to content

Commit f8150f0

Browse files
authored
Merge pull request #591 from Fr4nc3/macae-rfp-af-101725
Macae rfp af 101725
2 parents 836d2bb + 373cea5 commit f8150f0

File tree

6 files changed

+572
-506
lines changed

6 files changed

+572
-506
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)