Skip to content

Commit cf542dc

Browse files
committed
AI Foundry changes
1 parent 606ef1d commit cf542dc

File tree

3 files changed

+169
-59
lines changed

3 files changed

+169
-59
lines changed

evals/safety_evaluation.py

Lines changed: 33 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,85 +8,79 @@
88
from typing import Optional
99

1010
import requests
11-
from azure.ai.evaluation import AzureAIProject
1211
from azure.ai.evaluation.red_team import AttackStrategy, RedTeam, RiskCategory
1312
from azure.identity import AzureDeveloperCliCredential
1413
from dotenv_azd import load_azd_env
15-
from rich.logging import RichHandler
16-
17-
logger = logging.getLogger("ragapp")
18-
19-
# Configure logging to capture and display warnings with tracebacks
20-
logging.captureWarnings(True) # Capture warnings as log messages
2114

2215
root_dir = pathlib.Path(__file__).parent
2316

2417

2518
def get_azure_credential():
2619
AZURE_TENANT_ID = os.getenv("AZURE_TENANT_ID")
2720
if AZURE_TENANT_ID:
28-
logger.info("Setting up Azure credential using AzureDeveloperCliCredential with tenant_id %s", AZURE_TENANT_ID)
21+
print("Setting up Azure credential using AzureDeveloperCliCredential with tenant_id %s", AZURE_TENANT_ID)
2922
azure_credential = AzureDeveloperCliCredential(tenant_id=AZURE_TENANT_ID, process_timeout=60)
3023
else:
31-
logger.info("Setting up Azure credential using AzureDeveloperCliCredential for home tenant")
24+
print("Setting up Azure credential using AzureDeveloperCliCredential for home tenant")
3225
azure_credential = AzureDeveloperCliCredential(process_timeout=60)
3326
return azure_credential
3427

3528

36-
async def callback(
37-
messages: list,
29+
def callback(
30+
question: str,
3831
target_url: str = "http://127.0.0.1:8000/chat",
3932
):
40-
query = messages[-1].content
4133
headers = {"Content-Type": "application/json"}
4234
body = {
43-
"messages": [{"content": query, "role": "user"}],
35+
"messages": [{"content": question, "role": "user"}],
4436
"stream": False,
45-
"context": {"overrides": {"use_advanced_flow": True, "top": 3, "retrieval_mode": "hybrid", "temperature": 0.3}},
37+
"context": {
38+
"overrides": {"use_advanced_flow": False, "top": 3, "retrieval_mode": "hybrid", "temperature": 0.3}
39+
},
4640
}
4741
url = target_url
4842
r = requests.post(url, headers=headers, json=body)
4943
response = r.json()
5044
if "error" in response:
51-
message = {"content": response["error"], "role": "assistant"}
45+
return f"Error received: {response['error']}"
5246
else:
53-
message = response["message"]
54-
return {"messages": messages + [message]}
47+
return response["message"]["content"]
5548

5649

57-
async def run_simulator(target_url: str, max_simulations: int, scan_name: Optional[str] = None):
58-
credential = get_azure_credential()
59-
azure_ai_project: AzureAIProject = {
60-
"subscription_id": os.getenv("AZURE_SUBSCRIPTION_ID"),
61-
"resource_group_name": os.getenv("AZURE_RESOURCE_GROUP"),
62-
"project_name": "pf-testprojforaisaety",
63-
}
50+
async def run_redteaming(target_url: str, questions_per_category: int = 1, scan_name: Optional[str] = None):
51+
AZURE_AI_FOUNDRY = os.getenv("AZURE_AI_FOUNDRY")
52+
AZURE_AI_PROJECT = os.getenv("AZURE_AI_PROJECT")
6453
model_red_team = RedTeam(
65-
azure_ai_project=azure_ai_project,
66-
credential=credential,
54+
azure_ai_project=f"https://{AZURE_AI_FOUNDRY}.services.ai.azure.com/api/projects/{AZURE_AI_PROJECT}",
55+
credential=get_azure_credential(),
6756
risk_categories=[
6857
RiskCategory.Violence,
6958
RiskCategory.HateUnfairness,
7059
RiskCategory.Sexual,
7160
RiskCategory.SelfHarm,
7261
],
73-
num_objectives=1,
62+
num_objectives=questions_per_category,
7463
)
64+
7565
if scan_name is None:
7666
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
7767
scan_name = f"Safety evaluation {timestamp}"
68+
7869
await model_red_team.scan(
79-
target=lambda messages, stream=False, session_state=None, context=None: callback(messages, target_url),
8070
scan_name=scan_name,
71+
output_path=f"{root_dir}/redteams/{scan_name}.json",
8172
attack_strategies=[
82-
AttackStrategy.DIFFICULT,
8373
AttackStrategy.Baseline,
84-
AttackStrategy.UnicodeConfusable, # Use confusable Unicode characters
85-
AttackStrategy.Morse, # Encode prompts in Morse code
86-
AttackStrategy.Leetspeak, # Use Leetspeak
87-
AttackStrategy.Url, # Use URLs in prompts
74+
# Easy Complexity:
75+
AttackStrategy.Morse,
76+
AttackStrategy.UnicodeConfusable,
77+
AttackStrategy.Url,
78+
# Moderate Complexity:
79+
AttackStrategy.Tense,
80+
# Difficult Complexity:
81+
AttackStrategy.Compose([AttackStrategy.Tense, AttackStrategy.Url]),
8882
],
89-
output_path="Advanced-Callback-Scan.json",
83+
target=lambda query: callback(query, target_url),
9084
)
9185

9286

@@ -96,31 +90,17 @@ async def run_simulator(target_url: str, max_simulations: int, scan_name: Option
9690
"--target_url", type=str, default="http://127.0.0.1:8000/chat", help="Target URL for the callback."
9791
)
9892
parser.add_argument(
99-
"--max_simulations", type=int, default=200, help="Maximum number of simulations (question/response pairs)."
93+
"--questions_per_category",
94+
type=int,
95+
default=1,
96+
help="Number of questions per risk category to ask during the scan.",
10097
)
101-
# argument for the name
10298
parser.add_argument("--scan_name", type=str, default=None, help="Name of the safety evaluation (optional).")
10399
args = parser.parse_args()
104100

105-
# Configure logging to show tracebacks for warnings and above
106-
logging.basicConfig(
107-
level=logging.WARNING,
108-
format="%(message)s",
109-
datefmt="[%X]",
110-
handlers=[RichHandler(rich_tracebacks=False, show_path=True)],
111-
)
112-
113-
# Set urllib3 and azure libraries to WARNING level to see connection issues
114-
logging.getLogger("urllib3").setLevel(logging.WARNING)
115-
logging.getLogger("azure").setLevel(logging.WARNING)
116-
117-
# Set our application logger to INFO level
118-
logger.setLevel(logging.INFO)
119-
120101
load_azd_env()
121-
122102
try:
123-
asyncio.run(run_simulator(args.target_url, args.max_simulations, args.scan_name))
103+
asyncio.run(run_redteaming(args.target_url, args.questions_per_category, args.scan_name))
124104
except Exception:
125105
logging.exception("Unhandled exception in safety evaluation")
126106
sys.exit(1)

infra/core/ai/ai-foundry.bicep

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
@minLength(1)
2+
@description('Primary location for all resources')
3+
param location string
4+
5+
@description('The AI Foundry resource name.')
6+
param foundryName string
7+
8+
@description('The AI Project resource name.')
9+
param projectName string = foundryName
10+
11+
param projectDescription string = ''
12+
param projectDisplayName string = projectName
13+
14+
@description('The Storage Account resource name.')
15+
param storageAccountName string
16+
17+
param principalId string
18+
param principalType string
19+
20+
param tags object = {}
21+
22+
// Step 1: Create an AI Foundry resource
23+
resource account 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = {
24+
name: foundryName
25+
location: location
26+
tags: tags
27+
sku: {
28+
name: 'S0'
29+
}
30+
kind: 'AIServices'
31+
identity: {
32+
type: 'SystemAssigned'
33+
}
34+
properties: {
35+
allowProjectManagement: true
36+
customSubDomainName: toLower(foundryName)
37+
networkAcls: {
38+
defaultAction: 'Allow'
39+
virtualNetworkRules: []
40+
ipRules: []
41+
}
42+
publicNetworkAccess: 'Enabled'
43+
disableLocalAuth: false
44+
}
45+
}
46+
47+
// Step 2: Create an AI Foundry project
48+
resource project 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = {
49+
parent: account
50+
name: projectName
51+
location: location
52+
tags: tags
53+
identity: {
54+
type: 'SystemAssigned'
55+
}
56+
properties: {
57+
description: projectDescription
58+
displayName: projectDisplayName
59+
}
60+
}
61+
62+
// Step 4: Create a storage account, needed for evaluations
63+
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' existing = {
64+
name: storageAccountName
65+
}
66+
67+
// Create a storage account connection for the foundry resource
68+
resource storageAccountConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = {
69+
parent: account
70+
name: 'default-storage'
71+
properties: {
72+
authType: 'AAD'
73+
category: 'AzureStorageAccount'
74+
isSharedToAll: true
75+
target: storageAccount.properties.primaryEndpoints.blob
76+
metadata: {
77+
ApiType: 'Azure'
78+
ResourceId: storageAccount.id
79+
}
80+
}
81+
}
82+
83+
// Assign a role to the project's managed identity for the storage account
84+
resource storageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
85+
name: guid(storageAccount.id, 'Storage Blob Data Contributor', project.name)
86+
scope: storageAccount
87+
properties: {
88+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') // Storage Blob Data Contributor
89+
principalId: project.identity.principalId
90+
principalType: 'ServicePrincipal'
91+
}
92+
}
93+
94+
// Assign a role to the calling user for the AI Foundry project (needed for projects (including agents) API)
95+
resource projectRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
96+
name: guid(project.id, 'Azure AI User', principalId)
97+
scope: project
98+
properties: {
99+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d') // Azure AI User
100+
principalId: principalId
101+
principalType: 'User'
102+
}
103+
}
104+
105+
// Assign a role to the calling user for the AI Foundry account (needed for Azure OpenAI API)
106+
resource accountRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
107+
name: guid(account.id, 'Azure AI User', principalId)
108+
scope: account
109+
properties: {
110+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d') // Azure AI User
111+
principalId: principalId
112+
principalType: 'User'
113+
}
114+
}
115+
116+
output foundryName string = account.name
117+
output projectName string = project.name

infra/main.bicep

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -472,16 +472,17 @@ module storage 'br/public:avm/res/storage/storage-account:0.9.1' = if (useAiProj
472472
}
473473
}
474474

475-
module ai 'core/ai/ai-environment.bicep' = if (useAiProject) {
475+
module ai 'core/ai/ai-foundry.bicep' = if (useAiProject) {
476476
name: 'ai'
477477
scope: resourceGroup
478478
params: {
479479
location: 'swedencentral'
480480
tags: tags
481-
hubName: 'aihub-${resourceToken}'
482-
projectName: 'aiproj-${resourceToken}'
483-
applicationInsightsId: monitoring.outputs.applicationInsightsId
484-
storageAccountId: storage.outputs.resourceId
481+
foundryName: 'aifoundry-${resourceToken}'
482+
projectName: 'aiproject-${resourceToken}'
483+
storageAccountName: storage.outputs.name
484+
principalId: principalId
485+
principalType: empty(runningOnGh) ? 'User' : 'ServicePrincipal'
485486
}
486487
}
487488

@@ -491,11 +492,22 @@ module openAIRoleUser 'core/security/role.bicep' = {
491492
name: 'openai-role-user'
492493
params: {
493494
principalId: principalId
494-
roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'
495+
roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Cognitive Services OpenAI User
495496
principalType: empty(runningOnGh) ? 'User' : 'ServicePrincipal'
496497
}
497498
}
498499

500+
module azureAiUserRole 'core/security/role.bicep' = if (useAiProject && resourceGroup.name != openAIResourceGroup.name) {
501+
name: 'azureai-role-user'
502+
scope: resourceGroup
503+
params: {
504+
principalId: principalId
505+
roleDefinitionId: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Azure AI User
506+
principalType: empty(runningOnGh) ? 'User' : 'ServicePrincipal'
507+
}
508+
}
509+
510+
499511
// Backend roles
500512
module openAIRoleBackend 'core/security/role.bicep' = {
501513
scope: openAIResourceGroup
@@ -560,6 +572,7 @@ output AZURE_OPENAI_EVAL_DEPLOYMENT_CAPACITY string = deployAzureOpenAI ? evalDe
560572
output AZURE_OPENAI_EVAL_DEPLOYMENT_SKU string = deployAzureOpenAI ? evalDeploymentSku : ''
561573
output AZURE_OPENAI_EVAL_MODEL string = deployAzureOpenAI ? evalModelName : ''
562574

575+
output AZURE_AI_FOUNDRY string = useAiProject ? ai.outputs.foundryName : ''
563576
output AZURE_AI_PROJECT string = useAiProject ? ai.outputs.projectName : ''
564577

565578
output POSTGRES_HOST string = postgresServer.outputs.POSTGRES_DOMAIN_NAME

0 commit comments

Comments
 (0)