Skip to content

Commit 656f4c8

Browse files
committed
Add Azure AI Search support to FoundryAgentTemplate
Refactored FoundryAgentTemplate to support both Azure AI Search (raw tool) and legacy MCP tool paths, with priority given to Azure AI Search if configured. Added logic to create and clean up server-side Azure AI agents for the Azure Search path, and ensured code interpreter is only attached in the MCP path due to incompatibility. Updated MagenticAgentFactory and ReasoningAgentTemplate to use a consistent 'project_endpoint' parameter name.
1 parent 863a9ee commit 656f4c8

File tree

3 files changed

+158
-49
lines changed

3 files changed

+158
-49
lines changed

src/backend/v4/magentic_agents/foundry_agent.py

Lines changed: 154 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,25 @@
77
ChatAgent,
88
ChatMessage,
99
Role,
10-
HostedFileSearchTool,
11-
HostedVectorStoreContent,
1210
HostedCodeInterpreterTool,
1311
)
12+
from azure.ai.projects.models import ConnectionType
13+
from agent_framework_azure_ai import AzureAIAgentClient # Provided by agent_framework
14+
15+
1416
from v4.magentic_agents.common.lifecycle import AzureAgentBase
1517
from v4.magentic_agents.models.agent_models import MCPConfig, SearchConfig
1618
from v4.config.agent_registry import agent_registry
1719

1820

1921
class FoundryAgentTemplate(AzureAgentBase):
20-
"""Agent that uses Azure AI Search (RAG) and optional MCP tool via agent_framework."""
22+
"""Agent that uses Azure AI Search (raw tool) OR MCP tool + optional Code Interpreter.
23+
24+
Priority:
25+
1. Azure AI Search (if search_config contains required Azure Search fields)
26+
2. MCP tool (legacy path)
27+
Code Interpreter is only attached on the MCP path (unless you want it also with Azure Search—currently skipped for incompatibility per request).
28+
"""
2129

2230
def __init__(
2331
self,
@@ -37,41 +45,39 @@ def __init__(
3745
self.search = search_config
3846
self.logger = logging.getLogger(__name__)
3947

48+
# Decide early whether Azure Search mode should be activated
49+
self._use_azure_search = self._is_azure_search_requested()
50+
51+
# Placeholder for server-created Azure AI agent id (if Azure Search path)
52+
self._azure_server_agent_id: Optional[str] = None
53+
4054
# -------------------------
41-
# Tool construction helpers
55+
# Mode detection
4256
# -------------------------
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).")
47-
return None
57+
def _is_azure_search_requested(self) -> bool:
58+
"""Determine if Azure AI Search raw tool path should be used."""
59+
if not self.search:
60+
return False
61+
# Minimal heuristic: presence of required attributes
4862

49-
try:
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"
63+
has_index = hasattr(self.search, "index_name") and bool(self.search.index_name)
64+
if has_index:
65+
self.logger.info(
66+
"Azure AI Search requested (connection_id=%s, index=%s).",
67+
getattr(self.search, "connection_name", None),
68+
getattr(self.search, "index_name", None),
5569
)
56-
self.logger.info("Created HostedFileSearchTool with vector store: %s", self.search.vector_store_id)
57-
return file_search_tool
58-
except Exception as ex:
59-
self.logger.error("File search tool creation failed: %s", ex)
60-
return None
70+
return True
71+
return False
72+
73+
6174

6275
async def _collect_tools(self) -> List:
63-
"""Collect tool definitions for ChatAgent."""
76+
"""Collect tool definitions for ChatAgent (MCP path only)."""
6477
tools: List = []
6578

66-
# File Search tool (RAG)
67-
if self.search:
68-
print("Adding File Search tool.")
69-
# search_tool = await self._make_file_search_tool()
70-
# if search_tool:
71-
# tools.append(search_tool)
72-
# self.logger.info("Added File Search tool.")
7379

74-
# Code Interpreter
80+
# Code Interpreter (only in MCP path per incompatibility note)
7581
if self.enable_code_interpreter:
7682
try:
7783
code_tool = HostedCodeInterpreterTool()
@@ -85,28 +91,110 @@ async def _collect_tools(self) -> List:
8591
tools.append(self.mcp_tool)
8692
self.logger.info("Added MCP tool: %s", self.mcp_tool.name)
8793

88-
self.logger.info("Total tools collected: %d", len(tools))
94+
self.logger.info("Total tools collected (MCP path): %d", len(tools))
8995
return tools
9096

97+
# -------------------------
98+
# Azure Search helper
99+
# -------------------------
100+
async def _create_azure_search_enabled_client(self):
101+
"""
102+
Create a server-side Azure AI agent with raw Azure AI Search tool and return an AzureAIAgentClient.
103+
This mirrors your example while fitting existing lifecycle.
104+
105+
If these assumptions differ, adjust accordingly.
106+
"""
107+
108+
connection_id = getattr(self.search, "connection_name", "")
109+
index_name = getattr(self.search, "index_name", "")
110+
query_type = getattr(self.search, "search_query_type", "vector")
111+
112+
# ai_search_conn_id = ""
113+
# async for connection in self.client.project_client.connections.list():
114+
# if connection.type == ConnectionType.AZURE_AI_SEARCH:
115+
# ai_search_conn_id = connection.id
116+
# break
117+
if not connection_id or not index_name:
118+
self.logger.error(
119+
"Missing azure_search_connection_id or azure_search_index_name in search_config; aborting Azure Search path."
120+
)
121+
return None
122+
123+
try:
124+
azure_agent = await self.client.project_client.agents.create_agent(
125+
model=self.model_deployment_name,
126+
name=self.agent_name,
127+
instructions=(
128+
f"{self.agent_instructions} "
129+
"Always use the Azure AI Search tool and configured index for knowledge retrieval."
130+
),
131+
tools=[{"type": "azure_ai_search"}],
132+
tool_resources={
133+
"azure_ai_search": {
134+
"indexes": [
135+
{
136+
"index_connection_id": connection_id,
137+
"index_name": index_name,
138+
"query_type": query_type,
139+
}
140+
]
141+
}
142+
},
143+
)
144+
self._azure_server_agent_id = azure_agent.id
145+
self.logger.info(
146+
"Created Azure server agent with Azure AI Search tool (agent_id=%s, index=%s).",
147+
azure_agent.id,
148+
index_name,
149+
)
150+
151+
chat_client = AzureAIAgentClient(
152+
project_client=self.client.project_client,
153+
agent_id=azure_agent.id,
154+
)
155+
return chat_client
156+
except Exception as ex:
157+
self.logger.error("Failed to create Azure Search enabled agent: %s", ex)
158+
return None
159+
91160
# -------------------------
92161
# Agent lifecycle override
93162
# -------------------------
94163
async def _after_open(self) -> None:
95164
"""Initialize ChatAgent after connections are established."""
96165
try:
97-
tools = await self._collect_tools()
98-
99-
self._agent = ChatAgent(
100-
chat_client=self.client,
101-
instructions=self.agent_instructions,
102-
name=self.agent_name,
103-
description=self.agent_description,
104-
tools=tools if tools else None,
105-
tool_choice="auto" if tools else "none",
106-
temperature=0.7,
107-
model_id=self.model_deployment_name,
108-
)
109-
166+
if self._use_azure_search:
167+
# Azure Search mode (skip MCP + Code Interpreter due to incompatibility)
168+
self.logger.info("Initializing agent in Azure AI Search mode (exclusive).")
169+
chat_client = await self._create_azure_search_enabled_client()
170+
if not chat_client:
171+
raise RuntimeError("Azure AI Search mode requested but setup failed.")
172+
173+
# In Azure Search raw tool path, tools/tool_choice are handled server-side.
174+
self._agent = ChatAgent(
175+
chat_client=chat_client,
176+
instructions=self.agent_instructions,
177+
name=self.agent_name,
178+
description=self.agent_description,
179+
tool_choice="required", # Force usage
180+
temperature=0.7,
181+
model_id=self.model_deployment_name,
182+
)
183+
else:
184+
# use MCP path
185+
self.logger.info("Initializing agent in MCP mode.")
186+
tools = await self._collect_tools()
187+
self._agent = ChatAgent(
188+
chat_client=self.client,
189+
instructions=self.agent_instructions,
190+
name=self.agent_name,
191+
description=self.agent_description,
192+
tools=tools if tools else None,
193+
tool_choice="auto" if tools else "none",
194+
temperature=0.7,
195+
model_id=self.model_deployment_name,
196+
)
197+
110198
self.logger.info("Initialized ChatAgent '%s'", self.agent_name)
111199
except Exception as ex:
112200
self.logger.error("Failed to initialize ChatAgent: %s", ex)
@@ -128,10 +216,32 @@ async def invoke(self, prompt: str):
128216
raise RuntimeError("Agent not initialized; call open() first.")
129217

130218
messages = [ChatMessage(role=Role.USER, text=prompt)]
131-
219+
132220
async for update in self._agent.run_stream(messages=messages):
133221
yield update
134222

223+
# -------------------------
224+
# Cleanup (optional override if you want to delete server-side agent)
225+
# -------------------------
226+
async def close(self) -> None:
227+
"""Extend base close to optionally delete server-side Azure agent."""
228+
try:
229+
if self._use_azure_search and self._azure_server_agent_id and hasattr(self, "project_client"):
230+
try:
231+
await self.project_client.agents.delete_agent(self._azure_server_agent_id)
232+
self.logger.info(
233+
"Deleted Azure server agent (id=%s) during close.", self._azure_server_agent_id
234+
)
235+
except Exception as ex:
236+
self.logger.warning(
237+
"Failed to delete Azure server agent (id=%s): %s",
238+
self._azure_server_agent_id,
239+
ex,
240+
)
241+
finally:
242+
await super().close()
243+
244+
135245
# -------------------------
136246
# Factory
137247
# -------------------------

src/backend/v4/magentic_agents/magentic_agent_factory.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,12 @@ async def create_agent_from_config(self, user_id: str, agent_obj: SimpleNamespac
102102
if use_reasoning:
103103
# Get reasoning specific configuration
104104
azure_openai_endpoint = config.AZURE_OPENAI_ENDPOINT
105-
106105
agent = ReasoningAgentTemplate(
107106
agent_name=agent_obj.name,
108107
agent_description=getattr(agent_obj, "description", ""),
109108
agent_instructions=getattr(agent_obj, "system_message", ""),
110109
model_deployment_name=deployment_name,
111-
azure_openai_endpoint=azure_openai_endpoint,
110+
project_endpoint=azure_openai_endpoint, # type: ignore
112111
search_config=search_config,
113112
mcp_config=mcp_config,
114113
)

src/backend/v4/magentic_agents/reasoning_agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def __init__(
4343
agent_description: str,
4444
agent_instructions: str,
4545
model_deployment_name: str,
46-
azure_ai_project_endpoint: str,
46+
project_endpoint: str | None = "",
4747
search_config: SearchConfig | None = None,
4848
mcp_config: MCPConfig | None = None,
4949
max_search_docs: int = 3,
@@ -55,7 +55,7 @@ def __init__(
5555
agent_description: Description of the agent's purpose
5656
agent_instructions: System instructions for the agent
5757
model_deployment_name: Reasoning model deployment (e.g., "o1", "o3-mini")
58-
azure_ai_project_endpoint: Azure AI Project endpoint URL
58+
project_endpoint: Azure AI Project endpoint URL
5959
search_config: Optional search configuration for Azure AI Search
6060
mcp_config: Optional MCP server configuration
6161
max_search_docs: Maximum number of search documents to retrieve
@@ -65,7 +65,7 @@ def __init__(
6565
self.agent_description = agent_description
6666
self.base_instructions = agent_instructions
6767
self.model_deployment_name = model_deployment_name
68-
self.project_endpoint = azure_ai_project_endpoint
68+
self.project_endpoint = project_endpoint
6969
self.search_config = search_config
7070
self.max_search_docs = max_search_docs
7171

0 commit comments

Comments
 (0)