Skip to content

Commit 38aa901

Browse files
committed
Merge branch 'main' of https://github.com/microsoft/promptflow into nirovins/change_logging_mechanism
2 parents 6f1dd13 + 9f51888 commit 38aa901

21 files changed

Lines changed: 807 additions & 492 deletions

File tree

‎.github/workflows/promptflow-release-testing-matrix.yml‎

Lines changed: 279 additions & 235 deletions
Large diffs are not rendered by default.

‎src/promptflow-azure/promptflow/azure/operations/_arm_connection_operations.py‎

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
_ScopeDependentOperations,
1111
)
1212

13-
from promptflow._sdk._errors import ConnectionClassNotFoundError
14-
from promptflow._sdk.entities._connection import CustomConnection, _Connection
13+
from promptflow._sdk.entities._connection import _Connection
1514
from promptflow.azure._restclient.flow_service_caller import FlowServiceCaller
1615
from promptflow.core._connection_provider._workspace_connection_provider import WorkspaceConnectionProvider
1716
from promptflow.core._errors import OpenURLFailedUserError
@@ -45,29 +44,8 @@ def __init__(
4544
self._credential,
4645
)
4746

48-
@classmethod
49-
def _convert_core_connection_to_sdk_connection(cls, core_conn):
50-
# TODO: Refine this and connection operation ones to (devkit) _Connection._from_core_object
51-
sdk_conn_mapping = _Connection.SUPPORTED_TYPES
52-
sdk_conn_cls = sdk_conn_mapping.get(core_conn.type)
53-
if sdk_conn_cls is None:
54-
raise ConnectionClassNotFoundError(
55-
f"Correspond sdk connection type not found for core connection type: {core_conn.type!r}, "
56-
f"please re-install the 'promptflow' package."
57-
)
58-
common_args = {
59-
"name": core_conn.name,
60-
"module": core_conn.module,
61-
"expiry_time": core_conn.expiry_time,
62-
"created_date": core_conn.created_date,
63-
"last_modified_date": core_conn.last_modified_date,
64-
}
65-
if sdk_conn_cls is CustomConnection:
66-
return sdk_conn_cls(configs=core_conn.configs, secrets=core_conn.secrets, **common_args)
67-
return sdk_conn_cls(**dict(core_conn), **common_args)
68-
6947
def get(self, name, **kwargs):
70-
return self._convert_core_connection_to_sdk_connection(self._provider.get(name))
48+
return _Connection._from_core_connection(self._provider.get(name))
7149

7250
@classmethod
7351
def _direct_get(cls, name, subscription_id, resource_group_name, workspace_name, credential):
@@ -76,7 +54,7 @@ def _direct_get(cls, name, subscription_id, resource_group_name, workspace_name,
7654
permission(workspace/list secrets). As create azure pf_client requires workspace read permission.
7755
"""
7856
provider = WorkspaceConnectionProvider(subscription_id, resource_group_name, workspace_name, credential)
79-
return provider.get(name=name)
57+
return _Connection._from_core_connection(provider.get(name=name))
8058

8159
# Keep this as promptflow tools is using this method
8260
_build_connection_dict = WorkspaceConnectionProvider._build_connection_dict

‎src/promptflow-core/promptflow/core/_connection_provider/_workspace_connection_provider.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,11 @@ def open_url(cls, token, url, action, host="management.azure.com", method="GET",
167167

168168
@classmethod
169169
def validate_and_fallback_connection_type(cls, name, type_name, category, metadata):
170+
# Note: Legacy CustomKeys may store different connection types, e.g. openai, serp.
171+
# In this case, type name will not be None.
170172
if type_name:
171173
return type_name
172174
# Below category has corresponding connection type in PromptFlow, so we can fall back directly.
173-
# Note: CustomKeys may store different connection types for now, e.g. openai, serp.
174175
if category in [
175176
ConnectionCategory.AzureOpenAI,
176177
ConnectionCategory.OpenAI,
@@ -179,6 +180,8 @@ def validate_and_fallback_connection_type(cls, name, type_name, category, metada
179180
ConnectionCategory.Serverless,
180181
]:
181182
return category
183+
if category == ConnectionCategory.CustomKeys:
184+
return CustomConnection.__name__
182185
if category == ConnectionCategory.CognitiveService:
183186
kind = get_case_insensitive_key(metadata, "Kind")
184187
if kind == "Content Safety":
@@ -343,6 +346,8 @@ def _build_connection_dict(cls, name, subscription_id, resource_group_name, work
343346
raise OpenURLUserAuthenticationError(message=auth_error_message)
344347
except ClientAuthenticationError as e:
345348
raise UserErrorException(target=ErrorTarget.CORE, message=str(e), error=e)
349+
except UserErrorException:
350+
raise
346351
except Exception as e:
347352
raise SystemErrorException(target=ErrorTarget.CORE, message=str(e), error=e)
348353

‎src/promptflow-core/promptflow/core/_serving/monitor/metrics.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def record_tracing_metrics(self, flow_run: FlowRunInfo, node_runs: Dict[str, Run
213213
try:
214214
for _, run in node_runs.items():
215215
flow_id = flow_run.flow_id if flow_run is not None else "default"
216-
if len(run.system_metrics) > 0:
216+
if run.system_metrics and len(run.system_metrics) > 0:
217217
duration = run.system_metrics.get("duration", None)
218218
if duration is not None:
219219
duration = duration * 1000

‎src/promptflow-core/promptflow/executor/_process_manager.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
ProcessTerminatedTimeout,
2121
SpawnedForkProcessManagerStartFailure,
2222
)
23+
from promptflow.executor._prompty_executor import PromptyExecutor
2324
from promptflow.executor._script_executor import ScriptExecutor
2425
from promptflow.executor.flow_executor import FlowExecutor
2526
from promptflow.storage import AbstractRunStorage
@@ -509,6 +510,13 @@ def create_spawned_fork_process_manager(
509510

510511

511512
def _create_executor_fork(*, flow_executor: FlowExecutor, storage: AbstractRunStorage):
513+
if isinstance(flow_executor, PromptyExecutor):
514+
return PromptyExecutor(
515+
flow_file=flow_executor._flow_file,
516+
connections=flow_executor._connections,
517+
working_dir=flow_executor._working_dir,
518+
storage=storage,
519+
)
512520
if isinstance(flow_executor, ScriptExecutor):
513521
return ScriptExecutor(
514522
flow_file=flow_executor._flow_file,

‎src/promptflow-core/promptflow/executor/_script_executor.py‎

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import asyncio
2+
import contextlib
23
import dataclasses
4+
import functools
35
import importlib
46
import inspect
57
import uuid
@@ -60,6 +62,15 @@ def __init__(
6062
self._message_format = MessageFormatType.BASIC
6163
self._multimedia_processor = BasicMultimediaProcessor()
6264

65+
@contextlib.contextmanager
66+
def _exec_line_context(self, run_id, line_number):
67+
# TODO: refactor NodeLogManager, for script executor, we don't have node concept.
68+
log_manager = NodeLogManager()
69+
# No need to clear node context, log_manger will be cleared after the with block.
70+
log_manager.set_node_context(run_id, "Flex", line_number)
71+
with log_manager, self._update_operation_context(run_id, line_number):
72+
yield
73+
6374
def exec_line(
6475
self,
6576
inputs: Mapping[str, Any],
@@ -69,20 +80,16 @@ def exec_line(
6980
**kwargs,
7081
) -> LineResult:
7182
run_id = run_id or str(uuid.uuid4())
72-
# TODO: refactor NodeLogManager, for script executor, we don't have node concept.
73-
log_manager = NodeLogManager()
74-
# No need to clear node context, log_manger will be cleared after the with block.
75-
log_manager.set_node_context(run_id, "Flex", index)
76-
with log_manager, self._update_operation_context(run_id, index):
83+
with self._exec_line_context(run_id, index):
7784
return self._exec_line(inputs, index, run_id, allow_generator_output=allow_generator_output)
7885

79-
def _exec_line(
86+
def _exec_line_preprocess(
8087
self,
8188
inputs: Mapping[str, Any],
8289
index: Optional[int] = None,
8390
run_id: Optional[str] = None,
8491
allow_generator_output: bool = False,
85-
) -> LineResult:
92+
):
8693
line_run_id = run_id if index is None else f"{run_id}_{index}"
8794
run_tracker = RunTracker(self._storage)
8895
run_tracker.allow_generator_types = allow_generator_output
@@ -98,8 +105,22 @@ def _exec_line(
98105
# Executor will add line_number to batch inputs if there is no line_number in the original inputs,
99106
# which should be removed, so, we only preserve the inputs that are contained in self._inputs.
100107
inputs = {k: inputs[k] for k in self._inputs if k in inputs}
101-
output = None
102-
traces = []
108+
return run_info, inputs, run_tracker, None, []
109+
110+
def _exec_line(
111+
self,
112+
inputs: Mapping[str, Any],
113+
index: Optional[int] = None,
114+
run_id: Optional[str] = None,
115+
allow_generator_output: bool = False,
116+
) -> LineResult:
117+
run_info, inputs, run_tracker, output, traces = self._exec_line_preprocess(
118+
inputs,
119+
index,
120+
run_id,
121+
allow_generator_output,
122+
)
123+
line_run_id = run_info.run_id
103124
try:
104125
Tracer.start_tracing(line_run_id)
105126
if self._is_async:
@@ -118,12 +139,60 @@ def _exec_line(
118139
run_tracker.end_run(line_run_id, ex=e, traces=traces)
119140
finally:
120141
run_tracker.persist_flow_run(run_info)
142+
return self._construct_line_result(output, run_info)
143+
144+
def _construct_line_result(self, output, run_info):
121145
line_result = LineResult(output, {}, run_info, {})
122146
# Return line result with index
123-
if index is not None and isinstance(line_result.output, dict):
124-
line_result.output[LINE_NUMBER_KEY] = index
147+
if run_info.index is not None and isinstance(line_result.output, dict):
148+
line_result.output[LINE_NUMBER_KEY] = run_info.index
125149
return line_result
126150

151+
async def exec_line_async(
152+
self,
153+
inputs: Mapping[str, Any],
154+
index: Optional[int] = None,
155+
run_id: Optional[str] = None,
156+
allow_generator_output: bool = False,
157+
**kwargs,
158+
) -> LineResult:
159+
run_id = run_id or str(uuid.uuid4())
160+
with self._exec_line_context(run_id, index):
161+
return await self._exec_line_async(inputs, index, run_id, allow_generator_output=allow_generator_output)
162+
163+
async def _exec_line_async(
164+
self,
165+
inputs: Mapping[str, Any],
166+
index: Optional[int] = None,
167+
run_id: Optional[str] = None,
168+
allow_generator_output: bool = False,
169+
) -> LineResult:
170+
run_info, inputs, run_tracker, output, traces = self._exec_line_preprocess(
171+
inputs,
172+
index,
173+
run_id,
174+
allow_generator_output,
175+
)
176+
line_run_id = run_info.run_id
177+
try:
178+
Tracer.start_tracing(line_run_id)
179+
if self._is_async:
180+
output = await self._func(**inputs)
181+
else:
182+
partial_func = functools.partial(self._func, **inputs)
183+
output = await asyncio.get_event_loop().run_in_executor(None, partial_func)
184+
output = self._stringify_generator_output(output) if not allow_generator_output else output
185+
traces = Tracer.end_tracing(line_run_id)
186+
output_dict = convert_eager_flow_output_to_dict(output)
187+
run_tracker.end_run(line_run_id, result=output_dict, traces=traces)
188+
except Exception as e:
189+
if not traces:
190+
traces = Tracer.end_tracing(line_run_id)
191+
run_tracker.end_run(line_run_id, ex=e, traces=traces)
192+
finally:
193+
run_tracker.persist_flow_run(run_info)
194+
return self._construct_line_result(output, run_info)
195+
127196
def _stringify_generator_output(self, output):
128197
if isinstance(output, dict):
129198
return super()._stringify_generator_output(output)

‎src/promptflow-core/tests/azureml-serving/unittests/test_workspace_connection_provider.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,33 @@ def test_build_azure_openai_connection_from_rest_object(self):
5656
}
5757
build_from_data_and_assert(data, expected)
5858

59+
def test_build_legacy_openai_connection_from_rest_object(self):
60+
# Legacy OpenAI connection with type in metadata
61+
# Test this not convert to CustomConnection
62+
data = {
63+
"id": "mock_id",
64+
"name": "legacy_open_ai",
65+
"type": "Microsoft.MachineLearningServices/workspaces/connections",
66+
"properties": {
67+
"authType": "CustomKeys",
68+
"credentials": {"keys": {"api_key": "***"}},
69+
"category": "CustomKeys",
70+
"target": "<api-base>",
71+
"metadata": {
72+
"azureml.flow.connection_type": "OpenAI",
73+
"azureml.flow.module": "promptflow.connections",
74+
"organization": "mock",
75+
},
76+
},
77+
}
78+
expected = {
79+
"type": "OpenAIConnection",
80+
"module": "promptflow.connections",
81+
"name": "legacy_open_ai",
82+
"value": {"api_key": "***", "organization": "mock"},
83+
}
84+
build_from_data_and_assert(data, expected)
85+
5986
def test_build_strong_type_openai_connection_from_rest_object(self):
6087
data = {
6188
"id": "mock_id",
@@ -199,6 +226,31 @@ def test_build_custom_keys_connection_from_rest_object(self):
199226
}
200227
build_from_data_and_assert(data, expected)
201228

229+
def test_build_strong_type_custom_connection_from_rest_object(self):
230+
# Test on CustomKeys type without meta
231+
data = {
232+
"id": "mock_id",
233+
"name": "custom_connection",
234+
"type": "Microsoft.MachineLearningServices/workspaces/connections",
235+
"properties": {
236+
"authType": "CustomKeys",
237+
"credentials": {"keys": {"my_key1": "***", "my_key2": "***"}},
238+
"category": "CustomKeys",
239+
"target": "<api-base>",
240+
"metadata": {
241+
"general_key": "general_value",
242+
},
243+
},
244+
}
245+
expected = {
246+
"type": "CustomConnection",
247+
"module": "promptflow.connections",
248+
"name": "custom_connection",
249+
"value": {"my_key1": "***", "my_key2": "***", "general_key": "general_value"},
250+
"secret_keys": ["my_key1", "my_key2"],
251+
}
252+
build_from_data_and_assert(data, expected)
253+
202254
def test_build_cognitive_search_connection_from_rest_object(self):
203255
# Test on ApiKey type with CognitiveSearch category
204256
data = {

‎src/promptflow-core/tests/core/e2etests/test_eager_flow.py‎

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
from dataclasses import is_dataclass
23

34
import pytest
@@ -25,6 +26,18 @@ def func_entry(input_str: str) -> str:
2526
return "Hello " + input_str
2627

2728

29+
async def func_entry_async(input_str: str) -> str:
30+
await asyncio.sleep(1)
31+
return "Hello " + input_str
32+
33+
34+
function_entries = [
35+
(ClassEntry(), {"input_str": "world"}, "Hello world"),
36+
(func_entry, {"input_str": "world"}, "Hello world"),
37+
(func_entry_async, {"input_str": "world"}, "Hello world"),
38+
]
39+
40+
2841
@pytest.mark.e2etest
2942
class TestEagerFlow:
3043
@pytest.mark.parametrize(
@@ -64,16 +77,28 @@ def test_flow_run(self, flow_folder, inputs, ensure_output, init_kwargs):
6477
line_result2 = executor.exec_line(inputs=inputs, index=0)
6578
assert line_result1.output == line_result2.output
6679

67-
@pytest.mark.parametrize(
68-
"entry, inputs, expected_output",
69-
[(ClassEntry(), {"input_str": "world"}, "Hello world"), (func_entry, {"input_str": "world"}, "Hello world")],
70-
)
80+
@pytest.mark.parametrize("entry, inputs, expected_output", function_entries)
7181
def test_flow_run_with_function_entry(self, entry, inputs, expected_output):
7282
executor = FlowExecutor.create(entry, {})
7383
line_result = executor.exec_line(inputs=inputs)
7484
assert line_result.run_info.status == Status.Completed
7585
assert line_result.output == expected_output
7686

87+
@pytest.mark.asyncio
88+
@pytest.mark.parametrize("entry, inputs, expected_output", function_entries)
89+
async def test_flow_run_with_function_entry_async(self, entry, inputs, expected_output):
90+
executor = FlowExecutor.create(entry, {})
91+
task1 = asyncio.create_task(executor.exec_line_async(inputs=inputs))
92+
task2 = asyncio.create_task(executor.exec_line_async(inputs=inputs))
93+
line_result1, line_result2 = await asyncio.gather(task1, task2)
94+
for line_result in [line_result1, line_result2]:
95+
assert line_result.run_info.status == Status.Completed
96+
assert line_result.output == expected_output
97+
delta_sec = (line_result2.run_info.end_time - line_result1.run_info.end_time).total_seconds()
98+
delta_desc = f"{delta_sec}s from {line_result1.run_info.end_time} to {line_result2.run_info.end_time}"
99+
msg = f"The two tasks should run concurrently, but got {delta_desc}"
100+
assert 0 <= delta_sec < 0.1, msg
101+
77102
def test_flow_run_with_invalid_case(self):
78103
flow_folder = "dummy_flow_with_exception"
79104
flow_file = get_yaml_file(flow_folder, root=EAGER_FLOW_ROOT)

0 commit comments

Comments
 (0)