Skip to content

Commit 2e79777

Browse files
authored
Use AAD instead of key vault for Computer Vision API (#1062)
* Initial changes to remove keyvault and use AAD instead * rm keyvault * Fix Bicep * Role rename * Make mypy happy
1 parent 3424475 commit 2e79777

File tree

10 files changed

+62
-77
lines changed

10 files changed

+62
-77
lines changed

app/backend/app.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,6 @@ async def setup_clients():
214214
AZURE_SEARCH_SERVICE = os.environ["AZURE_SEARCH_SERVICE"]
215215
AZURE_SEARCH_INDEX = os.environ["AZURE_SEARCH_INDEX"]
216216
SEARCH_SECRET_NAME = os.getenv("SEARCH_SECRET_NAME")
217-
VISION_SECRET_NAME = os.getenv("VISION_SECRET_NAME")
218217
AZURE_KEY_VAULT_NAME = os.getenv("AZURE_KEY_VAULT_NAME")
219218
# Shared by all OpenAI deployments
220219
OPENAI_HOST = os.getenv("OPENAI_HOST", "azure")
@@ -257,13 +256,11 @@ async def setup_clients():
257256
azure_credential = DefaultAzureCredential(exclude_shared_token_cache_credential=True)
258257

259258
# Fetch any necessary secrets from Key Vault
260-
vision_key = None
261259
search_key = None
262-
if AZURE_KEY_VAULT_NAME and (VISION_SECRET_NAME or SEARCH_SECRET_NAME):
260+
if AZURE_KEY_VAULT_NAME:
263261
key_vault_client = SecretClient(
264262
vault_url=f"https://{AZURE_KEY_VAULT_NAME}.vault.azure.net", credential=azure_credential
265263
)
266-
vision_key = VISION_SECRET_NAME and (await key_vault_client.get_secret(VISION_SECRET_NAME)).value
267264
search_key = SEARCH_SECRET_NAME and (await key_vault_client.get_secret(SEARCH_SECRET_NAME)).value
268265
await key_vault_client.close()
269266

@@ -348,16 +345,16 @@ async def setup_clients():
348345
)
349346

350347
if USE_GPT4V:
351-
if vision_key is None:
352-
raise ValueError("Vision key must be set (in Key Vault) to use the vision approach.")
348+
349+
token_provider = get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default")
353350

354351
current_app.config[CONFIG_ASK_VISION_APPROACH] = RetrieveThenReadVisionApproach(
355352
search_client=search_client,
356353
openai_client=openai_client,
357354
blob_container_client=blob_container_client,
358355
auth_helper=auth_helper,
359356
vision_endpoint=AZURE_VISION_ENDPOINT,
360-
vision_key=vision_key,
357+
vision_token_provider=token_provider,
361358
gpt4v_deployment=AZURE_OPENAI_GPT4V_DEPLOYMENT,
362359
gpt4v_model=AZURE_OPENAI_GPT4V_MODEL,
363360
embedding_model=OPENAI_EMB_MODEL,
@@ -374,7 +371,7 @@ async def setup_clients():
374371
blob_container_client=blob_container_client,
375372
auth_helper=auth_helper,
376373
vision_endpoint=AZURE_VISION_ENDPOINT,
377-
vision_key=vision_key,
374+
vision_token_provider=token_provider,
378375
gpt4v_deployment=AZURE_OPENAI_GPT4V_DEPLOYMENT,
379376
gpt4v_model=AZURE_OPENAI_GPT4V_MODEL,
380377
embedding_model=OPENAI_EMB_MODEL,

app/backend/approaches/approach.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
2+
from abc import ABC
23
from dataclasses import dataclass
3-
from typing import Any, AsyncGenerator, List, Optional, Union, cast
4+
from typing import Any, AsyncGenerator, Awaitable, Callable, List, Optional, Union, cast
5+
from urllib.parse import urljoin
46

57
import aiohttp
68
from azure.search.documents.aio import SearchClient
@@ -74,7 +76,7 @@ class ThoughtStep:
7476
props: Optional[dict[str, Any]] = None
7577

7678

77-
class Approach:
79+
class Approach(ABC):
7880
def __init__(
7981
self,
8082
search_client: SearchClient,
@@ -85,6 +87,8 @@ def __init__(
8587
embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text"
8688
embedding_model: str,
8789
openai_host: str,
90+
vision_endpoint: str,
91+
vision_token_provider: Callable[[], Awaitable[str]],
8892
):
8993
self.search_client = search_client
9094
self.openai_client = openai_client
@@ -94,6 +98,8 @@ def __init__(
9498
self.embedding_deployment = embedding_deployment
9599
self.embedding_model = embedding_model
96100
self.openai_host = openai_host
101+
self.vision_endpoint = vision_endpoint
102+
self.vision_token_provider = vision_token_provider
97103

98104
def build_filter(self, overrides: dict[str, Any], auth_claims: dict[str, Any]) -> Optional[str]:
99105
exclude_category = overrides.get("exclude_category")
@@ -188,12 +194,14 @@ async def compute_text_embedding(self, q: str):
188194
query_vector = embedding.data[0].embedding
189195
return VectorizedQuery(vector=query_vector, k_nearest_neighbors=50, fields="embedding")
190196

191-
async def compute_image_embedding(self, q: str, vision_endpoint: str, vision_key: str):
192-
endpoint = f"{vision_endpoint}computervision/retrieval:vectorizeText"
197+
async def compute_image_embedding(self, q: str):
198+
endpoint = urljoin(self.vision_endpoint, "computervision/retrieval:vectorizeText")
199+
headers = {"Content-Type": "application/json"}
193200
params = {"api-version": "2023-02-01-preview", "modelVersion": "latest"}
194-
headers = {"Content-Type": "application/json", "Ocp-Apim-Subscription-Key": vision_key}
195201
data = {"text": q}
196202

203+
headers["Authorization"] = "Bearer " + await self.vision_token_provider()
204+
197205
async with aiohttp.ClientSession() as session:
198206
async with session.post(
199207
url=endpoint, params=params, headers=headers, json=data, raise_for_status=True

app/backend/approaches/chatreadretrievereadvision.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Coroutine, Optional, Union
1+
from typing import Any, Awaitable, Callable, Coroutine, Optional, Union
22

33
from azure.search.documents.aio import SearchClient
44
from azure.storage.blob.aio import ContainerClient
@@ -40,7 +40,7 @@ def __init__(
4040
query_language: str,
4141
query_speller: str,
4242
vision_endpoint: str,
43-
vision_key: str,
43+
vision_token_provider: Callable[[], Awaitable[str]]
4444
):
4545
self.search_client = search_client
4646
self.blob_container_client = blob_container_client
@@ -55,7 +55,7 @@ def __init__(
5555
self.query_language = query_language
5656
self.query_speller = query_speller
5757
self.vision_endpoint = vision_endpoint
58-
self.vision_key = vision_key
58+
self.vision_token_provider = vision_token_provider
5959
self.chatgpt_token_limit = get_token_limit(gpt4v_model)
6060

6161
@property
@@ -126,7 +126,7 @@ async def run_until_final_call(
126126
vector = (
127127
await self.compute_text_embedding(query_text)
128128
if field == "embedding"
129-
else await self.compute_image_embedding(query_text, self.vision_endpoint, self.vision_key)
129+
else await self.compute_image_embedding(query_text)
130130
)
131131
vectors.append(vector)
132132

app/backend/approaches/retrievethenreadvision.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
from typing import Any, AsyncGenerator, Optional, Union
2+
from typing import Any, AsyncGenerator, Awaitable, Callable, Optional, Union
33

44
from azure.search.documents.aio import SearchClient
55
from azure.storage.blob.aio import ContainerClient
@@ -53,7 +53,7 @@ def __init__(
5353
query_language: str,
5454
query_speller: str,
5555
vision_endpoint: str,
56-
vision_key: str,
56+
vision_token_provider: Callable[[], Awaitable[str]]
5757
):
5858
self.search_client = search_client
5959
self.blob_container_client = blob_container_client
@@ -68,7 +68,7 @@ def __init__(
6868
self.query_language = query_language
6969
self.query_speller = query_speller
7070
self.vision_endpoint = vision_endpoint
71-
self.vision_key = vision_key
71+
self.vision_token_provider = vision_token_provider
7272

7373
async def run(
7474
self,
@@ -100,7 +100,7 @@ async def run(
100100
vector = (
101101
await self.compute_text_embedding(q)
102102
if field == "embedding"
103-
else await self.compute_image_embedding(q, self.vision_endpoint, self.vision_key)
103+
else await self.compute_image_embedding(q)
104104
)
105105
vectors.append(vector)
106106

infra/main.bicep

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ var actualSearchServiceSemanticRankerLevel = (searchServiceSkuName == 'free') ?
3131
param useSearchServiceKey bool = searchServiceSkuName == 'free'
3232

3333
param storageAccountName string = ''
34-
param keyVaultResourceGroupName string = ''
3534
param storageResourceGroupName string = ''
3635
param storageResourceGroupLocation string = location
3736
param storageContainerName string = 'content'
@@ -47,12 +46,12 @@ param openAiServiceName string = ''
4746
param openAiResourceGroupName string = ''
4847
param useGPT4V bool = false
4948

49+
param keyVaultResourceGroupName string = ''
5050
param keyVaultServiceName string = ''
51-
param computerVisionSecretName string = 'computerVisionSecret'
5251
param searchServiceSecretName string = 'searchServiceSecret'
5352

5453
@description('Location for the OpenAI resource group')
55-
@allowed(['canadaeast', 'eastus', 'eastus2', 'francecentral', 'switzerlandnorth', 'uksouth', 'japaneast', 'northcentralus', 'australiaeast', 'swedencentral'])
54+
@allowed([ 'canadaeast', 'eastus', 'eastus2', 'francecentral', 'switzerlandnorth', 'uksouth', 'japaneast', 'northcentralus', 'australiaeast', 'swedencentral' ])
5655
@metadata({
5756
azd: {
5857
type: 'location'
@@ -70,7 +69,7 @@ param documentIntelligenceResourceGroupName string = ''
7069
// Limited regions for new version:
7170
// https://learn.microsoft.com/azure/ai-services/document-intelligence/concept-layout
7271
@description('Location for the Document Intelligence resource group')
73-
@allowed(['eastus', 'westus2', 'westeurope'])
72+
@allowed([ 'eastus', 'westus2', 'westeurope' ])
7473
@metadata({
7574
azd: {
7675
type: 'location'
@@ -129,7 +128,7 @@ var resourceToken = toLower(uniqueString(subscription().id, environmentName, loc
129128
var tags = { 'azd-env-name': environmentName }
130129
var computerVisionName = !empty(computerVisionServiceName) ? computerVisionServiceName : '${abbrs.cognitiveServicesComputerVision}${resourceToken}'
131130

132-
var useKeyVault = useGPT4V || useSearchServiceKey
131+
var useKeyVault = useSearchServiceKey
133132
var tenantIdForAuth = !empty(authTenantId) ? authTenantId : tenantId
134133
var authenticationIssuerUri = '${environment().authentication.loginEndpoint}${tenantIdForAuth}/v2.0'
135134

@@ -182,7 +181,6 @@ module monitoring 'core/monitor/monitoring.bicep' = if (useApplicationInsights)
182181
}
183182
}
184183

185-
186184
module applicationInsightsDashboard 'backend-dashboard.bicep' = if (useApplicationInsights) {
187185
name: 'application-insights-dashboard'
188186
scope: resourceGroup
@@ -193,7 +191,6 @@ module applicationInsightsDashboard 'backend-dashboard.bicep' = if (useApplicati
193191
}
194192
}
195193

196-
197194
// Create an App Service Plan to group applications under the same payment plan and SKU
198195
module appServicePlan 'core/host/appserviceplan.bicep' = {
199196
name: 'appserviceplan'
@@ -224,7 +221,7 @@ module backend 'core/host/appservice.bicep' = {
224221
appCommandLine: 'python3 -m gunicorn main:app'
225222
scmDoBuildDuringDeployment: true
226223
managedIdentity: true
227-
allowedOrigins: [allowedOrigin]
224+
allowedOrigins: [ allowedOrigin ]
228225
clientAppId: clientAppId
229226
serverAppId: serverAppId
230227
clientSecretSettingName: !empty(clientAppSecret) ? 'AZURE_CLIENT_APP_SECRET' : ''
@@ -238,7 +235,6 @@ module backend 'core/host/appservice.bicep' = {
238235
AZURE_SEARCH_SERVICE: searchService.outputs.name
239236
AZURE_SEARCH_SEMANTIC_RANKER: actualSearchServiceSemanticRankerLevel
240237
AZURE_VISION_ENDPOINT: useGPT4V ? computerVision.outputs.endpoint : ''
241-
VISION_SECRET_NAME: useGPT4V ? computerVisionSecretName: ''
242238
SEARCH_SECRET_NAME: useSearchServiceKey ? searchServiceSecretName : ''
243239
AZURE_KEY_VAULT_NAME: useKeyVault ? keyVault.outputs.name : ''
244240
AZURE_SEARCH_QUERY_LANGUAGE: searchQueryLanguage
@@ -361,9 +357,8 @@ module computerVision 'core/ai/cognitiveservices.bicep' = if (useGPT4V) {
361357
}
362358
}
363359

364-
365-
// Currently, we only need Key Vault for storing Computer Vision key,
366-
// which is only used for GPT-4V.
360+
// Currently, we only need Key Vault for storing Search service key,
361+
// which is only used for free tier
367362
module keyVault 'core/security/keyvault.bicep' = if (useKeyVault) {
368363
name: 'keyvault'
369364
scope: keyVaultResourceGroup
@@ -388,16 +383,12 @@ module secrets 'secrets.bicep' = if (useKeyVault) {
388383
scope: keyVaultResourceGroup
389384
params: {
390385
keyVaultName: useKeyVault ? keyVault.outputs.name : ''
391-
storeComputerVisionSecret: useGPT4V
392-
computerVisionId: useGPT4V ? computerVision.outputs.id : ''
393-
computerVisionSecretName: computerVisionSecretName
394386
storeSearchServiceSecret: useSearchServiceKey
395387
searchServiceId: useSearchServiceKey ? searchService.outputs.id : ''
396388
searchServiceSecretName: searchServiceSecretName
397389
}
398390
}
399391

400-
401392
module searchService 'core/search/search-services.bicep' = {
402393
name: 'search-service'
403394
scope: searchServiceResourceGroup
@@ -443,7 +434,7 @@ module storage 'core/storage/storage-account.bicep' = {
443434
}
444435

445436
// USER ROLES
446-
var principalType = empty(runningOnGh) && empty(runningOnAdo) ? 'User': 'ServicePrincipal'
437+
var principalType = empty(runningOnGh) && empty(runningOnAdo) ? 'User' : 'ServicePrincipal'
447438

448439
module openAiRoleUser 'core/security/role.bicep' = if (openAiHost == 'azure') {
449440
scope: openAiResourceGroup
@@ -455,9 +446,10 @@ module openAiRoleUser 'core/security/role.bicep' = if (openAiHost == 'azure') {
455446
}
456447
}
457448

458-
module documentIntelligenceRoleUser 'core/security/role.bicep' = {
459-
scope: documentIntelligenceResourceGroup
460-
name: 'documentintelligence-role-user'
449+
// For both document intelligence and computer vision
450+
module cognitiveServicesRoleUser 'core/security/role.bicep' = {
451+
scope: resourceGroup
452+
name: 'cognitiveservices-role-user'
461453
params: {
462454
principalId: principalId
463455
roleDefinitionId: 'a97b65f3-24c7-4388-baec-2e87135dc908'
@@ -537,7 +529,6 @@ module openAiRoleSearchService 'core/security/role.bicep' = if (openAiHost == 'a
537529
}
538530
}
539531

540-
541532
module storageRoleBackend 'core/security/role.bicep' = {
542533
scope: storageResourceGroup
543534
name: 'storage-role-backend'
@@ -582,6 +573,17 @@ module searchReaderRoleBackend 'core/security/role.bicep' = if (useAuthenticatio
582573
}
583574
}
584575

576+
// For computer vision access by the backend
577+
module cognitiveServicesRoleBackend 'core/security/role.bicep' = if (useGPT4V) {
578+
scope: resourceGroup
579+
name: 'cognitiveservices-role-backend'
580+
params: {
581+
principalId: backend.outputs.identityPrincipalId
582+
roleDefinitionId: 'a97b65f3-24c7-4388-baec-2e87135dc908'
583+
principalType: 'ServicePrincipal'
584+
}
585+
}
586+
585587
output AZURE_LOCATION string = location
586588
output AZURE_TENANT_ID string = tenantId
587589
output AZURE_AUTH_TENANT_ID string = authTenantId
@@ -605,7 +607,6 @@ output OPENAI_API_KEY string = (openAiHost == 'openai') ? openAiApiKey : ''
605607
output OPENAI_ORGANIZATION string = (openAiHost == 'openai') ? openAiApiOrganization : ''
606608

607609
output AZURE_VISION_ENDPOINT string = useGPT4V ? computerVision.outputs.endpoint : ''
608-
output VISION_SECRET_NAME string = useGPT4V ? computerVisionSecretName : ''
609610
output AZURE_KEY_VAULT_NAME string = useKeyVault ? keyVault.outputs.name : ''
610611

611612
output AZURE_DOCUMENTINTELLIGENCE_SERVICE string = documentIntelligence.outputs.name

infra/secrets.bicep

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,8 @@
11
param keyVaultName string
2-
param storeComputerVisionSecret bool
3-
param computerVisionId string
4-
param computerVisionSecretName string
52
param storeSearchServiceSecret bool
63
param searchServiceId string
74
param searchServiceSecretName string
85

9-
module computerVisionKVSecret 'core/security/keyvault-secret.bicep' = if (storeComputerVisionSecret) {
10-
name: 'keyvault-secret'
11-
params: {
12-
keyVaultName: storeComputerVisionSecret ? keyVaultName : ''
13-
name: computerVisionSecretName
14-
secretValue: storeComputerVisionSecret ? listKeys(computerVisionId, '2023-05-01').key1 : ''
15-
}
16-
}
176

187
module searchServiceKVSecret 'core/security/keyvault-secret.bicep' = if (storeSearchServiceSecret) {
198
name: 'searchservice-secret'

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ lint.select = ["E", "F", "I", "UP"]
44
lint.ignore = ["E501", "E701"] # line too long, multiple statements on one line
55
src = ["app/backend", "scripts"]
66

7-
[tool.ruff.isort]
7+
[tool.ruff.lint.isort]
88
known-local-folder = ["scripts"]
99

1010
[tool.black]

0 commit comments

Comments
 (0)