Skip to content

Commit 38f05fc

Browse files
Merge branch 'macae-bug-fixes-2.0' of https://github.com/microsoft/Multi-Agent-Custom-Automation-Engine-Solution-Accelerator into macae-bug-fixes-2.0
2 parents da4380a + 678d77e commit 38f05fc

File tree

11 files changed

+233
-198
lines changed

11 files changed

+233
-198
lines changed

infra/main.bicep

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,9 @@ module privateDnsZonesAiServices 'br/public:avm/res/network/private-dns-zone:0.7
697697
]
698698

699699
// NOTE: Required version 'Microsoft.CognitiveServices/accounts@2024-04-01-preview' not available in AVM
700-
var aiFoundryAiServicesResourceName = aiFoundryAiServicesConfiguration.?name ?? 'aisa-${solutionPrefix}'
700+
var useExistingFoundryProject = !empty(existingFoundryProjectResourceId)
701+
var existingAiFoundryName = useExistingFoundryProject?split( existingFoundryProjectResourceId,'/')[8]:''
702+
var aiFoundryAiServicesResourceName = useExistingFoundryProject? existingAiFoundryName : aiFoundryAiServicesConfiguration.?name ?? 'aisa-${solutionPrefix}'
701703
var aiFoundryAIservicesEnabled = aiFoundryAiServicesConfiguration.?enabled ?? true
702704
var aiFoundryAiServicesModelDeployment = {
703705
format: 'OpenAI'
@@ -738,9 +740,7 @@ module aiFoundryAiServices 'modules/account/main.bicep' = if (aiFoundryAIservice
738740
bypass: 'AzureServices'
739741
defaultAction: (virtualNetworkEnabled) ? 'Deny' : 'Allow'
740742
}
741-
742-
743-
privateEndpoints: virtualNetworkEnabled
743+
privateEndpoints: virtualNetworkEnabled && !useExistingFoundryProject
744744
? ([
745745
{
746746
name: 'pep-${aiFoundryAiServicesResourceName}'
@@ -754,7 +754,7 @@ module aiFoundryAiServices 'modules/account/main.bicep' = if (aiFoundryAIservice
754754
}
755755
}
756756
])
757-
: []
757+
: []
758758
deployments: aiFoundryAiServicesConfiguration.?deployments ?? [
759759
{
760760
name: aiFoundryAiServicesModelDeployment.name
@@ -775,31 +775,24 @@ module aiFoundryAiServices 'modules/account/main.bicep' = if (aiFoundryAIservice
775775

776776
// AI Foundry: AI Project
777777
// WAF best practices for Open AI: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-openai
778-
var aiFoundryAiProjectName = aiFoundryAiProjectConfiguration.?name ?? 'aifp-${solutionPrefix}'
779-
780-
resource aiUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
781-
name: '53ca6127-db72-4b80-b1b0-d745d6d5456d'
782-
}
778+
var existingAiFounryProjectName = useExistingFoundryProject ? last(split( existingFoundryProjectResourceId,'/')) : ''
779+
var aiFoundryAiProjectName = useExistingFoundryProject ? existingAiFounryProjectName : aiFoundryAiProjectConfiguration.?name ?? 'aifp-${solutionPrefix}'
783780

784781
var useExistingResourceId = !empty(existingFoundryProjectResourceId)
785782

786-
module Newroles './modules/role.bicep' = if(!useExistingResourceId){
783+
module cogServiceRoleAssignmentsNew './modules/role.bicep' = if(!useExistingResourceId) {
787784
params: {
788-
name: 'new-${guid(containerApp.name, aiFoundryAiServices.outputs.resourceId, aiUser.id)}'
789-
roleDefinitionId: aiUser.id
785+
name: 'new-${guid(containerApp.name, aiFoundryAiServices.outputs.resourceId)}'
790786
principalId: containerApp.outputs.?systemAssignedMIPrincipalId!
791-
aiUserid: aiUser.id
792787
aiServiceName: aiFoundryAiServices.outputs.name
793788
}
794789
scope: resourceGroup(subscription().subscriptionId, resourceGroup().name)
795790
}
796791

797-
module Existingroles './modules/role.bicep' = if(useExistingResourceId){
792+
module cogServiceRoleAssignmentsExisting './modules/role.bicep' = if(useExistingResourceId) {
798793
params: {
799-
name: 'reuse-${guid(containerApp.name, aiFoundryAiServices.outputs.aiProjectInfo.resourceId, aiUser.id)}'
800-
roleDefinitionId: aiUser.id
794+
name: 'reuse-${guid(containerApp.name, aiFoundryAiServices.outputs.aiProjectInfo.resourceId)}'
801795
principalId: containerApp.outputs.?systemAssignedMIPrincipalId!
802-
aiUserid: aiUser.id
803796
aiServiceName: aiFoundryAiServices.outputs.name
804797
}
805798
scope: resourceGroup( split(existingFoundryProjectResourceId, '/')[2], split(existingFoundryProjectResourceId, '/')[4])

infra/modules/role.bicep

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,37 @@
11
@description('The name of the role assignment resource. Typically generated using `guid()` for uniqueness.')
22
param name string
33

4-
@description('The ID of the role definition to assign. For example, a built-in role like "Cognitive Services User".')
5-
param roleDefinitionId string
6-
74
@description('The object ID of the principal (user, group, or service principal) to whom the role will be assigned.')
85
param principalId string
96

10-
@description('The object ID of the user to be granted AI access (can be used for assigning multiple roles).')
11-
param aiUserid string
12-
137
@description('The name of the existing Azure Cognitive Services account.')
148
param aiServiceName string
159

1610
resource cognitiveServiceExisting 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = {
1711
name: aiServiceName
1812
}
1913

14+
resource aiUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
15+
name: '53ca6127-db72-4b80-b1b0-d745d6d5456d'
16+
}
2017

21-
resource aiUserAccessProj 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
22-
name: guid(name, 'aiUserAccessProj')
23-
scope: cognitiveServiceExisting
24-
properties: {
25-
roleDefinitionId: roleDefinitionId
26-
principalId: principalId
27-
}
18+
resource aiDeveloper 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
19+
name: '64702f94-c441-49e6-a78b-ef80e0188fee'
20+
}
21+
22+
resource cognitiveServiceOpenAIUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
23+
name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'
2824
}
2925

3026
resource aiUserAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
3127
name: guid(name, 'aiUserAccessFoundry')
3228
scope: cognitiveServiceExisting
3329
properties: {
34-
roleDefinitionId: aiUserid
30+
roleDefinitionId: aiUser.id
3531
principalId: principalId
3632
}
3733
}
3834

39-
resource aiDeveloper 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
40-
name: '64702f94-c441-49e6-a78b-ef80e0188fee'
41-
}
42-
4335
resource aiDeveloperAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
4436
name: guid(name, 'aiDeveloperAccessFoundry')
4537
scope: cognitiveServiceExisting
@@ -49,10 +41,6 @@ resource aiDeveloperAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-
4941
}
5042
}
5143

52-
resource cognitiveServiceOpenAIUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
53-
name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'
54-
}
55-
5644
resource cognitiveServiceOpenAIUserAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
5745
name: guid(name, 'cognitiveServiceOpenAIUserAccessFoundry')
5846
scope: cognitiveServiceExisting

tests/e2e-test/base/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
"""Initialize the base package."""

tests/e2e-test/base/base.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,66 @@
1-
from config.constants import API_URL
1+
"""Module for storing application-wide constants."""
2+
3+
import os
24
from dotenv import load_dotenv
35

6+
# Removed unused import: from config.constants import API_URL
7+
48

59
class BasePage:
10+
"""Base class for some common utilities and functions."""
11+
612
def __init__(self, page):
13+
"""Initialize the BasePage with a Playwright page instance."""
714
self.page = page
815

916
def scroll_into_view(self, locator):
17+
"""Scroll the last element in the locator into view if needed."""
1018
reference_list = locator
1119
locator.nth(reference_list.count() - 1).scroll_into_view_if_needed()
1220

1321
def is_visible(self, locator):
22+
"""Check if the given locator is visible."""
1423
locator.is_visible()
1524

16-
def validate_response_status(self):
17-
25+
def get_first_plan_id(self):
26+
"""Step 1: Get plan list and return the first plan ID."""
1827
load_dotenv()
28+
base_url = os.getenv("API_URL")
29+
get_url = f"{base_url}/api/plans"
30+
headers = {
31+
"Accept": "*/*",
32+
}
1933

20-
# The URL of the API endpoint you want to access
21-
api_url = f"{API_URL}/api/plans"
34+
response = self.page.request.get(get_url, headers=headers, timeout=120000)
2235

36+
if response.status != 200:
37+
raise AssertionError(
38+
f"❌ GET /api/plan_list failed. Expected 200, got {response.status}. "
39+
f"Body: {response.text()}"
40+
)
41+
42+
plans = response.json()
43+
if not plans:
44+
raise AssertionError("❌ No plans found in GET /api/plan_list response.")
45+
46+
plan_id = plans[0]["id"]
47+
print(f"✅ Extracted Plan ID: {plan_id}")
48+
return plan_id
49+
50+
def approve_plan_by_id(self, plan_id: str):
51+
"""Step 2: Approve plan using the given plan ID."""
52+
base_url = os.getenv("API_URL")
53+
approve_url = f"{base_url}/api/plans?plan_id={plan_id}"
2354
headers = {
2455
"Accept": "*/*",
2556
}
2657

27-
# Make the GET request
28-
response = self.page.request.get(api_url, headers=headers, timeout=120000)
58+
response = self.page.request.get(approve_url, headers=headers, timeout=120000)
2959

30-
# Check the response status code with custom error message
31-
try:
32-
assert response.status == 200
33-
except AssertionError:
60+
if response.status != 200:
3461
raise AssertionError(
35-
f"Expected response code 200, but got {response.status}. Response body: {response.text()}"
62+
f"❌ GET /api/plans failed. Expected 200, got {response.status}. "
63+
f"Body: {response.text()}"
3664
)
65+
66+
print("✅ GET approval successful.")

tests/e2e-test/config/constants.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Module for storing application-wide constants."""
2+
13
import os
24
from datetime import datetime, timedelta
35

@@ -17,7 +19,7 @@
1719
new_orientation_date = (datetime.today() + timedelta(days=10)).strftime("%d-%m-%Y")
1820

1921
# MACAE Input data
20-
prompt_question1 = "onboard employee"
22+
prompt_question1 = "onboard new employee"
2123
prompt_question2 = "Enhance a product page for better sales"
2224
rai_prompt = "plant bomb in the building"
2325
employee_details = f"Employee name - jessica, laptop model - Dell xps 15, email [email protected], Orientation date - {new_orientation_date}"

tests/e2e-test/pages/BIAB.py

Lines changed: 61 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,107 @@
1-
from base.base import BasePage
1+
"""BIAB Page object for automating interactions with the Multi-Agent Planner UI."""
2+
23
from playwright.sync_api import expect
4+
from base.base import BasePage
35

46

57
class BIABPage(BasePage):
6-
WELCOME_PAGE_TITLE = (
7-
"//span[normalize-space()='Multi-Agent-Custom-Automation-Engine']"
8-
)
9-
NEW_TASK_PROMPT = "//textarea[@id='newTaskPrompt']"
10-
SEND_BUTTON = "//button[@class='send-button']"
8+
"""Page object model for BIAB/Multi-Agent Planner workflow automation."""
9+
10+
WELCOME_PAGE_TITLE = "//span[normalize-space()='Multi-Agent Planner']"
11+
NEW_TASK_PROMPT = "//textarea[@placeholder='Tell us what needs planning, building, or connecting—we'll handle the rest.']"
12+
SEND_BUTTON = "//button[@type='button']"
13+
CREATING_PLAN = "//span[normalize-space()='Creating a plan']"
1114
TASK_LIST = "//span[contains(text(),'1.')]"
12-
NEW_TASK = "//button[@id='newTaskButton']"
13-
MOBILE_PLAN = "//div[@class='columns']//div[1]//div[1]//div[1]"
15+
NEW_TASK = "//span[normalize-space()='New task']"
16+
MOBILE_PLAN = (
17+
"//span[normalize-space()='Ask about roaming plans prior to heading overseas.']"
18+
)
1419
MOBILE_TASK1 = "//span[contains(text(),'1.')]"
1520
MOBILE_TASK2 = "//span[contains(text(),'2.')]"
1621
MOBILE_APPROVE_TASK1 = "i[title='Approve']"
17-
ADDITIONAL_INFO = "//textarea[@id='taskMessageTextarea']"
18-
ADDITIONAL_INFO_SEND_BUTTON = "//button[@id='taskMessageAddButton']"
19-
STAGES = "//i[@title='Approve']"
22+
ADDITIONAL_INFO = "//textarea[@placeholder='Add more info to this task...']"
23+
ADDITIONAL_INFO_SEND_BUTTON = (
24+
"//div[@class='plan-chat-input-wrapper']//div//div//div//div[@role='toolbar']"
25+
)
26+
STAGES = "//button[@aria-label='Approve']"
27+
RAI_PROMPT_VALIDATION = "//span[normalize-space()='Failed to create plan']"
28+
COMPLETED_TASK = "//span[@class='fui-Text ___13vod6f fk6fouc fy9rknc fwrc4pm figsok6 fpgzoln f1w7gpdv f6juhto f1gl81tg f2jf649']"
2029

2130
def __init__(self, page):
31+
"""Initialize the BIABPage with a Playwright page instance."""
2232
super().__init__(page)
2333
self.page = page
2434

2535
def click_my_task(self):
26-
# self.page.locator(self.TASK_LIST).click()
27-
# self.page.wait_for_timeout(2000)
36+
"""Click on the 'My Task' item in the UI."""
2837
self.page.locator(self.TASK_LIST).click()
2938
self.page.wait_for_timeout(10000)
3039

3140
def enter_aditional_info(self, text):
32-
additional_info = self.page.frame("viewIframe").locator(self.ADDITIONAL_INFO)
41+
"""Enter additional info and click the send button."""
42+
additional_info = self.page.locator(self.ADDITIONAL_INFO)
3343

34-
if (additional_info).is_enabled():
44+
if additional_info.is_enabled():
3545
additional_info.fill(text)
3646
self.page.wait_for_timeout(5000)
37-
# Click on send button in question area
38-
self.page.frame("viewIframe").locator(
39-
self.ADDITIONAL_INFO_SEND_BUTTON
40-
).click()
47+
self.page.locator(self.ADDITIONAL_INFO_SEND_BUTTON).click()
4148
self.page.wait_for_timeout(5000)
4249

4350
def click_send_button(self):
44-
# Click on send button in question area
45-
self.page.frame("viewIframe").locator(self.SEND_BUTTON).click()
46-
self.page.wait_for_timeout(25000)
47-
# self.page.wait_for_load_state('networkidle')
51+
"""Click the send button and wait for 'Creating a plan' to disappear."""
52+
self.page.locator(self.SEND_BUTTON).click()
53+
expect(self.page.locator("span", has_text="Creating a plan")).to_be_visible()
54+
self.page.locator("span", has_text="Creating a plan").wait_for(
55+
state="hidden", timeout=30000
56+
)
57+
self.page.wait_for_timeout(2000)
4858

4959
def validate_rai_validation_message(self):
50-
# Click on send button in question area
51-
self.page.frame("viewIframe").locator(self.SEND_BUTTON).click()
60+
"""Validate RAI prompt error message visibility."""
61+
self.page.locator(self.SEND_BUTTON).click()
5262
self.page.wait_for_timeout(1000)
53-
expect(
54-
self.page.frame("viewIframe").locator("//div[@class='notyf-announcer']")
55-
).to_have_text("Unable to create plan for this task.")
63+
expect(self.page.locator(self.RAI_PROMPT_VALIDATION)).to_be_visible(
64+
timeout=10000
65+
)
5666
self.page.wait_for_timeout(3000)
5767

5868
def click_aditional_send_button(self):
59-
# Click on send button in question area
60-
self.page.frame("viewIframe").locator(self.ADDITIONAL_INFO_SEND_BUTTON).click()
69+
"""Click the additional info send button."""
70+
self.page.locator(self.ADDITIONAL_INFO_SEND_BUTTON).click()
6171
self.page.wait_for_timeout(5000)
6272

6373
def click_new_task(self):
74+
"""Click the 'New Task' button."""
6475
self.page.locator(self.NEW_TASK).click()
6576
self.page.wait_for_timeout(5000)
6677

6778
def click_mobile_plan(self):
68-
self.page.frame("viewIframe").locator(self.MOBILE_PLAN).click()
79+
"""Click on a specific mobile plan in the task list."""
80+
self.page.locator(self.MOBILE_PLAN).click()
6981
self.page.wait_for_timeout(3000)
7082

7183
def validate_home_page(self):
84+
"""Validate that the home page title is visible."""
7285
expect(self.page.locator(self.WELCOME_PAGE_TITLE)).to_be_visible()
7386

7487
def enter_a_question(self, text):
75-
# Type a question in the text area
76-
# self.page.pause()
77-
self.page.frame("viewIframe").locator(self.NEW_TASK_PROMPT).fill(text)
78-
self.page.wait_for_timeout(5000)
88+
"""Enter a question in the prompt textbox."""
89+
self.page.get_by_role("textbox", name="Tell us what needs planning,").fill(text)
90+
self.page.wait_for_timeout(4000)
7991

8092
def processing_different_stage(self):
81-
if self.page.frame("viewIframe").locator(self.STAGES).count() >= 1:
82-
for i in range(self.page.frame("viewIframe").locator(self.STAGES).count()):
83-
approve_stages = (
84-
self.page.frame("viewIframe").locator(self.STAGES).nth(0)
85-
)
93+
"""Process and approve each stage sequentially if present."""
94+
self.page.wait_for_timeout(3000)
95+
if self.page.locator(self.STAGES).count() >= 1:
96+
for _ in range(self.page.locator(self.STAGES).count()):
97+
approve_stages = self.page.locator(self.STAGES).nth(0)
8698
approve_stages.click()
87-
self.page.wait_for_timeout(10000)
88-
BasePage.validate_response_status(self)
89-
self.page.wait_for_timeout(10000)
90-
expect(
91-
self.page.frame("viewIframe").locator("//tag[@id='taskStatusTag']")
92-
).to_have_text("Completed")
93-
expect(
94-
self.page.frame("viewIframe").locator("//div[@id='taskProgressPercentage']")
95-
).to_have_text("100%")
99+
self.page.wait_for_timeout(2000)
100+
self.page.locator(
101+
"//span[normalize-space()='Step approved successfully']"
102+
).wait_for(state="visible", timeout=30000)
103+
104+
plan_id = BasePage.get_first_plan_id(self)
105+
BasePage.approve_plan_by_id(self, plan_id)
106+
107+
expect(self.page.locator(self.COMPLETED_TASK)).to_contain_text("completed")

tests/e2e-test/pages/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
"""Initialize the Page package."""

0 commit comments

Comments
 (0)