Skip to content

Commit 6a57db2

Browse files
authored
Add batch timeout override (#3501)
# Description Add environment variable option to set batch timeout limit. Evaluation is only local currently, so no need to check user input. # All Promptflow Contribution checklist: - [x] **The pull request does not introduce [breaking changes].** - [x] **CHANGELOG is updated for new features, bug fixes or other significant changes.** - [x] **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 - [x] Title of the pull request is clear and informative. - [x] 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 - [x] Pull request includes test coverage for the included changes.
1 parent 274121e commit 6a57db2

File tree

7 files changed

+64
-10
lines changed

7 files changed

+64
-10
lines changed

src/promptflow-evals/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features Added
66
- Introduced `JailbreakAdversarialSimulator` for customers who need to do run jailbreak and non jailbreak adversarial simulations at the same time. More info in the README.md in `/promptflow/evals/synthetic/README.md#jailbreak-simulator`
7+
- Exposed batch evaluation run timeout via "PF_BATCH_TIMEOUT_SEC" environment variable. This variable can be used to set the timeout for the batch evaluation for each evaluator and target separately only, not the entire API call.
78

89
### Bugs Fixed
910
- Large simulation was causing a jinja exception, this has been fixed.

src/promptflow-evals/promptflow/evals/_constants.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ class Prefixes:
2323

2424
CONTENT_SAFETY_DEFECT_RATE_THRESHOLD_DEFAULT = 4
2525

26-
BATCH_RUN_TIMEOUT = 3600
26+
PF_BATCH_TIMEOUT_SEC_DEFAULT = 3600
27+
PF_BATCH_TIMEOUT_SEC = "PF_BATCH_TIMEOUT_SEC"

src/promptflow-evals/promptflow/evals/evaluate/_batch_run_client/batch_run_context.py

+10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from promptflow._sdk._constants import PF_FLOW_ENTRY_IN_TMP, PF_FLOW_META_LOAD_IN_SUBPROCESS
77
from promptflow._utils.user_agent_utils import ClientUserAgentUtil
8+
from promptflow.evals._constants import PF_BATCH_TIMEOUT_SEC, PF_BATCH_TIMEOUT_SEC_DEFAULT
89
from promptflow.tracing._integrations._openai_injector import inject_openai_api, recover_openai_api
910

1011
from ..._user_agent import USER_AGENT
@@ -16,6 +17,7 @@
1617
class BatchRunContext:
1718
def __init__(self, client):
1819
self.client = client
20+
self._is_timeout_set_by_system = False
1921

2022
def __enter__(self):
2123
if isinstance(self.client, CodeClient):
@@ -26,6 +28,10 @@ def __enter__(self):
2628
os.environ[PF_FLOW_ENTRY_IN_TMP] = "true"
2729
os.environ[PF_FLOW_META_LOAD_IN_SUBPROCESS] = "false"
2830

31+
if os.environ.get(PF_BATCH_TIMEOUT_SEC) is None:
32+
os.environ[PF_BATCH_TIMEOUT_SEC] = str(PF_BATCH_TIMEOUT_SEC_DEFAULT)
33+
self._is_timeout_set_by_system = True
34+
2935
# For addressing the issue of asyncio event loop closed on Windows
3036
set_event_loop_policy()
3137

@@ -36,3 +42,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
3642
if isinstance(self.client, ProxyClient):
3743
os.environ.pop(PF_FLOW_ENTRY_IN_TMP, None)
3844
os.environ.pop(PF_FLOW_META_LOAD_IN_SUBPROCESS, None)
45+
46+
if self._is_timeout_set_by_system:
47+
os.environ.pop(PF_BATCH_TIMEOUT_SEC, None)
48+
self._is_timeout_set_by_system = False

src/promptflow-evals/promptflow/evals/evaluate/_batch_run_client/code_client.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
import pandas as pd
99

1010
from promptflow.contracts.types import AttrDict
11-
from promptflow.evals.evaluate._utils import _apply_column_mapping, _has_aggregator, load_jsonl
11+
from promptflow.evals.evaluate._utils import _apply_column_mapping, _has_aggregator, get_int_env_var, load_jsonl
1212
from promptflow.tracing import ThreadPoolExecutorWithContext as ThreadPoolExecutor
1313

14-
from ..._constants import BATCH_RUN_TIMEOUT
14+
from ..._constants import PF_BATCH_TIMEOUT_SEC, PF_BATCH_TIMEOUT_SEC_DEFAULT
1515

1616
LOGGER = logging.getLogger(__name__)
1717

@@ -24,15 +24,17 @@ def __init__(self, run, input_data, evaluator_name=None, aggregated_metrics=None
2424
self.aggregated_metrics = aggregated_metrics
2525

2626
def get_result_df(self, exclude_inputs=False):
27-
result_df = self.run.result(timeout=BATCH_RUN_TIMEOUT)
27+
batch_run_timeout = get_int_env_var(PF_BATCH_TIMEOUT_SEC, PF_BATCH_TIMEOUT_SEC_DEFAULT)
28+
result_df = self.run.result(timeout=batch_run_timeout)
2829
if exclude_inputs:
2930
result_df = result_df.drop(columns=[col for col in result_df.columns if col.startswith("inputs.")])
3031
return result_df
3132

3233
def get_aggregated_metrics(self):
3334
try:
35+
batch_run_timeout = get_int_env_var(PF_BATCH_TIMEOUT_SEC, PF_BATCH_TIMEOUT_SEC_DEFAULT)
3436
aggregated_metrics = (
35-
self.aggregated_metrics.result(timeout=BATCH_RUN_TIMEOUT)
37+
self.aggregated_metrics.result(timeout=batch_run_timeout)
3638
if self.aggregated_metrics is not None
3739
else None
3840
)

src/promptflow-evals/promptflow/evals/evaluate/_batch_run_client/proxy_client.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
from promptflow.client import PFClient
1010
from promptflow.tracing import ThreadPoolExecutorWithContext as ThreadPoolExecutor
1111

12-
from ..._constants import BATCH_RUN_TIMEOUT
13-
1412
LOGGER = logging.getLogger(__name__)
1513

1614

@@ -41,13 +39,13 @@ def run(self, flow, data, column_mapping=None, **kwargs):
4139
return ProxyRun(run=eval_future)
4240

4341
def get_details(self, proxy_run, all_results=False):
44-
run = proxy_run.run.result(timeout=BATCH_RUN_TIMEOUT)
42+
run = proxy_run.run.result()
4543
result_df = self._pf_client.get_details(run, all_results=all_results)
4644
result_df.replace("(Failed)", np.nan, inplace=True)
4745
return result_df
4846

4947
def get_metrics(self, proxy_run):
50-
run = proxy_run.run.result(timeout=BATCH_RUN_TIMEOUT)
48+
run = proxy_run.run.result()
5149
return self._pf_client.get_metrics(run)
5250

5351
@staticmethod

src/promptflow-evals/promptflow/evals/evaluate/_utils.py

+16
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,22 @@ def _has_aggregator(evaluator):
204204
return hasattr(evaluator, "__aggregate__")
205205

206206

207+
def get_int_env_var(env_var_name, default_value=None):
208+
"""
209+
The function `get_int_env_var` retrieves an integer environment variable value, with an optional
210+
default value if the variable is not set or cannot be converted to an integer.
211+
212+
:param env_var_name: The name of the environment variable you want to retrieve the value of
213+
:param default_value: The default value is the value that will be returned if the environment
214+
variable is not found or if it cannot be converted to an integer
215+
:return: an integer value.
216+
"""
217+
try:
218+
return int(os.environ.get(env_var_name, default_value))
219+
except Exception:
220+
return default_value
221+
222+
207223
def set_event_loop_policy():
208224
import asyncio
209225
import platform

src/promptflow-evals/tests/evals/unittests/test_batch_run_context.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import os
12
from unittest.mock import MagicMock
23

34
import pytest
45

56
from promptflow.client import PFClient
7+
from promptflow.evals._constants import PF_BATCH_TIMEOUT_SEC, PF_BATCH_TIMEOUT_SEC_DEFAULT
68
from promptflow.evals._user_agent import USER_AGENT
7-
from promptflow.evals.evaluate._batch_run_client import BatchRunContext, CodeClient
9+
from promptflow.evals.evaluate._batch_run_client import BatchRunContext, CodeClient, ProxyClient
810

911

1012
@pytest.fixture
@@ -50,3 +52,27 @@ def test_with_pfclient(self, mocker, pf_client_mock):
5052
pass
5153

5254
mock_recover_openai_api.assert_not_called()
55+
56+
def test_batch_timeout_default(self):
57+
before_timeout = os.environ.get(PF_BATCH_TIMEOUT_SEC)
58+
assert before_timeout is None
59+
60+
with BatchRunContext(ProxyClient(PFClient)):
61+
during_timeout = int(os.environ.get(PF_BATCH_TIMEOUT_SEC))
62+
assert during_timeout == PF_BATCH_TIMEOUT_SEC_DEFAULT
63+
64+
# Default timeout should be reset after exiting BatchRunContext
65+
after_timeout = os.environ.get(PF_BATCH_TIMEOUT_SEC)
66+
assert after_timeout is None
67+
68+
def test_batch_timeout_custom(self):
69+
custom_timeout = 1000
70+
os.environ[PF_BATCH_TIMEOUT_SEC] = str(custom_timeout)
71+
72+
with BatchRunContext(ProxyClient(PFClient)):
73+
during_timeout = int(os.environ.get(PF_BATCH_TIMEOUT_SEC))
74+
assert during_timeout == custom_timeout
75+
76+
# Custom timeouts should not be reset after exiting BatchRunContext
77+
after_timeout = int(os.environ.get(PF_BATCH_TIMEOUT_SEC))
78+
assert after_timeout == custom_timeout

0 commit comments

Comments
 (0)