Skip to content

Commit 8558b4d

Browse files
authored
[pf-evals] ContentSafety: Use new way to check RAI availability and perf improve via reusing the token (#3446)
# Description Please add an informative description that covers that changes made by the pull request and link all relevant issues. # All Promptflow Contribution checklist: - [ ] **The pull request does not introduce [breaking changes].** - [ ] **CHANGELOG is updated for new features, bug fixes or other significant changes.** - [ ] **I have read the [contribution guidelines](../CONTRIBUTING.md).** - [ ] **Create an issue and link to the pull request to get dedicated review from promptflow team. Learn more: [suggested workflow](../CONTRIBUTING.md#suggested-workflow).** ## General Guidelines and Best Practices - [ ] Title of the pull request is clear and informative. - [ ] There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. For more information on cleaning up the commits in your PR, [see this page](https://github.com/Azure/azure-powershell/blob/master/documentation/development-docs/cleaning-up-commits.md). ### Testing Guidelines - [ ] Pull request includes test coverage for the included changes.
1 parent 2d532be commit 8558b4d

13 files changed

+1098
-499
lines changed

src/promptflow-evals/promptflow/evals/evaluators/_content_safety/flow/evaluate_with_rai_service.py

+52-20
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import List
55
from urllib.parse import urlparse
66

7+
import jwt
78
import numpy as np
89
import requests
910
from azure.core.credentials import TokenCredential
@@ -20,22 +21,33 @@
2021
USER_AGENT = "{}/{}".format("promptflow-evals", version)
2122

2223

23-
def ensure_service_availability(rai_svc_url: str):
24-
svc_liveness_url = rai_svc_url.split("/subscriptions")[0] + "/meta/version"
25-
response = requests.get(svc_liveness_url)
24+
def ensure_service_availability(rai_svc_url: str, token: str, capability: str = None):
25+
headers = {
26+
"Authorization": f"Bearer {token}",
27+
"Content-Type": "application/json",
28+
"User-Agent": USER_AGENT,
29+
}
30+
31+
svc_liveness_url = rai_svc_url + "/checkannotation"
32+
response = requests.get(svc_liveness_url, headers=headers)
33+
2634
if response.status_code != 200:
27-
raise Exception("RAI service is not available in this region")
35+
raise Exception(f"RAI service is not available in this region. Status Code: {response.status_code}")
36+
37+
capabilities = response.json()
38+
39+
if capability and capability not in capabilities:
40+
raise Exception(f"Capability '{capability}' is not available in this region")
2841

2942

30-
def submit_request(question: str, answer: str, metric: str, rai_svc_url: str, credential: TokenCredential):
43+
def submit_request(question: str, answer: str, metric: str, rai_svc_url: str, token: str):
3144
user_text = f"<Human>{question}</><System>{answer}</>"
3245
normalized_user_text = user_text.replace("'", '\\"')
3346
payload = {"UserTextList": [normalized_user_text], "AnnotationTask": Tasks.CONTENT_HARM, "MetricList": [metric]}
3447

3548
url = rai_svc_url + "/submitannotation"
36-
bearer_token = credential.get_token("https://management.azure.com/.default").token
3749
headers = {
38-
"Authorization": f"Bearer {bearer_token}",
50+
"Authorization": f"Bearer {token}",
3951
"Content-Type": "application/json",
4052
"User-Agent": USER_AGENT,
4153
}
@@ -50,15 +62,14 @@ def submit_request(question: str, answer: str, metric: str, rai_svc_url: str, cr
5062
return operation_id
5163

5264

53-
def fetch_result(operation_id: str, rai_svc_url: str, credential: TokenCredential):
65+
def fetch_result(operation_id: str, rai_svc_url: str, credential: TokenCredential, token: str):
5466
start = time.time()
5567
request_count = 0
5668

5769
url = rai_svc_url + "/operations/" + operation_id
58-
bearer_token = credential.get_token("https://management.azure.com/.default").token
59-
headers = {"Authorization": f"Bearer {bearer_token}", "Content-Type": "application/json"}
60-
6170
while True:
71+
token = fetch_or_reuse_token(credential, token)
72+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
6273
response = requests.get(url, headers=headers)
6374
if response.status_code == 200:
6475
return response.json()
@@ -144,9 +155,8 @@ def parse_response(batch_response: List[dict], metric_name: str) -> List[List[di
144155
return result
145156

146157

147-
def _get_service_discovery_url(azure_ai_project, credential):
148-
bearer_token = credential.get_token("https://management.azure.com/.default").token
149-
headers = {"Authorization": f"Bearer {bearer_token}", "Content-Type": "application/json"}
158+
def _get_service_discovery_url(azure_ai_project, token):
159+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
150160
response = requests.get(
151161
f"https://management.azure.com/subscriptions/{azure_ai_project['subscription_id']}/"
152162
f"resourceGroups/{azure_ai_project['resource_group_name']}/"
@@ -161,8 +171,8 @@ def _get_service_discovery_url(azure_ai_project, credential):
161171
return f"{base_url.scheme}://{base_url.netloc}"
162172

163173

164-
def get_rai_svc_url(project_scope: dict, credential: TokenCredential):
165-
discovery_url = _get_service_discovery_url(azure_ai_project=project_scope, credential=credential)
174+
def get_rai_svc_url(project_scope: dict, token: str):
175+
discovery_url = _get_service_discovery_url(azure_ai_project=project_scope, token=token)
166176
subscription_id = project_scope["subscription_id"]
167177
resource_group_name = project_scope["resource_group_name"]
168178
project_name = project_scope["project_name"]
@@ -176,6 +186,27 @@ def get_rai_svc_url(project_scope: dict, credential: TokenCredential):
176186
return rai_url
177187

178188

189+
def fetch_or_reuse_token(credential: TokenCredential, token: str = None):
190+
acquire_new_token = True
191+
try:
192+
if token:
193+
# Decode the token to get its expiration time
194+
decoded_token = jwt.decode(token, options={"verify_signature": False})
195+
exp_time = decoded_token["exp"]
196+
current_time = time.time()
197+
198+
# Check if the token is near expiry
199+
if (exp_time - current_time) >= 300:
200+
acquire_new_token = False
201+
except Exception:
202+
pass
203+
204+
if acquire_new_token:
205+
token = credential.get_token("https://management.azure.com/.default").token
206+
207+
return token
208+
209+
179210
@tool
180211
def evaluate_with_rai_service(
181212
question: str, answer: str, metric_name: str, project_scope: dict, credential: TokenCredential
@@ -186,12 +217,13 @@ def evaluate_with_rai_service(
186217
credential = DefaultAzureCredential()
187218

188219
# Get RAI service URL from discovery service and check service availability
189-
rai_svc_url = get_rai_svc_url(project_scope, credential)
190-
ensure_service_availability(rai_svc_url)
220+
token = fetch_or_reuse_token(credential)
221+
rai_svc_url = get_rai_svc_url(project_scope, token)
222+
ensure_service_availability(rai_svc_url, token, Tasks.CONTENT_HARM)
191223

192224
# Submit annotation request and fetch result
193-
operation_id = submit_request(question, answer, metric_name, rai_svc_url, credential)
194-
annotation_response = fetch_result(operation_id, rai_svc_url, credential)
225+
operation_id = submit_request(question, answer, metric_name, rai_svc_url, token)
226+
annotation_response = fetch_result(operation_id, rai_svc_url, credential, token)
195227
result = parse_response(annotation_response, metric_name)
196228

197229
return result

src/promptflow-evals/tests/evals/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66

77
import jwt
88
import pytest
9+
from azure.ai.ml._ml_client import MLClient
910
from pytest_mock import MockerFixture
1011

1112
from promptflow.client import PFClient
1213
from promptflow.core import AzureOpenAIModelConfiguration
1314
from promptflow.executor._line_execution_process_pool import _process_wrapper
1415
from promptflow.executor._process_manager import create_spawned_fork_process_manager
1516
from promptflow.tracing._integrations._openai_injector import inject_openai_api
16-
from azure.ai.ml._ml_client import MLClient
1717

1818
try:
1919
from promptflow.recording.local import recording_array_reset

src/promptflow-evals/tests/evals/e2etests/test_builtin_evaluators.py

+13
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ def test_individual_evaluator_service_based(self, project_scope, azure_cred):
4242
assert score["violence_score"] < 1.0
4343
assert score["violence_reason"], "violence_reason must not be None or empty."
4444

45+
@pytest.mark.skip(reason="Not working in ci pipeline. For local run.")
46+
def test_content_safety_service_unavailable(self, project_scope, azure_cred):
47+
eval_fn = ViolenceEvaluator(project_scope, azure_cred)
48+
project_scope["project_name"] = "pf-evals-ws-westus2"
49+
50+
with pytest.raises(Exception) as exc_info:
51+
eval_fn(
52+
question="What is the capital of Japan?",
53+
answer="The capital of Japan is Tokyo.",
54+
)
55+
56+
assert "RAI service is not available in this region" in exc_info._excinfo[1].inner_exception.args[0]
57+
4558
@pytest.mark.parametrize("parallel", [False, True])
4659
def test_composite_evaluator_qa(self, model_config, parallel):
4760
qa_eval = QAEvaluator(model_config, parallel=parallel)

src/promptflow-evals/tests/recordings/azure/test_adv_simulator_TestAdvSimulator_test_adv_qa_sim_responds_with_one_response.yaml

+38-60
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,43 @@ interactions:
4040
x-content-type-options:
4141
- nosniff
4242
x-request-time:
43-
- '0.025'
43+
- '0.024'
44+
status:
45+
code: 200
46+
message: OK
47+
- request:
48+
body: '[{"ver": 1, "name": "Microsoft.ApplicationInsights.Event", "time": "2024-06-06T23:20:59.838896Z",
49+
"sampleRate": 100.0, "iKey": "00000000-0000-0000-0000-000000000000", "tags":
50+
{"foo": "bar"}}]'
51+
headers:
52+
Accept:
53+
- application/json
54+
Accept-Encoding:
55+
- gzip, deflate
56+
Connection:
57+
- keep-alive
58+
Content-Length:
59+
- '2172'
60+
Content-Type:
61+
- application/json
62+
User-Agent:
63+
- azsdk-python-azuremonitorclient/unknown Python/3.10.14 (Windows-10-10.0.22631-SP0)
64+
method: POST
65+
uri: https://eastus-8.in.applicationinsights.azure.com/v2.1/track
66+
response:
67+
body:
68+
string: '{"itemsReceived": 2, "itemsAccepted": 2, "appId": null, "errors": []}'
69+
headers:
70+
content-type:
71+
- application/json; charset=utf-8
72+
server:
73+
- Microsoft-HTTPAPI/2.0
74+
strict-transport-security:
75+
- max-age=31536000
76+
transfer-encoding:
77+
- chunked
78+
x-content-type-options:
79+
- nosniff
4480
status:
4581
code: 200
4682
message: OK
@@ -129558,65 +129594,7 @@ interactions:
129558129594
x-content-type-options:
129559129595
- nosniff
129560129596
x-request-time:
129561-
- '0.164'
129562-
status:
129563-
code: 200
129564-
message: OK
129565-
- request:
129566-
body: '[{"ver": 1, "name": "Microsoft.ApplicationInsights.Event", "time": "2024-06-04T18:17:47.066535Z",
129567-
"sampleRate": 100.0, "iKey": "8b52b368-4c91-4226-b7f7-be52822f0509", "tags":
129568-
{"ai.device.locale": "en_US", "ai.device.osVersion": "10.0.22631", "ai.device.type":
129569-
"Other", "ai.internal.sdkVersion": "uwm_py3.10.14:otel1.25.0:ext1.0.0b26", "ai.cloud.role":
129570-
"***.py", "ai.internal.nodeName": "ninhu-desktop2", "ai.operation.id": "00000000000000000000000000000000",
129571-
"ai.operation.parentId": "0000000000000000"}, "data": {"baseType": "EventData",
129572-
"baseData": {"ver": 2, "name": "adversarial.simulator.call.start", "properties":
129573-
{"level": "INFO", "from_ci": "False", "request_id": "43a91461-447d-48c5-8ef7-63353d0162af",
129574-
"first_call": "True", "activity_name": "adversarial.simulator.call", "activity_type":
129575-
"PublicApi", "user_agent": "", "scenario": "AdversarialScenario.ADVERSARIAL_QA",
129576-
"max_conversation_turns": "1", "max_simulation_results": "1", "python_version":
129577-
"3.10.14", "installation_id": "ca79f281-c213-5434-91e6-88ee00a05a6a"}}}}, {"ver":
129578-
1, "name": "Microsoft.ApplicationInsights.Event", "time": "2024-06-04T18:17:47.066535Z",
129579-
"sampleRate": 100.0, "iKey": "8b52b368-4c91-4226-b7f7-be52822f0509", "tags":
129580-
{"ai.device.locale": "en_US", "ai.device.osVersion": "10.0.22631", "ai.device.type":
129581-
"Other", "ai.internal.sdkVersion": "uwm_py3.10.14:otel1.25.0:ext1.0.0b26", "ai.cloud.role":
129582-
"***.py", "ai.internal.nodeName": "ninhu-desktop2", "ai.operation.id": "00000000000000000000000000000000",
129583-
"ai.operation.parentId": "0000000000000000"}, "data": {"baseType": "EventData",
129584-
"baseData": {"ver": 2, "name": "adversarial.simulator.call.complete", "properties":
129585-
{"level": "INFO", "from_ci": "False", "request_id": "43a91461-447d-48c5-8ef7-63353d0162af",
129586-
"first_call": "True", "activity_name": "adversarial.simulator.call", "activity_type":
129587-
"PublicApi", "user_agent": "", "scenario": "AdversarialScenario.ADVERSARIAL_QA",
129588-
"max_conversation_turns": "1", "max_simulation_results": "1", "completion_status":
129589-
"Success", "duration_ms": "0.0", "python_version": "3.10.14", "installation_id":
129590-
"ca79f281-c213-5434-91e6-88ee00a05a6a"}}}}]'
129591-
headers:
129592-
Accept:
129593-
- application/json
129594-
Accept-Encoding:
129595-
- gzip, deflate
129596-
Connection:
129597-
- keep-alive
129598-
Content-Length:
129599-
- '2121'
129600-
Content-Type:
129601-
- application/json
129602-
User-Agent:
129603-
- azsdk-python-azuremonitorclient/unknown Python/3.10.14 (Windows-10-10.0.22631-SP0)
129604-
method: POST
129605-
uri: https://eastus-8.in.applicationinsights.azure.com/v2.1/track
129606-
response:
129607-
body:
129608-
string: '{"itemsReceived": 2, "itemsAccepted": 2, "appId": null, "errors": []}'
129609-
headers:
129610-
content-type:
129611-
- application/json; charset=utf-8
129612-
server:
129613-
- Microsoft-HTTPAPI/2.0
129614-
strict-transport-security:
129615-
- max-age=31536000
129616-
transfer-encoding:
129617-
- chunked
129618-
x-content-type-options:
129619-
- nosniff
129597+
- '0.057'
129620129598
status:
129621129599
code: 200
129622129600
message: OK

src/promptflow-evals/tests/recordings/azure/test_adv_simulator_TestAdvSimulator_test_adv_sim_init_with_prod_url.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ interactions:
4040
x-content-type-options:
4141
- nosniff
4242
x-request-time:
43-
- '0.025'
43+
- '0.033'
4444
status:
4545
code: 200
4646
message: OK

src/promptflow-evals/tests/recordings/azure/test_adv_simulator_TestAdvSimulator_test_adv_summarization_jailbreak_sim_responds_with_responses.yaml

+7-29
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ interactions:
4040
x-content-type-options:
4141
- nosniff
4242
x-request-time:
43-
- '0.021'
43+
- '0.025'
4444
status:
4545
code: 200
4646
message: OK
@@ -129558,36 +129558,14 @@ interactions:
129558129558
x-content-type-options:
129559129559
- nosniff
129560129560
x-request-time:
129561-
- '0.078'
129561+
- '0.020'
129562129562
status:
129563129563
code: 200
129564129564
message: OK
129565129565
- request:
129566-
body: '[{"ver": 1, "name": "Microsoft.ApplicationInsights.Event", "time": "2024-06-04T18:21:13.871575Z",
129567-
"sampleRate": 100.0, "iKey": "8b52b368-4c91-4226-b7f7-be52822f0509", "tags":
129568-
{"ai.device.locale": "en_US", "ai.device.osVersion": "10.0.22631", "ai.device.type":
129569-
"Other", "ai.internal.sdkVersion": "uwm_py3.10.14:otel1.25.0:ext1.0.0b26", "ai.cloud.role":
129570-
"***.py", "ai.internal.nodeName": "ninhu-desktop2", "ai.operation.id": "00000000000000000000000000000000",
129571-
"ai.operation.parentId": "0000000000000000"}, "data": {"baseType": "EventData",
129572-
"baseData": {"ver": 2, "name": "adversarial.simulator.call.start", "properties":
129573-
{"level": "INFO", "from_ci": "False", "request_id": "92065092-b71d-4ea2-a860-044dbedb93f6",
129574-
"first_call": "True", "activity_name": "adversarial.simulator.call", "activity_type":
129575-
"PublicApi", "user_agent": "", "scenario": "AdversarialScenario.ADVERSARIAL_SUMMARIZATION",
129576-
"max_conversation_turns": "1", "max_simulation_results": "1", "jailbreak": "True",
129577-
"python_version": "3.10.14", "installation_id": "ca79f281-c213-5434-91e6-88ee00a05a6a"}}}},
129578-
{"ver": 1, "name": "Microsoft.ApplicationInsights.Event", "time": "2024-06-04T18:21:13.871575Z",
129579-
"sampleRate": 100.0, "iKey": "8b52b368-4c91-4226-b7f7-be52822f0509", "tags":
129580-
{"ai.device.locale": "en_US", "ai.device.osVersion": "10.0.22631", "ai.device.type":
129581-
"Other", "ai.internal.sdkVersion": "uwm_py3.10.14:otel1.25.0:ext1.0.0b26", "ai.cloud.role":
129582-
"***.py", "ai.internal.nodeName": "ninhu-desktop2", "ai.operation.id": "00000000000000000000000000000000",
129583-
"ai.operation.parentId": "0000000000000000"}, "data": {"baseType": "EventData",
129584-
"baseData": {"ver": 2, "name": "adversarial.simulator.call.complete", "properties":
129585-
{"level": "INFO", "from_ci": "False", "request_id": "92065092-b71d-4ea2-a860-044dbedb93f6",
129586-
"first_call": "True", "activity_name": "adversarial.simulator.call", "activity_type":
129587-
"PublicApi", "user_agent": "", "scenario": "AdversarialScenario.ADVERSARIAL_SUMMARIZATION",
129588-
"max_conversation_turns": "1", "max_simulation_results": "1", "jailbreak": "True",
129589-
"completion_status": "Success", "duration_ms": "0.0", "python_version": "3.10.14",
129590-
"installation_id": "ca79f281-c213-5434-91e6-88ee00a05a6a"}}}}]'
129566+
body: '[{"ver": 1, "name": "Microsoft.ApplicationInsights.Event", "time": "2024-06-06T23:20:59.838896Z",
129567+
"sampleRate": 100.0, "iKey": "00000000-0000-0000-0000-000000000000", "tags":
129568+
{"foo": "bar"}}]'
129591129569
headers:
129592129570
Accept:
129593129571
- application/json
@@ -129596,7 +129574,7 @@ interactions:
129596129574
Connection:
129597129575
- keep-alive
129598129576
Content-Length:
129599-
- '2185'
129577+
- '2237'
129600129578
Content-Type:
129601129579
- application/json
129602129580
User-Agent:
@@ -134402,7 +134380,7 @@ interactions:
134402134380
x-content-type-options:
134403134381
- nosniff
134404134382
x-request-time:
134405-
- '0.025'
134383+
- '0.019'
134406134384
status:
134407134385
code: 200
134408134386
message: OK

0 commit comments

Comments
 (0)