Skip to content

Commit bb5331e

Browse files
Refactor agent creation and configuration to avoid mutating original team objects and streamline AzureAIClient initialization
1 parent 8d1d1c7 commit bb5331e

5 files changed

Lines changed: 28 additions & 219 deletions

File tree

src/backend/common/utils/utils_af.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,15 @@ async def create_RAI_agent(
8888
)
8989

9090
model_deployment_name = config.AZURE_OPENAI_RAI_DEPLOYMENT_NAME
91-
team.team_id = "rai_team" # Use a fixed team ID for RAI agent
92-
team.name = "RAI Team"
93-
team.description = "Team responsible for Responsible AI checks"
91+
92+
# Create a copy to avoid mutating the caller's team config.
93+
# The original team object is reused later (e.g., for orchestration init),
94+
# so mutating it would corrupt the real team name/id.
95+
rai_team = team.model_copy()
96+
rai_team.team_id = "rai_team"
97+
rai_team.name = "RAI Team"
98+
rai_team.description = "Team responsible for Responsible AI checks"
99+
94100
agent = FoundryAgentTemplate(
95101
agent_name=agent_name,
96102
agent_description=agent_description,
@@ -101,7 +107,7 @@ async def create_RAI_agent(
101107
project_endpoint=config.AZURE_AI_PROJECT_ENDPOINT,
102108
mcp_config=None,
103109
search_config=None,
104-
team_config=team,
110+
team_config=rai_team,
105111
memory_store=memory_store,
106112
)
107113

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11

2-
import logging
32
import secrets
43
import string
5-
from typing import Optional
6-
7-
from common.database.database_base import DatabaseBase
8-
from common.models.messages_af import TeamConfiguration
94

105

116
def generate_assistant_id(prefix: str = "asst_", length: int = 24) -> str:
@@ -21,22 +16,3 @@ def generate_assistant_id(prefix: str = "asst_", length: int = 24) -> str:
2116
# cryptographically strong randomness
2217
random_part = "".join(secrets.choice(alphabet) for _ in range(length))
2318
return f"{prefix}{random_part}"
24-
25-
26-
async def get_database_team_agent_id(
27-
memory_store: DatabaseBase, team_config: TeamConfiguration, agent_name: str
28-
) -> Optional[str]:
29-
"""Retrieve existing team agent from database, if any."""
30-
agent_id = None
31-
try:
32-
currentAgent = await memory_store.get_team_agent(
33-
team_id=team_config.team_id, agent_name=agent_name
34-
)
35-
if currentAgent and currentAgent.agent_foundry_id:
36-
agent_id = currentAgent.agent_foundry_id
37-
38-
except (
39-
Exception
40-
) as ex: # Consider narrowing this to specific exceptions if possible
41-
logging.error("Failed to initialize Get database team agent: %s", ex)
42-
return agent_id

src/backend/v4/callbacks/response_handlers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import re
99
from typing import Any
1010

11-
from agent_framework import ChatMessage, AgentRunUpdateEvent
11+
from agent_framework import ChatMessage
1212

1313
from v4.config.settings import connection_config
1414
from v4.models.messages import (
@@ -108,7 +108,7 @@ def agent_response_callback(
108108

109109
async def streaming_agent_response_callback(
110110
agent_id: str,
111-
update, # AgentRunUpdateEvent.data or similar streaming update object
111+
update, # Streaming update object (e.g. AgentResponseUpdate, ChatMessage)
112112
is_final: bool,
113113
user_id: str | None = None,
114114
) -> None:
@@ -119,7 +119,7 @@ async def streaming_agent_response_callback(
119119
return
120120

121121
try:
122-
# Handle both AgentRunUpdateEvent.data and raw text updates
122+
# Handle various streaming update object shapes
123123
chunk_text = getattr(update, "text", None)
124124

125125
# If text is None, don't fall back to str(update) as that would show object repr

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

Lines changed: 8 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
from azure.ai.agents.aio import AgentsClient
1616
from azure.identity.aio import DefaultAzureCredential
1717
from common.database.database_base import DatabaseBase
18-
from common.models.messages_af import CurrentTeamAgent, TeamConfiguration
18+
from common.models.messages_af import TeamConfiguration
1919
from common.utils.utils_agents import (
2020
generate_assistant_id,
21-
get_database_team_agent_id,
2221
)
2322
from v4.common.services.team_service import TeamService
2423
from v4.config.agent_registry import agent_registry
@@ -148,13 +147,12 @@ async def _after_open(self) -> None:
148147
"""Subclasses must build self._agent here."""
149148
raise NotImplementedError
150149

151-
def get_chat_client(self, chat_client) -> AzureAIClient:
150+
def get_chat_client(self) -> AzureAIClient:
152151
"""Return the underlying ChatClientProtocol (AzureAIClient).
153152
154-
Uses agent_name with use_latest_version=True to get the latest agent version
153+
Uses agent_name with use_latest_version=True to get the latest agent version.
154+
Agent reuse is handled automatically by the SDK via agent_name.
155155
"""
156-
if chat_client:
157-
return chat_client
158156
if (
159157
self._agent
160158
and self._agent.chat_client
@@ -173,176 +171,16 @@ def get_chat_client(self, chat_client) -> AzureAIClient:
173171
)
174172
return chat_client
175173

176-
async def resolve_agent_id(self, agent_id: str) -> Optional[str]:
177-
"""Resolve agent ID via Projects SDK first (for RAI agents), fallback to AgentsClient.
178-
179-
Args:
180-
agent_id: The agent ID to resolve
181-
182-
Returns:
183-
The resolved agent ID if found, None otherwise
184-
"""
185-
# Try Projects SDK first (RAI agents were created via project_client)
186-
try:
187-
if self.project_client:
188-
agent = await self.project_client.agents.get_agent(agent_id)
189-
if agent and agent.id:
190-
self.logger.info(
191-
"RAI.AgentReuseSuccess: Resolved agent via Projects SDK (id=%s)",
192-
agent.id,
193-
)
194-
return agent.id
195-
except Exception as ex:
196-
self.logger.warning(
197-
"RAI.AgentReuseMiss: Projects SDK get_agent failed (reason=ProjectsGetFailed, id=%s): %s",
198-
agent_id,
199-
ex,
200-
)
201-
202-
# Fallback via AgentsClient (endpoint)
203-
try:
204-
if self.client:
205-
agent = await self.client.get_agent(agent_id=agent_id)
206-
if agent and agent.id:
207-
self.logger.info(
208-
"RAI.AgentReuseSuccess: Resolved agent via AgentsClient (id=%s)",
209-
agent.id,
210-
)
211-
return agent.id
212-
except Exception as ex:
213-
self.logger.warning(
214-
"RAI.AgentReuseMiss: AgentsClient get_agent failed (reason=EndpointGetFailed, id=%s): %s",
215-
agent_id,
216-
ex,
217-
)
218-
219-
self.logger.error(
220-
"RAI.AgentReuseMiss: Agent ID not resolvable via any client (reason=ClientMismatch, id=%s)",
221-
agent_id,
222-
)
223-
return None
224-
225-
def get_agent_id(self, chat_client) -> str:
226-
"""Return the underlying agent ID or generate a new one.
174+
def get_agent_id(self) -> str:
175+
"""Generate a local agent ID for the ChatAgent wrapper.
227176
228-
Note: The new AzureAIClient doesn't expose agent_id directly.
229-
We generate a new ID if not available.
177+
The new AzureAIClient identifies agents by name (not ID) on the server side.
178+
This ID is only used locally for the ChatAgent wrapper instance.
230179
"""
231-
# Generate a new agent ID since AzureAIClient doesn't expose agent_id
232180
id = generate_assistant_id()
233181
self.logger.info("Generated new agent ID: %s", id)
234182
return id
235183

236-
async def get_database_team_agent(self) -> Optional[AzureAIClient]:
237-
"""Retrieve existing team agent from database, if any.
238-
239-
NOTE: Agent reuse is currently DISABLED to ensure fresh agents are created
240-
with the correct Azure AI Search configuration.
241-
This prevents issues with stale agents that may not have the search tool configured.
242-
243-
To re-enable agent reuse, set ENABLE_AGENT_REUSE=true in environment.
244-
"""
245-
import os
246-
247-
# DISABLED: Always create fresh agents to ensure Azure AI Search tool is configured
248-
enable_reuse = os.environ.get("ENABLE_AGENT_REUSE", "false").lower() == "true"
249-
if not enable_reuse:
250-
self.logger.info(
251-
"Agent reuse DISABLED: Creating fresh agent with search tools (agent_name=%s)",
252-
self.agent_name,
253-
)
254-
return None
255-
256-
chat_client = None
257-
try:
258-
agent_id = await get_database_team_agent_id(
259-
self.memory_store, self.team_config, self.agent_name
260-
)
261-
262-
if not agent_id:
263-
self.logger.info(
264-
"RAI reuse: no stored agent id (agent_name=%s)", self.agent_name
265-
)
266-
return None
267-
268-
# Use resolve_agent_id to try Projects SDK first, then AgentsClient
269-
resolved = await self.resolve_agent_id(agent_id)
270-
if not resolved:
271-
self.logger.error(
272-
"RAI.AgentReuseMiss: stored id %s not resolvable (agent_name=%s)",
273-
agent_id,
274-
self.agent_name,
275-
)
276-
return None
277-
278-
# Create client with resolved ID
279-
if self.agent_name == "RAIAgent" and self.project_client:
280-
chat_client = AzureAIClient(
281-
project_endpoint=self.project_endpoint,
282-
agent_id=resolved,
283-
credential=self.creds,
284-
)
285-
self.logger.info(
286-
"RAI.AgentReuseSuccess: Created AzureAIClient (id=%s)",
287-
resolved,
288-
)
289-
else:
290-
chat_client = AzureAIClient(
291-
project_endpoint=self.project_endpoint,
292-
agent_id=resolved,
293-
model_deployment_name=self.model_deployment_name,
294-
credential=self.creds,
295-
)
296-
self.logger.info(
297-
"Created AzureAIClient via endpoint (id=%s)", resolved
298-
)
299-
300-
except Exception as ex:
301-
self.logger.error(
302-
"Failed to initialize Get database team agent (agent_name=%s): %s",
303-
self.agent_name,
304-
ex,
305-
)
306-
return chat_client
307-
308-
async def save_database_team_agent(self) -> None:
309-
"""Save current team agent to database (only if truly new or changed)."""
310-
try:
311-
if self._agent is None or self._agent.id is None:
312-
self.logger.error("Cannot save database team agent: agent or agent_id is None")
313-
return
314-
315-
# Use the agent ID from ChatAgent (set during creation)
316-
agent_id = self._agent.id
317-
318-
# Check if stored ID matches current ID
319-
stored_id = await get_database_team_agent_id(
320-
self.memory_store, self.team_config, self.agent_name
321-
)
322-
if stored_id == agent_id:
323-
self.logger.info(
324-
"RAI reuse: id unchanged (id=%s); skip save.", agent_id
325-
)
326-
return
327-
328-
currentAgent = CurrentTeamAgent(
329-
team_id=self.team_config.team_id,
330-
team_name=self.team_config.name,
331-
agent_name=self.agent_name,
332-
agent_foundry_id=agent_id,
333-
agent_description=self.agent_description,
334-
agent_instructions=self.agent_instructions,
335-
)
336-
await self.memory_store.add_team_agent(currentAgent)
337-
self.logger.info(
338-
"Saved team agent to database (agent_name=%s, id=%s)",
339-
self.agent_name,
340-
agent_id,
341-
)
342-
343-
except Exception as ex:
344-
self.logger.error("Failed to save database: %s", ex)
345-
346184
async def _prepare_mcp_tool(self) -> None:
347185
"""Translate MCPConfig to a HostedMCPTool (agent_framework construct)."""
348186
if not self.mcp_cfg:

src/backend/v4/magentic_agents/foundry_agent.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ async def _collect_tools(self) -> List:
115115
# -------------------------
116116
# Azure Search helper
117117
# -------------------------
118-
async def _create_azure_search_enabled_client(self, chatClient=None) -> Optional[AzureAIClient]:
118+
async def _create_azure_search_enabled_client(self) -> Optional[AzureAIClient]:
119119
"""
120120
Create a server-side Azure AI agent with Azure AI Search tool using create_version.
121121
@@ -132,10 +132,6 @@ async def _create_azure_search_enabled_client(self, chatClient=None) -> Optional
132132
Returns:
133133
AzureAIClient | None
134134
"""
135-
if chatClient:
136-
self.logger.info("Reusing existing chatClient for agent '%s' (already has Azure Search configured)", self.agent_name)
137-
return chatClient
138-
139135
if not self.search:
140136
self.logger.error("Search configuration missing.")
141137
return None
@@ -244,25 +240,23 @@ async def _after_open(self) -> None:
244240
temp = 0.1
245241

246242
try:
247-
chatClient = await self.get_database_team_agent()
248-
249243
if self._use_azure_search:
250244
# Azure Search mode (skip MCP + Code Interpreter due to incompatibility)
251245
self.logger.info(
252246
"Initializing agent '%s' in Azure AI Search mode (exclusive) with index=%s.",
253247
self.agent_name,
254248
getattr(self.search, "index_name", "N/A") if self.search else "N/A"
255249
)
256-
chat_client = await self._create_azure_search_enabled_client(chatClient)
250+
chat_client = await self._create_azure_search_enabled_client()
257251
if not chat_client:
258252
raise RuntimeError(
259253
"Azure AI Search mode requested but setup failed."
260254
)
261255

262256
# In Azure Search raw tool path, tools/tool_choice are handled server-side.
263257
self._agent = ChatAgent(
264-
id=self.get_agent_id(chat_client),
265-
chat_client=self.get_chat_client(chat_client),
258+
id=self.get_agent_id(),
259+
chat_client=chat_client,
266260
instructions=self.agent_instructions,
267261
name=self.agent_name,
268262
description=self.agent_description,
@@ -272,12 +266,12 @@ async def _after_open(self) -> None:
272266
default_options={"store": False}, # Client-managed conversation to avoid stale tool call IDs across rounds
273267
)
274268
else:
275-
# use MCP path
269+
# MCP path (also used by RAI agent which has no tools)
276270
self.logger.info("Initializing agent in MCP mode.")
277271
tools = await self._collect_tools()
278272
self._agent = ChatAgent(
279-
id=self.get_agent_id(chatClient),
280-
chat_client=self.get_chat_client(chatClient),
273+
id=self.get_agent_id(),
274+
chat_client=self.get_chat_client(),
281275
instructions=self.agent_instructions,
282276
name=self.agent_name,
283277
description=self.agent_description,
@@ -314,12 +308,7 @@ async def invoke(self, prompt: str):
314308

315309
messages = [ChatMessage(role=Role.USER, text=prompt)]
316310

317-
agent_saved = False
318311
async for update in self._agent.run_stream(messages):
319-
# Save agent ID only once on first update
320-
if not agent_saved:
321-
await self.save_database_team_agent()
322-
agent_saved = True
323312
yield update
324313

325314
# -------------------------

0 commit comments

Comments
 (0)