diff --git a/.github/workflows/deploy-waf.yml b/.github/workflows/deploy-waf.yml index eb0e5a61..108cb688 100644 --- a/.github/workflows/deploy-waf.yml +++ b/.github/workflows/deploy-waf.yml @@ -105,6 +105,10 @@ jobs: id: deploy run: | set -e + + # Generate current timestamp in desired format: YYYY-MM-DDTHH:MM:SS.SSSSSSSZ + current_date=$(date -u +"%Y-%m-%dT%H:%M:%S.%7NZ") + az deployment group create \ --resource-group ${{ env.RESOURCE_GROUP_NAME }} \ --template-file infra/main.bicep \ @@ -118,6 +122,7 @@ jobs: enablePrivateNetworking=true \ enableScalability=true \ createdBy="Pipeline" \ + tags="{'SecurityControl':'Ignore','Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" - name: Send Notification on Failure diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 026b9424..ca921566 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -125,6 +125,9 @@ jobs: IMAGE_TAG="latest" fi + # Generate current timestamp in desired format: YYYY-MM-DDTHH:MM:SS.SSSSSSSZ + current_date=$(date -u +"%Y-%m-%dT%H:%M:%S.%7NZ") + az deployment group create \ --resource-group ${{ env.RESOURCE_GROUP_NAME }} \ --template-file infra/main.bicep \ @@ -139,6 +142,7 @@ jobs: azureAiServiceLocation='${{ env.AZURE_LOCATION }}' \ gptModelCapacity=150 \ createdBy="Pipeline" \ + tags="{'SecurityControl':'Ignore','Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" \ --output json - name: Extract Web App and API App URLs diff --git a/data/agent_teams/marketing.json b/data/agent_teams/marketing.json index 4817df76..a6de2ec5 100644 --- a/data/agent_teams/marketing.json +++ b/data/agent_teams/marketing.json @@ -37,7 +37,7 @@ "use_reasoning": false, "index_name": "", "index_foundry_name": "", - "coding_tools": true + "coding_tools": false }, { "input_key": "", diff --git a/data/agent_teams/retail.json b/data/agent_teams/retail.json index 86bdfed4..34fb89a8 100644 --- a/data/agent_teams/retail.json +++ b/data/agent_teams/retail.json @@ -37,7 +37,7 @@ "use_reasoning": false, "index_name": "macae-index", "index_foundry_name": "", - "coding_tools": true + "coding_tools": false }, { "input_key": "", diff --git a/infra/main.bicep b/infra/main.bicep index 1b409557..13e3f198 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -223,8 +223,8 @@ var allTags = union( }, tags ) -@description('Optional created by user name') -param createdBy string = empty(deployer().userPrincipalName) ? '' : split(deployer().userPrincipalName, '@')[0] +@description('Tag, Created by user name') +param createdBy string = contains(deployer(), 'userPrincipalName')? split(deployer().userPrincipalName, '@')[0]: deployer().objectId resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { name: 'default' @@ -232,6 +232,7 @@ resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { tags: { ...allTags TemplateName: 'MACAE' + Type: enablePrivateNetworking ? 'WAF' : 'Non-WAF' CreatedBy: createdBy } } @@ -1559,11 +1560,11 @@ module containerApp 'br/public:avm/res/app/container-app:0.18.1' = { } { name: 'AZURE_AI_SUBSCRIPTION_ID' - value: subscription().subscriptionId + value: aiFoundryAiServicesSubscriptionId } { name: 'AZURE_AI_RESOURCE_GROUP' - value: resourceGroup().name + value: aiFoundryAiServicesResourceGroupName } { name: 'AZURE_AI_PROJECT_NAME' @@ -1921,7 +1922,11 @@ module searchService 'br/public:avm/res/search/search-service:0.11.1' = { managedIdentities: { systemAssigned: true } - publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + + // Enabled the Public access because other services are not able to connect with search search AVM module when public access is disabled + + // publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + publicNetworkAccess: 'Enabled' networkRuleSet: { bypass: 'AzureServices' } @@ -1951,23 +1956,27 @@ module searchService 'br/public:avm/res/search/search-service:0.11.1' = { principalType: 'ServicePrincipal' } ] - privateEndpoints: enablePrivateNetworking - ? [ - { - name: 'pep-search-${solutionSuffix}' - customNetworkInterfaceName: 'nic-search-${solutionSuffix}' - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: [ - { - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.search]!.outputs.resourceId - } - ] - } - subnetResourceId: virtualNetwork!.outputs.subnetResourceIds[0] - service: 'searchService' - } - ] - : [] + + //Removing the Private endpoints as we are facing the issue with connecting to search service while comminicating with agents + + privateEndpoints:[] + // privateEndpoints: enablePrivateNetworking + // ? [ + // { + // name: 'pep-search-${solutionSuffix}' + // customNetworkInterfaceName: 'nic-search-${solutionSuffix}' + // privateDnsZoneGroup: { + // privateDnsZoneGroupConfigs: [ + // { + // privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.search]!.outputs.resourceId + // } + // ] + // } + // subnetResourceId: virtualNetwork!.outputs.subnetResourceIds[0] + // service: 'searchService' + // } + // ] + // : [] } } @@ -2012,7 +2021,7 @@ module keyvault 'br/public:avm/res/key-vault/vault:0.12.1' = { enableSoftDelete: true softDeleteRetentionInDays: 7 diagnosticSettings: enableMonitoring - ? [{ workspaceResourceId: logAnalyticsWorkspace!.outputs.resourceId }] + ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : [] // WAF aligned configuration for Private Networking privateEndpoints: enablePrivateNetworking diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 021073c4..3003b0c2 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -223,8 +223,8 @@ var allTags = union( }, tags ) -@description('Optional created by user name') -param createdBy string = empty(deployer().userPrincipalName) ? '' : split(deployer().userPrincipalName, '@')[0] +@description('Tag, Created by user name') +param createdBy string = contains(deployer(), 'userPrincipalName')? split(deployer().userPrincipalName, '@')[0]: deployer().objectId resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { name: 'default' @@ -232,6 +232,7 @@ resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { tags: { ...allTags TemplateName: 'MACAE' + Type: enablePrivateNetworking ? 'WAF' : 'Non-WAF' CreatedBy: createdBy } } @@ -1591,11 +1592,11 @@ module containerApp 'br/public:avm/res/app/container-app:0.18.1' = { } { name: 'AZURE_AI_SUBSCRIPTION_ID' - value: subscription().subscriptionId + value: aiFoundryAiServicesSubscriptionId } { name: 'AZURE_AI_RESOURCE_GROUP' - value: resourceGroup().name + value: aiFoundryAiServicesResourceGroupName } { name: 'AZURE_AI_PROJECT_NAME' @@ -1962,7 +1963,11 @@ module searchService 'br/public:avm/res/search/search-service:0.11.1' = { managedIdentities: { systemAssigned: true } - publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + + // Enabled the Public access because other services are not able to connect with search search AVM module when public access is disabled + + // publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + publicNetworkAccess: 'Enabled' networkRuleSet: { bypass: 'AzureServices' } @@ -1992,23 +1997,27 @@ module searchService 'br/public:avm/res/search/search-service:0.11.1' = { principalType: 'ServicePrincipal' } ] - privateEndpoints: enablePrivateNetworking - ? [ - { - name: 'pep-search-${solutionSuffix}' - customNetworkInterfaceName: 'nic-search-${solutionSuffix}' - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: [ - { - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.search]!.outputs.resourceId - } - ] - } - subnetResourceId: virtualNetwork!.outputs.subnetResourceIds[0] - service: 'searchService' - } - ] - : [] + privateEndpoints:[] + + // Removing the Private endpoints as we are facing the issue with connecting to search service while comminicating with agents + + // privateEndpoints: enablePrivateNetworking + // ? [ + // { + // name: 'pep-search-${solutionSuffix}' + // customNetworkInterfaceName: 'nic-search-${solutionSuffix}' + // privateDnsZoneGroup: { + // privateDnsZoneGroupConfigs: [ + // { + // privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.search]!.outputs.resourceId + // } + // ] + // } + // subnetResourceId: virtualNetwork!.outputs.subnetResourceIds[0] + // service: 'searchService' + // } + // ] + // : [] } } @@ -2053,7 +2062,7 @@ module keyvault 'br/public:avm/res/key-vault/vault:0.12.1' = { enableSoftDelete: true softDeleteRetentionInDays: 7 diagnosticSettings: enableMonitoring - ? [{ workspaceResourceId: logAnalyticsWorkspace!.outputs.resourceId }] + ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : [] // WAF aligned configuration for Private Networking privateEndpoints: enablePrivateNetworking diff --git a/src/backend/v3/magentic_agents/foundry_agent.py b/src/backend/v3/magentic_agents/foundry_agent.py index 1e91539a..2cc62f63 100644 --- a/src/backend/v3/magentic_agents/foundry_agent.py +++ b/src/backend/v3/magentic_agents/foundry_agent.py @@ -2,11 +2,11 @@ import asyncio import logging -from typing import List, Optional +from typing import Awaitable, List, Optional from azure.ai.agents.models import (AzureAISearchTool, BingGroundingTool, CodeInterpreterToolDefinition) -from semantic_kernel.agents import AzureAIAgent # pylint: disable=E0611 +from semantic_kernel.agents import Agent, AzureAIAgent # pylint: disable=E0611 from v3.magentic_agents.common.lifecycle import AzureAgentBase from v3.magentic_agents.models.agent_models import MCPConfig, SearchConfig @@ -43,6 +43,7 @@ def __init__(self, agent_name: str, if self.model_deployment_name in ["o3", "o4-mini"]: raise ValueError("The current version of Foundry agents do not support reasoning models.") + # Uncomment to enable bing grounding capabilities (requires Bing connection in Foundry and uncommenting other code) # async def _make_bing_tool(self) -> Optional[BingGroundingTool]: # """Create Bing search tool for web search.""" # if not all([self.client, self.bing.connection_name]): @@ -119,19 +120,24 @@ async def _collect_tools_and_resources(self) -> tuple[List, dict]: return tools, tool_resources async def _after_open(self) -> None: - - # Collect all tools - tools, tool_resources = await self._collect_tools_and_resources() - - # Create agent definition with all tools - definition = await self.client.agents.create_agent( - model=self.model_deployment_name, - name=self.agent_name, - description=self.agent_description, - instructions=self.agent_instructions, - tools=tools, - tool_resources=tool_resources - ) + """Initialize the AzureAIAgent with the collected tools and MCP plugin.""" + + # Try to get existing agent definition from Foundry + definition = await self._get_azure_ai_agent_definition(self.agent_name) + # If not found in Foundry, create a new one + if definition is None: + # Collect all tools + tools, tool_resources = await self._collect_tools_and_resources() + + # Create agent definition with all tools + definition = await self.client.agents.create_agent( + model=self.model_deployment_name, + name=self.agent_name, + description=self.agent_description, + instructions=self.agent_instructions, + tools=tools, + tool_resources=tool_resources + ) # Add MCP plugins if available plugins = [self.mcp_plugin] if self.mcp_plugin else [] @@ -146,25 +152,25 @@ async def _after_open(self) -> None: self.logger.error("Failed to create AzureAIAgent: %s", ex) raise - # After self._agent creation in _after_open: - # Diagnostics - try: - tool_names = [t.get("function", {}).get("name") for t in (definition.tools or []) if isinstance(t, dict)] - self.logger.info( - "Foundry agent '%s' initialized. Azure tools: %s | MCP plugin: %s", - self.agent_name, - tool_names, - getattr(self.mcp_plugin, 'name', None) - ) - if not tool_names and not plugins: - self.logger.warning( - "Foundry agent '%s' has no Azure tool definitions and no MCP plugin. " - "Subsequent tool calls may fail.", self.agent_name - ) - except Exception as diag_ex: - self.logger.warning("Diagnostics collection failed: %s", diag_ex) - - self.logger.info("%s initialized with %d tools and %d plugins", self.agent_name, len(tools), len(plugins)) + # # After self._agent creation in _after_open: + # # Diagnostics + # try: + # tool_names = [t.get("function", {}).get("name") for t in (definition.tools or []) if isinstance(t, dict)] + # self.logger.info( + # "Foundry agent '%s' initialized. Azure tools: %s | MCP plugin: %s", + # self.agent_name, + # tool_names, + # getattr(self.mcp_plugin, 'name', None) + # ) + # if not tool_names and not plugins: + # self.logger.warning( + # "Foundry agent '%s' has no Azure tool definitions and no MCP plugin. " + # "Subsequent tool calls may fail.", self.agent_name + # ) + # except Exception as diag_ex: + # self.logger.warning("Diagnostics collection failed: %s", diag_ex) + + # self.logger.info("%s initialized with %d tools and %d plugins", self.agent_name, len(tools), len(plugins)) async def fetch_run_details(self, thread_id: str, run_id: str): """Fetch and log run details after a failure.""" @@ -180,6 +186,42 @@ async def fetch_run_details(self, thread_id: str, run_id: str): except Exception as ex: self.logger.error("Could not fetch run details: %s", ex) + async def _get_azure_ai_agent_definition(self, agent_name: str)-> Awaitable[Agent | None]: + """ + Gets an Azure AI Agent with the specified name and instructions using AIProjectClient if it is already created. + """ + # # First try to get an existing agent with this name as assistant_id + try: + agent_id = None + agent_list = self.client.agents.list_agents() + async for agent in agent_list: + if agent.name == agent_name: + agent_id = agent.id + break + # If the agent already exists, we can use it directly + # Get the existing agent definition + if agent_id is not None: + logging.info(f"Agent with ID {agent_id} exists.") + + existing_definition = await self.client.agents.get_agent(agent_id) + + return existing_definition + else: + return None + except Exception as e: + # The Azure AI Projects SDK throws an exception when the agent doesn't exist + # (not returning None), so we catch it and proceed to create a new agent + if "ResourceNotFound" in str(e) or "404" in str(e): + logging.info( + f"Agent with ID {agent_name} not found. Will create a new one." + ) + else: + # Log unexpected errors but still try to create a new agent + logging.warning( + f"Unexpected error while retrieving agent {agent_name}: {str(e)}. Attempting to create new agent." + ) + + async def create_foundry_agent(agent_name:str, agent_description:str, agent_instructions:str, diff --git a/src/backend/v3/orchestration/human_approval_manager.py b/src/backend/v3/orchestration/human_approval_manager.py index 5d91133c..54c8123f 100644 --- a/src/backend/v3/orchestration/human_approval_manager.py +++ b/src/backend/v3/orchestration/human_approval_manager.py @@ -62,8 +62,7 @@ def __init__(self, *args, **kwargs): """ final_append = """ -The final answer should not include any offers of further conversation or assistance. The application will not all further interaction with the user. -The final answer should be a complete and final response to the user's original request. + DO NOT EVER OFFER TO HELP FURTHER IN THE FINAL ANSWER! Just provide the final answer and end with a polite closing. """ # kwargs["task_ledger_facts_prompt"] = ORCHESTRATOR_TASK_LEDGER_FACTS_PROMPT + facts_append