Skip to content

Commit 8468663

Browse files
authored
[evaluation] feat: Allow users to update useragent (#41718)
* refactor: `USER_AGENT` constant -> UserAgentSingleton Moving to a singleton allows us to: * preserve similar ergonomics (import + use vs. forward through parameters) * allows us to dynamically change the useragent * refactor: Refactor all uses of USER_AGENT constant to UserAgentSingleton().value * fix: Set user agent * feat: Add __str__ for UserAgentSingleton * feat: Add a parameter to evaluate that allows users to set user_agent * tests: Add tests for user-agent override * tests,fix: Add missing recording fixtures * tests,fix: Use "https://Sanitized.openai.azure.com" as sanitized endpoint * tests: Update assets.json * refactor: _user_agent -> user_agent * fix,tests: Refactor model_config fixture Was previously relying on a local override of the mock_model_config fixture, to set azure_endpoint to a value that wouldn't cause a recording conflict. This worked locally when exclusively running the tests that needed the overriden value. In CI, the override would fail and the relevant tests failed. I *suspect* this is essentially a race condition caused by fixture scope. "Session" scoped fixtures are only instantiated once per session. So whether or not the override applies depends on whether the normal fixture or the overriden one is requested first. This hasn't been validated, but seems plausible. This entire mess can probably be avoided by just using a fixture with a different name. * chore: Update assets.json
1 parent 59296b5 commit 8468663

File tree

19 files changed

+287
-59
lines changed

19 files changed

+287
-59
lines changed

sdk/evaluation/azure-ai-evaluation/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "python",
44
"TagPrefix": "python/evaluation/azure-ai-evaluation",
5-
"Tag": "python/evaluation/azure-ai-evaluation_64eea0ea34"
5+
"Tag": "python/evaluation/azure-ai-evaluation_e9c7adf5b1"
66
}

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/_aoai/aoai_grader.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from azure.ai.evaluation._constants import DEFAULT_AOAI_API_VERSION
77
from azure.ai.evaluation._exceptions import ErrorBlame, ErrorCategory, ErrorTarget, EvaluationException
8+
from azure.ai.evaluation._user_agent import UserAgentSingleton
89
from typing import Any, Dict, Union
910
from azure.ai.evaluation._common._experimental import experimental
1011

@@ -71,6 +72,9 @@ def get_client(self) -> Any:
7172
:return: The OpenAI client.
7273
:rtype: [~openai.OpenAI, ~openai.AzureOpenAI]
7374
"""
75+
default_headers = {
76+
"User-Agent": UserAgentSingleton().value
77+
}
7478
if "azure_endpoint" in self._model_config:
7579
from openai import AzureOpenAI
7680
# TODO set default values?
@@ -79,11 +83,13 @@ def get_client(self) -> Any:
7983
api_key=self._model_config.get("api_key", None), # Default-style access to appease linters.
8084
api_version=DEFAULT_AOAI_API_VERSION, # Force a known working version
8185
azure_deployment=self._model_config.get("azure_deployment", ""),
86+
default_headers=default_headers
8287
)
8388
from openai import OpenAI
8489
# TODO add default values for base_url and organization?
8590
return OpenAI(
8691
api_key=self._model_config["api_key"],
8792
base_url=self._model_config.get("base_url", ""),
8893
organization=self._model_config.get("organization", ""),
94+
default_headers=default_headers
8995
)

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/_common/rai_service.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@
2121
from azure.ai.evaluation._exceptions import ErrorBlame, ErrorCategory, ErrorTarget, EvaluationException
2222
from azure.ai.evaluation._http_utils import AsyncHttpPipeline, get_async_http_client
2323
from azure.ai.evaluation._model_configurations import AzureAIProject
24+
from azure.ai.evaluation._user_agent import UserAgentSingleton
2425
from azure.ai.evaluation._common.utils import is_onedp_project
2526
from azure.core.credentials import TokenCredential
2627
from azure.core.exceptions import HttpResponseError
27-
from azure.core.pipeline.policies import AsyncRetryPolicy
28+
from azure.core.pipeline.policies import AsyncRetryPolicy, UserAgentPolicy
2829

2930
from .constants import (
3031
CommonConstants,
@@ -35,11 +36,6 @@
3536
)
3637
from .utils import get_harm_severity_level, retrieve_content_type
3738

38-
try:
39-
version = importlib.metadata.version("azure-ai-evaluation")
40-
except importlib.metadata.PackageNotFoundError:
41-
version = "unknown"
42-
USER_AGENT = "{}/{}".format("azure-ai-evaluation", version)
4339

4440
USER_TEXT_TEMPLATE_DICT: Dict[str, Template] = {
4541
"DEFAULT": Template("<Human>{$query}</><System>{$response}</>"),
@@ -101,7 +97,7 @@ def get_common_headers(token: str, evaluator_name: Optional[str] = None) -> Dict
10197
:return: The common headers.
10298
:rtype: Dict
10399
"""
104-
user_agent = f"{USER_AGENT} (type=evaluator; subtype={evaluator_name})" if evaluator_name else USER_AGENT
100+
user_agent = f"{UserAgentSingleton().value} (type=evaluator; subtype={evaluator_name})" if evaluator_name else UserAgentSingleton().value
105101
return {
106102
"Authorization": f"Bearer {token}",
107103
"User-Agent": user_agent,
@@ -645,7 +641,7 @@ async def evaluate_with_rai_service(
645641
"""
646642

647643
if is_onedp_project(project_scope):
648-
client = AIProjectClient(endpoint=project_scope, credential=credential)
644+
client = AIProjectClient(endpoint=project_scope, credential=credential, user_agent_policy=UserAgentPolicy(base_user_agent=UserAgentSingleton().value))
649645
token = await fetch_or_reuse_token(credential=credential, workspace=COG_SRV_WORKSPACE)
650646
await ensure_service_availability_onedp(client, token, annotation_task)
651647
operation_id = await submit_request_onedp(client, data, metric_name, token, annotation_task, evaluator_name)
@@ -788,7 +784,7 @@ async def evaluate_with_rai_service_multimodal(
788784
"""
789785

790786
if is_onedp_project(project_scope):
791-
client = AIProjectClient(endpoint=project_scope, credential=credential)
787+
client = AIProjectClient(endpoint=project_scope, credential=credential, user_agent_policy=UserAgentPolicy(base_user_agent=UserAgentSingleton().value))
792788
token = await fetch_or_reuse_token(credential=credential, workspace=COG_SRV_WORKSPACE)
793789
await ensure_service_availability_onedp(client, token, Tasks.CONTENT_HARM)
794790
operation_id = await submit_multimodal_request_onedp(client, messages, metric_name, token)

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/_evaluate/_batch_run/eval_run_context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
PF_DISABLE_TRACING,
2222
)
2323

24-
from ..._user_agent import USER_AGENT
24+
from ..._user_agent import UserAgentSingleton
2525
from .._utils import set_event_loop_policy
2626
from .batch_clients import BatchClient
2727
from ._run_submitter_client import RunSubmitterClient
@@ -50,7 +50,7 @@ def __enter__(self) -> None:
5050
self._original_cwd = os.getcwd()
5151

5252
if isinstance(self.client, CodeClient):
53-
ClientUserAgentUtil.append_user_agent(USER_AGENT)
53+
ClientUserAgentUtil.append_user_agent(UserAgentSingleton().value)
5454
inject_openai_api()
5555

5656
if isinstance(self.client, ProxyClient):

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/_evaluate/_evaluate.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright (c) Microsoft Corporation. All rights reserved.
33
# ---------------------------------------------------------
44
import inspect
5+
import contextlib
56
import json
67
import logging
78
import os
@@ -30,7 +31,7 @@
3031
DEFAULT_OAI_EVAL_RUN_NAME
3132
)
3233
from .._model_configurations import AzureAIProject, EvaluationResult, EvaluatorConfig
33-
from .._user_agent import USER_AGENT
34+
from .._user_agent import UserAgentSingleton
3435
from ._batch_run import (
3536
EvalRunContext,
3637
CodeClient,
@@ -729,6 +730,8 @@ def evaluate(
729730
Defaults to false, which means that evaluations will continue regardless of failures.
730731
If such failures occur, metrics may be missing, and evidence of failures can be found in the evaluation's logs.
731732
:paramtype fail_on_evaluator_errors: bool
733+
:keyword user_agent: A string to append to the default user-agent sent with evaluation http requests
734+
:paramtype user_agent: Optional[str]
732735
:return: Evaluation results.
733736
:rtype: ~azure.ai.evaluation.EvaluationResult
734737
@@ -752,17 +755,19 @@ def evaluate(
752755
https://{resource_name}.services.ai.azure.com/api/projects/{project_name}
753756
"""
754757
try:
755-
return _evaluate(
756-
evaluation_name=evaluation_name,
757-
target=target,
758-
data=data,
759-
evaluators_and_graders=evaluators,
760-
evaluator_config=evaluator_config,
761-
azure_ai_project=azure_ai_project,
762-
output_path=output_path,
763-
fail_on_evaluator_errors=fail_on_evaluator_errors,
764-
**kwargs,
765-
)
758+
user_agent: Optional[str] = kwargs.get("user_agent")
759+
with UserAgentSingleton().add_useragent_product(user_agent) if user_agent else contextlib.nullcontext():
760+
return _evaluate(
761+
evaluation_name=evaluation_name,
762+
target=target,
763+
data=data,
764+
evaluators_and_graders=evaluators,
765+
evaluator_config=evaluator_config,
766+
azure_ai_project=azure_ai_project,
767+
output_path=output_path,
768+
fail_on_evaluator_errors=fail_on_evaluator_errors,
769+
**kwargs,
770+
)
766771
except Exception as e:
767772
# Handle multiprocess bootstrap error
768773
bootstrap_error = (
@@ -997,7 +1002,7 @@ def _preprocess_data(
9971002
batch_run_client = RunSubmitterClient()
9981003
batch_run_data = input_data_df
9991004
elif kwargs.pop("_use_pf_client", True):
1000-
batch_run_client = ProxyClient(user_agent=USER_AGENT)
1005+
batch_run_client = ProxyClient(user_agent=UserAgentSingleton().value)
10011006
# Ensure the absolute path is passed to pf.run, as relative path doesn't work with
10021007
# multiple evaluators. If the path is already absolute, abspath will return the original path.
10031008
batch_run_data = os.path.abspath(data)
@@ -1179,4 +1184,4 @@ def _turn_error_logs_into_exception(log_path: str) -> None:
11791184
target=ErrorTarget.EVALUATE,
11801185
category=ErrorCategory.FAILED_EXECUTION,
11811186
blame=ErrorBlame.UNKNOWN,
1182-
)
1187+
)

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/_evaluate/_telemetry/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
from azure.ai.evaluation._model_configurations import AzureAIProject, EvaluationResult
1919

20-
from ..._user_agent import USER_AGENT
2120
from .._utils import _trace_destination_from_project_scope
2221

2322
LOGGER = logging.getLogger(__name__)

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/_evaluate/_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import pandas as pd
1616
from tqdm import tqdm
1717

18+
from azure.core.pipeline.policies import UserAgentPolicy
1819
from azure.ai.evaluation._legacy._adapters.entities import Run
1920

2021
from azure.ai.evaluation._constants import (
@@ -26,6 +27,7 @@
2627
from azure.ai.evaluation._exceptions import ErrorBlame, ErrorCategory, ErrorTarget, EvaluationException
2728
from azure.ai.evaluation._model_configurations import AzureAIProject
2829
from azure.ai.evaluation._version import VERSION
30+
from azure.ai.evaluation._user_agent import UserAgentSingleton
2931
from azure.ai.evaluation._azure._clients import LiteMLClient
3032

3133
LOGGER = logging.getLogger(__name__)
@@ -148,7 +150,8 @@ def _log_metrics_and_instance_results_onedp(
148150
)
149151
client = EvaluationServiceOneDPClient(
150152
endpoint=project_url,
151-
credential=credentials
153+
credential=credentials,
154+
user_agent_policy=UserAgentPolicy(base_user_agent=UserAgentSingleton().value)
152155
)
153156

154157
# Massaging before artifacts are put on disk

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/_evaluators/_common/_base_prompty_eval.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
from . import EvaluatorBase
2121

2222
try:
23-
from ..._user_agent import USER_AGENT
23+
from ..._user_agent import UserAgentSingleton
2424
except ImportError:
25-
USER_AGENT = "None"
25+
class UserAgentSingleton:
26+
@property
27+
def value(self) -> str:
28+
return "None"
2629

2730
T = TypeVar("T")
2831

@@ -60,7 +63,7 @@ def __init__(self, *, result_key: str, prompty_file: str, model_config: dict, ev
6063
super().__init__(eval_last_turn=eval_last_turn, threshold=threshold, _higher_is_better=_higher_is_better)
6164

6265
subclass_name = self.__class__.__name__
63-
user_agent = f"{USER_AGENT} (type=evaluator subtype={subclass_name})"
66+
user_agent = f"{UserAgentSingleton().value} (type=evaluator subtype={subclass_name})"
6467
prompty_model_config = construct_prompty_model_config(
6568
validate_model_config(model_config),
6669
self._DEFAULT_OPEN_API_VERSION,

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/_evaluators/_groundedness/_groundedness.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
from ..._common.utils import construct_prompty_model_config, validate_model_config
1313

1414
try:
15-
from ..._user_agent import USER_AGENT
15+
from ..._user_agent import UserAgentSingleton
1616
except ImportError:
17-
USER_AGENT = "None"
17+
class UserAgentSingleton:
18+
@property
19+
def value(self) -> str:
20+
return "None"
1821

1922

2023
class GroundednessEvaluator(PromptyEvaluatorBase[Union[str, float]]):
@@ -165,7 +168,7 @@ def __call__( # pylint: disable=docstring-missing-param
165168
prompty_model_config = construct_prompty_model_config(
166169
validate_model_config(self._model_config),
167170
self._DEFAULT_OPEN_API_VERSION,
168-
USER_AGENT,
171+
UserAgentSingleton().value,
169172
)
170173
self._flow = AsyncPrompty.load(source=self._prompty_file, model=prompty_model_config)
171174

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/_http_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from typing_extensions import Self, Unpack
99

10-
from azure.ai.evaluation._user_agent import USER_AGENT
10+
from azure.ai.evaluation._user_agent import UserAgentSingleton
1111
from azure.core.configuration import Configuration
1212
from azure.core.pipeline import AsyncPipeline, Pipeline
1313
from azure.core.pipeline.policies import (
@@ -454,7 +454,7 @@ def get_http_client(**kwargs: Any) -> HttpPipeline:
454454
:returns: An HttpPipeline with a set of applied policies:
455455
:rtype: HttpPipeline
456456
"""
457-
kwargs.setdefault("user_agent_policy", UserAgentPolicy(base_user_agent=USER_AGENT))
457+
kwargs.setdefault("user_agent_policy", UserAgentPolicy(base_user_agent=UserAgentSingleton().value))
458458
return HttpPipeline(**kwargs)
459459

460460

@@ -464,5 +464,5 @@ def get_async_http_client(**kwargs: Any) -> AsyncHttpPipeline:
464464
:returns: An AsyncHttpPipeline with a set of applied policies:
465465
:rtype: AsyncHttpPipeline
466466
"""
467-
kwargs.setdefault("user_agent_policy", UserAgentPolicy(base_user_agent=USER_AGENT))
467+
kwargs.setdefault("user_agent_policy", UserAgentPolicy(base_user_agent=UserAgentSingleton().value))
468468
return AsyncHttpPipeline(**kwargs)

0 commit comments

Comments
 (0)