Skip to content

Commit cb1fb5d

Browse files
caohy1988claude
andcommitted
feat(ontology): add --skip-property-graph for user-owned graph DDL (#104)
Lets users with their own CREATE PROPERTY GRAPH DDL — managed by Terraform, dbt, or hand-authored — populate base tables from BQ AA traces without overwriting the graph object on every run. Changes - ontology_orchestrator.build_ontology_graph gains skip_property_graph: bool = False. When True, phase 5 is not invoked: no OntologyPropertyGraphCompiler is constructed, no CREATE OR REPLACE PROPERTY GRAPH runs. - Result dict gains property_graph_status with values "created" / "failed" / "skipped:user_requested", plus skipped_reason ("user_requested") when phase 5 was skipped. - ontology-build CLI gains --skip-property-graph and threads property_graph_status through to the curated output dict so JSON consumers can distinguish "skipped" from "failed" without parsing stderr. - Exit handling: skipped_reason == "user_requested" exits 0 silently; the existing exit-1-with-error behavior is preserved for actual graph-creation failures. Tests - test_skip_property_graph_does_not_construct_compiler asserts the compiler class is never called (mock.assert_not_called) when the flag is set. - test_property_graph_status_created_on_success and test_property_graph_status_failed_on_compiler_false cover the two default-mode status values. - CLI tests cover exit 0 with status="skipped:user_requested", default skip_property_graph=False threading, and exit 1 with status="failed" on actual creation failure. 135/135 tests in test_ontology_orchestrator.py + test_cli.py pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8c42683 commit cb1fb5d

4 files changed

Lines changed: 272 additions & 13 deletions

File tree

src/bigquery_agent_analytics/cli.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,16 @@ def ontology_build(
12381238
no_ai_generate: bool = typer.Option(
12391239
False, help="Skip AI.GENERATE; fetch raw payloads instead."
12401240
),
1241+
skip_property_graph: bool = typer.Option(
1242+
False,
1243+
"--skip-property-graph",
1244+
help=(
1245+
"Skip CREATE OR REPLACE PROPERTY GRAPH. Use when the caller "
1246+
"owns their own property-graph DDL and only wants the SDK to "
1247+
"populate base tables. CLI exits 0 with "
1248+
"property_graph_status='skipped:user_requested'."
1249+
),
1250+
),
12411251
fmt: str = typer.Option(
12421252
"json",
12431253
"--format",
@@ -1261,6 +1271,7 @@ def ontology_build(
12611271
table_id=table_id,
12621272
endpoint=endpoint,
12631273
use_ai_generate=not no_ai_generate,
1274+
skip_property_graph=skip_property_graph,
12641275
)
12651276

12661277
output = {
@@ -1271,9 +1282,19 @@ def ontology_build(
12711282
"tables_created": result["tables_created"],
12721283
"rows_materialized": result["rows_materialized"],
12731284
"property_graph_created": result["property_graph_created"],
1285+
"property_graph_status": result.get(
1286+
"property_graph_status",
1287+
"created" if result["property_graph_created"] else "failed",
1288+
),
12741289
}
12751290
typer.echo(format_output(output, fmt))
12761291

1292+
# Distinguish "user-requested skip" (exit 0) from "creation failed"
1293+
# (exit 1). Same property_graph_created=False, different operator
1294+
# intent — JSON consumers read property_graph_status to tell them
1295+
# apart without parsing stderr.
1296+
if result.get("skipped_reason") == "user_requested":
1297+
return
12771298
if not result["property_graph_created"]:
12781299
typer.echo(
12791300
"Error: Property Graph creation failed. "

src/bigquery_agent_analytics/ontology_orchestrator.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -300,14 +300,16 @@ def build_ontology_graph(
300300
endpoint: str = "gemini-2.5-flash",
301301
use_ai_generate: bool = True,
302302
location: Optional[str] = None,
303+
skip_property_graph: bool = False,
303304
) -> dict[str, Any]:
304305
"""Run the full ontology graph pipeline end-to-end.
305306
306307
1. Load the YAML spec (or use pre-loaded ``spec``).
307308
2. Extract an ``ExtractedGraph`` from agent telemetry.
308309
3. Create physical tables (if not exists).
309310
4. Materialize extracted nodes/edges into tables.
310-
5. Create the BigQuery Property Graph.
311+
5. Create the BigQuery Property Graph (skipped when
312+
``skip_property_graph=True``).
311313
312314
Args:
313315
session_ids: Sessions to extract from.
@@ -323,10 +325,22 @@ def build_ontology_graph(
323325
endpoint: AI.GENERATE model endpoint.
324326
use_ai_generate: If True, uses server-side AI extraction.
325327
location: BigQuery location.
328+
skip_property_graph: When True, skip phase 5 (do not run
329+
``CREATE OR REPLACE PROPERTY GRAPH``). Use this when the
330+
caller owns their own property-graph DDL and only wants
331+
the SDK to populate base tables. The result dict reports
332+
``property_graph_created=False`` with
333+
``skipped_reason="user_requested"`` and
334+
``property_graph_status="skipped:user_requested"``, which
335+
callers (and the CLI) use to distinguish a deliberate
336+
skip from a creation failure.
326337
327338
Returns:
328339
A dict with keys: ``spec``, ``graph``, ``tables_created``,
329340
``rows_materialized``, ``property_graph_created``,
341+
``property_graph_status`` (one of ``"created"``, ``"failed"``,
342+
``"skipped:user_requested"``), ``skipped_reason`` (only set
343+
when phase 5 was skipped, e.g. ``"user_requested"``),
330344
``graph_name``, ``graph_ref``.
331345
"""
332346
from .ontology_graph import OntologyGraphManager
@@ -391,24 +405,36 @@ def build_ontology_graph(
391405
rows_materialized = materializer.materialize(graph, session_ids)
392406
logger.info("Rows materialized: %s", rows_materialized)
393407

394-
# 5. Create property graph.
395-
compiler = OntologyPropertyGraphCompiler(
396-
project_id=project_id,
397-
dataset_id=dataset_id,
398-
spec=spec,
399-
location=location,
400-
)
401-
pg_created = compiler.create_property_graph(graph_name=name)
402-
403408
graph_ref = f"{project_id}.{dataset_id}.{name}"
404-
logger.info("Property Graph %r created=%s.", graph_ref, pg_created)
405409

406-
return {
410+
# 5. Create property graph (or skip when caller owns the DDL).
411+
result: dict[str, Any] = {
407412
"spec": spec,
408413
"graph": graph,
409414
"tables_created": tables_created,
410415
"rows_materialized": rows_materialized,
411-
"property_graph_created": pg_created,
412416
"graph_name": name,
413417
"graph_ref": graph_ref,
414418
}
419+
if skip_property_graph:
420+
logger.info(
421+
"Property Graph creation skipped (skip_property_graph=True); "
422+
"caller owns the DDL for graph %r.",
423+
graph_ref,
424+
)
425+
result["property_graph_created"] = False
426+
result["skipped_reason"] = "user_requested"
427+
result["property_graph_status"] = "skipped:user_requested"
428+
else:
429+
compiler = OntologyPropertyGraphCompiler(
430+
project_id=project_id,
431+
dataset_id=dataset_id,
432+
spec=spec,
433+
location=location,
434+
)
435+
pg_created = compiler.create_property_graph(graph_name=name)
436+
logger.info("Property Graph %r created=%s.", graph_ref, pg_created)
437+
result["property_graph_created"] = pg_created
438+
result["property_graph_status"] = "created" if pg_created else "failed"
439+
440+
return result

tests/test_cli.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2472,3 +2472,111 @@ def test_bad_spec_path_exit_2(self):
24722472
],
24732473
)
24742474
assert result.exit_code == 2
2475+
2476+
@patch("bigquery_agent_analytics.ontology_orchestrator.build_ontology_graph")
2477+
def test_skip_property_graph_exits_zero_with_status(self, mock_build):
2478+
"""--skip-property-graph: exit 0, status='skipped:user_requested'."""
2479+
from bigquery_agent_analytics.ontology_models import ExtractedGraph
2480+
2481+
mock_build.return_value = {
2482+
"graph_name": "g",
2483+
"graph_ref": "proj.ds.g",
2484+
"graph": ExtractedGraph(name="test"),
2485+
"tables_created": {"mako_DecisionPoint": "p.d.decision_points"},
2486+
"rows_materialized": {"mako_DecisionPoint": 2},
2487+
"property_graph_created": False,
2488+
"skipped_reason": "user_requested",
2489+
"property_graph_status": "skipped:user_requested",
2490+
"spec": MagicMock(),
2491+
}
2492+
2493+
result = runner.invoke(
2494+
app,
2495+
[
2496+
"ontology-build",
2497+
"--project-id=proj",
2498+
"--dataset-id=ds",
2499+
f"--spec-path={self._SPEC_PATH}",
2500+
"--session-ids=sess1",
2501+
"--env=p.d",
2502+
"--skip-property-graph",
2503+
],
2504+
)
2505+
assert result.exit_code == 0
2506+
# Skip path must NOT print the "Property Graph creation failed" stderr.
2507+
assert "Property Graph creation failed" not in result.output
2508+
parsed = json.loads(result.output)
2509+
assert parsed["property_graph_created"] is False
2510+
assert parsed["property_graph_status"] == "skipped:user_requested"
2511+
2512+
# Flag is threaded through to the orchestrator.
2513+
_, kwargs = mock_build.call_args
2514+
assert kwargs["skip_property_graph"] is True
2515+
2516+
@patch("bigquery_agent_analytics.ontology_orchestrator.build_ontology_graph")
2517+
def test_default_invocation_omits_skip_flag(self, mock_build):
2518+
"""Default invocation passes skip_property_graph=False."""
2519+
from bigquery_agent_analytics.ontology_models import ExtractedGraph
2520+
2521+
mock_build.return_value = {
2522+
"graph_name": "g",
2523+
"graph_ref": "proj.ds.g",
2524+
"graph": ExtractedGraph(name="test"),
2525+
"tables_created": {},
2526+
"rows_materialized": {},
2527+
"property_graph_created": True,
2528+
"property_graph_status": "created",
2529+
"spec": MagicMock(),
2530+
}
2531+
2532+
result = runner.invoke(
2533+
app,
2534+
[
2535+
"ontology-build",
2536+
"--project-id=proj",
2537+
"--dataset-id=ds",
2538+
f"--spec-path={self._SPEC_PATH}",
2539+
"--session-ids=sess1",
2540+
"--env=p.d",
2541+
],
2542+
)
2543+
assert result.exit_code == 0
2544+
parsed = json.loads(result.output)
2545+
assert parsed["property_graph_status"] == "created"
2546+
2547+
_, kwargs = mock_build.call_args
2548+
assert kwargs["skip_property_graph"] is False
2549+
2550+
@patch("bigquery_agent_analytics.ontology_orchestrator.build_ontology_graph")
2551+
def test_property_graph_failure_status_failed(self, mock_build):
2552+
"""When the orchestrator reports failure, exit 1 with status='failed'.
2553+
2554+
Distinguishes the failure path from the user-requested-skip path by
2555+
asserting the status field, not just the exit code.
2556+
"""
2557+
from bigquery_agent_analytics.ontology_models import ExtractedGraph
2558+
2559+
mock_build.return_value = {
2560+
"graph_name": "g",
2561+
"graph_ref": "proj.ds.g",
2562+
"graph": ExtractedGraph(name="test"),
2563+
"tables_created": {},
2564+
"rows_materialized": {},
2565+
"property_graph_created": False,
2566+
"property_graph_status": "failed",
2567+
"spec": MagicMock(),
2568+
}
2569+
2570+
result = runner.invoke(
2571+
app,
2572+
[
2573+
"ontology-build",
2574+
"--project-id=proj",
2575+
"--dataset-id=ds",
2576+
f"--spec-path={self._SPEC_PATH}",
2577+
"--session-ids=sess1",
2578+
"--env=p.d",
2579+
],
2580+
)
2581+
assert result.exit_code == 1
2582+
assert "Property Graph creation failed" in result.output

tests/test_ontology_orchestrator.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,3 +469,107 @@ def test_partial_table_creation_raises(
469469
# Materialize and property graph should NOT have been called.
470470
mock_mat_cls.return_value.materialize.assert_not_called()
471471
mock_pg_cls.return_value.create_property_graph.assert_not_called()
472+
473+
@patch(
474+
"bigquery_agent_analytics.ontology_property_graph"
475+
".OntologyPropertyGraphCompiler"
476+
)
477+
@patch("bigquery_agent_analytics.ontology_materializer.OntologyMaterializer")
478+
@patch("bigquery_agent_analytics.ontology_graph.OntologyGraphManager")
479+
def test_skip_property_graph_does_not_construct_compiler(
480+
self, mock_mgr_cls, mock_mat_cls, mock_pg_cls
481+
):
482+
"""When skip_property_graph=True, the compiler is never constructed."""
483+
mock_mgr_cls.return_value.extract_graph.return_value = ExtractedGraph(
484+
name="test"
485+
)
486+
mock_mat_cls.return_value.create_tables.return_value = dict(
487+
_ALL_YMGO_TABLES
488+
)
489+
mock_mat_cls.return_value.materialize.return_value = {}
490+
491+
result = build_ontology_graph(
492+
session_ids=["sess1"],
493+
spec_path=_DEMO_SPEC_PATH,
494+
project_id="proj",
495+
dataset_id="ds",
496+
env="p.d",
497+
skip_property_graph=True,
498+
)
499+
500+
# Compiler must not be constructed and create_property_graph must
501+
# not be called when skip_property_graph=True.
502+
mock_pg_cls.assert_not_called()
503+
mock_pg_cls.return_value.create_property_graph.assert_not_called()
504+
505+
# Tables and rows still produced.
506+
mock_mat_cls.return_value.create_tables.assert_called_once()
507+
mock_mat_cls.return_value.materialize.assert_called_once()
508+
509+
# Result reports the skip distinctly from a creation failure.
510+
assert result["property_graph_created"] is False
511+
assert result["skipped_reason"] == "user_requested"
512+
assert result["property_graph_status"] == "skipped:user_requested"
513+
514+
@patch(
515+
"bigquery_agent_analytics.ontology_property_graph"
516+
".OntologyPropertyGraphCompiler"
517+
)
518+
@patch("bigquery_agent_analytics.ontology_materializer.OntologyMaterializer")
519+
@patch("bigquery_agent_analytics.ontology_graph.OntologyGraphManager")
520+
def test_property_graph_status_created_on_success(
521+
self, mock_mgr_cls, mock_mat_cls, mock_pg_cls
522+
):
523+
"""Default flow with successful graph creation reports 'created'."""
524+
mock_mgr_cls.return_value.extract_graph.return_value = ExtractedGraph(
525+
name="test"
526+
)
527+
mock_mat_cls.return_value.create_tables.return_value = dict(
528+
_ALL_YMGO_TABLES
529+
)
530+
mock_mat_cls.return_value.materialize.return_value = {}
531+
mock_pg_cls.return_value.create_property_graph.return_value = True
532+
533+
result = build_ontology_graph(
534+
session_ids=["sess1"],
535+
spec_path=_DEMO_SPEC_PATH,
536+
project_id="proj",
537+
dataset_id="ds",
538+
env="p.d",
539+
)
540+
541+
assert result["property_graph_created"] is True
542+
assert result["property_graph_status"] == "created"
543+
assert "skipped_reason" not in result
544+
545+
@patch(
546+
"bigquery_agent_analytics.ontology_property_graph"
547+
".OntologyPropertyGraphCompiler"
548+
)
549+
@patch("bigquery_agent_analytics.ontology_materializer.OntologyMaterializer")
550+
@patch("bigquery_agent_analytics.ontology_graph.OntologyGraphManager")
551+
def test_property_graph_status_failed_on_compiler_false(
552+
self, mock_mgr_cls, mock_mat_cls, mock_pg_cls
553+
):
554+
"""Default flow where create_property_graph returns False reports
555+
'failed' (distinct from 'skipped:user_requested')."""
556+
mock_mgr_cls.return_value.extract_graph.return_value = ExtractedGraph(
557+
name="test"
558+
)
559+
mock_mat_cls.return_value.create_tables.return_value = dict(
560+
_ALL_YMGO_TABLES
561+
)
562+
mock_mat_cls.return_value.materialize.return_value = {}
563+
mock_pg_cls.return_value.create_property_graph.return_value = False
564+
565+
result = build_ontology_graph(
566+
session_ids=["sess1"],
567+
spec_path=_DEMO_SPEC_PATH,
568+
project_id="proj",
569+
dataset_id="ds",
570+
env="p.d",
571+
)
572+
573+
assert result["property_graph_created"] is False
574+
assert result["property_graph_status"] == "failed"
575+
assert "skipped_reason" not in result

0 commit comments

Comments
 (0)