Skip to content

Commit bd1979d

Browse files
Merge pull request #587 from microsoft/dev
feat: using ai search tool for ChatWithCallTranscript Kernel function, use existing ai foundry project, integrating deployment and test automation pipeline
2 parents a915d9e + 91a513c commit bd1979d

28 files changed

+1913
-661
lines changed

.github/workflows/CAdeploy.yml

Lines changed: 301 additions & 132 deletions
Large diffs are not rendered by default.

.github/workflows/test_automation.yml

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ on:
1111
schedule:
1212
- cron: '0 13 * * *' # Runs at 1 PM UTC
1313
workflow_dispatch:
14+
workflow_call:
15+
inputs:
16+
CA_WEB_URL:
17+
required: true
18+
type: string
1419

1520
env:
16-
url: ${{ vars.CLIENT_ADVISOR_URL }}
21+
url: ${{ inputs.CA_WEB_URL }}
1722
accelerator_name: "Client Advisor"
1823

1924
jobs:
@@ -36,6 +41,42 @@ jobs:
3641
- name: Ensure browsers are installed
3742
run: python -m playwright install --with-deps chromium
3843

44+
45+
- name: Validate URL
46+
run: |
47+
if [ -z "${{ env.url }}" ]; then
48+
echo "ERROR: No URL provided for testing"
49+
exit 1
50+
51+
fi
52+
53+
echo "Testing URL: ${{ env.url }}"
54+
55+
56+
- name: Wait for Application to be Ready
57+
run: |
58+
echo "Waiting for application to be ready at ${{ env.url }} "
59+
max_attempts=10
60+
attempt=1
61+
62+
while [ $attempt -le $max_attempts ]; do
63+
echo "Attempt $attempt: Checking if application is ready..."
64+
if curl -f -s "${{ env.url }}" > /dev/null; then
65+
echo "Application is ready!"
66+
break
67+
68+
fi
69+
70+
if [ $attempt -eq $max_attempts ]; then
71+
echo "Application is not ready after $max_attempts attempts"
72+
exit 1
73+
fi
74+
75+
echo "Application not ready, waiting 30 seconds..."
76+
sleep 30
77+
attempt=$((attempt + 1))
78+
done
79+
3980
- name: Run tests(1)
4081
id: test1
4182
run: |
@@ -108,4 +149,4 @@ jobs:
108149
# Send the notification
109150
curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA}}" \
110151
-H "Content-Type: application/json" \
111-
-d "$EMAIL_BODY" || echo "Failed to send notification"
152+
-d "$EMAIL_BODY" || echo "Failed to send notification"

docs/CustomizingAzdParameters.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ By default this template will use the environment name as the prefix to prevent
2121
| `AZURE_ENV_OPENAI_LOCATION` | string | `eastus2` | Location of the Azure OpenAI resource. Choose from (allowed values: `swedencentral`, `australiaeast`). |
2222
| `AZURE_LOCATION` | string | `japaneast` | Sets the Azure region for resource deployment. |
2323
| `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | `<Existing Workspace Id>` | Reuses an existing Log Analytics Workspace instead of provisioning a new one. |
24+
| `RESOURCE_GROUP_NAME_FOUNDRY` | string | `<Existing AI Foundry Project>` | Reuses an existing AI Foundry Project instead of provisioning a new one. |
2425

2526
## How to Set a Parameter
2627
To customize any of the above values, run the following command **before** `azd up`:

docs/DeploymentGuide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ When you start the deployment, most parameters will have **default values**, but
119119
| **Azure OpenAI API Version** | Set the API version for OpenAI model deployments. | `2025-04-01-preview` |
120120
| **AZURE\_LOCATION** | Sets the Azure region for resource deployment. | `japaneast` |
121121
| **Existing Log Analytics Workspace** | To reuse an existing Log Analytics Workspace ID instead of creating a new one. | *(empty)* |
122+
| **Existing AI Foundry Project Resource ID** | To reuse an existing AI Foundry Project Resource ID instead of creating a new one. | *(empty)* |
122123

123124

124125

infra/deploy_ai_foundry.bicep

Lines changed: 104 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ param gptDeploymentCapacity int
99
param embeddingModel string
1010
param embeddingDeploymentCapacity int
1111
param existingLogAnalyticsWorkspaceId string = ''
12+
param azureExistingAIProjectResourceId string = ''
1213

1314
// Load the abbrevations file required to name the azure resources.
1415
var abbrs = loadJsonContent('./abbreviations.json')
@@ -52,6 +53,31 @@ var existingLawSubscription = useExisting ? split(existingLogAnalyticsWorkspaceI
5253
var existingLawResourceGroup = useExisting ? split(existingLogAnalyticsWorkspaceId, '/')[4] : ''
5354
var existingLawName = useExisting ? split(existingLogAnalyticsWorkspaceId, '/')[8] : ''
5455

56+
var existingOpenAIEndpoint = !empty(azureExistingAIProjectResourceId)
57+
? format('https://{0}.openai.azure.com/', split(azureExistingAIProjectResourceId, '/')[8])
58+
: ''
59+
var existingProjEndpoint = !empty(azureExistingAIProjectResourceId)
60+
? format(
61+
'https://{0}.services.ai.azure.com/api/projects/{1}',
62+
split(azureExistingAIProjectResourceId, '/')[8],
63+
split(azureExistingAIProjectResourceId, '/')[10]
64+
)
65+
: ''
66+
var existingAIFoundryName = !empty(azureExistingAIProjectResourceId)
67+
? split(azureExistingAIProjectResourceId, '/')[8]
68+
: ''
69+
var existingAIProjectName = !empty(azureExistingAIProjectResourceId)
70+
? split(azureExistingAIProjectResourceId, '/')[10]
71+
: ''
72+
var existingAIServiceSubscription = !empty(azureExistingAIProjectResourceId)
73+
? split(azureExistingAIProjectResourceId, '/')[2]
74+
: ''
75+
var existingAIServiceResourceGroup = !empty(azureExistingAIProjectResourceId)
76+
? split(azureExistingAIProjectResourceId, '/')[4]
77+
: ''
78+
var aiSearchConnectionName = 'foundry-search-connection-${solutionName}'
79+
var aiAppInsightConnectionName = 'foundry-app-insights-connection-${solutionName}'
80+
5581
resource existingLogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = if (useExisting) {
5682
name: existingLawName
5783
scope: resourceGroup(existingLawSubscription, existingLawResourceGroup)
@@ -69,25 +95,6 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = if
6995
}
7096
}
7197

72-
// resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
73-
// name: applicationInsightsName
74-
// location: location
75-
// kind: 'web'
76-
// properties: {
77-
// Application_Type: 'web'
78-
// DisableIpMasking: false
79-
// DisableLocalAuth: false
80-
// Flow_Type: 'Bluefield'
81-
// ForceCustomerStorageForProfiler: false
82-
// ImmediatePurgeDataOn30Days: true
83-
// IngestionMode: 'ApplicationInsights'
84-
// publicNetworkAccessForIngestion: 'Enabled'
85-
// publicNetworkAccessForQuery: 'Disabled'
86-
// Request_Source: 'rest'
87-
// WorkspaceResourceId: logAnalytics.id
88-
// }
89-
// }
90-
9198
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
9299
name: applicationInsightsName
93100
location: location
@@ -100,7 +107,7 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
100107
}
101108
}
102109

103-
resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = {
110+
resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = if (empty(azureExistingAIProjectResourceId)) {
104111
name: aiFoundryName
105112
location: location
106113
sku: {
@@ -123,7 +130,7 @@ resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = {
123130
}
124131
}
125132

126-
resource aiFoundryProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = {
133+
resource aiFoundryProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = if (empty(azureExistingAIProjectResourceId)) {
127134
parent: aiFoundry
128135
name: aiProjectName
129136
location: location
@@ -138,7 +145,7 @@ resource aiFoundryProject 'Microsoft.CognitiveServices/accounts/projects@2025-04
138145

139146
@batchSize(1)
140147
resource aiFModelDeployments 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [
141-
for aiModeldeployment in aiModelDeployments: {
148+
for aiModeldeployment in aiModelDeployments: if (empty(azureExistingAIProjectResourceId)) {
142149
parent: aiFoundry
143150
name: aiModeldeployment.name
144151
properties: {
@@ -185,8 +192,8 @@ resource aiSearch 'Microsoft.Search/searchServices@2025-02-01-preview' = {
185192
}
186193
}
187194

188-
resource aiSearchFoundryConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' ={
189-
name: 'foundry-search-connection'
195+
resource aiSearchFoundryConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = if (empty(azureExistingAIProjectResourceId)) {
196+
name: aiSearchConnectionName
190197
parent: aiFoundry
191198
properties: {
192199
category: 'CognitiveSearch'
@@ -201,18 +208,56 @@ resource aiSearchFoundryConnection 'Microsoft.CognitiveServices/accounts/connect
201208
}
202209
}
203210

211+
module existing_AIProject_SearchConnectionModule 'deploy_aifp_aisearch_connection.bicep' = if (!empty(azureExistingAIProjectResourceId)) {
212+
name: 'aiProjectSearchConnectionDeployment'
213+
scope: resourceGroup(existingAIServiceSubscription, existingAIServiceResourceGroup)
214+
params: {
215+
existingAIProjectName: existingAIProjectName
216+
existingAIFoundryName: existingAIFoundryName
217+
aiSearchName: aiSearchName
218+
aiSearchResourceId: aiSearch.id
219+
aiSearchLocation: aiSearch.location
220+
aiSearchConnectionName: aiSearchConnectionName
221+
}
222+
}
223+
224+
resource cognitiveServicesOpenAIUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
225+
name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'
226+
}
227+
228+
module assignOpenAIRoleToAISearch 'deploy_foundry_role_assignment.bicep' = {
229+
name: 'assignOpenAIRoleToAISearch'
230+
scope: resourceGroup(existingAIServiceSubscription, existingAIServiceResourceGroup)
231+
params: {
232+
roleDefinitionId: cognitiveServicesOpenAIUser.id
233+
roleAssignmentName: guid(resourceGroup().id, aiSearch.id, cognitiveServicesOpenAIUser.id, 'openai-foundry')
234+
aiFoundryName: !empty(azureExistingAIProjectResourceId) ? existingAIFoundryName : aiFoundryName
235+
aiProjectName: !empty(azureExistingAIProjectResourceId) ? existingAIProjectName : aiProjectName
236+
principalId: aiSearch.identity.principalId
237+
}
238+
}
239+
204240
@description('This is the built-in Search Index Data Reader role.')
205241
resource searchIndexDataReaderRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
206242
scope: aiSearch
207243
name: '1407120a-92aa-4202-b7e9-c0e197c71c8f'
208244
}
209245

210-
resource searchIndexDataReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
211-
name: guid(aiSearch.id, aiFoundry.id, searchIndexDataReaderRoleDefinition.id)
246+
resource searchIndexDataReaderRoleAssignmentToAIFP 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (empty(azureExistingAIProjectResourceId)) {
247+
name: guid(aiSearch.id, aiFoundryProject.id, searchIndexDataReaderRoleDefinition.id)
212248
scope: aiSearch
213249
properties: {
214250
roleDefinitionId: searchIndexDataReaderRoleDefinition.id
215-
principalId: aiFoundry.identity.principalId
251+
principalId: aiFoundryProject.identity.principalId
252+
principalType: 'ServicePrincipal'
253+
}
254+
}
255+
resource assignSearchIndexDataReaderToExistingAiProject 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(azureExistingAIProjectResourceId)) {
256+
name: guid(resourceGroup().id, existingAIProjectName, searchIndexDataReaderRoleDefinition.id, 'Existing')
257+
scope: aiSearch
258+
properties: {
259+
roleDefinitionId: searchIndexDataReaderRoleDefinition.id
260+
principalId: assignOpenAIRoleToAISearch.outputs.aiProjectPrincipalId
216261
principalType: 'ServicePrincipal'
217262
}
218263
}
@@ -223,18 +268,28 @@ resource searchServiceContributorRoleDefinition 'Microsoft.Authorization/roleDef
223268
name: '7ca78c08-252a-4471-8644-bb5ff32d4ba0'
224269
}
225270

226-
resource searchServiceContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
227-
name: guid(aiSearch.id, aiFoundry.id, searchServiceContributorRoleDefinition.id)
271+
resource searchServiceContributorRoleAssignmentToAIFP 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (empty(azureExistingAIProjectResourceId)) {
272+
name: guid(aiSearch.id, aiFoundryProject.id, searchServiceContributorRoleDefinition.id)
228273
scope: aiSearch
229274
properties: {
230275
roleDefinitionId: searchServiceContributorRoleDefinition.id
231-
principalId: aiFoundry.identity.principalId
276+
principalId: aiFoundryProject.identity.principalId
232277
principalType: 'ServicePrincipal'
233278
}
234279
}
235280

236-
resource appInsightsFoundryConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = {
237-
name: 'foundry-app-insights-connection'
281+
resource searchServiceContributorRoleAssignmentExisting 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(azureExistingAIProjectResourceId)) {
282+
name: guid(resourceGroup().id, existingAIProjectName, searchServiceContributorRoleDefinition.id, 'Existing')
283+
scope: aiSearch
284+
properties: {
285+
roleDefinitionId: searchServiceContributorRoleDefinition.id
286+
principalId: assignOpenAIRoleToAISearch.outputs.aiProjectPrincipalId
287+
principalType: 'ServicePrincipal'
288+
}
289+
}
290+
291+
resource appInsightsFoundryConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = if (empty(azureExistingAIProjectResourceId)) {
292+
name: aiAppInsightConnectionName
238293
parent: aiFoundry
239294
properties: {
240295
category: 'AppInsights'
@@ -251,14 +306,6 @@ resource appInsightsFoundryConnection 'Microsoft.CognitiveServices/accounts/conn
251306
}
252307
}
253308

254-
// resource azureOpenAIApiKeyEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
255-
// parent: keyVault
256-
// name: 'AZURE-OPENAI-KEY'
257-
// properties: {
258-
// value: aiFoundry.listKeys().key1 //aiServices_m.listKeys().key1
259-
// }
260-
// }
261-
262309
resource azureOpenAIApiVersionEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
263310
parent: keyVault
264311
name: 'AZURE-OPENAI-PREVIEW-API-VERSION'
@@ -271,7 +318,10 @@ resource azureOpenAIEndpointEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-
271318
parent: keyVault
272319
name: 'AZURE-OPENAI-ENDPOINT'
273320
properties: {
274-
value: aiFoundry.properties.endpoints['OpenAI Language Model Instance API'] //aiServices_m.properties.endpoint
321+
// value: aiFoundry.properties.endpoints['OpenAI Language Model Instance API'] //aiServices_m.properties.endpoint
322+
value: !empty(existingOpenAIEndpoint)
323+
? existingOpenAIEndpoint
324+
: aiFoundry.properties.endpoints['OpenAI Language Model Instance API']
275325
}
276326
}
277327

@@ -283,14 +333,6 @@ resource azureOpenAIEmbeddingModelEntry 'Microsoft.KeyVault/vaults/secrets@2021-
283333
}
284334
}
285335

286-
// resource azureSearchAdminKeyEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
287-
// parent: keyVault
288-
// name: 'AZURE-SEARCH-KEY'
289-
// properties: {
290-
// value: aiSearch.listAdminKeys().primaryKey
291-
// }
292-
// }
293-
294336
resource azureSearchServiceEndpointEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
295337
parent: keyVault
296338
name: 'AZURE-SEARCH-ENDPOINT'
@@ -310,21 +352,27 @@ resource azureSearchIndexEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-pre
310352
output keyvaultName string = keyvaultName
311353
output keyvaultId string = keyVault.id
312354

313-
output aiFoundryProjectEndpoint string = aiFoundryProject.properties.endpoints['AI Foundry API']
314-
output aiServicesTarget string = aiFoundry.properties.endpoint //aiServices_m.properties.endpoint
315-
output aoaiEndpoint string = aiFoundry.properties.endpoints['OpenAI Language Model Instance API'] //aiServices_m.properties.endpoint
316-
output aiFoundryName string = aiFoundryName //aiServicesName_m
317-
output aiFoundryId string = aiFoundry.id //aiServices_m.id
355+
output resourceGroupNameFoundry string = !empty(existingAIServiceResourceGroup)
356+
? existingAIServiceResourceGroup
357+
: resourceGroup().name
358+
output aiFoundryProjectEndpoint string = !empty(existingProjEndpoint)
359+
? existingProjEndpoint
360+
: aiFoundryProject.properties.endpoints['AI Foundry API']
361+
output aoaiEndpoint string = !empty(existingOpenAIEndpoint)
362+
? existingOpenAIEndpoint
363+
: aiFoundry.properties.endpoints['OpenAI Language Model Instance API'] //aiServices_m.properties.endpoint
364+
output aiFoundryName string = !empty(existingAIFoundryName) ? existingAIFoundryName : aiFoundryName //aiServicesName_m
318365

319366
output aiSearchName string = aiSearchName
320367
output aiSearchId string = aiSearch.id
321368
output aiSearchTarget string = 'https://${aiSearch.name}.search.windows.net'
322369
output aiSearchService string = aiSearch.name
323-
output aiFoundryProjectName string = aiFoundryProject.name
370+
output aiFoundryProjectName string = !empty(existingAIProjectName) ? existingAIProjectName : aiFoundryProject.name
324371

325372
output applicationInsightsId string = applicationInsights.id
326373
output logAnalyticsWorkspaceResourceName string = useExisting ? existingLogAnalyticsWorkspace.name : logAnalytics.name
327374
output logAnalyticsWorkspaceResourceGroup string = useExisting ? existingLawResourceGroup : resourceGroup().name
328375

329-
330376
output applicationInsightsConnectionString string = applicationInsights.properties.ConnectionString
377+
378+
output aiSearchFoundryConnectionName string = aiSearchConnectionName
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
param existingAIProjectName string
2+
param existingAIFoundryName string
3+
param aiSearchName string
4+
param aiSearchResourceId string
5+
param aiSearchLocation string
6+
param aiSearchConnectionName string
7+
8+
resource projectAISearchConnection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview' = {
9+
name: '${existingAIFoundryName}/${existingAIProjectName}/${aiSearchConnectionName}'
10+
properties: {
11+
category: 'CognitiveSearch'
12+
target: 'https://${aiSearchName}.search.windows.net'
13+
authType: 'AAD'
14+
isSharedToAll: true
15+
metadata: {
16+
ApiType: 'Azure'
17+
ResourceId: aiSearchResourceId
18+
location: aiSearchLocation
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)