Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/backend/af/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

# agent_framework substitutes
from agent_framework.azure import AzureOpenAIChatClient
#from agent_framework_azure_ai import AzureOpenAIChatClient
from agent_framework import ChatOptions

from af.models.messages import MPlan, WebsocketMessageType
Expand Down
38 changes: 28 additions & 10 deletions src/backend/af/magentic_agents/common/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,23 @@
from contextlib import AsyncExitStack
from typing import Any, Optional

from azure.ai.projects.aio import AIProjectClient
# from agent_framework.azure import AzureAIAgentClient
from agent_framework_azure_ai import AzureAIAgentClient
from azure.identity.aio import DefaultAzureCredential

from agent_framework import (
ChatMessage,
Role,
ChatOptions,
HostedMCPTool,
AggregateContextProvider,
ChatAgent,
ChatClientProtocol,
ChatMessageStoreProtocol,
ContextProvider,
Middleware,
ToolMode,
ToolProtocol,
)
from agent_framework import HostedMCPTool

from af.magentic_agents.models.agent_models import MCPConfig
Expand All @@ -24,7 +38,7 @@ def __init__(self, mcp: MCPConfig | None = None) -> None:
self._stack: AsyncExitStack | None = None
self.mcp_cfg: MCPConfig | None = mcp
self.mcp_tool: HostedMCPTool | None = None
self._agent: Any | None = None # delegate target (e.g., AzureAIAgentClient)
self._agent: ChatAgent | None = None

async def open(self) -> "MCPEnabledBase":
if self._stack is not None:
Expand Down Expand Up @@ -89,19 +103,22 @@ def _prepare_mcp_tool(self) -> None:

class AzureAgentBase(MCPEnabledBase):
"""
Extends MCPEnabledBase with Azure credential + AIProjectClient contexts.
Extends MCPEnabledBase with Azure credential + AzureAIAgentClient contexts.
Subclasses:
- create or attach an Azure AI Agent definition
- instantiate an AzureAIAgentClient and assign to self._agent
- optionally register themselves via agent_registry
"""

def __init__(self, mcp: MCPConfig | None = None) -> None:
def __init__(self, mcp: MCPConfig | None = None, model_deployment_name: str | None = None) -> None:
super().__init__(mcp=mcp)
self.creds: Optional[DefaultAzureCredential] = None
self.client: Optional[AIProjectClient] = None
self.client: Optional[AzureAIAgentClient] = None
self.project_endpoint: Optional[str] = None
self._created_ephemeral: bool = False # reserved if you add ephemeral agent cleanup
self._created_ephemeral: bool = (
False # reserved if you add ephemeral agent cleanup
)
self.model_deployment_name = model_deployment_name

async def open(self) -> "AzureAgentBase":
if self._stack is not None:
Expand All @@ -120,9 +137,10 @@ async def open(self) -> "AzureAgentBase":
await self._stack.enter_async_context(self.creds)

# Create AIProjectClient
self.client = AIProjectClient(
endpoint=self.project_endpoint,
credential=self.creds,
self.client = AzureAIAgentClient(
project_endpoint=self.project_endpoint,
model_deployment_name=self.model_deployment_name,
async_credential=self.creds,
)
await self._stack.enter_async_context(self.client)

Expand Down
137 changes: 83 additions & 54 deletions src/backend/af/magentic_agents/foundry_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,26 @@
import logging
from typing import List, Optional

from azure.ai.agents.models import Agent, AzureAISearchTool, CodeInterpreterToolDefinition
from agent_framework_azure_ai import AzureAIAgentClient
from agent_framework import ChatMessage, Role, ChatOptions, HostedMCPTool

from azure.ai.agents.models import (
Agent,
AzureAISearchTool,
CodeInterpreterToolDefinition,
)

from agent_framework import (
ChatMessage,
Role,
ChatOptions,
HostedMCPTool,
AggregateContextProvider,
ChatAgent,
ChatClientProtocol,
ChatMessageStoreProtocol,
ContextProvider,
Middleware,
ToolMode,
ToolProtocol,
)
from af.magentic_agents.common.lifecycle import AzureAgentBase
from af.magentic_agents.models.agent_models import MCPConfig, SearchConfig
from af.config.agent_registry import agent_registry
Expand Down Expand Up @@ -41,20 +57,33 @@ def __init__(
self.logger = logging.getLogger(__name__)

if self.model_deployment_name in {"o3", "o4-mini"}:
raise ValueError("Foundry agents do not support reasoning models in this implementation.")
raise ValueError(
"Foundry agents do not support reasoning models in this implementation."
)

# -------------------------
# Tool construction helpers
# -------------------------
async def _make_azure_search_tool(self) -> Optional[AzureAISearchTool]:
"""Create Azure AI Search tool (RAG capability)."""
if not (self.client and self.search and self.search.connection_name and self.search.index_name):
self.logger.info("Azure AI Search tool not enabled (missing config or client).")
if not (
self.client
and self.search
and self.search.connection_name
and self.search.index_name
):
self.logger.info(
"Azure AI Search tool not enabled (missing config or client)."
)
return None

try:
self._search_connection = await self.client.connections.get(name=self.search.connection_name)
self.logger.info("Found Azure AI Search connection: %s", self._search_connection.id)
self._search_connection = await self.client.connections.get(
name=self.search.connection_name
)
self.logger.info(
"Found Azure AI Search connection: %s", self._search_connection.id
)

return AzureAISearchTool(
index_connection_id=self._search_connection.id,
Expand Down Expand Up @@ -102,54 +131,39 @@ async def _collect_tools_and_resources(self) -> tuple[List, dict]:
# Agent lifecycle override
# -------------------------
async def _after_open(self) -> None:
"""Create or reuse Azure AI agent definition and wrap with AzureAIAgentClient."""
definition = await self._get_azure_ai_agent_definition(self.agent_name)

if definition is not None:
if not await self._check_connection_compatibility(definition):
try:
await self.client.agents.delete_agent(definition.id)
self.logger.info(
"Deleted incompatible existing agent '%s'; will recreate with new connection settings.",
self.agent_name,
)
except Exception as ex:
self.logger.warning(
"Failed deleting incompatible agent '%s': %s (will still recreate).",
self.agent_name,
ex,
)
definition = None

if definition is None:
# Instantiate persistent AzureAIAgentClient bound to existing agent_id
try:
# AzureAIAgentClient(
# project_client=self.client,
# agent_id=str(definition.id),
# agent_name=self.agent_name,
# )
tools, tool_resources = await self._collect_tools_and_resources()
definition = await self.client.agents.create_agent(
model=self.model_deployment_name,
self._agent = ChatAgent(
chat_client=self.client,
instructions=self.agent_description + " " + self.agent_instructions,
name=self.agent_name,
description=self.agent_description,
instructions=self.agent_instructions,
tools=tools,
tool_resources=tool_resources,
tools=tools if tools else None,
tool_choice="auto" if tools else "none",
allow_multiple_tool_calls=True,
temperature=0.7,
)
self.logger.info("Created new Azure AI agent definition '%s'", self.agent_name)

# Instantiate persistent AzureAIAgentClient bound to existing agent_id
try:
self._agent = AzureAIAgentClient(
project_client=self.client,
agent_id=str(definition.id),
agent_name=self.agent_name,
)
except Exception as ex:
self.logger.error("Failed to initialize AzureAIAgentClient: %s", ex)
raise

# Register agent globally
try:
agent_registry.register_agent(self)
self.logger.info("Registered agent '%s' in global registry.", self.agent_name)
self.logger.info(
"Registered agent '%s' in global registry.", self.agent_name
)
except Exception as reg_ex:
self.logger.warning("Could not register agent '%s': %s", self.agent_name, reg_ex)
self.logger.warning(
"Could not register agent '%s': %s", self.agent_name, reg_ex
)

# -------------------------
# Definition compatibility
Expand All @@ -163,21 +177,29 @@ async def _check_connection_compatibility(self, existing_definition: Agent) -> b

tool_resources = getattr(existing_definition, "tool_resources", None)
if not tool_resources:
self.logger.info("Existing agent has no tool resources; incompatible with search requirement.")
self.logger.info(
"Existing agent has no tool resources; incompatible with search requirement."
)
return False

azure_search = tool_resources.get("azure_ai_search", {})
indexes = azure_search.get("indexes", [])
if not indexes:
self.logger.info("Existing agent has no Azure AI Search indexes; incompatible.")
self.logger.info(
"Existing agent has no Azure AI Search indexes; incompatible."
)
return False

existing_conn_id = indexes[0].get("index_connection_id")
if not existing_conn_id:
self.logger.info("Existing agent missing index_connection_id; incompatible.")
self.logger.info(
"Existing agent missing index_connection_id; incompatible."
)
return False

current_connection = await self.client.connections.get(name=self.search.connection_name)
current_connection = await self.client.connections.get(
name=self.search.connection_name
)
same = existing_conn_id == current_connection.id
if same:
self.logger.info("Search connection compatible: %s", existing_conn_id)
Expand All @@ -197,7 +219,9 @@ async def _get_azure_ai_agent_definition(self, agent_name: str) -> Agent | None:
try:
async for agent in self.client.agents.list_agents():
if agent.name == agent_name:
self.logger.info("Found existing agent '%s' (id=%s).", agent_name, agent.id)
self.logger.info(
"Found existing agent '%s' (id=%s).", agent_name, agent.id
)
return await self.client.agents.get_agent(agent.id)
return None
except Exception as e:
Expand Down Expand Up @@ -226,7 +250,12 @@ async def fetch_run_details(self, thread_id: str, run_id: str) -> None:
getattr(run, "usage", None),
)
except Exception as ex:
self.logger.error("Failed fetching run details (thread=%s run=%s): %s", thread_id, run_id, ex)
self.logger.error(
"Failed fetching run details (thread=%s run=%s): %s",
thread_id,
run_id,
ex,
)

# -------------------------
# Invocation (streaming)
Expand Down Expand Up @@ -257,10 +286,10 @@ async def invoke(self, prompt: str):
temperature=0.7,
)

async for update in self._agent.get_streaming_response(
async for update in self._agent.run_stream(
messages=messages,
chat_options=chat_options,
instructions=self.agent_instructions,
# chat_options=chat_options,
# instructions=self.agent_instructions,
):
yield update

Expand All @@ -287,4 +316,4 @@ async def create_foundry_agent(
search_config=search_config,
)
await agent.open()
return agent
return agent
10 changes: 10 additions & 0 deletions src/backend/af/magentic_agents/reasoning_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@
import uuid
from dataclasses import dataclass
from typing import AsyncIterator, List, Optional
# from agent_framework.azure import AzureAIAgentClient
from agent_framework_azure_ai import AzureAIAgentClient
from agent_framework import (
ChatMessage,
ChatOptions,
ChatResponseUpdate,
HostedMCPTool,
Role,
AggregateContextProvider,
ChatAgent,
ChatClientProtocol,
ChatMessageStoreProtocol,
ContextProvider,
Middleware,
ToolMode,
ToolProtocol,
)
from azure.identity.aio import DefaultAzureCredential
from azure.ai.projects.aio import AIProjectClient
Expand Down Expand Up @@ -201,6 +210,7 @@ async def _invoke_stream_internal(self, prompt: str) -> AsyncIterator[ChatRespon
name=self.mcp_config.name,
description=self.mcp_config.description,
server_label=self.mcp_config.name.replace(" ", "_"),
url=self.mcp_config.url
)
)

Expand Down
2 changes: 1 addition & 1 deletion src/backend/af/orchestration/human_approval_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def __init__(self, user_id: str, *args, **kwargs):
ORCHESTRATOR_TASK_LEDGER_PLAN_UPDATE_PROMPT + plan_append
)
kwargs["final_answer_prompt"] = ORCHESTRATOR_FINAL_ANSWER_PROMPT + final_append
kwargs["current_user_id"] = user_id # retained for downstream usage if needed
#kwargs["current_user_id"] = user_id # retained for downstream usage if needed

self.current_user_id = user_id
super().__init__(*args, **kwargs)
Expand Down
Loading
Loading