Skip to content

Commit 2af3703

Browse files
authored
Merge pull request #605 from Fr4nc3/macae-rfp-af-101725
Add Azure AI Search support to FoundryAgentTemplate
2 parents 863a9ee + 656f4c8 commit 2af3703

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)