Skip to content

Commit 3a43719

Browse files
authored
Evaluation: Fix file being used by another process error during target function execution (Azure#38021)
* Evaluation: Disable target snapshot upload by default * update changlog
1 parent 6564d25 commit 3a43719

File tree

8 files changed

+69
-32
lines changed

8 files changed

+69
-32
lines changed

sdk/evaluation/azure-ai-evaluation/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
### Bugs Fixed
1212
- Non adversarial simulator works with `gpt-4o` models using the `json_schema` response format
13+
- Fixed an issue where the `evaluate` API would fail with "[WinError 32] The process cannot access the file because it is being used by another process" when venv folder and target function file are in the same directory.
1314
- Fix evaluate API failure when `trace.destination` is set to `none`
1415

1516
### Other Changes
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# ---------------------------------------------------------
22
# Copyright (c) Microsoft Corporation. All rights reserved.
33
# ---------------------------------------------------------
4-
from .batch_run_context import BatchRunContext
4+
from .eval_run_context import EvalRunContext
55
from .code_client import CodeClient
66
from .proxy_client import ProxyClient
7+
from .target_run_context import TargetRunContext
78

8-
__all__ = ["CodeClient", "ProxyClient", "BatchRunContext"]
9+
__all__ = ["CodeClient", "ProxyClient", "EvalRunContext", "TargetRunContext"]
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
from .proxy_client import ProxyClient
2323

2424

25-
class BatchRunContext:
26-
"""Context manager for batch run clients.
25+
class EvalRunContext:
26+
"""Context manager for eval batch run.
2727
2828
:param client: The client to run in the context.
2929
:type client: Union[
30-
~azure.ai.evaluation._evaluate._batch_run_client.code_client.CodeClient,
31-
~azure.ai.evaluation._evaluate._batch_run_client.proxy_client.ProxyClient
30+
~azure.ai.evaluation._evaluate._batch_run.code_client.CodeClient,
31+
~azure.ai.evaluation._evaluate._batch_run.proxy_client.ProxyClient
3232
]
3333
"""
3434

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# ---------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# ---------------------------------------------------------
4+
import os
5+
import types
6+
from typing import Optional, Type
7+
8+
from promptflow._sdk._constants import PF_FLOW_ENTRY_IN_TMP
9+
10+
11+
class TargetRunContext:
12+
"""Context manager for target batch run.
13+
14+
:param upload_snapshot: Whether to upload target snapshot.
15+
:type upload_snapshot: bool
16+
"""
17+
18+
def __init__(self, upload_snapshot: bool) -> None:
19+
self._upload_snapshot = upload_snapshot
20+
21+
def __enter__(self) -> None:
22+
# Address "[WinError 32] The process cannot access the file" error,
23+
# caused by conflicts when the venv and target function are in the same directory.
24+
# Setting PF_FLOW_ENTRY_IN_TMP to true uploads only the flex entry file (flow.flex.yaml).
25+
if not self._upload_snapshot:
26+
os.environ[PF_FLOW_ENTRY_IN_TMP] = "true"
27+
28+
def __exit__(
29+
self,
30+
exc_type: Optional[Type[BaseException]],
31+
exc_value: Optional[BaseException],
32+
exc_tb: Optional[types.TracebackType],
33+
) -> None:
34+
if not self._upload_snapshot:
35+
os.environ.pop(PF_FLOW_ENTRY_IN_TMP, None)

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

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
)
2727
from .._model_configurations import AzureAIProject, EvaluationResult, EvaluatorConfig
2828
from .._user_agent import USER_AGENT
29-
from ._batch_run_client import BatchRunContext, CodeClient, ProxyClient
29+
from ._batch_run import EvalRunContext, CodeClient, ProxyClient, TargetRunContext
3030
from ._utils import (
3131
_apply_column_mapping,
3232
_log_metrics_and_instance_results,
@@ -395,7 +395,7 @@ def _apply_target_to_data(
395395
pf_client: PFClient,
396396
initial_data: pd.DataFrame,
397397
evaluation_name: Optional[str] = None,
398-
_run_name: Optional[str] = None,
398+
**kwargs,
399399
) -> Tuple[pd.DataFrame, Set[str], Run]:
400400
"""
401401
Apply the target function to the data set and return updated data and generated columns.
@@ -410,22 +410,22 @@ def _apply_target_to_data(
410410
:type initial_data: pd.DataFrame
411411
:param evaluation_name: The name of the evaluation.
412412
:type evaluation_name: Optional[str]
413-
:param _run_name: The name of target run. Used for testing only.
414-
:type _run_name: Optional[str]
415413
:return: The tuple, containing data frame and the list of added columns.
416414
:rtype: Tuple[pandas.DataFrame, List[str]]
417415
"""
418-
# We are manually creating the temporary directory for the flow
419-
# because the way tempdir remove temporary directories will
420-
# hang the debugger, because promptflow will keep flow directory.
421-
run: Run = pf_client.run(
422-
flow=target,
423-
display_name=evaluation_name,
424-
data=data,
425-
properties={EvaluationRunProperties.RUN_TYPE: "eval_run", "isEvaluatorRun": "true"},
426-
stream=True,
427-
name=_run_name,
428-
)
416+
_run_name = kwargs.get("_run_name")
417+
upload_target_snaphot = kwargs.get("_upload_target_snapshot", False)
418+
419+
with TargetRunContext(upload_target_snaphot):
420+
run: Run = pf_client.run(
421+
flow=target,
422+
display_name=evaluation_name,
423+
data=data,
424+
properties={EvaluationRunProperties.RUN_TYPE: "eval_run", "isEvaluatorRun": "true"},
425+
stream=True,
426+
name=_run_name,
427+
)
428+
429429
target_output: pd.DataFrame = pf_client.runs.get_details(run, all_results=True)
430430
# Remove input and output prefix
431431
generated_columns = {
@@ -706,7 +706,7 @@ def _evaluate( # pylint: disable=too-many-locals,too-many-statements
706706
target_generated_columns: Set[str] = set()
707707
if data is not None and target is not None:
708708
input_data_df, target_generated_columns, target_run = _apply_target_to_data(
709-
target, data, pf_client, input_data_df, evaluation_name, _run_name=kwargs.get("_run_name")
709+
target, data, pf_client, input_data_df, evaluation_name, **kwargs
710710
)
711711

712712
for evaluator_name, mapping in column_mapping.items():
@@ -738,7 +738,7 @@ def _evaluate( # pylint: disable=too-many-locals,too-many-statements
738738
def eval_batch_run(
739739
batch_run_client: TClient, *, data=Union[str, os.PathLike, pd.DataFrame]
740740
) -> Dict[str, __EvaluatorInfo]:
741-
with BatchRunContext(batch_run_client):
741+
with EvalRunContext(batch_run_client):
742742
runs = {
743743
evaluator_name: batch_run_client.run(
744744
flow=evaluator,
@@ -752,7 +752,7 @@ def eval_batch_run(
752752
for evaluator_name, evaluator in evaluators.items()
753753
}
754754

755-
# get_details needs to be called within BatchRunContext scope in order to have user agent populated
755+
# get_details needs to be called within EvalRunContext scope in order to have user agent populated
756756
return {
757757
evaluator_name: {
758758
"result": batch_run_client.get_details(run, all_results=True),

sdk/evaluation/azure-ai-evaluation/tests/unittests/test_batch_run_context.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from azure.ai.evaluation._constants import PF_BATCH_TIMEOUT_SEC, PF_BATCH_TIMEOUT_SEC_DEFAULT
88
from azure.ai.evaluation._user_agent import USER_AGENT
9-
from azure.ai.evaluation._evaluate._batch_run_client import BatchRunContext, CodeClient, ProxyClient
9+
from azure.ai.evaluation._evaluate._batch_run import EvalRunContext, CodeClient, ProxyClient
1010

1111

1212
@pytest.fixture
@@ -20,15 +20,15 @@ def pf_client_mock():
2020

2121

2222
@pytest.mark.unittest
23-
class TestBatchRunContext:
23+
class TestEvalRunContext:
2424
def test_with_codeclient(self, mocker, code_client_mock):
2525
mock_append_user_agent = mocker.patch(
2626
"promptflow._utils.user_agent_utils.ClientUserAgentUtil.append_user_agent"
2727
)
2828
mock_inject_openai_api = mocker.patch("promptflow.tracing._integrations._openai_injector.inject_openai_api")
2929
mock_recover_openai_api = mocker.patch("promptflow.tracing._integrations._openai_injector.recover_openai_api")
3030

31-
with BatchRunContext(code_client_mock):
31+
with EvalRunContext(code_client_mock):
3232
# TODO: Failed to mock inject_openai_api and recover_openai_api for some reason.
3333
# Need to investigate further.
3434
# mock_inject_openai_api.assert_called_once()
@@ -46,7 +46,7 @@ def test_with_pfclient(self, mocker, pf_client_mock):
4646
mock_inject_openai_api = mocker.patch("promptflow.tracing._integrations._openai_injector.inject_openai_api")
4747
mock_recover_openai_api = mocker.patch("promptflow.tracing._integrations._openai_injector.recover_openai_api")
4848

49-
with BatchRunContext(code_client_mock):
49+
with EvalRunContext(code_client_mock):
5050
mock_append_user_agent.assert_not_called()
5151
mock_inject_openai_api.assert_not_called()
5252
pass
@@ -57,22 +57,22 @@ def test_batch_timeout_default(self):
5757
before_timeout = os.environ.get(PF_BATCH_TIMEOUT_SEC)
5858
assert before_timeout is None
5959

60-
with BatchRunContext(ProxyClient(PFClient)):
60+
with EvalRunContext(ProxyClient(PFClient)):
6161
during_timeout = int(os.environ.get(PF_BATCH_TIMEOUT_SEC))
6262
assert during_timeout == PF_BATCH_TIMEOUT_SEC_DEFAULT
6363

64-
# Default timeout should be reset after exiting BatchRunContext
64+
# Default timeout should be reset after exiting EvalRunContext
6565
after_timeout = os.environ.get(PF_BATCH_TIMEOUT_SEC)
6666
assert after_timeout is None
6767

6868
def test_batch_timeout_custom(self):
6969
custom_timeout = 1000
7070
os.environ[PF_BATCH_TIMEOUT_SEC] = str(custom_timeout)
7171

72-
with BatchRunContext(ProxyClient(PFClient)):
72+
with EvalRunContext(ProxyClient(PFClient)):
7373
during_timeout = int(os.environ.get(PF_BATCH_TIMEOUT_SEC))
7474
assert during_timeout == custom_timeout
7575

76-
# Custom timeouts should not be reset after exiting BatchRunContext
76+
# Custom timeouts should not be reset after exiting EvalRunContext
7777
after_timeout = int(os.environ.get(PF_BATCH_TIMEOUT_SEC))
7878
assert after_timeout == custom_timeout

0 commit comments

Comments
 (0)