Skip to content

Commit 8e52bd2

Browse files
cleop-googlecopybara-github
authored andcommitted
feat: GenAI SDK client(multimodal) - Return the assembled table as a DataFrame from datasets.assemble.
BREAKING CHANGE: `datasets.assemble` now returns `tuple[str, Optional[bigframes.pandas.DataFrame]]` (the BigQuery table id without the `bq://` prefix, and the assembled table as a BigFrames DataFrame) instead of the BigQuery URI `str`. By default no DataFrame is loaded (`load_dataframe=False`): the second element is `None`, no BigQuery read is performed, and `bigframes` is not required. Pass `load_dataframe=True` to load the assembled table. Before: ``` uri = client.datasets.assemble(name=..., gemini_request_read_config=...) ``` After: ``` # Returns a (table_id, dataframe) tuple; no DataFrame is loaded by default. table_id, _ = client.datasets.assemble(name=..., gemini_request_read_config=...) # Pass load_dataframe=True to also get the assembled table as a BigFrames DataFrame. table_id, df = client.datasets.assemble( name=..., gemini_request_read_config=..., load_dataframe=True ) ``` PiperOrigin-RevId: 938035395
1 parent 35edd83 commit 8e52bd2

3 files changed

Lines changed: 140 additions & 12 deletions

File tree

agentplatform/_genai/_datasets_utils.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,63 @@ async def save_dataframe_to_bigquery_async(
280280
await asyncio.to_thread(bq_client.delete_table, temp_table_id)
281281

282282

283+
def load_dataframe_from_bigquery(
284+
*,
285+
bigquery_uri: str,
286+
project: str,
287+
location: str,
288+
credentials: google.auth.credentials.Credentials,
289+
) -> "bigframes.pandas.DataFrame": # type: ignore # noqa: F821
290+
"""Loads a BigQuery table into a BigFrames DataFrame.
291+
292+
Args:
293+
bigquery_uri: The URI of the BigQuery table, with or without the `bq://`
294+
prefix.
295+
project: The project to use for the BigFrames session.
296+
location: The location to use for the BigFrames session.
297+
credentials: The credentials to use for the BigFrames session.
298+
299+
Returns:
300+
A BigFrames DataFrame backed by the BigQuery table.
301+
"""
302+
bigframes = _try_import_bigframes()
303+
session_options = bigframes.BigQueryOptions(
304+
credentials=credentials,
305+
project=project,
306+
location=location,
307+
)
308+
with bigframes.connect(session_options) as session:
309+
return session.read_gbq(bigquery_uri.removeprefix("bq://"))
310+
311+
312+
async def load_dataframe_from_bigquery_async(
313+
*,
314+
bigquery_uri: str,
315+
project: str,
316+
location: str,
317+
credentials: google.auth.credentials.Credentials,
318+
) -> "bigframes.pandas.DataFrame": # type: ignore # noqa: F821
319+
"""Loads a BigQuery table into a BigFrames DataFrame.
320+
321+
Args:
322+
bigquery_uri: The URI of the BigQuery table, with or without the `bq://`
323+
prefix.
324+
project: The project to use for the BigFrames session.
325+
location: The location to use for the BigFrames session.
326+
credentials: The credentials to use for the BigFrames session.
327+
328+
Returns:
329+
A BigFrames DataFrame backed by the BigQuery table.
330+
"""
331+
return await asyncio.to_thread(
332+
load_dataframe_from_bigquery,
333+
bigquery_uri=bigquery_uri,
334+
project=project,
335+
location=location,
336+
credentials=credentials,
337+
)
338+
339+
283340
def resolve_dataset_name(resource_name_or_id: str, project: str, location: str) -> str:
284341
"""Resolves a dataset name or ID to a full resource name."""
285342
if "/" not in resource_name_or_id:

agentplatform/_genai/datasets.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,8 +1324,9 @@ def assemble(
13241324
gemini_request_read_config: Optional[
13251325
types.GeminiRequestReadConfigOrDict
13261326
] = None,
1327+
load_dataframe: bool = False,
13271328
config: Optional[types.AssembleDatasetConfigOrDict] = None,
1328-
) -> str:
1329+
) -> tuple[str, Optional["bigframes.pandas.DataFrame"]]: # type: ignore # noqa: F821
13291330
"""Assemble the dataset into a BigQuery table.
13301331
13311332
Waits for the assemble operation to complete before returning.
@@ -1338,12 +1339,20 @@ def assemble(
13381339
Optional. The read config to use to assemble the dataset. If
13391340
not provided, the read config attached to the dataset will be
13401341
used.
1342+
load_dataframe:
1343+
Optional. Whether to load the assembled BigQuery table into a
1344+
BigFrames DataFrame and return it. If False, the returned
1345+
DataFrame is None, no BigQuery read is performed, and `bigframes`
1346+
is not required. Defaults to False.
13411347
config:
13421348
Optional. A configuration for assembling the dataset. If not
13431349
provided, the default configuration will be used.
13441350
13451351
Returns:
1346-
The URI of the bigquery table of the assembled dataset.
1352+
A tuple `(table_id, dataframe)`, where `table_id` is the BigQuery
1353+
table id of the assembled dataset (without the `bq://` prefix) and
1354+
`dataframe` is the assembled table loaded as a BigFrames DataFrame.
1355+
`dataframe` is None if `load_dataframe` is False.
13471356
"""
13481357
if isinstance(config, dict):
13491358
config = types.AssembleDatasetConfig(**config)
@@ -1363,7 +1372,17 @@ def assemble(
13631372
operation=operation,
13641373
timeout_seconds=config.timeout,
13651374
)
1366-
return response["bigqueryDestination"] # type: ignore[no-any-return]
1375+
bigquery_uri = response["bigqueryDestination"]
1376+
table_id = bigquery_uri.removeprefix("bq://")
1377+
dataframe = None
1378+
if load_dataframe:
1379+
dataframe = _datasets_utils.load_dataframe_from_bigquery(
1380+
bigquery_uri=bigquery_uri,
1381+
project=self._api_client.project,
1382+
location=self._api_client.location,
1383+
credentials=self._api_client._credentials,
1384+
)
1385+
return (table_id, dataframe)
13671386

13681387
def assess_tuning_resources(
13691388
self,
@@ -2713,8 +2732,9 @@ async def assemble(
27132732
gemini_request_read_config: Optional[
27142733
types.GeminiRequestReadConfigOrDict
27152734
] = None,
2735+
load_dataframe: bool = False,
27162736
config: Optional[types.AssembleDatasetConfigOrDict] = None,
2717-
) -> str:
2737+
) -> tuple[str, Optional["bigframes.pandas.DataFrame"]]: # type: ignore # noqa: F821
27182738
"""Assemble the dataset into a BigQuery table.
27192739
27202740
Waits for the assemble operation to complete before returning.
@@ -2727,12 +2747,20 @@ async def assemble(
27272747
Optional. The read config to use to assemble the dataset. If
27282748
not provided, the read config attached to the dataset will be
27292749
used.
2750+
load_dataframe:
2751+
Optional. Whether to load the assembled BigQuery table into a
2752+
BigFrames DataFrame and return it. If False, the returned
2753+
DataFrame is None, no BigQuery read is performed, and `bigframes`
2754+
is not required. Defaults to False.
27302755
config:
27312756
Optional. A configuration for assembling the dataset. If not
27322757
provided, the default configuration will be used.
27332758
27342759
Returns:
2735-
The URI of the bigquery table of the assembled dataset.
2760+
A tuple `(table_id, dataframe)`, where `table_id` is the BigQuery
2761+
table id of the assembled dataset (without the `bq://` prefix) and
2762+
`dataframe` is the assembled table loaded as a BigFrames DataFrame.
2763+
`dataframe` is None if `load_dataframe` is False.
27362764
"""
27372765
if isinstance(config, dict):
27382766
config = types.AssembleDatasetConfig(**config)
@@ -2752,7 +2780,17 @@ async def assemble(
27522780
operation=operation,
27532781
timeout_seconds=config.timeout,
27542782
)
2755-
return response["bigqueryDestination"] # type: ignore[no-any-return]
2783+
bigquery_uri = response["bigqueryDestination"]
2784+
table_id = bigquery_uri.removeprefix("bq://")
2785+
dataframe = None
2786+
if load_dataframe:
2787+
dataframe = await _datasets_utils.load_dataframe_from_bigquery_async(
2788+
bigquery_uri=bigquery_uri,
2789+
project=self._api_client.project,
2790+
location=self._api_client.location,
2791+
credentials=self._api_client._credentials,
2792+
)
2793+
return (table_id, dataframe)
27562794

27572795
async def assess_tuning_resources(
27582796
self,

tests/unit/agentplatform/genai/replays/test_assemble_multimodal_datasets.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@
1414
#
1515
# pylint: disable=protected-access,bad-continuation,missing-function-docstring
1616

17+
from unittest import mock
18+
1719
from tests.unit.agentplatform.genai.replays import pytest_helper
20+
from agentplatform._genai import _datasets_utils
1821
from agentplatform._genai import types
1922

23+
import pandas as pd
2024
import pytest
2125

2226
METADATA_SCHEMA_URI = (
@@ -26,6 +30,25 @@
2630
DATASET = "projects/vertex-sdk-dev/locations/us-central1/datasets/8810841321427173376"
2731

2832

33+
@pytest.fixture
34+
def mock_import_bigframes(is_replay_mode):
35+
if is_replay_mode:
36+
with mock.patch.object(
37+
_datasets_utils, "_try_import_bigframes"
38+
) as mock_try_import:
39+
bigframes = mock.MagicMock()
40+
dataframe = (
41+
bigframes.connect.return_value.__enter__.return_value.read_gbq.return_value
42+
)
43+
dataframe.head.return_value.to_pandas.return_value = pd.DataFrame(
44+
{"request": ["What is the capital of France?"]}
45+
)
46+
mock_try_import.return_value = bigframes
47+
yield mock_try_import
48+
else:
49+
yield None
50+
51+
2952
def test_assemble_dataset(client):
3053
operation = client.datasets._assemble_multimodal_dataset(
3154
name=DATASET,
@@ -38,8 +61,8 @@ def test_assemble_dataset(client):
3861
assert isinstance(operation, types.MultimodalDatasetOperation)
3962

4063

41-
def test_assemble_dataset_public(client):
42-
bigquery_destination = client.datasets.assemble(
64+
def test_assemble_dataset_public(client, mock_import_bigframes):
65+
table_id, dataframe = client.datasets.assemble(
4366
name=DATASET,
4467
gemini_request_read_config=types.GeminiRequestReadConfig(
4568
template_config=types.GeminiTemplateConfig(
@@ -54,8 +77,13 @@ def test_assemble_dataset_public(client):
5477
),
5578
)
5679
),
80+
load_dataframe=True,
5781
)
58-
assert bigquery_destination.startswith(f"bq://{BIGQUERY_TABLE_NAME}")
82+
assert table_id.startswith(BIGQUERY_TABLE_NAME)
83+
assert not table_id.startswith("bq://")
84+
assert dataframe is not None
85+
head_rows = dataframe.head().to_pandas()
86+
assert head_rows["request"].tolist() == ["What is the capital of France?"]
5987

6088

6189
pytestmark = pytest_helper.setup(
@@ -80,8 +108,8 @@ async def test_assemble_dataset_async(client):
80108

81109

82110
@pytest.mark.asyncio
83-
async def test_assemble_dataset_public_async(client):
84-
bigquery_destination = await client.aio.datasets.assemble(
111+
async def test_assemble_dataset_public_async(client, mock_import_bigframes):
112+
table_id, dataframe = await client.aio.datasets.assemble(
85113
name=DATASET,
86114
gemini_request_read_config=types.GeminiRequestReadConfig(
87115
template_config=types.GeminiTemplateConfig(
@@ -96,5 +124,10 @@ async def test_assemble_dataset_public_async(client):
96124
),
97125
)
98126
),
127+
load_dataframe=True,
99128
)
100-
assert bigquery_destination.startswith(f"bq://{BIGQUERY_TABLE_NAME}")
129+
assert table_id.startswith(BIGQUERY_TABLE_NAME)
130+
assert not table_id.startswith("bq://")
131+
assert dataframe is not None
132+
head_rows = dataframe.head().to_pandas()
133+
assert head_rows["request"].tolist() == ["What is the capital of France?"]

0 commit comments

Comments
 (0)