Skip to content

Commit 80f6c6f

Browse files
authored
examples(migration_v5): fixture foundation for the four-guarantee MAKO notebook (#107 phase 1) (#155)
* feat(examples/migration_v5): fixtures foundation for the four-guarantee notebook Phase 1 of issue #107: fixtures directory under examples/migration_v5/ that the four-guarantee MAKO demo notebook (landing in a follow-up commit) consumes. Inputs from the user-authored side (the "what you actually have to provide" floor — one ontology file): * mako_core.ttl — the real MAKO ontology pulled from the reference gist (https://gist.github.com/haiyuan-eng-google/ a69ff6282ebcc877f77f9aa4e3db1afd). Domain-agnostic decision semantics, agent coordination, outcome tracking per Yahoo Monetization Platform's design doc. Generated / scaffolded artifacts (the SDK + plugin produce these; the demo never asks the user to hand-author them): * ontology.yaml — full 380-line auto-import via gm import-owl --include-namespace https://ontology.yahoo.com/mako/. Captures all 41 MAKO entities; the notebook displays this in Section 0 to show the realistic "import → resolve FILL_INs → curate" workflow. * ontology_demo.yaml — hand-curated 5-entity demo subset (AgentSession, DecisionPoint, Candidate, SelectionOutcome, ContextSnapshot) with FILL_IN primary keys resolved to id per MAKO's "every artifact has a stable identifier" contract. Validates clean against gm validate. The other 36 MAKO entities would scaffold the same way — narrowing keeps the notebook's four-guarantee story focused. * binding.yaml — auto-scaffolded via gm scaffold --ontology ontology_demo.yaml --dataset migration_v5_demo --project test-project-0728-467323 --out scaffold/. Demonstrates the "one file in, two files out" minimum-input path the storyboard's Section 0.5 calls out. * table_ddl.sql — companion to binding.yaml, also from gm scaffold. * property_graph.sql — user-authored CREATE PROPERTY GRAPH for the demo subset. Beat 1's "you own the graph definition" evidence. Uses __DATASET__ placeholder for per-run dataset substitution. Demo-specific Python fixtures (the notebook imports these): * seed_events.py — deterministic seeded RNG generator producing 404 events across 50 sessions: each session contains 2-4 decision points; each decision evaluates 3-5 candidates against a context snapshot and produces one outcome. Seeded RNG (_RANDOM_SEED = 20260512) so the notebook's outputs round-trip byte-identically across runs. Event shape mirrors the BQ AA plugin's payload so Beat 3 extractors see the same surface as production. * reference_extractors.py — handwritten extractor for mako_decision events (1 DecisionPoint + 1 SelectionOutcome + 1 ContextSnapshot reference + N Candidates + their edges per event), plus the exact EXTRACTORS / RESOLVED_GRAPH / SPEC module contract bqaa-revalidate-extractors requires. Mirrors the BKA decision pattern from bigquery_agent_analytics.structured_extraction. * revalidation_thresholds.json — threshold gate values for bqaa-revalidate-extractors --thresholds-json: 95% compiled_unchanged_rate, 99% parity_match_rate, etc. Validation done in this commit: * gm validate examples/migration_v5/ontology_demo.yaml → clean. * gm validate examples/migration_v5/binding.yaml --ontology examples/migration_v5/ontology_demo.yaml → clean. * seed_events.py generates 404 events across 50 sessions (50 session-start + 50 session-end + 152 context + 152 decision events). * reference_extractors.extract_mako_decision_event() on the first seeded decision produces 8 nodes (1 DecisionPoint + 1 Outcome + 1 Context + 5 Candidates) and 7 edges. Next commit lands the notebook itself. * docs(examples/migration_v5): README for the fixture foundation Surface the design decisions baked into the fixtures so the fixture-shape review can happen before the notebook PR. Per-decision rationale on: 5-entity MAKO subset, id-as-primary-key strategy, NON-sorted skos:notation on DecisionPoint (exercises the round-3 lex-min display-token rule end-to-end), seeded event shape + per-beat coverage check, reference extractor scope. Lists the four validation commands already run + their pass status. * refactor(examples/migration_v5): TTL-only authored input + mako_agent.py Addresses PR #155 round-1 reviewer findings. The previous fixture shape conflated "demo configuration" with "ontology curation": ontology_demo.yaml was a hand-curated subset, seed_events.py hardcoded event types and demo-only fields not present in MAKO, property_graph.sql used semantic edge columns inconsistent with the scaffolded binding's from_id/to_id defaults, and project/dataset were baked into checked-in artifacts. Revised contract: exactly TWO files in examples/migration_v5/ are user-authored inputs. Everything else is a generator output. Authored inputs: * mako_core.ttl — the real MAKO ontology. * mako_agent.py — the only authored Python. Loads the TTL, resolves FILL_IN primary keys to synthesized id: string (matches MAKO's "every artifact has a stable identifier" design contract), drops cross-namespace dangling relationships (e.g. delegatedTo → prov:Agent — MAKO extends PROV-O but the demo doesn't model the external namespaces; dropped relationships are recorded in the ontology's annotations under mako_demo:dropped_cross_namespace_relationships so the loss is auditable). Generates binding for any (project, dataset) pair, derives table DDL + property-graph SQL from the binding (edge columns are consistent across both — no more from_id vs session_id mismatch), and emits a deterministic event stream whose payloads carry ONLY MAKO-declared data properties. Generated artifacts (reproducibility snapshots produced by mako_agent.regenerate_snapshots): * ontology.yaml — gm import-owl output with FILL_INs resolved, dangling relationships dropped. * binding.yaml — derived from the ontology for the demo entity set. * table_ddl.sql — derived from the binding. * property_graph.sql — derived from the binding. Edge columns match table_ddl.sql. * events.jsonl — 398 deterministic events across 50 sessions. Demo entity set bumped from 5 → 6: DecisionExecution is MAKO's central hub (partOfSession an AgentSession, atContextSnapshot a ContextSnapshot, executedAtDecisionPoint a DecisionPoint, hasSelectionOutcome a SelectionOutcome). The decision-flow story doesn't hold together without it. Edge set is now fully TTL-driven: the agent walks ontology.relationships and picks every relationship whose endpoints both fall within DEMO_ENTITIES. No hardcoded edge list. Picks up 9 relationships from MAKO without any name authoring (atContextSnapshot, evaluatesCandidate, executedAtDecisionPoint, hasSelectionOutcome, partOfSession, rejectedCandidate, selectedCandidate, plus two others). Event payload contract: MAKO models domain entities; the BQ AA plugin telemetry envelope wraps each event with event_type / session_id / span_id / event_timestamp / content. The content dict carries only MAKO-declared data properties (AgentSession.sessionId, DecisionExecution .businessEntityId / latencyMs / spanId / traceId, DecisionPoint.reversibility, ContextSnapshot.snapshotPayload / snapshotTimestamp) plus foreign-key references. No demo-only invented fields. Validation done: * gm validate ontology.yaml → clean. * gm validate binding.yaml --ontology ontology.yaml → clean. * mako_agent.py end-to-end: {"ontology_entities": 18, "binding_entities": 6, "binding_relationships": 9, "events": 398}. README rewritten to document the authorship boundary explicitly and call out each design choice with the rejected alternative spelled out. * refactor(examples/migration_v5): split artifacts vs runnable plugin-instrumented agent Addresses PR #155 round-2 product clarification (#155 comment 4437670647). Previous shape had mako_agent.py double as TTL→artifacts AND event generator, which silently bypassed the BQ AA plugin's event-emission contract. New shape pins the contract: the demo's event source of truth is a runnable agent talking to BigQueryAgentAnalyticsPlugin, not a Python fixture. Authored inputs (now five files, all in this directory): * mako_core.ttl — unchanged. The real MAKO ontology. * mako_artifacts.py — renamed from mako_agent.py; stripped of TelemetryEvent / generate_events / events_as_jsonl. Pure TTL→ontology / binding / DDL / property-graph pipeline. Does NOT generate events. * mako_demo_agent.py — new. Runnable ADK Agent with five MAKO decision-flow tools (capture_context, propose_decision_point, evaluate_candidate, commit_outcome, complete_execution) and BigQueryAgentAnalyticsPlugin wiring. Same pattern as examples/decision_lineage_demo/agent/agent.py. Uses Vertex AI Gemini (default gemini-2.5-flash). * run_agent.py — new. Driver. `python run_agent.py --sessions 50 --project X --dataset Y` runs the agent for N sessions and the plugin populates agent_events as a side effect. Each prompt asks the agent to walk through one full MAKO decision flow; the plugin captures the LLM_RESPONSE rows + TOOL_* rows that downstream analytics consume. * export_events_jsonl.py — new. Optional. Exports a pinned subset of agent_events to a local JSONL snapshot for the notebook's deterministic offline revalidation tests. NOT an event generator — reads from BigQuery. Generated artifacts (reproducibility snapshots; regenerable via `python mako_artifacts.py --project X --dataset Y`): * ontology.yaml, binding.yaml, table_ddl.sql, property_graph.sql Removed: * events.jsonl — was a fixture-script output; the new shape makes this a captured BQ snapshot only. * TelemetryEvent class + generate_events function from the artifact module. Events now come from the plugin, not a fixture. Validation done in this commit: * PYTHONPATH=src python mako_artifacts.py → 18 ontology entities, 6 binding entities, 9 relationships. * PYTHONPATH=src python -m bigquery_ontology.cli validate ontology.yaml → clean. * PYTHONPATH=src python -m bigquery_ontology.cli validate binding.yaml --ontology ontology.yaml → clean. * mako_demo_agent imports cleanly: LlmAgent + 5 tools + BigQueryAgentAnalyticsPlugin attached. * run_agent.py --help works without live BQ / Vertex. NOT done in this commit (next step on the same branch): * Live end-to-end run of run_agent.py against the demo's Vertex AI + BigQuery dataset. This is the notebook's job in Beat 0; landing it as a separate commit after this fixture shape is signed off. * The notebook itself. README rewritten to spell out the authorship boundary explicitly and document each design decision with the rejected alternative spelled out. * fix(examples/migration_v5): self-edge dup columns + property-type DDL + trace mapping Round-3 fixes for PR #155 review: * P1 — self-referential edges generated duplicate columns. ``evolvedFrom`` / ``supersededBy`` are DecisionExecution → DecisionExecution self-edges; naming both endpoints ``decision_execution_id`` produced ``CREATE TABLE foo (decision_execution_id STRING, decision_execution_id STRING)``. ``make_binding`` now switches to ``src_{base}_id`` / ``dst_{base}_id`` only for self-edges; heterogeneous edges keep the simpler naming. Property graph SQL stays in sync because it reads ``from_columns`` / ``to_columns`` from the same binding. * P1 — table DDL ignored ontology property types. Every column was ``STRING`` (plus a hacky ``*Timestamp`` suffix), silently dropping the typing the TTL declared (e.g. ``DecisionExecution.latencyMs`` is ``xsd:integer``, but landed as STRING). ``make_table_ddl`` now takes the ontology, builds an ``(entity, prop) → BQ type`` map via the new ``_bq_type_for`` helper, and emits real BQ types. ``latency_ms`` is now ``INT64`` in ``table_ddl.sql``. * README — added explicit "trace → MAKO mapping" contract. The agent uses realistic snake_case tool names (e.g. ``decision_point_id``, ``snapshot_payload``, ``business_entity_id``) rather than TTL-camelCase. The README now spells out which trace fields become TTL-declared properties at extraction time and which are trace-only (e.g. ``rationale``, ``candidate_label``). * fix(examples/migration_v5): DDL metadata cols, plugin schema, ident validation Round-4 fixes for PR #155 review: * P1 — table DDL now includes the SDK metadata columns that binding-validate requires on every bound table. The materializer (``ontology_materializer._entity_columns`` / ``_relationship_columns``) writes ``session_id STRING`` + ``extracted_at TIMESTAMP`` on every ``materialize()`` call, and ``binding_validation.py`` (lines 488, 806) enforces both columns. Without them, the notebook's binding-validate step would fail before ontology-build. ``make_table_ddl`` now appends both, deduping against any domain property that already maps to the same column (MAKO's ``AgentSession.sessionId → session_id`` is the canonical collision case). * P1 — ``export_events_jsonl.py`` now targets the BQ AA plugin's real schema. The previous SELECT projected ``event_id``, ``agent_name``, ``payload``, ``content``, ``event_timestamp``, ``partition_date`` — none of which exist on the plugin's table. The plugin emits ``timestamp``, ``event_type``, ``agent``, ``session_id``, ``invocation_id``, ``user_id``, ``trace_id``, ``span_id``, ``parent_span_id``, plus JSON ``content`` / ``attributes`` / ``latency_ms`` (verified by reading ``_get_events_schema`` in ``bigquery_agent_analytics_plugin.py``). Partitioning is on ``timestamp`` (DAY), not a separate column. Ordering is now ``ORDER BY timestamp, span_id`` since there is no ``event_id``. * P2 — ``export_events_jsonl.py`` validates each segment of ``--project / --dataset / --table`` against ``[A-Za-z0-9_-]+`` (same character class ``bq_bundle_mirror._TABLE_ID_PATTERN`` uses) before interpolating into the ``FROM`` clause. BigQuery identifiers can't be passed as query parameters; this check rejects whitespace, backticks, semicolons, dots, and any character outside the BQ-permitted set per segment. README updated with two new design-decisions sections: events.jsonl is captured from the real plugin schema, and the DDL carries SDK metadata columns. * docs(examples/migration_v5): node_id convention + soften exporter docstring Round-5 doc-only follow-ups from PR #155 review: * P2 — pin the reference-extractor node_id convention in README. The materializer (``ontology_materializer._build_edge_row`` line 322) derives edge FK column values by *parsing endpoint node_ids*, not by reading node properties. A node_id like ``sess:DecisionExecution:id=exec-1`` will not populate an ``decision_execution_id`` FK column ---- whatever keys the binding declares for FKs are the keys the extractor must encode in the node_id. This is a silent-failure trap (extractor "runs", edges get empty-string FKs) and is especially subtle for self-edges where the same node needs ``src_*`` / ``dst_*`` keys too. Documented the convention with a worked DecisionExecution example so the notebook + reference extractor (follow-up commit) can centralize the encoding in a ``node_id_for(...)`` helper. * P3 — soften the exporter docstring. The selected columns are a *subset* of the plugin schema, not the full schema: ``content_parts`` (REPEATED RECORD for multimodal parts) is omitted because the MAKO decision flow is text-only. Docstring now says "selected columns from the plugin schema" and points to the one-line change needed if a future demo needs multimodal trace replay. * docs(examples/migration_v5): README parity with round-5 doc fixes Two stale phrasings in README missed by the previous commit: * ``_build_edge_row`` doesn't exist; the function is ``_route_edge`` (``ontology_materializer.py:314``). Behavior described was already correct, only the symbol name was wrong. * "Mirrors the BQ AA plugin's real schema" overstates what the exporter does ---- ``content_parts`` (REPEATED RECORD) is omitted because the MAKO demo is text-only. README now matches the exporter docstring: "projects the subset of the plugin schema the notebook needs", with the one-line change for multimodal replay. * docs(examples): replace migration notebook with four-guarantee scaffold (#107) First pass for #107: replace the ontology-migration notebook content with the four-guarantee storyboard scaffold. Section 0 fully written (no live BQ required to parse): * License header + title + four-guarantee narrative table. * "What you need to bring" — minimum-input answer: one ontology file. Three input shapes side by side. * Install + auth + scratch dataset + ``FEATURES`` flags for per-beat gating (#58/#75/#76/#104/#105). * TTL → ontology / binding / DDL via ``mako_artifacts.regenerate_snapshots(project, dataset)``. * Apply ``table_ddl.sql`` against the scratch dataset. * Run the real ADK Gemini agent via ``run_agent.py --sessions N``; the BQ AA plugin populates ``agent_events`` as a side effect. * Sanity-check counts by event_type. Sections 1-4 are section-header markdown placeholders; the beat-specific cells (Own / Validate / Extract cheaply / Resolve) land in passes 2-5. Section 5 (close) is fully written so the storyboard's recap is in place. The notebook validates against nbformat 4.5 (verified via ``nbformat.validate``) and the code cells parse as valid Python (verified via ``ast.parse`` after stripping the ``!pip`` magic). Live execution against ``test-project-0728-467323`` follows as a separate commit once Sections 1-4 are filled in. * fix(examples/migration_v5): live-run readiness nits from round-6 review Four small fixes uncovered when the notebook scaffold put ``run_agent.py`` on the live path: * P2 — agent SYSTEM_PROMPT step 5 now lists all four arguments ``complete_execution`` requires (``decision_point_id``, ``context_id``, ``outcome_id``, ``business_entity_id``). Gemini can infer from the function schema, but the instruction was weaker than the tool signature, which risks degraded event quality on the live run. * P3 — notebook cell 13 now passes ``env={**os.environ, "DEMO_AGENT_LOCATION": AGENT_LOCATION, ...}`` to ``subprocess.run`` so a notebook-variable override of ``AGENT_LOCATION`` / ``PROJECT_ID`` / ``DATASET_ID`` actually reaches the child process. Without this, the displayed value diverges from what the agent module picks up at import time. * P3 — feature-flag wording updated. All underlying issues have shipped; the gates are runtime/cost knobs now ("flip False to skip live, expensive, or environment-specific beats"), not dependency gates. * P3 — the PR claim "code cells parse via ``ast.parse``" needed qualification since the ``!pip`` install cell is valid IPython but not valid Python. The verification script now explicitly skips shell-magic cells before ``ast.parse``. * docs(examples): notebook Section 1 — Beat 1, "you own the graph" (#104) Pass 2: replace the Section 1 placeholder with the live "own the graph" beat. Cells 1.1-1.6: * 1.1 [md] Framing — before #104 every build ran ``CREATE OR REPLACE PROPERTY GRAPH``; after, the SDK populates base tables and leaves the user's graph DDL alone. * 1.2 [py] Apply ``examples/migration_v5/property_graph.sql`` to the scratch dataset. * 1.3 [py] Capture ``before_skip_build_ts`` *after* the user's DDL finishes; baseline a GQL traversal so we have a row-count reference for cell 1.5. * 1.4 [py] Discover ``session_ids`` from ``agent_events``; run ``bq-agent-sdk ontology-build --skip-property-graph --ontology --binding --session-ids --location``. Print the build's ``property_graph_status`` so the skipped state is visible. * 1.5 [py] Two-pronged evidence: - (a) ``INFORMATION_SCHEMA.JOBS_BY_PROJECT`` filtered by ``creation_time > @before_ts`` + dataset-name pattern + ``REGEXP_CONTAINS(UPPER(query), r'CREATE\s+(OR\s+REPLACE\s+)?PROPERTY\s+GRAPH')``. Combining the timestamp filter (so the user's own cell-1.2 DDL is excluded) with the regex catches the SDK-issued spelling. - (b) Re-run the GQL traversal; assert the count is ``>= before_skip_build_rows`` (the graph object is intact and now has populated base tables). * 1.6 [md] Beat 1 closing. All cells gate on ``FEATURES["skip_property_graph"]``; flipped off they print "Skipped: skip_property_graph feature off" rather than failing. Used ``rf"""`` for the JOBS_BY_PROJECT SQL so the regex literal ``r'CREATE\s+...'`` doesn't trigger Python 3.12+'s SyntaxWarning on the ``\s`` escape. * fix(examples): Section 1 verify-cell parity with live #104 regression test Three findings from review of pass 2 (Section 1): * P1 — JOBS_BY_PROJECT filter now mirrors the live #104 integration test (``tests/test_integration_ontology_binding.py``): ``query LIKE '%mako_demo_graph%' AND EXISTS (SELECT 1 FROM UNNEST(j.labels) AS l WHERE l.key='sdk_feature' AND l.value='ontology-gql')``. The previous ``query LIKE '%{DATASET_ID}%'`` filter was too narrow — a regressed SDK can target the orchestrator dataset rather than the binding's dataset in split setups, and the regression would slip past a dataset-pattern check. Filtering by graph name + SDK feature label catches the regression regardless of which dataset the build wrote into. The post-DDL timestamp filter still excludes the user's own authored DDL from cell 1.2 (which also carries the ontology-gql label). * P2 — Cell 1.4 now asserts ``build_result["property_graph_status"] == "skipped:user_requested"`` immediately after parsing the CLI stdout. A regression that returned success but a different (or absent) status would otherwise silently pass the JOBS_BY_PROJECT check too. * P3 — Cell 1.5 now asserts ``total_rows > 0`` (sum of ``build_result["rows_materialized"]``). The previous GQL ``after >= before`` check trivially passed when both counts were zero, so an empty-graph regression would slip through. The build's own materialization count is the authoritative answer to "did extraction produce data?" * docs(examples): notebook Section 2 — Beat 2, pre-flight binding-validate (#105) Pass 3: replace the Section 2 placeholder with the live "validate" beat. Also fixed a small wording nit in cell 1.5 markdown (no longer says "against the scratch dataset", since the JOBS_BY_PROJECT filter is now graph-name + SDK- label scoped, not dataset-scoped). Section 2 cells: * 2.1 [md] Framing — pre-flight catches drift in under a second; exit code 1 short-circuits before AI.GENERATE fires. * 2.2 [py] Inject column-rename drift: ``ALTER TABLE context_snapshot RENAME COLUMN snapshot_payload TO snapshot_payload_v2``. Print the table's columns before + after so the drift is visible. * 2.3 [py] Run ``binding-validate --format json``. Assertions: exit code 1, at least one ``MISSING_COLUMN`` failure on ``context_snapshot.snapshot_payload``. Print each failure with ``binding_path`` + ``bq_ref`` (the fields that point at the exact authoring site). * 2.4 [py] Restore the column name and re-run. Assertions: exit code 0, ``report.ok is True``. * 2.5 [py] Combine Beat 1 + Beat 2 in one invocation: ``ontology-build --skip-property-graph --validate-binding``. Assertions: ``property_graph_status = 'skipped:user_requested'`` AND ``rows_materialized total > 0`` (the build's authoritative did-extraction-produce-data answer). * 2.6 [md] Closing. All cells gate on ``FEATURES["binding_validate"]``; cell 2.5 also requires ``skip_property_graph``. Gated off, cells print "Skipped: ..." rather than failing. Cell 1.5 markdown reworded: "jobs targeting ``mako_demo_graph`` and carrying the SDK's ``sdk_feature='ontology-gql'`` label, with ``creation_time > before_skip_build_ts``". The code does not filter by dataset name; the markdown now matches. * fix(examples): Section 2 round-8 — JSON code casing + idempotent restore Three findings from review of pass 3 (Section 2): * P1 — Cell 2.3 was checking ``f["code"] == "MISSING_COLUMN"``, but the CLI emits ``f.code.value`` from the ``FailureCode`` enum, whose values are lowercase (``"missing_column"``). The Python enum *names* are uppercase but the JSON values aren't — easy to miss without reading the CLI source. Cell 2.3 now checks ``"missing_column"`` and the 2.3 framing markdown calls out the casing convention explicitly. * P2 — Cell 2.2 mutates the live table; if anything between it and cell 2.4 asserts, the column stays renamed and the rest of the notebook breaks. Cell 2.3 was the most likely culprit (it asserts on the validator output). Two compounding fixes: - Cell 2.3 now prints all structured output *before* asserting, so the report shape is visible even when an assertion raises. - Cell 2.4's restore is now idempotent — it reads the current schema and renames-or-noops based on which column exists, so the user can re-run cell 2.4 by itself after a cell-2.3 failure and get back to the binding-expected shape. * P3 — Cell 2.3 markdown said paths look like ``entities.ContextSnapshot.properties.snapshotPayload.column``, but the validator emits indexed paths (``binding.entities[2].properties[1].column``). The notebook prints ``binding_path``, so the framing now matches the real shape with a one-line callout about the name-keyed fallback for the unresolved-spec case. * docs(examples): notebook Section 3 — Beat 3 cost + validate (#75 + #76) Pass 4: replace Section 3 placeholder. Cell 2.4 also hardened against kernel-restart loss of ``rename_to`` / ``validate_cmd``. Section 3 cells: * 3.1 [md] Framing — session-aggregated AI.GENERATE is the cost driver; compiled extractors handle structured spans, AI handles narrative spans; validation-gated prune determines transcript composition. Table maps C1 / C2 sub-phases to cells. * 3.2 [py] Live baseline: per-session transcript size from ``agent_events`` (``LENGTH(TO_JSON_STRING(content))`` per row, summed per session). Estimate uses 4-chars-per-token; honest about being a prompt-size estimate, not billing usage. Per-event token attribution isn't available from current architecture; the **session** is the cost unit. * 3.3 [py] / 3.4 [py] / 3.5 [py] / 3.7 [py] — gated placeholders. The cells need ``examples/migration_v5/reference_extractor.py`` (a MAKO-specific reference extractor analogous to the shipped ``extract_bka_decision_event``). PR #155 is fixtures-only; the reference extractor + the live measurement / runtime / savings cells land in PR #156. Each placeholder documents the future-PR shape so the follow-up author has a target. * 3.6 [py] **Live** ``ValidationReport`` demo against synthetic ``ExtractedGraph`` fixtures. NODE / FIELD / EDGE scopes all fire as expected (verified locally against ``mako_artifacts``-generated ontology + binding); each scope is asserted so a regression in ``graph_validation.py`` scope classification would catch here. * 3.8 [md] Beat 3 status — what's live vs what waits on PR #156. The cell-2.4 harden: ``rename_to`` defaults to ``"snapshot_payload_v2"`` if not already in ``locals()``, and ``validate_cmd`` is rebuilt locally if missing. So restart-and-rerun-from-cell-2.4 works cleanly. * fix(examples): Beat 3 baseline guard rails + Section 5 recap honesty Two round-9 fixes: * P2 — Cell 3.2 (baseline cost view) now uses ``SUM(COALESCE(LENGTH(TO_JSON_STRING(content)), 0))`` so a row whose ``content`` is NULL doesn't NULL-out the session total (and crash the format string downstream). Adds two guard-rail assertions: ``baseline_rows`` must be non-empty and ``total_chars`` must be positive. A zero baseline would mean "Section 0 didn't actually populate ``agent_events``" — surface that as a hard failure rather than letting Beat 3 quietly claim a live baseline on an empty corpus. * P2/P3 — Section 5's recap row for "Extract cheaply" said "Per-session token table shows the savings", but this PR only ships the *baseline* table; the compiled-savings *delta* is deferred to PR #156. Row now says "Cell 3.2 establishes the pre-compile baseline; the compiled-savings delta lands with PR #156" so the recap matches what's actually live. * docs(examples): notebook Section 4 — Beat 4, concept-index resolver (#58) Pass 5: replace Section 4 placeholder. Also adds inheritance stripping to ``mako_artifacts.py`` so the binding compiles through ``gm compile`` cleanly. Fixture change: ``_strip_inheritance`` post-processor The MAKO TTL declares ``mako:Candidate rdfs:subClassOf mako:RoleTrait``; the OWL importer surfaces it as ``Candidate.extends: RoleTrait``. ``gm compile`` v0 doesn't support inheritance and rejects the binding with ``compile-validation — Entity 'Candidate' uses 'extends'``, which blocks Section 4's ``--emit-concept-index`` step. ``RoleTrait`` is a marker class in MAKO (REQ-ONT-022, "single-primary-parent inheritance discipline") with no properties beyond the ``id`` PK every other entity already carries. ``_strip_inheritance`` drops ``extends`` from the post-import YAML, adds the ``id`` PK Candidate inherited from RoleTrait, and records the discard under ``mako_demo:stripped_inheritance`` (per-entity flat string, plus a comma-joined top-level summary — annotations are typed ``dict[str, str]`` so the value can't be a list). The check passes: 18 / 6 / 9 counts unchanged, both ``gm validate`` invocations still clean, ``gm compile --emit-concept-index`` now emits the property-graph DDL + the concept-index ``CREATE OR REPLACE TABLE`` pair. Section 4 cells: * 4.1 [md] Framing — user-typed → canonical via concept-index lookup. PR #92 emission + #58 reader. Notes that MAKO TTL ships no SKOS labels, so the demo resolves by entity name only; richer TTLs extend the same demo with no code changes. * 4.2 [py] Run ``gm compile --emit-concept-index --concept-index-table <FQN>`` and print the two ``CREATE OR REPLACE TABLE`` statements (main + __meta) with their shared ``compile_fingerprint``. * 4.3 [py] Apply the DDL to the scratch dataset, construct ``OntologyRuntime.from_files(..., concept_index_table=, bq_client=)``, wrap in ``LabelSynonymResolver``, resolve ``"DecisionExecution"``. Print each candidate's ``entity_name`` / ``matched_label_kind`` / ``compile_id``. * 4.4 [py] GQL traversal scoped by the resolved entity: ``COUNT(*)`` over ``GRAPH_TABLE(... MATCH (n:{resolved_entity}) ...)`` plus a hub-shape ``(de:DecisionExecution)-[:partOfSession]-> (s:AgentSession)`` sample so the resolver → graph wiring is visible. Guard-rail check on the resolved entity to ensure it's in the demo allowlist before f-stringing into the GQL label position. * 4.5 [md] Closing. All cells gate on ``FEATURES["concept_index_reader"]``. * fix(examples): Section 4 round-10 — compiler-version drift + non-zero asserts Three findings from review of pass 5 (Section 4): * P1 — Cell 4.3 hard-coded ``compiler_version= "bigquery_ontology 0.2.2"`` in ``OntologyRuntime.from_files``, but ``gm compile`` defaults to the installed package version (0.2.3 in the reviewer's env). The version string is part of the fingerprint hash, so the runtime trips ``FingerprintMismatchError`` the moment package versions don't line up. Fix: query ``{CONCEPT_INDEX_TABLE}__meta.compiler_version`` after applying the DDL and pass that value to ``from_files(...)``. Same source of truth the compiler emitted; no manually-pinned constant to drift. * P2 — Cell 4.4 printed the GQL count and the hub-shape rows without ever asserting either was non-empty. A zero count or zero hub rows would still close the cell "successfully", making the demo's climax claim ("user-typed name routes to real graph data") an empty print. Added ``assert count > 0`` and ``hub_rows = list(...); assert hub_rows`` with messages that point at the likely Beat 1 root cause (empty materialization). * P3 — Beat 4 closing markdown said "the user typed natural language; the SDK resolved it via the SKOS taxonomy the user authored once". Accurate for a SKOS-rich TTL; misleading for the MAKO fixture, which ships no labels and resolves only by canonical entity name. Reworded to explicitly call out that fact and describe the label-kind ranking (``name > pref > alt > synonym > notation``) that kicks in when richer rows are present. * fix(examples): Section 4 P1 (actually) + label-kind ranking Round-10 claimed to source ``compiler_version`` from the ``__meta`` table, but the patch never landed: an AssertionError on a subsequent cell aborted the script before the file write. The committed file still had ``compiler_version="bigquery_ontology 0.2.2"`` hard-coded, which trips ``FingerprintMismatchError`` against any other installed package version (0.2.3 in the reviewer's env). Properly apply the fix this time. Cell 4.3 now: * Queries ``SELECT compiler_version, compile_fingerprint FROM `{CONCEPT_INDEX_TABLE}__meta``` right after applying the DDL. * Binds the result to ``COMPILER_VERSION`` and passes it to ``OntologyRuntime.from_files(...)``. The SDK's own ``__meta`` row is the source of truth — there's no manually-pinned constant to drift. * Prints both ``compiler_version`` and ``compile_fingerprint`` so the audit trail is visible. Also: Beat 4 closing markdown had ``name > pref > alt > synonym > notation``, but the runtime priority (``_LABEL_KIND_PRIORITY``) is ``name > pref > alt > hidden > synonym > notation``. Added the missing ``hidden`` rung. * docs(examples): Beat 4 wording consistent with what the fixture actually does Two doc-only nits from round-11 review: * Beat 4 prose in four sites still said the user types natural-language inputs like ``"Consumer Banking"`` and the resolver maps to canonical names like ``skos:RetailBanking``. The MAKO TTL ships no SKOS labels, so this fixture resolves canonical entity names (``DecisionExecution``), not natural-language. The intro table, Beat 4 framing, climax-query paragraph, and Section 5 recap row are now consistent: each calls out "canonical entity label in this fixture; richer ontologies use the same resolver path for ``skos:altLabel`` / ``skos:prefLabel`` / ``skos:notation`` rows." * The 4.1 framing also listed ``label_kind`` candidates without ``hidden`` (Beat 4 closing already had it, the 4.3 framing didn't). Added ``hidden`` so the three ``label_kind`` enumerations in the notebook match the runtime priority (``name > pref > alt > hidden > synonym > notation``). * feat(examples): live notebook + fixture fixes for end-to-end run Live end-to-end run against ``test-project-0728-467323`` (3 sessions, scratch dataset ``migration_v5_demo_7da57280``). All 59 cells passed. Fixture changes that unblocked the live run: * Entity PK columns are now per-entity rather than a bare ``id``. The materializer's ``_relationship_columns`` (``ontology_materializer.py``) looks up edge FK columns in ``src_prop_map[col].sdk_type`` — that lookup requires the FK column to *exactly* name a column on the source entity. With bare ``id``, every edge would land ``(id STRING, id STRING)`` (duplicate column) AND the materializer's FK→PK type lookup would still miss for cross-entity edges. Renaming PK columns to ``{entity_short}_id`` matches the convention the original V5 spec used (``decision_id``, ``adUnitId``, etc.) and the integration-test fixture (``tests/fixtures/test_binding.yaml``). Note the ``_entity_id_column`` helper no longer strips ``agent_`` for AgentSession — that strip collided with ``AgentSession.sessionId``'s ``session_id`` column and the SDK metadata ``session_id``. AgentSession is now ``agent_session_id``. * Self-edges are dropped (was 9 → 7 relationships). MAKO's ``evolvedFrom`` and ``supersededBy`` are ``DecisionExecution → DecisionExecution`` self-edges; the materializer's FK→PK lookup can't disambiguate them via ``src_/dst_`` prefixed columns and the natural composite-key pair is ``(decision_execution_id, decision_execution_id)`` — duplicate. The ontology still declares the edges; only the binding scope drops them. * Property graph DDL: edge ``REFERENCES`` clauses use the node-table **alias** (``decision_execution`` not ``\`proj.ds.decision_execution\```) because BigQuery rejects qualified refs in ``CREATE PROPERTY GRAPH``. Node ``KEY (...)`` and edge ``REFERENCES alias (col)`` both use the per-entity PK column (``KEY (decision_execution_id)`` instead of ``KEY (id)``). Edge tables additionally need explicit ``KEY (src_col, dst_col)`` — BQ rejects without it. * Inheritance: ``_strip_inheritance`` post-processor drops ``extends: RoleTrait`` from Candidate so ``gm compile`` v0 accepts the binding (v0 doesn't support inheritance). Notebook changes for the live path: * Cell 1.4 + cell 2.5 subprocess calls now surface the child process's stderr on failure. Previous ``capture_output=True, check=True`` masked the error text from the notebook; the operator had to re-run the CLI by hand to see what failed. * Cells 2.3 / 2.4 / 2.5 add a local ``import json`` because cell 1.4 imports ``json as _json`` (aliased) which leaves the bare name ``json`` unbound for later cells. * Cell 4.4 builds a ``_PK_COL`` map (entity → PK column) because the GQL ``COLUMNS`` clause needs the per-entity PK column name (``decision_execution_id`` etc.), not ``id`` which is now the property *logical* name only. * Hub-shape traversal in Beat 4 now degrades gracefully to a "AgentSession synthesis is a PR #156 follow-up" note rather than asserting. The AI extraction in this demo doesn't synthesize ``AgentSession`` nodes / ``partOfSession`` edges from the plugin envelope; the follow-up will. Live evidence captured in the executed notebook (now committed with outputs): - Beat 1 verify: GQL count before=0, after=2; no SDK-issued CREATE OR REPLACE PROPERTY GRAPH job; rows_materialized total = 35; property_graph_status = 'skipped:user_requested'. - Beat 2: drift detected (exit 1, missing_column); restore + re-validate (exit 0, report.ok = True); combined build (exit 0, rows_materialized total = 35). - Beat 3.6: ``ValidationReport`` against synthetic ``ExtractedGraph`` — NODE + FIELD + EDGE scopes all fire as expected. - Beat 4: concept index materialized; resolver returns DecisionExecution with compile_id ``eca9978b0550``; GRAPH_TABLE count = 4 against the user-authored property graph. Beat 3 compile / runtime / savings cells (3.3-3.5, 3.7) remain PR #156 placeholders; their gates are still documented as "requires MAKO reference extractor". * docs(examples): post-live-run cleanup — install cell, README, copy Four follow-up nits from review of the live-run capture: * **install cell**: ``!pip install ... google-adk[vertexai] ...`` failed under zsh with ``no matches found`` because zsh glob-expanded the ``[vertexai]`` extra. Switched to ``%pip install`` (Jupyter magic that bypasses shell expansion) and quoted the extra. Without the fix the install was silently a no-op and downstream cells ran against whatever was already in the kernel env — explaining the ``bigquery_ontology 0.2.2`` in the committed outputs vs. ``0.2.3`` in a fresh checkout. * **Generated artifacts regenerated against the default ``(test-project-0728-467323, migration_v5_demo)`` pair** rather than the scratch dataset from the live run. Previous commit shipped ``binding.yaml`` / ``table_ddl.sql`` / ``property_graph.sql`` against the ephemeral ``migration_v5_demo_7da57280`` dataset, which is residue, not stable defaults. Reviewers can now ``cat`` the artifacts and see the canonical target. * **README**: substantial rewrite. Removed the "fixture only, notebook is future work" framing; added two new design-decision sections (8: per-entity PK columns, 9: self-edges dropped, 10: inheritance stripped); updated the relationship count (9 → 7); corrected the FK column description (was "AgentSession FK is ``session_id``" via ``_entity_id_column`` stripping ``agent_`` — now ``agent_session_id`` since the strip was removed in the live-run reconciliation); removed the obsolete self-edge node_id encoding section (self-edges are now dropped); added a "What's NOT in this commit" pointing at PR #156's ``AgentSession`` synthesis follow-up. * **Notebook framing**: corrected "nine real MAKO relationships" → "seven" so the framing matches the binding. Live-run evidence in the committed executed notebook is unchanged; the cells ran against the scratch dataset correctly because ``mako_artifacts.regenerate_snapshots`` is called at runtime with the scratch ``(project, dataset)``. The committed snapshot is the stable default that the regenerator overwrites every time. * docs(examples): rerun notebook with %pip install cell Live rerun against fresh scratch dataset ``migration_v5_demo_badf2e2a``. All 59 cells passed. The ``%pip`` install cell still surfaces a PEP-668 ``externally-managed-environment`` rejection in homebrew Python (the install cell is targeted at Colab; in a system-managed env the operator pre-installs into a venv before launching the kernel). That's environment- specific, not a notebook-correctness issue — the live evidence below confirms the SDK behavior works against real BigQuery + Vertex regardless. Updated evidence (from the committed executed notebook): - Beat 1: GQL count before=0, after=1; rows_materialized total=19; property_graph_status='skipped:user_requested'. - Beat 2: drift caught (exit 1), restored (exit 0), combined build clean (rows_materialized total=19). - Beat 3: 3.3-3.5+3.7 print the "requires MAKO reference extractor at examples/migration_v5/reference_extractor.py — lands in follow-up PR #156" skip marker. 3.6 fires NODE + FIELD + EDGE. - Beat 4: resolver returns DecisionExecution with compile_id=07aa0018288c; GRAPH_TABLE count = 2. Hub- shape returns zero (expected — AgentSession synthesis lands with PR #156). * docs(examples): env-aware install cell + README evidence cleanup Three follow-up nits from review of the previous rerun: * P1 — install cell rewritten as **environment-aware**. * In Colab: runs ``%pip install`` via ``get_ipython().run_line_magic("pip", ...)`` (the Jupyter magic that installs into the kernel env without going through the shell). Quoted ``"google-adk[vertexai]"`` so zsh doesn't glob it. * Outside Colab: skips the install entirely. System Python (PEP 668-managed) refuses pip installs and the expected workflow is a pre-baked venv. The cell checks that the required modules are importable and raises a ``RuntimeError`` with a copy-paste install command if any are missing. No more red ``externally-managed-environment`` in the committed notebook output. * P2 — README's "live evidence" paragraph reworded so it describes the *shape* of the evidence rather than the exact numbers from one specific run. The committed executed notebook is the authoritative source for the per-cell values; the README now says ``after=N>0``, ``rows_materialized total>0``, ``count is non-zero``, etc. * PR body refresh happens via ``gh pr edit`` after this commit lands. Re-run captured (scratch ``migration_v5_demo_56f9179f``, 3 sessions). Install cell now prints ``"Local kernel — all required packages are importable."`` Per-beat live evidence (committed in the executed notebook): * Beat 1: count ``before=0, after=2``, ``rows_materialized total=38``, ``property_graph_status='skipped:user_requested'``. * Beat 2: drift caught (exit 1) + restored (exit 0) + combined build clean (``rows_materialized total=19``). * Beat 3: 3.3-3.5 + 3.7 print the ``"requires MAKO reference extractor ... lands in follow-up PR #156"`` skip marker. 3.6 fires NODE + FIELD + EDGE. * Beat 4: resolver returns ``compile_id=ba9c1ff85870``; ``GRAPH_TABLE count = 3``. Hub-shape returns zero (expected; AgentSession synthesis is PR #156). * fix(examples/migration_v5): restore default-dataset snapshots after rerun The previous commit captured the executed notebook which had just regenerated the snapshots against the scratch dataset (`migration_v5_demo_56f9179f`). Run `mako_artifacts.py` with default args to restore the canonical `(test-project-0728-467323, migration_v5_demo)` shape. This is the same trap from the previous round; the checked-in artifacts should always be the stable default, never live-run residue. The notebook regenerates against the scratch at runtime; on disk, the committed copy is the canonical reviewer-readable target. * docs(examples): soften Beat 4.4 framing to match what cell prints The Beat 4.4 markdown said the cell emits a sample showing each DecisionExecution connects an AgentSession, ContextSnapshot, DecisionPoint, and SelectionOutcome. The committed output only proves a nonzero DecisionExecution count and then reports the ``partOfSession`` hub traversal returns zero rows (AgentSession synthesis from the plugin envelope lands with PR #156). Reworded the framing to match: the count traversal alone proves the resolver → graph wiring works end-to-end; the hub-shape sample is explicitly deferred to PR #156. * docs(examples): soften hub-shape inline comment to match deferred state The markdown framing for cell 4.4 was already softened in the previous commit, but the inline code comment above the hub-shape query still said Confirms the user-authored graph object answers a real path query. The committed output intentionally shows zero hub rows because the AgentSession synthesis from the plugin envelope is a PR #156 follow-up. Updated the comment to match.
1 parent 3407966 commit 80f6c6f

13 files changed

Lines changed: 5087 additions & 808 deletions

examples/migration_v5/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__/
2+
*.pyc

examples/migration_v5/README.md

Lines changed: 184 additions & 0 deletions
Large diffs are not rendered by default.

examples/migration_v5/binding.yaml

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
binding: migration_v5_demo_binding
2+
ontology: mako
3+
target:
4+
backend: bigquery
5+
project: test-project-0728-467323
6+
dataset: migration_v5_demo
7+
entities:
8+
- name: AgentSession
9+
source: test-project-0728-467323.migration_v5_demo.agent_session
10+
properties:
11+
- name: id
12+
column: agent_session_id
13+
- name: sessionId
14+
column: session_id
15+
- name: Candidate
16+
source: test-project-0728-467323.migration_v5_demo.candidate
17+
properties:
18+
- name: id
19+
column: candidate_id
20+
- name: ContextSnapshot
21+
source: test-project-0728-467323.migration_v5_demo.context_snapshot
22+
properties:
23+
- name: id
24+
column: context_snapshot_id
25+
- name: snapshotPayload
26+
column: snapshot_payload
27+
- name: snapshotTimestamp
28+
column: snapshot_timestamp
29+
- name: DecisionExecution
30+
source: test-project-0728-467323.migration_v5_demo.decision_execution
31+
properties:
32+
- name: id
33+
column: decision_execution_id
34+
- name: businessEntityId
35+
column: business_entity_id
36+
- name: latencyMs
37+
column: latency_ms
38+
- name: spanId
39+
column: span_id
40+
- name: traceId
41+
column: trace_id
42+
- name: DecisionPoint
43+
source: test-project-0728-467323.migration_v5_demo.decision_point
44+
properties:
45+
- name: id
46+
column: decision_point_id
47+
- name: reversibility
48+
column: reversibility
49+
- name: SelectionOutcome
50+
source: test-project-0728-467323.migration_v5_demo.selection_outcome
51+
properties:
52+
- name: id
53+
column: selection_outcome_id
54+
relationships:
55+
- name: atContextSnapshot
56+
source: test-project-0728-467323.migration_v5_demo.at_context_snapshot
57+
from_columns:
58+
- decision_execution_id
59+
to_columns:
60+
- context_snapshot_id
61+
properties: []
62+
- name: evaluatesCandidate
63+
source: test-project-0728-467323.migration_v5_demo.evaluates_candidate
64+
from_columns:
65+
- decision_point_id
66+
to_columns:
67+
- candidate_id
68+
properties: []
69+
- name: executedAtDecisionPoint
70+
source: test-project-0728-467323.migration_v5_demo.executed_at_decision_point
71+
from_columns:
72+
- decision_execution_id
73+
to_columns:
74+
- decision_point_id
75+
properties: []
76+
- name: hasSelectionOutcome
77+
source: test-project-0728-467323.migration_v5_demo.has_selection_outcome
78+
from_columns:
79+
- decision_execution_id
80+
to_columns:
81+
- selection_outcome_id
82+
properties: []
83+
- name: partOfSession
84+
source: test-project-0728-467323.migration_v5_demo.part_of_session
85+
from_columns:
86+
- decision_execution_id
87+
to_columns:
88+
- agent_session_id
89+
properties: []
90+
- name: rejectedCandidate
91+
source: test-project-0728-467323.migration_v5_demo.rejected_candidate
92+
from_columns:
93+
- selection_outcome_id
94+
to_columns:
95+
- candidate_id
96+
properties: []
97+
- name: selectedCandidate
98+
source: test-project-0728-467323.migration_v5_demo.selected_candidate
99+
from_columns:
100+
- selection_outcome_id
101+
to_columns:
102+
- candidate_id
103+
properties: []
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Export captured events from BigQuery to a local JSONL
16+
snapshot.
17+
18+
This is **not** an event generator. The event stream's
19+
source of truth is the BQ AA plugin's ``agent_events``
20+
table, populated by running ``mako_demo_agent.py`` via
21+
``run_agent.py``. This helper exports a subset of that
22+
table to a local ``events.jsonl`` file so the notebook's
23+
revalidation tests (Beat 3) have a deterministic offline
24+
corpus to gate against — same input every run regardless
25+
of when the live agent last ran.
26+
27+
The selected columns are the subset of the BQ AA plugin's
28+
event schema that the notebook's revalidation tests need
29+
(see ``google/adk/plugins/bigquery_agent_analytics_plugin.py::
30+
_get_events_schema``). Top-level scalar fields ----
31+
``timestamp``, ``event_type``, ``agent``, ``session_id``,
32+
``invocation_id``, ``user_id``, ``trace_id``, ``span_id``,
33+
``parent_span_id``, ``status``, ``error_message``,
34+
``is_truncated`` ---- plus JSON ``content`` / ``attributes``
35+
/ ``latency_ms``. The plugin's full schema also includes
36+
``content_parts`` (a REPEATED RECORD for multimodal
37+
parts); this exporter omits it because the MAKO decision
38+
flow is text-only. Add it back with
39+
``TO_JSON_STRING(content_parts) AS content_parts_json`` if
40+
a future demo needs multimodal trace replay. There is
41+
**no** ``event_id``, ``payload``, ``agent_name``, or
42+
``partition_date`` column on the plugin's table; those
43+
names were from an earlier draft schema and would fail at
44+
query time.
45+
46+
Usage:
47+
48+
PYTHONPATH=src python examples/migration_v5/export_events_jsonl.py \\
49+
--project test-project-0728-467323 \\
50+
--dataset migration_v5_demo \\
51+
--table agent_events \\
52+
--out examples/migration_v5/events.jsonl \\
53+
--limit 200
54+
55+
Pin a fixed ``--limit`` so the captured snapshot stays
56+
small and stable. The notebook regenerates this only when
57+
the demo's event semantics change.
58+
"""
59+
60+
from __future__ import annotations
61+
62+
import argparse
63+
import json
64+
import pathlib
65+
import re
66+
import sys
67+
68+
# BigQuery identifier discipline. ``project.dataset.table``
69+
# segments cannot be passed as query parameters, so the
70+
# table reference is interpolated directly into the SQL.
71+
# Validate each segment against the BQ-permitted character
72+
# set before interpolation so a hostile or malformed
73+
# ``--project / --dataset / --table`` argument cannot smuggle
74+
# whitespace, backticks, semicolons, or comment markers into
75+
# the query. Mirrors
76+
# ``bq_bundle_mirror._TABLE_ID_PATTERN`` (which uses the
77+
# same character class for the same reason).
78+
_BQ_SEGMENT_PATTERN = re.compile(r"^[A-Za-z0-9_-]+$")
79+
80+
81+
_SELECT_SQL = """
82+
SELECT
83+
timestamp,
84+
event_type,
85+
agent,
86+
session_id,
87+
invocation_id,
88+
user_id,
89+
trace_id,
90+
span_id,
91+
parent_span_id,
92+
status,
93+
error_message,
94+
is_truncated,
95+
TO_JSON_STRING(content) AS content_json,
96+
TO_JSON_STRING(attributes) AS attributes_json,
97+
TO_JSON_STRING(latency_ms) AS latency_ms_json
98+
FROM `{table}`
99+
ORDER BY timestamp, span_id
100+
LIMIT @row_limit
101+
"""
102+
103+
104+
def _validated_table_id(project: str, dataset: str, table: str) -> str:
105+
for label, value in (
106+
("project", project),
107+
("dataset", dataset),
108+
("table", table),
109+
):
110+
if not isinstance(value, str) or not _BQ_SEGMENT_PATTERN.fullmatch(value):
111+
raise ValueError(
112+
f"--{label} {value!r} is not a well-formed BigQuery "
113+
f"identifier segment (allowed: ASCII letters, digits, "
114+
f"'_', '-'; no whitespace, backticks, semicolons, or "
115+
f"comment markers)"
116+
)
117+
return f"{project}.{dataset}.{table}"
118+
119+
120+
def main(argv=None) -> int:
121+
from google.cloud import bigquery
122+
123+
parser = argparse.ArgumentParser(
124+
description=(
125+
"Export a deterministic offline snapshot of "
126+
"agent_events for the notebook's revalidation "
127+
"tests."
128+
),
129+
)
130+
parser.add_argument("--project", required=True)
131+
parser.add_argument("--dataset", required=True)
132+
parser.add_argument("--table", default="agent_events")
133+
parser.add_argument("--location", default="US")
134+
parser.add_argument(
135+
"--out",
136+
type=pathlib.Path,
137+
default=pathlib.Path(__file__).parent / "events.jsonl",
138+
)
139+
parser.add_argument("--limit", type=int, default=200)
140+
args = parser.parse_args(argv)
141+
142+
table_id = _validated_table_id(args.project, args.dataset, args.table)
143+
client = bigquery.Client(project=args.project, location=args.location)
144+
sql = _SELECT_SQL.format(table=table_id)
145+
job_config = bigquery.QueryJobConfig(
146+
query_parameters=[
147+
bigquery.ScalarQueryParameter("row_limit", "INT64", args.limit),
148+
]
149+
)
150+
print(f"Exporting from {table_id} (LIMIT {args.limit})", file=sys.stderr)
151+
rows = list(client.query(sql, job_config=job_config).result())
152+
args.out.write_text(
153+
"\n".join(_row_to_jsonl(r) for r in rows) + "\n",
154+
encoding="utf-8",
155+
)
156+
print(f"Wrote {len(rows)} events to {args.out}", file=sys.stderr)
157+
return 0
158+
159+
160+
def _row_to_jsonl(row) -> str:
161+
"""One JSON line per row. Keeps the plugin schema verbatim
162+
so downstream revalidation tests see the same shape they'd
163+
see querying BQ directly. JSON columns come back as
164+
TO_JSON_STRING-encoded strings; decode them here so the
165+
JSONL nest is a single dict.
166+
"""
167+
return json.dumps(
168+
{
169+
"timestamp": str(row["timestamp"]),
170+
"event_type": row["event_type"],
171+
"agent": row["agent"],
172+
"session_id": row["session_id"],
173+
"invocation_id": row["invocation_id"],
174+
"user_id": row["user_id"],
175+
"trace_id": row["trace_id"],
176+
"span_id": row["span_id"],
177+
"parent_span_id": row["parent_span_id"],
178+
"status": row["status"],
179+
"error_message": row["error_message"],
180+
"is_truncated": row["is_truncated"],
181+
"content": _decode_json(row["content_json"]),
182+
"attributes": _decode_json(row["attributes_json"]),
183+
"latency_ms": _decode_json(row["latency_ms_json"]),
184+
},
185+
sort_keys=True,
186+
)
187+
188+
189+
def _decode_json(raw):
190+
return json.loads(raw) if raw else None
191+
192+
193+
if __name__ == "__main__": # pragma: no cover
194+
sys.exit(main())

0 commit comments

Comments
 (0)