{"id":1655,"date":"2026-03-30T11:51:07","date_gmt":"2026-03-30T11:51:07","guid":{"rendered":"https:\/\/cms.research.wpp.com\/?post_type=research_feed&#038;p=1655"},"modified":"2026-06-24T13:21:48","modified_gmt":"2026-06-24T13:21:48","slug":"data-discovery-agent-pod-technical-walkthrough","status":"publish","type":"research_feed","link":"https:\/\/cms.research.wpp.com\/?research_feed=data-discovery-agent-pod-technical-walkthrough","title":{"rendered":"Data Discovery Agent Pod: Technical walkthrough"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>Turning raw ad platform schemas into actionable intelligence through AI-powered semantic mapping.<\/em><\/p>\n\n\n\n<h1 class=\"wp-block-heading\">TL;DR<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Up to 90%\u00a0of enterprise data sits unused,\u00a0and our BigQuery advertising warehouse was no exception:\u00a014 platforms,\u00a0179 tables, 2,709 columns, 15.8 billion records, zero shared schema, and answering a question as simple as whether Pinterest carries geo data required a two-week manual investigation. Every platform invented its own vocabulary (Facebook says <code>amount_spent<\/code>, Google says <code>cost_micros<\/code>, TikTok says <code>spend<\/code>)\u00a0and manual reconciliation cost analysts two to four weeks per pass,\u00a0producing a spreadsheet that started decaying the moment it was finished.\u00a0<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We built the Data Discovery Agent,\u00a0an AI-powered pipeline that connects to BigQuery,\u00a0samples real data for grounding,\u00a0crawls external connector documentation,\u00a0and runs Gemini LLM inference with versioned prompts to autonomously annotate every column with confidence-scored mappings.\u00a0The agent compressed weeks of manual work into hours,\u00a0took us from zero structured understanding of our warehouse to a 58%\u00a0average completeness\u00a0(column presence per canonical sub-modality;\u00a0signal strength and population validation planned for Q2)\u00a0baseline across all 14 platforms,\u00a0and replaced tribal-knowledge spreadsheets with a searchable,\u00a0versioned genome report that turns\u00a0&#8220;do we have that data?&#8221;\u00a0from a research project into a dashboard lookup.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong><strong>If you don&#8217;t care about the technical details, read <a href=\"https:\/\/research.wpp.com\/blog\/why-your-data-genome-may-need-a-check-up-and-how-a-data-discovery-agent-can-help\">our blog post <\/a>instead. <\/strong><\/strong><\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Technical walkthrough<\/h1>\n\n\n\n<h1 class=\"wp-block-heading\">Introduction<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Advertising data warehouses are,\u00a0by nature,\u00a0enormous and messy.\u00a0Every major ad platform\u00a0&#8211;\u00a0Facebook,\u00a0Google,\u00a0TikTok,\u00a0and a dozen others\u00a0&#8211;\u00a0exposes its own schema,\u00a0its own naming conventions,\u00a0and its own definition of what a\u00a0&#8220;click&#8221;\u00a0or a\u00a0&#8220;spend&#8221;\u00a0actually means.\u00a0When these platforms feed into a centralised\u00a0<a href=\"https:\/\/cloud.google.com\/bigquery\/docs\"><strong>Google BigQuery<\/strong><\/a>\u00a0data lake,\u00a0the result is a sprawling collection of tables and columns that no single analyst can hold in their head.\u00a0In our case:\u00a0<strong>14 advertising platforms, 179 tables, and 2,709 columns<\/strong>\u00a0&#8211;\u00a0with over 15.8 billion records behind them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Historically,\u00a0making sense of this warehouse required manual schema annotation:\u00a0a data analyst opening each table,\u00a0reading every column name,\u00a0pulling sample rows,\u00a0cross-referencing API documentation,\u00a0and deciding which canonical category each column belonged to.\u00a0Conservatively,\u00a0that process took\u00a0<strong>two to four weeks<\/strong>\u00a0of focused work for a single pass.\u00a0And the result started decaying immediately\u00a0&#8211;\u00a0platforms update schemas quarterly,\u00a0sometimes monthly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To eliminate this bottleneck,\u00a0we architected and deployed the\u00a0<strong>Data Discovery Agent<\/strong>:\u00a0an AI-powered platform that autonomously discovers,\u00a0maps,\u00a0and scores the completeness of advertising data across an entire BigQuery data lake.\u00a0The agent connects to BigQuery,\u00a0fetches every table and column,\u00a0extracts real sample data for grounding,\u00a0crawls external documentation for cross-reference,\u00a0invokes\u00a0<a href=\"https:\/\/cloud.google.com\/vertex-ai\"><strong>Vertex AI<\/strong><\/a>\u00a0LLM inference with structured prompts,\u00a0and delivers a fully annotated mapping with confidence scores\u00a0&#8211;\u00a0compressing weeks of manual work into hours.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The system is built as a production-grade\u00a0<a href=\"https:\/\/fastapi.tiangolo.com\/\"><strong>FastAPI<\/strong><\/a>\u00a0web application,\u00a0deployed on\u00a0<a href=\"https:\/\/cloud.google.com\/run\/docs\"><strong>Google Cloud Run<\/strong><\/a>,\u00a0with a complete admin control panel,\u00a0role-based access via Google OAuth,\u00a0and a storage abstraction layer that operates identically on local filesystems and Google Cloud Storage.\u00a0This document provides a comprehensive technical walkthrough of its architecture,\u00a0pipeline,\u00a0deployment,\u00a0and results.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Agent architecture<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The Data Discovery Agent is structured as a\u00a0<strong>five-stage sequential pipeline<\/strong>\u00a0orchestrated by a FastAPI backend.\u00a0Unlike a chatbot or a single-prompt wrapper,\u00a0the agent executes an end-to-end workflow of discovery,\u00a0sampling,\u00a0documentation crawling,\u00a0LLM inference,\u00a0and enrichment reporting\u00a0&#8211;\u00a0with minimal human intervention.\u00a0The human&#8217;s role shifts from\u00a0<em>doing the mapping<\/em>\u00a0to\u00a0<em>reviewing the mapping<\/em>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The high-level data flow is illustrated below:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"720\" height=\"1024\" src=\"https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-720x1024.png\" alt=\"\" class=\"wp-image-602\" srcset=\"https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-720x1024.png 720w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-211x300.png 211w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-768x1092.png 768w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-1081x1536.png 1081w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-1441x2048.png 1441w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-scaled.png 1801w\" sizes=\"auto, (max-width: 720px) 100vw, 720px\" \/><figcaption class=\"wp-element-caption\"><em>Figure 1 &#8211; Data Discovery Agent pipeline: five stages from BigQuery data lake to executive dashboard.<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><br>The architecture was inspired by a clean separation of concerns.\u00a0The application core follows a layered pattern with dedicated service modules.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Service module inventory<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The backend is organised into focused service modules,\u00a0each responsible for a distinct domain:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Service Module<\/strong><\/th><th><strong>Core Responsibility<\/strong><\/th><\/tr><\/thead><tbody><tr><td><strong>BigQuery Service<\/strong><\/td><td>Connects to the BigQuery data lake, discovers datasets and tables, fetches column schemas and row counts, and saves timestamped fetch snapshots.<\/td><\/tr><tr><td><strong>Sample Service<\/strong><\/td><td>Extracts real sample rows from BigQuery tables using background-threaded parallel queries. Supports pause, resume, and abort controls.<\/td><\/tr><tr><td><strong>Adverity Service<\/strong><\/td><td>Crawls official Adverity connector documentation pages using Playwright, extracting structured field lists (name, description, dimension\/metric).<\/td><\/tr><tr><td><strong>Inference Service<\/strong><\/td><td>Builds versioned LLM prompts, manages sync and batch inference pipelines via Vertex AI, and parses structured JSON responses into column-to-submodality mappings.<\/td><\/tr><tr><td><strong>BQ Filter Service<\/strong><\/td><td>Manages regex-based include\/exclude rules that control which BigQuery datasets and tables appear in a fetch.<\/td><\/tr><tr><td><strong>User Service<\/strong><\/td><td>Handles user CRUD operations, domain allow-lists, email ban-lists, and role-based access control (admin\/user).<\/td><\/tr><tr><td><strong>LLM Config Service<\/strong><\/td><td>Loads and manages LLM model configurations, including model identifiers and API parameters.<\/td><\/tr><tr><td><strong>Vertex Model Service<\/strong><\/td><td>Discovers available Vertex AI regions and publisher models for inference.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Key system capabilities<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li><strong>Autonomous Schema Discovery:<\/strong>\u00a0The agent connects to BigQuery, applies configurable regex filters, and discovers all advertising-platform datasets, tables, and column schemas without manual intervention.<\/li>\n\n\n<li><strong>Evidence-Grounded LLM Inference:<\/strong>\u00a0Every mapping decision is grounded in real data &#8211; column names, data types, and actual sample values &#8211; not just schema metadata. This dramatically reduces hallucination risk.<\/li>\n\n\n<li><strong>Dual-Direction Mapping:<\/strong>\u00a0The system performs both forward mapping (BigQuery columns to canonical sub-modalities) and reverse mapping (Adverity connector fields to sub-modalities), closing the loop from &#8220;what do we have?&#8221; to &#8220;what should we enable?&#8221;<\/li>\n\n\n<li><strong>Completeness Scoring:<\/strong>\u00a0Each platform receives a quantitative completeness score measuring how many of the 24 canonical sub-modalities are covered, turning vague data quality concerns into actionable metrics.<\/li>\n\n\n<li><strong>Versioned Prompt Engineering:<\/strong>\u00a0All LLM prompts are version-tracked with SHA-256 content hashing, ensuring that every inference result can be traced back to the exact prompt that produced it.<\/li>\n\n\n<li><strong>Transparent Storage Abstraction:<\/strong>\u00a0A unified storage layer allows the application to operate identically on local filesystems (development) and Google Cloud Storage (production) by changing a single environment variable.<\/li>\n\n\n<li><strong>Production-Grade Access Control:<\/strong>\u00a0Google OAuth 2.0 authentication with domain allow-lists, email ban-lists, and admin\/user role separation ensures secure, auditable access.<\/li>\n\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">The modality schema: a canonical taxonomy<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">At the heart of the agent&#8217;s mapping logic is a canonical\u00a0<strong>modality schema<\/strong>\u00a0&#8211;\u00a0a structured taxonomy that defines what advertising data\u00a0<em>means<\/em>,\u00a0independent of any platform&#8217;s naming conventions.\u00a0The schema is organised around five high-level\u00a0<strong>modalities<\/strong>,\u00a0each representing a fundamental category of advertising measurement:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Modality<\/strong><\/th><th><strong>Sub-modalities<\/strong><\/th><th><strong>What It Captures<\/strong><\/th><\/tr><\/thead><tbody><tr><td><strong>Performance<\/strong><\/td><td>7<\/td><td>The numbers: spend, impressions, clicks, video plays, conversions, leads, CTR<\/td><\/tr><tr><td><strong>Creative<\/strong><\/td><td>4<\/td><td>What the ad looked like: creative IDs, asset paths, ad names, format types<\/td><\/tr><tr><td><strong>Audience<\/strong><\/td><td>6<\/td><td>Who saw it: gender, age group, interests, custom audiences, behavioural segments<\/td><\/tr><tr><td><strong>Geo<\/strong><\/td><td>5<\/td><td>Where they saw it: countries, regions, cities, postal codes, designated market areas<\/td><\/tr><tr><td><strong>Brand<\/strong><\/td><td>2<\/td><td>Who paid for it: brand name, advertiser identity<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This yields\u00a0<strong>24 sub-modalities<\/strong>\u00a0in total\u00a0&#8211;\u00a0the atomic units of the mapping.\u00a0The LLM&#8217;s task is to determine,\u00a0for each BigQuery column across all 179 tables,\u00a0which sub-modality\u00a0(if any)\u00a0it corresponds to,\u00a0and with what confidence.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The modality dictionary is fully editable through the admin UI,\u00a0meaning the taxonomy can be extended or refined without touching code.\u00a0It is injected into every LLM prompt as structured context,\u00a0ensuring consistent mapping behaviour across inference runs.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Technology stack<\/h1>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Component<\/strong><\/th><th><strong>Technology<\/strong><\/th><th><strong>Role<\/strong><\/th><\/tr><\/thead><tbody><tr><td><strong>Web Framework<\/strong><\/td><td><a href=\"https:\/\/fastapi.tiangolo.com\/\">FastAPI<\/a><\/td><td>Application backend, API routing, middleware, and session management<\/td><\/tr><tr><td><strong>LLM Engine<\/strong><\/td><td><a href=\"https:\/\/deepmind.google\/technologies\/gemini\/\">Google Gemini<\/a>\u00a0(via\u00a0<a href=\"https:\/\/cloud.google.com\/vertex-ai\">Vertex AI<\/a>)<\/td><td>Powers all schema-mapping and recommendation inference across the Gemini model family (including Gemini Flash for batch runs); chosen for low latency, strong instruction-following, and structured JSON output<\/td><\/tr><tr><td><strong>Data Warehouse<\/strong><\/td><td><a href=\"https:\/\/cloud.google.com\/bigquery\/docs\">Google BigQuery<\/a><\/td><td>Primary data source; the agent queries dataset schemas, table metadata, and sample rows<\/td><\/tr><tr><td><strong>Batch Inference<\/strong><\/td><td><a href=\"https:\/\/cloud.google.com\/vertex-ai\/docs\/predictions\/get-batch-predictions\">Vertex AI Batch Prediction<\/a><\/td><td>Processes bulk LLM requests asynchronously via JSONL files staged in GCS; more cost-effective for full runs across all platforms<\/td><\/tr><tr><td><strong>Documentation Crawling<\/strong><\/td><td><a href=\"https:\/\/playwright.dev\/python\/\">Playwright<\/a><\/td><td>Headless browser automation for crawling Adverity connector documentation pages and extracting structured field lists<\/td><\/tr><tr><td><strong>Frontend<\/strong><\/td><td><a href=\"https:\/\/jinja.palletsprojects.com\/\">Jinja2<\/a>\u00a0+\u00a0<a href=\"https:\/\/htmx.org\/\">HTMX<\/a><\/td><td>Server-side rendered HTML templates with HTMX for interactive, partial-page updates without a JavaScript framework<\/td><\/tr><tr><td><strong>Authentication<\/strong><\/td><td><a href=\"https:\/\/developers.google.com\/identity\/protocols\/oauth2\">Google OAuth 2.0<\/a>\u00a0(via\u00a0<a href=\"https:\/\/authlib.org\/\">Authlib<\/a>)<\/td><td>Secure user login with domain allow-lists, email ban-lists, and admin\/user role separation<\/td><\/tr><tr><td><strong>Application Data<\/strong><\/td><td><a href=\"https:\/\/cloud.google.com\/storage\/docs\">Google Cloud Storage<\/a><\/td><td>Persistent store for fetch results, inference outputs, platform configs, user records, and Adverity documentation<\/td><\/tr><tr><td><strong>Deployment<\/strong><\/td><td><a href=\"https:\/\/cloud.google.com\/run\/docs\">Google Cloud Run<\/a><\/td><td>Serverless container hosting with auto-scaling, health probes, and secret-backed environment variables<\/td><\/tr><tr><td><strong>Infrastructure<\/strong><\/td><td><a href=\"https:\/\/www.terraform.io\/\">Terraform<\/a><\/td><td>Infrastructure-as-code: GCS buckets, Artifact Registry, Cloud Run service, IAM roles, Secret Manager secrets<\/td><\/tr><tr><td><strong>Containerisation<\/strong><\/td><td><a href=\"https:\/\/www.docker.com\/\">Docker<\/a>\u00a0(multi-stage build)<\/td><td>Reproducible builds with a builder stage for dependency installation and a slim runtime stage running as non-root<\/td><\/tr><tr><td><strong>Secret Management<\/strong><\/td><td><a href=\"https:\/\/cloud.google.com\/secret-manager\/docs\">Google Secret Manager<\/a><\/td><td>Stores OAuth credentials and session-signing keys, injected into Cloud Run as environment variables<\/td><\/tr><tr><td><strong>Language<\/strong><\/td><td>Python 3.11<\/td><td>All application code, managed via\u00a0<a href=\"https:\/\/python-poetry.org\/\">Poetry<\/a>\u00a0for dependency resolution<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Pipeline deep-dive<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The following five stages take the system from a raw,\u00a0unannotated data warehouse to a fully mapped,\u00a0scored,\u00a0and actionable genome report.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Stage 1 &#8211; Discovery<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The agent connects to Google BigQuery and discovers all staging datasets matching a configurable naming pattern\u00a0(e.g.,\u00a0<code>^bq_cgh_mp_.*_staging$<\/code>).\u00a0For each dataset,\u00a0it fetches the full table inventory:\u00a0column names,\u00a0data types,\u00a0and row counts.\u00a0Intelligent regex filters\u00a0&#8211;\u00a0managed through the admin UI via\u00a0<code>bq_filters.json<\/code>\u00a0&#8211;\u00a0exclude temporary artefacts such as tables prefixed with\u00a0<code>stg_<\/code>,\u00a0suffixed with\u00a0<code>_tmp<\/code>\u00a0or\u00a0<code>_dbt_tmp<\/code>,\u00a0and any datasets that don&#8217;t match the expected naming convention.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Results are saved as a\u00a0<strong>timestamped fetch snapshot<\/strong>\u00a0(<code>fetches\/fetch_YYYYMMDD_HHMMSS\/results.json<\/code>).\u00a0Multiple fetches can coexist,\u00a0allowing admins to compare schema evolution over time.\u00a0Each fetch captures the complete state of the warehouse at a point in time\u00a0&#8211;\u00a0the starting point for all downstream inference work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Current warehouse dimensions:<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Metric<\/strong><\/th><th><strong>Count<\/strong><\/th><\/tr><\/thead><tbody><tr><td>Advertising Platforms<\/td><td>14<\/td><\/tr><tr><td>BigQuery Datasets<\/td><td>17<\/td><\/tr><tr><td>Tables<\/td><td>179<\/td><\/tr><tr><td>Total Columns<\/td><td>2,709<\/td><\/tr><tr><td>Total Records<\/td><td>15.8 billion+<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Platforms covered<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>#<\/th><th>Platform<\/th><th>#<\/th><th>Platform<\/th><\/tr><\/thead><tbody><tr><td>1<\/td><td>Amazon DSP<\/td><td>8<\/td><td>Pinterest<\/td><\/tr><tr><td>2<\/td><td>Facebook Ads<\/td><td>9<\/td><td>Snapchat<\/td><\/tr><tr><td>3<\/td><td>Google Ads<\/td><td>10<\/td><td>TikTok<\/td><\/tr><tr><td>4<\/td><td>Google Ads (YouTube)<\/td><td>11<\/td><td>The Trade Desk<\/td><\/tr><tr><td>5<\/td><td>Google DV360<\/td><td>12<\/td><td>Twitter\/X Ads<\/td><\/tr><tr><td>6<\/td><td>Google DV360 (YouTube)<\/td><td>13<\/td><td>Xandr<\/td><\/tr><tr><td>7<\/td><td>LinkedIn<\/td><td>14<\/td><td>IAS<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Stage 2 &#8211; Sampling<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Column names alone are often ambiguous.\u00a0A column called\u00a0<code>cost<\/code>\u00a0could mean total spend,\u00a0cost-per-click,\u00a0or an internal ID.\u00a0The agent resolves this ambiguity by extracting\u00a0<strong>actual sample data rows<\/strong>\u00a0from each table using background-threaded parallel queries against BigQuery.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These real values become critical evidence for the LLM:\u00a0seeing\u00a0<code>14.50<\/code>,\u00a0<code>0.83<\/code>,\u00a0<code>127.99<\/code>\u00a0in a\u00a0<code>cost<\/code>\u00a0column strongly suggests monetary spend,\u00a0not an identifier.\u00a0Seeing\u00a0<code>M<\/code>,\u00a0<code>F<\/code>,\u00a0<code>Unknown<\/code>\u00a0in a\u00a0<code>gender<\/code>\u00a0column confirms an audience dimension more reliably than the column name alone.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The extraction process supports\u00a0<strong>pause, resume, and abort<\/strong>\u00a0controls\u00a0&#8211;\u00a0essential when sampling across hundreds of tables with billions of rows.\u00a0Progress is tracked per-platform and persisted to storage,\u00a0so an interrupted extraction can resume exactly where it left off.\u00a0Samples are saved as\u00a0<code>samples_&lt;platform&gt;.json<\/code>\u00a0per fetch and are injected into the LLM prompt alongside the schema metadata.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Stage 3 &#8211; Documentation crawling<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The agent crawls the official connector documentation for each platform\u00a0&#8211;\u00a0specifically,\u00a0the authoritative\u00a0&#8220;Most Used Fields&#8221;\u00a0pages published by\u00a0<a href=\"https:\/\/www.adverity.com\/\">Adverity<\/a>,\u00a0the data integration platform that feeds data into the BigQuery warehouse.\u00a0Using\u00a0<a href=\"https:\/\/playwright.dev\/python\/\">Playwright<\/a>\u00a0for headless browser automation,\u00a0the crawler fetches each documentation page and extracts a structured field list:\u00a0name,\u00a0description,\u00a0and whether the field is a dimension or metric.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Each crawled page is saved in three formats\u00a0&#8211;\u00a0HTML,\u00a0Markdown,\u00a0and structured JSON\u00a0&#8211;\u00a0under\u00a0<code>adverity_docs\/<\/code>\u00a0in the storage backend.\u00a0These documents serve as the agent&#8217;s\u00a0&#8220;reference manual&#8221;:\u00a0a ground-truth checklist of what fields each platform\u00a0<em>could<\/em>\u00a0provide,\u00a0independent of what currently exists in the warehouse.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The crawl engine runs as a background task with progress tracking,\u00a0with a configurable delay between requests to avoid rate-limiting.\u00a0Platform URLs are managed through the admin UI,\u00a0making it straightforward to add new data sources.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Stage 4 &#8211; LLM inference<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the core intellectual step\u00a0&#8211;\u00a0where the agent reads every column and produces an annotated mapping.\u00a0The Inference Service builds\u00a0<strong>versioned prompts<\/strong>\u00a0containing three layers of context:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li><strong>Table schemas<\/strong>\u00a0&#8211; column names and data types for every table in the platform&#8217;s dataset.<\/li>\n\n\n<li><strong>Sample data<\/strong>\u00a0&#8211; real row values extracted in Stage 2, providing grounding evidence.<\/li>\n\n\n<li><strong>Modality definitions<\/strong>\u00a0&#8211; the target taxonomy of modalities and sub-modalities the LLM must map to.<\/li>\n\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">These structured prompts are sent to models from the\u00a0<strong><a href=\"https:\/\/deepmind.google\/technologies\/gemini\/\">Google Gemini<\/a><\/strong>\u00a0family via Vertex AI.\u00a0The model returns structured JSON:\u00a0for each canonical sub-modality,\u00a0it identifies matching columns,\u00a0assigns a\u00a0<strong>confidence score<\/strong>\u00a0(0.0 to 1.0),\u00a0and provides natural-language reasoning for each match.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Inference modes<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Two inference modes are supported,\u00a0selectable through the admin UI:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Mode<\/strong><\/th><th><strong>Mechanism<\/strong><\/th><th><strong>Best For<\/strong><\/th><\/tr><\/thead><tbody><tr><td><strong>Sync<\/strong><\/td><td>One LLM request per platform, processed sequentially via\u00a0<code>POST \/api\/inference\/sync<\/code><\/td><td>Quick testing, single-platform runs<\/td><\/tr><tr><td><strong>Batch<\/strong><\/td><td>Prompts written to JSONL files in GCS, submitted as Vertex AI batch prediction jobs, polled for completion<\/td><td>Full production runs across all 14 platforms; significantly more cost-effective<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">For batch mode,\u00a0the workflow is:\u00a0write prompts to JSONL,\u00a0submit batch jobs,\u00a0poll status until completion,\u00a0collect and parse results.\u00a0The UI provides real-time progress tracking for all active jobs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Prompt engineering<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">All prompt templates live in\u00a0<code>app\/prompts\/<\/code>\u00a0and are version-tracked with semantic versioning and SHA-256 content hashing via\u00a0<code>versions.json<\/code>.\u00a0This means every inference result records exactly which prompt version and content hash produced it\u00a0&#8211;\u00a0critical for reproducibility.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The base modality mapping prompt instructs the LLM to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Examine every column name and data type across all provided tables.<\/li>\n\n\n<li>Use sample data to confirm or reject potential matches.<\/li>\n\n\n<li>Assign confidence scores on a defined scale (0.9-1.0 for clear matches, 0.5-0.69 for ambiguous, below 0.5 for weak).<\/li>\n\n\n<li>Return\u00a0<strong>only valid JSON<\/strong>\u00a0&#8211; no markdown, no prose.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">A dedicated\u00a0<strong>Adverity recommendation prompt<\/strong>\u00a0performs the reverse mapping:\u00a0given Adverity connector documentation,\u00a0recommend which Adverity fields map to which sub-modalities.\u00a0This prompt enforces a critical rule\u00a0&#8211;\u00a0field names must appear\u00a0<strong>verbatim<\/strong>\u00a0in the documentation,\u00a0preventing the LLM from inventing fields that don&#8217;t exist.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Robust JSON parsing<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">LLM outputs are rarely perfectly formatted.\u00a0The Inference Service includes a\u00a0<strong>robust JSON parser<\/strong>\u00a0(<code>_robust_parse_llm_json<\/code>)\u00a0that handles common LLM formatting mistakes:\u00a0markdown code fences,\u00a0trailing commas,\u00a0single-quoted strings,\u00a0BOM characters,\u00a0leading\/trailing prose around the JSON object,\u00a0and escaped newlines inside strings.\u00a0This defensive parsing layer ensures that minor formatting issues never cause a pipeline failure.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Three inference pipelines<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The system supports three distinct inference types:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li><strong>Modality Mapping Inference<\/strong>\u00a0&#8211; Maps BigQuery columns to the canonical sub-modality taxonomy. The primary pipeline.<\/li>\n\n\n<li><strong>Adverity Recommendation Inference<\/strong>\u00a0&#8211; Given Adverity documentation, recommends which connector fields should be enabled for each sub-modality.<\/li>\n\n\n<li><strong>Field Extraction Inference<\/strong>\u00a0&#8211; Extracts structured field definitions from raw Adverity HTML\/Markdown documentation using chunked batch inference, producing clean per-platform field maps.<\/li>\n\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Each run records metadata:\u00a0timestamp,\u00a0model used,\u00a0prompt version,\u00a0prompt content hash,\u00a0and platforms processed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Stage 5 &#8211; Enrichment and dashboard<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The final stage closes the diagnostic loop by cross-referencing the warehouse mappings from Stage 4 against the connector field lists from Stage 3.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Enrichment recommendations<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The Adverity Recommendation inference goes beyond simply flagging missing geo data. It prescribes a fix: <em>&#8220;The connector for this platform offers a field called <code>country_code<\/code> (dimension); enable it, and your geo coverage improves from 2\/5 to 3\/5 sub-modalities.&#8221;<\/em> Diagnosis and prescription in one step, turning gap analysis into concrete action items for the data engineering team.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Completeness scoring<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Each platform receives a\u00a0<strong>completeness score<\/strong>:\u00a0how much of the canonical 24-sub-modality schema is actually present.\u00a0The approach is deliberately simple\u00a0&#8211;\u00a0a presence\/absence assay.\u00a0For each platform,\u00a0the denominator is 24\u00a0(total sub-modalities).\u00a0The numerator is how many have at least one column mapped above a configurable confidence threshold.\u00a0A sub-modality is either present or it isn&#8217;t\u00a0&#8211;\u00a0this handles deduplication naturally.\u00a0If three tables each have a\u00a0<code>spend<\/code>\u00a0column mapped to\u00a0<code>performance__spend_usd<\/code>,\u00a0the sub-modality counts once,\u00a0not three times.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The executive dashboard<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">All results render in a\u00a0<strong>FastAPI + Jinja2\/HTMX dashboard<\/strong>\u00a0that provides:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>Platform scorecards<\/strong>\u00a0&#8211; one card per advertising platform showing its name, banner image, and completeness percentage.<\/li>\n\n\n<li><strong>Modality drilldowns<\/strong>\u00a0&#8211; for each platform, expandable breakdowns into modalities and their sub-modalities.<\/li>\n\n\n<li><strong>Column mappings<\/strong>\u00a0&#8211; the specific BigQuery columns mapped to each sub-modality, with confidence scores and LLM reasoning.<\/li>\n\n\n<li><strong>Sample data preview<\/strong>\u00a0&#8211; actual row data from each table for manual verification.<\/li>\n\n\n<li><strong>Executive summary sidebar<\/strong>\u00a0&#8211; aggregated statistics across all platforms: total mappings, average completeness, record counts, highest and lowest performing platforms.<\/li>\n\n\n<li><strong>Interactive mapping visualiser<\/strong>\u00a0&#8211; a left-right mapping view, filterable by platform, modality, and confidence threshold.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Visibility of platforms is controlled via\u00a0<strong>Dashboard Settings<\/strong>,\u00a0where admins choose which fetch and which platforms appear to end users.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">PDF executive report generation<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The dashboard includes a one-click\u00a0<strong>PDF executive report generator<\/strong>\u00a0for stakeholders who need a polished,\u00a0offline-readable document.\u00a0When triggered,\u00a0the system:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li><strong>Aggregates all dashboard data<\/strong>\u00a0&#8211; platform stats, completeness scores, modality breakdowns, and mapped column details &#8211; into a single render context.<\/li>\n\n\n<li><strong>Renders a dedicated Jinja2 report template<\/strong>\u00a0(<code>exec_report.html<\/code>) &#8211; a professionally styled, multi-page A4 document with a cover page, executive overview, key metric summary cards, a platform comparison table, the full modality dictionary with descriptions, and per-platform detail sections showing completeness stats, modality coverage grids (with check\/miss indicators per sub-modality), and optionally the full column mapping list.<\/li>\n\n\n<li><strong>Converts the HTML to PDF<\/strong>\u00a0using\u00a0<a href=\"https:\/\/playwright.dev\/python\/\">Playwright<\/a>&#8216;s headless Chromium instance (<code>page.pdf(format=\"A4\", print_background=True)<\/code>), producing a pixel-perfect render with page breaks, headers, and footers showing the generation timestamp and page numbers.<\/li>\n\n\n<li><strong>Returns the PDF as a downloadable file<\/strong>\u00a0(e.g.,\u00a0<code>exec_report_20260327_112709.pdf<\/code>).<\/li>\n\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The report is designed for executive consumption:\u00a0the first page presents headline metrics\u00a0(total platforms,\u00a0tables,\u00a0columns,\u00a0records,\u00a0average completeness)\u00a0and identifies the best and worst performers.\u00a0Subsequent pages provide the modality dictionary for reference,\u00a0followed by detailed per-platform breakdowns with modality coverage grids showing exactly which sub-modalities are mapped\u00a0(with column counts)\u00a0and which remain gaps.\u00a0This gives leadership a complete,\u00a0self-contained snapshot of data integration maturity without requiring dashboard access.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Cloud deployment<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The system is deployed as a production-grade,\u00a0cloud-native service on\u00a0<strong>Google Cloud<\/strong>,\u00a0following a containerised,\u00a0infrastructure-as-code workflow from local development through to automated deployment.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Infrastructure-as-Code (Terraform)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">All infrastructure is defined as Terraform in\u00a0<code>deployment\/terraform\/<\/code>\u00a0with six modules:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Module<\/strong><\/th><th><strong>Resources Created<\/strong><\/th><\/tr><\/thead><tbody><tr><td><strong><code>apis<\/code><\/strong><\/td><td>Enables 10 GCP APIs (Cloud Run, Artifact Registry, Storage, BigQuery, Vertex AI, Secret Manager, IAM, Cloud Build, etc.)<\/td><\/tr><tr><td><strong><code>artifact_registry<\/code><\/strong><\/td><td>Docker image repository with a keep-last-5 cleanup policy<\/td><\/tr><tr><td><strong><code>gcs<\/code><\/strong><\/td><td>Versioned GCS bucket for application data (platforms, fetches, users, banners) with lifecycle rules<\/td><\/tr><tr><td><strong><code>iam<\/code><\/strong><\/td><td>Service account with roles: Storage Object Admin, BigQuery Data Viewer, BigQuery Job User, Vertex AI User, Secret Accessor<\/td><\/tr><tr><td><strong><code>secret_manager<\/code><\/strong><\/td><td>Three secrets:\u00a0<code>SECRET_KEY<\/code>,\u00a0<code>GOOGLE_CLIENT_ID<\/code>,\u00a0<code>GOOGLE_CLIENT_SECRET<\/code><\/td><\/tr><tr><td><strong><code>cloud_run<\/code><\/strong><\/td><td>Cloud Run v2 service with health probes, auto-scaling (0 to 3 instances), and secret-backed environment variables<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Containerisation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The application is packaged as a Docker image using a\u00a0<strong>multi-stage build<\/strong>:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li><strong>Builder stage<\/strong>\u00a0&#8211; Installs Python dependencies via Poetry, exports to\u00a0<code>requirements.txt<\/code>, and installs packages into a clean prefix.<\/li>\n\n\n<li><strong>Runtime stage<\/strong>\u00a0&#8211; Copies only the installed packages and application code into a slim Python 3.11 image. Runs as a non-root\u00a0<code>appuser<\/code>\u00a0for security. Seed data (platform configs, modality dictionaries, Adverity docs) is baked into the image; runtime data (fetches, users, inference results) lives in the GCS bucket.<\/li>\n\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Deployment pipeline<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A full deployment is executed with a single command:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>make deploy-all    # tf-init -&gt; tf-apply -&gt; docker-push -&gt; seed-data<br><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This runs Terraform to provision\/update all infrastructure,\u00a0builds and pushes the Docker image to Artifact Registry,\u00a0and syncs the local\u00a0<code>data\/<\/code>\u00a0directory to the GCS bucket.\u00a0Incremental deployments\u00a0(code-only changes)\u00a0use\u00a0<code>make deploy<\/code>\u00a0which skips the Terraform init step.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The\u00a0<code>seed-data<\/code>\u00a0target uses\u00a0<code>gsutil -m rsync<\/code>\u00a0to upload platform definitions,\u00a0modality dictionaries,\u00a0BQ filters,\u00a0Adverity documentation,\u00a0and other configuration data into the GCS bucket.\u00a0Without seeding,\u00a0a fresh deployment starts with an empty bucket and no platform or user configuration.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Storage abstraction layer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A critical architectural decision was the\u00a0<strong>storage abstraction layer<\/strong>\u00a0(<code>app\/core\/storage.py<\/code>).\u00a0All data I\/O goes through a unified\u00a0<code>StorageBackend<\/code>\u00a0interface with two implementations:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong><code>LocalStorage<\/code><\/strong>\u00a0&#8211; reads\/writes to the local\u00a0<code>data\/<\/code>\u00a0directory. Used during development (<code>STORAGE_BACKEND=local<\/code>).<\/li>\n\n\n<li><strong><code>GCSStorage<\/code><\/strong>\u00a0&#8211; reads\/writes to a GCS bucket under a configurable prefix. Used in production (<code>STORAGE_BACKEND=gcs<\/code>).<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Switching between backends requires changing only the\u00a0<code>STORAGE_BACKEND<\/code>\u00a0environment variable.\u00a0Every service module calls\u00a0<code>get_storage()<\/code>\u00a0and operates through the same API regardless of the underlying storage\u00a0&#8211;\u00a0ensuring that code tested locally behaves identically in production.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Authentication and access control<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The application uses\u00a0<strong>Google OAuth 2.0<\/strong>\u00a0(OpenID Connect)\u00a0for user authentication:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Users log in with their Google account via the standard OAuth consent flow.<\/li>\n\n\n<li><strong>Domain allow-lists<\/strong>\u00a0restrict login to specific email domains (e.g.,\u00a0<code>satalia.com<\/code>,\u00a0<code>choreograph.com<\/code>).<\/li>\n\n\n<li><strong>Email ban-lists<\/strong>\u00a0allow blocking specific users.<\/li>\n\n\n<li><strong>Role-based access<\/strong>\u00a0separates admin users (full control panel) from regular users (executive dashboard only).<\/li>\n\n\n<li>A\u00a0<strong>bypass mode<\/strong>\u00a0(<code>BYPASS_AUTH_AS_ADMIN=true<\/code>) enables local development without Google credentials.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">All user and auth settings are managed through the admin UI and persisted via the storage backend.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Results and impact<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">When the pipeline completes a full run across all fourteen platforms,\u00a0the executive dashboard delivers a comprehensive genome report of the data warehouse.\u00a0The following results are drawn from the live executive summary generated by the dashboard.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Current warehouse state<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Metric<\/strong><\/th><th><strong>Value<\/strong><\/th><\/tr><\/thead><tbody><tr><td>Platforms Monitored<\/td><td>14<\/td><\/tr><tr><td>Tables<\/td><td>179<\/td><\/tr><tr><td>Columns<\/td><td>2,709<\/td><\/tr><tr><td>Total Records<\/td><td>15.8B<\/td><\/tr><tr><td>Average Completeness<\/td><td><strong>58%<\/strong><\/td><\/tr><tr><td>Highest Completeness<\/td><td>Snapchat &#8211;\u00a0<strong>75%<\/strong>\u00a0(18\/24)<\/td><\/tr><tr><td>Lowest Completeness<\/td><td>Integral Ad Science &#8211;\u00a0<strong>25%<\/strong>\u00a0(6\/24)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Per-platform breakdown<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Platform<\/strong><\/th><th><strong>Completeness<\/strong><\/th><th><strong>Tables<\/strong><\/th><th><strong>Columns<\/strong><\/th><th><strong>Records<\/strong><\/th><\/tr><\/thead><tbody><tr><td>Facebook Ads<\/td><td>66% (16\/24)<\/td><td>15<\/td><td>364<\/td><td>3,147.7M<\/td><\/tr><tr><td>Google Ads<\/td><td>70% (17\/24)<\/td><td>21<\/td><td>399<\/td><td>2,284.6M<\/td><\/tr><tr><td>TikTok<\/td><td>62% (15\/24)<\/td><td>14<\/td><td>217<\/td><td>15.1M<\/td><\/tr><tr><td>LinkedIn<\/td><td>70% (17\/24)<\/td><td>12<\/td><td>143<\/td><td>366.4k<\/td><\/tr><tr><td>Amazon DSP<\/td><td>62% (15\/24)<\/td><td>18<\/td><td>213<\/td><td>18.2M<\/td><\/tr><tr><td>Integral Ad Science<\/td><td>25% (6\/24)<\/td><td>12<\/td><td>130<\/td><td>95.9M<\/td><\/tr><tr><td>Xandr<\/td><td>50% (12\/24)<\/td><td>11<\/td><td>156<\/td><td>1.2M<\/td><\/tr><tr><td>Google Ads (YouTube)<\/td><td>45% (11\/24)<\/td><td>9<\/td><td>112<\/td><td>10.9M<\/td><\/tr><tr><td>Display &amp; Video 360<\/td><td>70% (17\/24)<\/td><td>16<\/td><td>275<\/td><td>10,148.3M<\/td><\/tr><tr><td>Display &amp; Video 360 (YouTube)<\/td><td>54% (13\/24)<\/td><td>15<\/td><td>187<\/td><td>63.6M<\/td><\/tr><tr><td>Pinterest<\/td><td>70% (17\/24)<\/td><td>11<\/td><td>170<\/td><td>6.7M<\/td><\/tr><tr><td>Snapchat<\/td><td>75% (18\/24)<\/td><td>10<\/td><td>150<\/td><td>15M<\/td><\/tr><tr><td>The Trade Desk<\/td><td>45% (11\/24)<\/td><td>6<\/td><td>80<\/td><td>4.1M<\/td><\/tr><tr><td>Twitter Ads<\/td><td>50% (12\/24)<\/td><td>9<\/td><td>113<\/td><td>40.4k<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Headline findings<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Four platforms\u00a0&#8211;\u00a0Google Ads,\u00a0LinkedIn,\u00a0Display\u00a0&amp;\u00a0Video 360,\u00a0and Pinterest\u00a0&#8211;\u00a0cluster at\u00a0<strong>70% completeness<\/strong>\u00a0(17\/24 sub-modalities).\u00a0Snapchat leads the pack at\u00a0<strong>75%<\/strong>.\u00a0Several platforms sit in the 50-66%\u00a0range,\u00a0and a handful fall below 50%.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Performance metrics<\/strong>\u00a0(spend,\u00a0impressions,\u00a0clicks)\u00a0are well-represented across the board\u00a0&#8211;\u00a0these are the columns every platform exposes and every data team queries first.\u00a0But\u00a0<strong>Audience, Geo, and Brand modalities<\/strong>\u00a0show significant gaps,\u00a0particularly in measurement and programmatic categories.\u00a0Some findings were genuine surprises:\u00a0platforms assumed to lack audience data turned out to carry age range and gender columns that mapped cleanly.\u00a0The data was there all along\u00a0&#8211;\u00a0it just hadn&#8217;t been annotated.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At 58%,\u00a0the warehouse is more than half-mapped\u00a0&#8211;\u00a0but meaningful blind spots remain.\u00a0That&#8217;s a useful headline.\u00a0It is unequivocally better to know the current state than to assume it&#8217;s healthy without running the test.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Operational impact<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Before (Manual)<\/strong><\/th><th><strong>After (Agent)<\/strong><\/th><\/tr><\/thead><tbody><tr><td>2-4 weeks per full mapping pass<\/td><td>Hours for a complete run<\/td><\/tr><tr><td>Mapping decays immediately as schemas change<\/td><td>Re-run on demand; schema drift detected automatically<\/td><\/tr><tr><td>Tribal knowledge locked in spreadsheets<\/td><td>Structured, versioned, searchable mappings with confidence scores<\/td><\/tr><tr><td>Answering &#8220;do we have geo data from Pinterest?&#8221; required manual investigation<\/td><td>Dashboard lookup: instant answer with completeness score<\/td><\/tr><tr><td>Enrichment recommendations require manual cross-referencing<\/td><td>Automated prescriptions: &#8220;Enable\u00a0<code>country_code<\/code>\u00a0on this connector&#8221;<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The shift is from\u00a0<em>doing the mapping<\/em>\u00a0to\u00a0<em>reviewing the mapping<\/em>\u00a0&#8211;\u00a0a fundamentally different use of analyst time.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Admin workflow<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The typical end-to-end admin workflow from raw data to a published dashboard follows this sequence:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"404\" height=\"1024\" src=\"https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-404x1024.png\" alt=\"\" class=\"wp-image-604\" srcset=\"https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-404x1024.png 404w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-118x300.png 118w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-768x1949.png 768w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-807x2048.png 807w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-scaled.png 1009w\" sizes=\"auto, (max-width: 404px) 100vw, 404px\" \/><figcaption class=\"wp-element-caption\"><em>Figure 2 &#8211; End-to-end admin workflow: eleven steps from platform configuration to a live dashboard, with a feedback loop for prompt iteration.<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Each step is performed through the web-based admin UI.\u00a0The pipeline is intentionally manual at the trigger level\u00a0&#8211;\u00a0an admin decides when to run each stage\u00a0&#8211;\u00a0while the execution of each stage is fully automated.\u00a0This design provides human oversight at decision points while eliminating manual drudgery within each step.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Conclusion<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This technical walkthrough has presented the Data Discovery Agent:\u00a0a production-grade,\u00a0AI-powered platform that transforms the laborious process of advertising data warehouse annotation from weeks of manual spreadsheet work into an automated,\u00a0repeatable,\u00a0and auditable pipeline.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The agent&#8217;s five-stage architecture\u00a0&#8211;\u00a0Discovery,\u00a0Sampling,\u00a0Documentation Crawling,\u00a0LLM Inference,\u00a0and Enrichment\u00a0&#8211;\u00a0systematically builds context at each step so that the LLM&#8217;s mapping decisions are grounded in real evidence rather than speculation.\u00a0The result is a fully annotated data warehouse with confidence-scored mappings,\u00a0quantitative completeness metrics,\u00a0and actionable enrichment recommendations.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Lessons learned<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>Grounding is Everything:<\/strong>\u00a0Providing the LLM with real sample data alongside schema metadata was the single most impactful design decision. Column names alone are ambiguous; actual values resolve that ambiguity decisively.<\/li>\n\n\n<li><strong>Dual-Direction Mapping Closes the Loop:<\/strong>\u00a0Mapping warehouse columns forward (what do we have?) and connector fields backward (what could we enable?) transforms the output from a passive inventory into an active roadmap.<\/li>\n\n\n<li><strong>Storage Abstraction Pays for Itself:<\/strong>\u00a0The\u00a0<code>LocalStorage<\/code>\u00a0\/\u00a0<code>GCSStorage<\/code>\u00a0abstraction &#8211; a seemingly minor architectural decision &#8211; eliminated an entire class of development-vs-production bugs and made the system genuinely portable from day one.<\/li>\n\n\n<li><strong>Versioned Prompts Enable Iteration:<\/strong>\u00a0Content-hashing every prompt template and recording the hash alongside inference results made prompt engineering a disciplined, reproducible process rather than an ad-hoc exercise.<\/li>\n\n\n<li><strong>Build on a Unified Cloud Ecosystem:<\/strong>\u00a0Building entirely on\u00a0<strong>Google Cloud services &#8211; BigQuery, Vertex AI, Cloud Run, Cloud Storage, Secret Manager<\/strong>\u00a0&#8211; eliminated integration friction between components and allowed the project to move from prototype to production deployment without stitching together tools from multiple vendors.<\/li>\n\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">What we are building next<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li><strong>Scheduled Re-Scans:<\/strong>\u00a0Automated daily or weekly re-runs with alerting when a platform&#8217;s schema mutates &#8211; detecting drift before it causes downstream problems.<\/li>\n\n\n<li><strong>Automatic Warehouse Verification:<\/strong>\u00a0A verification layer that checks whether recommended Adverity fields are actually populated with non-null values, distinguishing between &#8220;this field exists&#8221; and &#8220;this field contains useful data.&#8221;<\/li>\n\n\n<li><strong>Text-to-SQL:<\/strong>\u00a0A natural-language-to-SQL layer that uses the column mappings to answer ad-hoc questions against the warehouse &#8211; turning the annotated genome into a conversational data interface.<\/li>\n\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p>Turning raw ad platform schemas into actionable intelligence through AI-powered semantic mapping. TL;DR Up to 90%\u00a0of enterprise data sits unused,\u00a0and our BigQuery advertising warehouse was no exception:\u00a014 platforms,\u00a0179 tables, 2,709 columns, 15.8 billion records, zero shared schema, and answering a question as simple as whether Pinterest carries geo data required a two-week manual investigation. Every [&hellip;]<\/p>\n","protected":false},"author":13,"featured_media":0,"template":"","meta":{"_acf_changed":false,"_ppma_block_editor_authors":""},"tags":[],"content_types":[{"id":51,"name":"Technical Report","slug":"technical-walkthrough"}],"ppma_author":[{"id":13,"display_name":"Tam\u00e1s Luk\u00e1cs","first_name":"Tam\u00e1s","last_name":"Luk\u00e1cs","nickname":"tamas.lukacs","user_nicename":"tamas-lukacs","user_email":"tamas.lukacs@satalia.com","biographical_info":"Tamas is a Senior Data and AI Engineer at Satalia with a background spanning data engineering, cloud architecture, and AI\/ML systems. He builds production-grade platforms that take AI from prototype to production - from LLM-powered discovery agents and RAG-based chatbots to high-throughput embedding services. His current focus is on scalable data and AI solutions for enterprise advertising clients on Google Cloud.","avatar_url":"https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/04\/2021_mod-1.jpg","job_title":"Senior Data & AI Engineer","is_lead":null,"display_as_researcher":null,"order_priority":null}],"class_list":["post-1655","research_feed","type-research_feed","status-publish","hentry","content_type-technical-walkthrough"],"acf":{"content":"<p><!-- wp:paragraph --><\/p>\n<p><em>Turning raw ad platform schemas into actionable intelligence through AI-powered semantic mapping.<\/em><\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading {\"level\":1} --><\/p>\n<h1 class=\"wp-block-heading\">TL;DR<\/h1>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Up to 90%\u00a0of enterprise data sits unused,\u00a0and our BigQuery advertising warehouse was no exception:\u00a014 platforms,\u00a0179 tables, 2,709 columns, 15.8 billion records, zero shared schema, and answering a question as simple as whether Pinterest carries geo data required a two-week manual investigation. Every platform invented its own vocabulary (Facebook says <code>amount_spent<\/code>, Google says <code>cost_micros<\/code>, TikTok says <code>spend<\/code>)\u00a0and manual reconciliation cost analysts two to four weeks per pass,\u00a0producing a spreadsheet that started decaying the moment it was finished.\u00a0<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>We built the Data Discovery Agent,\u00a0an AI-powered pipeline that connects to BigQuery,\u00a0samples real data for grounding,\u00a0crawls external connector documentation,\u00a0and runs Gemini LLM inference with versioned prompts to autonomously annotate every column with confidence-scored mappings.\u00a0The agent compressed weeks of manual work into hours,\u00a0took us from zero structured understanding of our warehouse to a 58%\u00a0average completeness\u00a0(column presence per canonical sub-modality;\u00a0signal strength and population validation planned for Q2)\u00a0baseline across all 14 platforms,\u00a0and replaced tribal-knowledge spreadsheets with a searchable,\u00a0versioned genome report that turns\u00a0&#8220;do we have that data?&#8221;\u00a0from a research project into a dashboard lookup.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:quote --><\/p>\n<blockquote class=\"wp-block-quote\"><p><!-- wp:paragraph --><\/p>\n<p><strong><strong>If you don&#8217;t care about the technical details, read <a href=\"https:\/\/research.wpp.com\/blog\/why-your-data-genome-may-need-a-check-up-and-how-a-data-discovery-agent-can-help\">our blog post <\/a>instead. <\/strong><\/strong><\/p>\n<p><!-- \/wp:paragraph --><\/p><\/blockquote>\n<p><!-- \/wp:quote --><\/p>\n<p><!-- wp:separator --><\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator --><\/p>\n<p><!-- wp:heading {\"level\":1} --><\/p>\n<h1 class=\"wp-block-heading\">Technical walkthrough<\/h1>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:heading {\"level\":1} --><\/p>\n<h1 class=\"wp-block-heading\">Introduction<\/h1>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Advertising data warehouses are,\u00a0by nature,\u00a0enormous and messy.\u00a0Every major ad platform\u00a0&#8211;\u00a0Facebook,\u00a0Google,\u00a0TikTok,\u00a0and a dozen others\u00a0&#8211;\u00a0exposes its own schema,\u00a0its own naming conventions,\u00a0and its own definition of what a\u00a0&#8220;click&#8221;\u00a0or a\u00a0&#8220;spend&#8221;\u00a0actually means.\u00a0When these platforms feed into a centralised\u00a0<a href=\"https:\/\/cloud.google.com\/bigquery\/docs\"><strong>Google BigQuery<\/strong><\/a>\u00a0data lake,\u00a0the result is a sprawling collection of tables and columns that no single analyst can hold in their head.\u00a0In our case:\u00a0<strong>14 advertising platforms, 179 tables, and 2,709 columns<\/strong>\u00a0&#8211;\u00a0with over 15.8 billion records behind them.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Historically,\u00a0making sense of this warehouse required manual schema annotation:\u00a0a data analyst opening each table,\u00a0reading every column name,\u00a0pulling sample rows,\u00a0cross-referencing API documentation,\u00a0and deciding which canonical category each column belonged to.\u00a0Conservatively,\u00a0that process took\u00a0<strong>two to four weeks<\/strong>\u00a0of focused work for a single pass.\u00a0And the result started decaying immediately\u00a0&#8211;\u00a0platforms update schemas quarterly,\u00a0sometimes monthly.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>To eliminate this bottleneck,\u00a0we architected and deployed the\u00a0<strong>Data Discovery Agent<\/strong>:\u00a0an AI-powered platform that autonomously discovers,\u00a0maps,\u00a0and scores the completeness of advertising data across an entire BigQuery data lake.\u00a0The agent connects to BigQuery,\u00a0fetches every table and column,\u00a0extracts real sample data for grounding,\u00a0crawls external documentation for cross-reference,\u00a0invokes\u00a0<a href=\"https:\/\/cloud.google.com\/vertex-ai\"><strong>Vertex AI<\/strong><\/a>\u00a0LLM inference with structured prompts,\u00a0and delivers a fully annotated mapping with confidence scores\u00a0&#8211;\u00a0compressing weeks of manual work into hours.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The system is built as a production-grade\u00a0<a href=\"https:\/\/fastapi.tiangolo.com\/\"><strong>FastAPI<\/strong><\/a>\u00a0web application,\u00a0deployed on\u00a0<a href=\"https:\/\/cloud.google.com\/run\/docs\"><strong>Google Cloud Run<\/strong><\/a>,\u00a0with a complete admin control panel,\u00a0role-based access via Google OAuth,\u00a0and a storage abstraction layer that operates identically on local filesystems and Google Cloud Storage.\u00a0This document provides a comprehensive technical walkthrough of its architecture,\u00a0pipeline,\u00a0deployment,\u00a0and results.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:separator --><\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator --><\/p>\n<p><!-- wp:heading {\"level\":1} --><\/p>\n<h1 class=\"wp-block-heading\">Agent architecture<\/h1>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The Data Discovery Agent is structured as a\u00a0<strong>five-stage sequential pipeline<\/strong>\u00a0orchestrated by a FastAPI backend.\u00a0Unlike a chatbot or a single-prompt wrapper,\u00a0the agent executes an end-to-end workflow of discovery,\u00a0sampling,\u00a0documentation crawling,\u00a0LLM inference,\u00a0and enrichment reporting\u00a0&#8211;\u00a0with minimal human intervention.\u00a0The human&#8217;s role shifts from\u00a0<em>doing the mapping<\/em>\u00a0to\u00a0<em>reviewing the mapping<\/em>.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The high-level data flow is illustrated below:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:image {\"id\":602,\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} --><\/p>\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"720\" height=\"1024\" src=\"https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-720x1024.png\" alt=\"\" class=\"wp-image-602\" srcset=\"https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-720x1024.png 720w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-211x300.png 211w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-768x1092.png 768w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-1081x1536.png 1081w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-1441x2048.png 1441w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-scaled.png 1801w\" sizes=\"auto, (max-width: 720px) 100vw, 720px\" \/><figcaption class=\"wp-element-caption\"><em>Figure 1 &#8211; Data Discovery Agent pipeline: five stages from BigQuery data lake to executive dashboard.<\/em><\/figcaption><\/figure>\n<p><!-- \/wp:image --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The architecture was inspired by a clean separation of concerns.\u00a0The application core follows a layered pattern with dedicated service modules.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Service module inventory<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The backend is organised into focused service modules,\u00a0each responsible for a distinct domain:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:table {\"hasFixedLayout\":true} --><\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Service Module<\/strong><\/th>\n<th><strong>Core Responsibility<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>BigQuery Service<\/strong><\/td>\n<td>Connects to the BigQuery data lake, discovers datasets and tables, fetches column schemas and row counts, and saves timestamped fetch snapshots.<\/td>\n<\/tr>\n<tr>\n<td><strong>Sample Service<\/strong><\/td>\n<td>Extracts real sample rows from BigQuery tables using background-threaded parallel queries. Supports pause, resume, and abort controls.<\/td>\n<\/tr>\n<tr>\n<td><strong>Adverity Service<\/strong><\/td>\n<td>Crawls official Adverity connector documentation pages using Playwright, extracting structured field lists (name, description, dimension\/metric).<\/td>\n<\/tr>\n<tr>\n<td><strong>Inference Service<\/strong><\/td>\n<td>Builds versioned LLM prompts, manages sync and batch inference pipelines via Vertex AI, and parses structured JSON responses into column-to-submodality mappings.<\/td>\n<\/tr>\n<tr>\n<td><strong>BQ Filter Service<\/strong><\/td>\n<td>Manages regex-based include\/exclude rules that control which BigQuery datasets and tables appear in a fetch.<\/td>\n<\/tr>\n<tr>\n<td><strong>User Service<\/strong><\/td>\n<td>Handles user CRUD operations, domain allow-lists, email ban-lists, and role-based access control (admin\/user).<\/td>\n<\/tr>\n<tr>\n<td><strong>LLM Config Service<\/strong><\/td>\n<td>Loads and manages LLM model configurations, including model identifiers and API parameters.<\/td>\n<\/tr>\n<tr>\n<td><strong>Vertex Model Service<\/strong><\/td>\n<td>Discovers available Vertex AI regions and publisher models for inference.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p><!-- \/wp:table --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Key system capabilities<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:list {\"ordered\":true} --><\/p>\n<ol class=\"wp-block-list\">\n<!-- wp:list-item --><\/p>\n<li><strong>Autonomous Schema Discovery:<\/strong>\u00a0The agent connects to BigQuery, applies configurable regex filters, and discovers all advertising-platform datasets, tables, and column schemas without manual intervention.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Evidence-Grounded LLM Inference:<\/strong>\u00a0Every mapping decision is grounded in real data &#8211; column names, data types, and actual sample values &#8211; not just schema metadata. This dramatically reduces hallucination risk.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Dual-Direction Mapping:<\/strong>\u00a0The system performs both forward mapping (BigQuery columns to canonical sub-modalities) and reverse mapping (Adverity connector fields to sub-modalities), closing the loop from &#8220;what do we have?&#8221; to &#8220;what should we enable?&#8221;<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Completeness Scoring:<\/strong>\u00a0Each platform receives a quantitative completeness score measuring how many of the 24 canonical sub-modalities are covered, turning vague data quality concerns into actionable metrics.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Versioned Prompt Engineering:<\/strong>\u00a0All LLM prompts are version-tracked with SHA-256 content hashing, ensuring that every inference result can be traced back to the exact prompt that produced it.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Transparent Storage Abstraction:<\/strong>\u00a0A unified storage layer allows the application to operate identically on local filesystems (development) and Google Cloud Storage (production) by changing a single environment variable.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Production-Grade Access Control:<\/strong>\u00a0Google OAuth 2.0 authentication with domain allow-lists, email ban-lists, and admin\/user role separation ensures secure, auditable access.<\/li>\n<p><!-- \/wp:list-item -->\n<\/ol>\n<p><!-- \/wp:list --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">The modality schema: a canonical taxonomy<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>At the heart of the agent&#8217;s mapping logic is a canonical\u00a0<strong>modality schema<\/strong>\u00a0&#8211;\u00a0a structured taxonomy that defines what advertising data\u00a0<em>means<\/em>,\u00a0independent of any platform&#8217;s naming conventions.\u00a0The schema is organised around five high-level\u00a0<strong>modalities<\/strong>,\u00a0each representing a fundamental category of advertising measurement:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:table {\"hasFixedLayout\":true} --><\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Modality<\/strong><\/th>\n<th><strong>Sub-modalities<\/strong><\/th>\n<th><strong>What It Captures<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Performance<\/strong><\/td>\n<td>7<\/td>\n<td>The numbers: spend, impressions, clicks, video plays, conversions, leads, CTR<\/td>\n<\/tr>\n<tr>\n<td><strong>Creative<\/strong><\/td>\n<td>4<\/td>\n<td>What the ad looked like: creative IDs, asset paths, ad names, format types<\/td>\n<\/tr>\n<tr>\n<td><strong>Audience<\/strong><\/td>\n<td>6<\/td>\n<td>Who saw it: gender, age group, interests, custom audiences, behavioural segments<\/td>\n<\/tr>\n<tr>\n<td><strong>Geo<\/strong><\/td>\n<td>5<\/td>\n<td>Where they saw it: countries, regions, cities, postal codes, designated market areas<\/td>\n<\/tr>\n<tr>\n<td><strong>Brand<\/strong><\/td>\n<td>2<\/td>\n<td>Who paid for it: brand name, advertiser identity<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p><!-- \/wp:table --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>This yields\u00a0<strong>24 sub-modalities<\/strong>\u00a0in total\u00a0&#8211;\u00a0the atomic units of the mapping.\u00a0The LLM&#8217;s task is to determine,\u00a0for each BigQuery column across all 179 tables,\u00a0which sub-modality\u00a0(if any)\u00a0it corresponds to,\u00a0and with what confidence.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The modality dictionary is fully editable through the admin UI,\u00a0meaning the taxonomy can be extended or refined without touching code.\u00a0It is injected into every LLM prompt as structured context,\u00a0ensuring consistent mapping behaviour across inference runs.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:separator --><\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator --><\/p>\n<p><!-- wp:heading {\"level\":1} --><\/p>\n<h1 class=\"wp-block-heading\">Technology stack<\/h1>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:table {\"hasFixedLayout\":true} --><\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Component<\/strong><\/th>\n<th><strong>Technology<\/strong><\/th>\n<th><strong>Role<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Web Framework<\/strong><\/td>\n<td><a href=\"https:\/\/fastapi.tiangolo.com\/\">FastAPI<\/a><\/td>\n<td>Application backend, API routing, middleware, and session management<\/td>\n<\/tr>\n<tr>\n<td><strong>LLM Engine<\/strong><\/td>\n<td><a href=\"https:\/\/deepmind.google\/technologies\/gemini\/\">Google Gemini<\/a>\u00a0(via\u00a0<a href=\"https:\/\/cloud.google.com\/vertex-ai\">Vertex AI<\/a>)<\/td>\n<td>Powers all schema-mapping and recommendation inference across the Gemini model family (including Gemini Flash for batch runs); chosen for low latency, strong instruction-following, and structured JSON output<\/td>\n<\/tr>\n<tr>\n<td><strong>Data Warehouse<\/strong><\/td>\n<td><a href=\"https:\/\/cloud.google.com\/bigquery\/docs\">Google BigQuery<\/a><\/td>\n<td>Primary data source; the agent queries dataset schemas, table metadata, and sample rows<\/td>\n<\/tr>\n<tr>\n<td><strong>Batch Inference<\/strong><\/td>\n<td><a href=\"https:\/\/cloud.google.com\/vertex-ai\/docs\/predictions\/get-batch-predictions\">Vertex AI Batch Prediction<\/a><\/td>\n<td>Processes bulk LLM requests asynchronously via JSONL files staged in GCS; more cost-effective for full runs across all platforms<\/td>\n<\/tr>\n<tr>\n<td><strong>Documentation Crawling<\/strong><\/td>\n<td><a href=\"https:\/\/playwright.dev\/python\/\">Playwright<\/a><\/td>\n<td>Headless browser automation for crawling Adverity connector documentation pages and extracting structured field lists<\/td>\n<\/tr>\n<tr>\n<td><strong>Frontend<\/strong><\/td>\n<td><a href=\"https:\/\/jinja.palletsprojects.com\/\">Jinja2<\/a>\u00a0+\u00a0<a href=\"https:\/\/htmx.org\/\">HTMX<\/a><\/td>\n<td>Server-side rendered HTML templates with HTMX for interactive, partial-page updates without a JavaScript framework<\/td>\n<\/tr>\n<tr>\n<td><strong>Authentication<\/strong><\/td>\n<td><a href=\"https:\/\/developers.google.com\/identity\/protocols\/oauth2\">Google OAuth 2.0<\/a>\u00a0(via\u00a0<a href=\"https:\/\/authlib.org\/\">Authlib<\/a>)<\/td>\n<td>Secure user login with domain allow-lists, email ban-lists, and admin\/user role separation<\/td>\n<\/tr>\n<tr>\n<td><strong>Application Data<\/strong><\/td>\n<td><a href=\"https:\/\/cloud.google.com\/storage\/docs\">Google Cloud Storage<\/a><\/td>\n<td>Persistent store for fetch results, inference outputs, platform configs, user records, and Adverity documentation<\/td>\n<\/tr>\n<tr>\n<td><strong>Deployment<\/strong><\/td>\n<td><a href=\"https:\/\/cloud.google.com\/run\/docs\">Google Cloud Run<\/a><\/td>\n<td>Serverless container hosting with auto-scaling, health probes, and secret-backed environment variables<\/td>\n<\/tr>\n<tr>\n<td><strong>Infrastructure<\/strong><\/td>\n<td><a href=\"https:\/\/www.terraform.io\/\">Terraform<\/a><\/td>\n<td>Infrastructure-as-code: GCS buckets, Artifact Registry, Cloud Run service, IAM roles, Secret Manager secrets<\/td>\n<\/tr>\n<tr>\n<td><strong>Containerisation<\/strong><\/td>\n<td><a href=\"https:\/\/www.docker.com\/\">Docker<\/a>\u00a0(multi-stage build)<\/td>\n<td>Reproducible builds with a builder stage for dependency installation and a slim runtime stage running as non-root<\/td>\n<\/tr>\n<tr>\n<td><strong>Secret Management<\/strong><\/td>\n<td><a href=\"https:\/\/cloud.google.com\/secret-manager\/docs\">Google Secret Manager<\/a><\/td>\n<td>Stores OAuth credentials and session-signing keys, injected into Cloud Run as environment variables<\/td>\n<\/tr>\n<tr>\n<td><strong>Language<\/strong><\/td>\n<td>Python 3.11<\/td>\n<td>All application code, managed via\u00a0<a href=\"https:\/\/python-poetry.org\/\">Poetry<\/a>\u00a0for dependency resolution<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p><!-- \/wp:table --><\/p>\n<p><!-- wp:separator --><\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator --><\/p>\n<p><!-- wp:heading {\"level\":1} --><\/p>\n<h1 class=\"wp-block-heading\">Pipeline deep-dive<\/h1>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The following five stages take the system from a raw,\u00a0unannotated data warehouse to a fully mapped,\u00a0scored,\u00a0and actionable genome report.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Stage 1 &#8211; Discovery<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The agent connects to Google BigQuery and discovers all staging datasets matching a configurable naming pattern\u00a0(e.g.,\u00a0<code>^bq_cgh_mp_.*_staging$<\/code>).\u00a0For each dataset,\u00a0it fetches the full table inventory:\u00a0column names,\u00a0data types,\u00a0and row counts.\u00a0Intelligent regex filters\u00a0&#8211;\u00a0managed through the admin UI via\u00a0<code>bq_filters.json<\/code>\u00a0&#8211;\u00a0exclude temporary artefacts such as tables prefixed with\u00a0<code>stg_<\/code>,\u00a0suffixed with\u00a0<code>_tmp<\/code>\u00a0or\u00a0<code>_dbt_tmp<\/code>,\u00a0and any datasets that don&#8217;t match the expected naming convention.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Results are saved as a\u00a0<strong>timestamped fetch snapshot<\/strong>\u00a0(<code>fetches\/fetch_YYYYMMDD_HHMMSS\/results.json<\/code>).\u00a0Multiple fetches can coexist,\u00a0allowing admins to compare schema evolution over time.\u00a0Each fetch captures the complete state of the warehouse at a point in time\u00a0&#8211;\u00a0the starting point for all downstream inference work.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p><strong>Current warehouse dimensions:<\/strong><\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:table {\"hasFixedLayout\":true} --><\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Metric<\/strong><\/th>\n<th><strong>Count<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Advertising Platforms<\/td>\n<td>14<\/td>\n<\/tr>\n<tr>\n<td>BigQuery Datasets<\/td>\n<td>17<\/td>\n<\/tr>\n<tr>\n<td>Tables<\/td>\n<td>179<\/td>\n<\/tr>\n<tr>\n<td>Total Columns<\/td>\n<td>2,709<\/td>\n<\/tr>\n<tr>\n<td>Total Records<\/td>\n<td>15.8 billion+<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p><!-- \/wp:table --><\/p>\n<p><!-- wp:heading {\"level\":3} --><\/p>\n<h3 class=\"wp-block-heading\">Platforms covered<\/h3>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:table {\"hasFixedLayout\":true} --><\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th>#<\/th>\n<th>Platform<\/th>\n<th>#<\/th>\n<th>Platform<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>1<\/td>\n<td>Amazon DSP<\/td>\n<td>8<\/td>\n<td>Pinterest<\/td>\n<\/tr>\n<tr>\n<td>2<\/td>\n<td>Facebook Ads<\/td>\n<td>9<\/td>\n<td>Snapchat<\/td>\n<\/tr>\n<tr>\n<td>3<\/td>\n<td>Google Ads<\/td>\n<td>10<\/td>\n<td>TikTok<\/td>\n<\/tr>\n<tr>\n<td>4<\/td>\n<td>Google Ads (YouTube)<\/td>\n<td>11<\/td>\n<td>The Trade Desk<\/td>\n<\/tr>\n<tr>\n<td>5<\/td>\n<td>Google DV360<\/td>\n<td>12<\/td>\n<td>Twitter\/X Ads<\/td>\n<\/tr>\n<tr>\n<td>6<\/td>\n<td>Google DV360 (YouTube)<\/td>\n<td>13<\/td>\n<td>Xandr<\/td>\n<\/tr>\n<tr>\n<td>7<\/td>\n<td>LinkedIn<\/td>\n<td>14<\/td>\n<td>IAS<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p><!-- \/wp:table --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Stage 2 &#8211; Sampling<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Column names alone are often ambiguous.\u00a0A column called\u00a0<code>cost<\/code>\u00a0could mean total spend,\u00a0cost-per-click,\u00a0or an internal ID.\u00a0The agent resolves this ambiguity by extracting\u00a0<strong>actual sample data rows<\/strong>\u00a0from each table using background-threaded parallel queries against BigQuery.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>These real values become critical evidence for the LLM:\u00a0seeing\u00a0<code>14.50<\/code>,\u00a0<code>0.83<\/code>,\u00a0<code>127.99<\/code>\u00a0in a\u00a0<code>cost<\/code>\u00a0column strongly suggests monetary spend,\u00a0not an identifier.\u00a0Seeing\u00a0<code>M<\/code>,\u00a0<code>F<\/code>,\u00a0<code>Unknown<\/code>\u00a0in a\u00a0<code>gender<\/code>\u00a0column confirms an audience dimension more reliably than the column name alone.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The extraction process supports\u00a0<strong>pause, resume, and abort<\/strong>\u00a0controls\u00a0&#8211;\u00a0essential when sampling across hundreds of tables with billions of rows.\u00a0Progress is tracked per-platform and persisted to storage,\u00a0so an interrupted extraction can resume exactly where it left off.\u00a0Samples are saved as\u00a0<code>samples_&lt;platform&gt;.json<\/code>\u00a0per fetch and are injected into the LLM prompt alongside the schema metadata.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Stage 3 &#8211; Documentation crawling<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The agent crawls the official connector documentation for each platform\u00a0&#8211;\u00a0specifically,\u00a0the authoritative\u00a0&#8220;Most Used Fields&#8221;\u00a0pages published by\u00a0<a href=\"https:\/\/www.adverity.com\/\">Adverity<\/a>,\u00a0the data integration platform that feeds data into the BigQuery warehouse.\u00a0Using\u00a0<a href=\"https:\/\/playwright.dev\/python\/\">Playwright<\/a>\u00a0for headless browser automation,\u00a0the crawler fetches each documentation page and extracts a structured field list:\u00a0name,\u00a0description,\u00a0and whether the field is a dimension or metric.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Each crawled page is saved in three formats\u00a0&#8211;\u00a0HTML,\u00a0Markdown,\u00a0and structured JSON\u00a0&#8211;\u00a0under\u00a0<code>adverity_docs\/<\/code>\u00a0in the storage backend.\u00a0These documents serve as the agent&#8217;s\u00a0&#8220;reference manual&#8221;:\u00a0a ground-truth checklist of what fields each platform\u00a0<em>could<\/em>\u00a0provide,\u00a0independent of what currently exists in the warehouse.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The crawl engine runs as a background task with progress tracking,\u00a0with a configurable delay between requests to avoid rate-limiting.\u00a0Platform URLs are managed through the admin UI,\u00a0making it straightforward to add new data sources.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Stage 4 &#8211; LLM inference<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>This is the core intellectual step\u00a0&#8211;\u00a0where the agent reads every column and produces an annotated mapping.\u00a0The Inference Service builds\u00a0<strong>versioned prompts<\/strong>\u00a0containing three layers of context:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:list {\"ordered\":true} --><\/p>\n<ol class=\"wp-block-list\">\n<!-- wp:list-item --><\/p>\n<li><strong>Table schemas<\/strong>\u00a0&#8211; column names and data types for every table in the platform&#8217;s dataset.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Sample data<\/strong>\u00a0&#8211; real row values extracted in Stage 2, providing grounding evidence.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Modality definitions<\/strong>\u00a0&#8211; the target taxonomy of modalities and sub-modalities the LLM must map to.<\/li>\n<p><!-- \/wp:list-item -->\n<\/ol>\n<p><!-- \/wp:list --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>These structured prompts are sent to models from the\u00a0<strong><a href=\"https:\/\/deepmind.google\/technologies\/gemini\/\">Google Gemini<\/a><\/strong>\u00a0family via Vertex AI.\u00a0The model returns structured JSON:\u00a0for each canonical sub-modality,\u00a0it identifies matching columns,\u00a0assigns a\u00a0<strong>confidence score<\/strong>\u00a0(0.0 to 1.0),\u00a0and provides natural-language reasoning for each match.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading {\"level\":3} --><\/p>\n<h3 class=\"wp-block-heading\">Inference modes<\/h3>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Two inference modes are supported,\u00a0selectable through the admin UI:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:table {\"hasFixedLayout\":true} --><\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Mode<\/strong><\/th>\n<th><strong>Mechanism<\/strong><\/th>\n<th><strong>Best For<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Sync<\/strong><\/td>\n<td>One LLM request per platform, processed sequentially via\u00a0<code>POST \/api\/inference\/sync<\/code><\/td>\n<td>Quick testing, single-platform runs<\/td>\n<\/tr>\n<tr>\n<td><strong>Batch<\/strong><\/td>\n<td>Prompts written to JSONL files in GCS, submitted as Vertex AI batch prediction jobs, polled for completion<\/td>\n<td>Full production runs across all 14 platforms; significantly more cost-effective<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p><!-- \/wp:table --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>For batch mode,\u00a0the workflow is:\u00a0write prompts to JSONL,\u00a0submit batch jobs,\u00a0poll status until completion,\u00a0collect and parse results.\u00a0The UI provides real-time progress tracking for all active jobs.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading {\"level\":3} --><\/p>\n<h3 class=\"wp-block-heading\">Prompt engineering<\/h3>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>All prompt templates live in\u00a0<code>app\/prompts\/<\/code>\u00a0and are version-tracked with semantic versioning and SHA-256 content hashing via\u00a0<code>versions.json<\/code>.\u00a0This means every inference result records exactly which prompt version and content hash produced it\u00a0&#8211;\u00a0critical for reproducibility.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The base modality mapping prompt instructs the LLM to:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:list --><\/p>\n<ul class=\"wp-block-list\">\n<!-- wp:list-item --><\/p>\n<li>Examine every column name and data type across all provided tables.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li>Use sample data to confirm or reject potential matches.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li>Assign confidence scores on a defined scale (0.9-1.0 for clear matches, 0.5-0.69 for ambiguous, below 0.5 for weak).<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li>Return\u00a0<strong>only valid JSON<\/strong>\u00a0&#8211; no markdown, no prose.<\/li>\n<p><!-- \/wp:list-item -->\n<\/ul>\n<p><!-- \/wp:list --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>A dedicated\u00a0<strong>Adverity recommendation prompt<\/strong>\u00a0performs the reverse mapping:\u00a0given Adverity connector documentation,\u00a0recommend which Adverity fields map to which sub-modalities.\u00a0This prompt enforces a critical rule\u00a0&#8211;\u00a0field names must appear\u00a0<strong>verbatim<\/strong>\u00a0in the documentation,\u00a0preventing the LLM from inventing fields that don&#8217;t exist.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading {\"level\":3} --><\/p>\n<h3 class=\"wp-block-heading\">Robust JSON parsing<\/h3>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>LLM outputs are rarely perfectly formatted.\u00a0The Inference Service includes a\u00a0<strong>robust JSON parser<\/strong>\u00a0(<code>_robust_parse_llm_json<\/code>)\u00a0that handles common LLM formatting mistakes:\u00a0markdown code fences,\u00a0trailing commas,\u00a0single-quoted strings,\u00a0BOM characters,\u00a0leading\/trailing prose around the JSON object,\u00a0and escaped newlines inside strings.\u00a0This defensive parsing layer ensures that minor formatting issues never cause a pipeline failure.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading {\"level\":3} --><\/p>\n<h3 class=\"wp-block-heading\">Three inference pipelines<\/h3>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The system supports three distinct inference types:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:list {\"ordered\":true} --><\/p>\n<ol class=\"wp-block-list\">\n<!-- wp:list-item --><\/p>\n<li><strong>Modality Mapping Inference<\/strong>\u00a0&#8211; Maps BigQuery columns to the canonical sub-modality taxonomy. The primary pipeline.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Adverity Recommendation Inference<\/strong>\u00a0&#8211; Given Adverity documentation, recommends which connector fields should be enabled for each sub-modality.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Field Extraction Inference<\/strong>\u00a0&#8211; Extracts structured field definitions from raw Adverity HTML\/Markdown documentation using chunked batch inference, producing clean per-platform field maps.<\/li>\n<p><!-- \/wp:list-item -->\n<\/ol>\n<p><!-- \/wp:list --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Each run records metadata:\u00a0timestamp,\u00a0model used,\u00a0prompt version,\u00a0prompt content hash,\u00a0and platforms processed.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Stage 5 &#8211; Enrichment and dashboard<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The final stage closes the diagnostic loop by cross-referencing the warehouse mappings from Stage 4 against the connector field lists from Stage 3.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading {\"level\":3} --><\/p>\n<h3 class=\"wp-block-heading\">Enrichment recommendations<\/h3>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The Adverity Recommendation inference goes beyond simply flagging missing geo data. It prescribes a fix: <em>&#8220;The connector for this platform offers a field called <code>country_code<\/code> (dimension); enable it, and your geo coverage improves from 2\/5 to 3\/5 sub-modalities.&#8221;<\/em> Diagnosis and prescription in one step, turning gap analysis into concrete action items for the data engineering team.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading {\"level\":3} --><\/p>\n<h3 class=\"wp-block-heading\">Completeness scoring<\/h3>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Each platform receives a\u00a0<strong>completeness score<\/strong>:\u00a0how much of the canonical 24-sub-modality schema is actually present.\u00a0The approach is deliberately simple\u00a0&#8211;\u00a0a presence\/absence assay.\u00a0For each platform,\u00a0the denominator is 24\u00a0(total sub-modalities).\u00a0The numerator is how many have at least one column mapped above a configurable confidence threshold.\u00a0A sub-modality is either present or it isn&#8217;t\u00a0&#8211;\u00a0this handles deduplication naturally.\u00a0If three tables each have a\u00a0<code>spend<\/code>\u00a0column mapped to\u00a0<code>performance__spend_usd<\/code>,\u00a0the sub-modality counts once,\u00a0not three times.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading {\"level\":3} --><\/p>\n<h3 class=\"wp-block-heading\">The executive dashboard<\/h3>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>All results render in a\u00a0<strong>FastAPI + Jinja2\/HTMX dashboard<\/strong>\u00a0that provides:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:list --><\/p>\n<ul class=\"wp-block-list\">\n<!-- wp:list-item --><\/p>\n<li><strong>Platform scorecards<\/strong>\u00a0&#8211; one card per advertising platform showing its name, banner image, and completeness percentage.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Modality drilldowns<\/strong>\u00a0&#8211; for each platform, expandable breakdowns into modalities and their sub-modalities.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Column mappings<\/strong>\u00a0&#8211; the specific BigQuery columns mapped to each sub-modality, with confidence scores and LLM reasoning.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Sample data preview<\/strong>\u00a0&#8211; actual row data from each table for manual verification.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Executive summary sidebar<\/strong>\u00a0&#8211; aggregated statistics across all platforms: total mappings, average completeness, record counts, highest and lowest performing platforms.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Interactive mapping visualiser<\/strong>\u00a0&#8211; a left-right mapping view, filterable by platform, modality, and confidence threshold.<\/li>\n<p><!-- \/wp:list-item -->\n<\/ul>\n<p><!-- \/wp:list --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Visibility of platforms is controlled via\u00a0<strong>Dashboard Settings<\/strong>,\u00a0where admins choose which fetch and which platforms appear to end users.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading {\"level\":3} --><\/p>\n<h3 class=\"wp-block-heading\">PDF executive report generation<\/h3>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The dashboard includes a one-click\u00a0<strong>PDF executive report generator<\/strong>\u00a0for stakeholders who need a polished,\u00a0offline-readable document.\u00a0When triggered,\u00a0the system:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:list {\"ordered\":true} --><\/p>\n<ol class=\"wp-block-list\">\n<!-- wp:list-item --><\/p>\n<li><strong>Aggregates all dashboard data<\/strong>\u00a0&#8211; platform stats, completeness scores, modality breakdowns, and mapped column details &#8211; into a single render context.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Renders a dedicated Jinja2 report template<\/strong>\u00a0(<code>exec_report.html<\/code>) &#8211; a professionally styled, multi-page A4 document with a cover page, executive overview, key metric summary cards, a platform comparison table, the full modality dictionary with descriptions, and per-platform detail sections showing completeness stats, modality coverage grids (with check\/miss indicators per sub-modality), and optionally the full column mapping list.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Converts the HTML to PDF<\/strong>\u00a0using\u00a0<a href=\"https:\/\/playwright.dev\/python\/\">Playwright<\/a>&#8216;s headless Chromium instance (<code>page.pdf(format=\"A4\", print_background=True)<\/code>), producing a pixel-perfect render with page breaks, headers, and footers showing the generation timestamp and page numbers.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Returns the PDF as a downloadable file<\/strong>\u00a0(e.g.,\u00a0<code>exec_report_20260327_112709.pdf<\/code>).<\/li>\n<p><!-- \/wp:list-item -->\n<\/ol>\n<p><!-- \/wp:list --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The report is designed for executive consumption:\u00a0the first page presents headline metrics\u00a0(total platforms,\u00a0tables,\u00a0columns,\u00a0records,\u00a0average completeness)\u00a0and identifies the best and worst performers.\u00a0Subsequent pages provide the modality dictionary for reference,\u00a0followed by detailed per-platform breakdowns with modality coverage grids showing exactly which sub-modalities are mapped\u00a0(with column counts)\u00a0and which remain gaps.\u00a0This gives leadership a complete,\u00a0self-contained snapshot of data integration maturity without requiring dashboard access.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:separator --><\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator --><\/p>\n<p><!-- wp:heading {\"level\":1} --><\/p>\n<h1 class=\"wp-block-heading\">Cloud deployment<\/h1>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The system is deployed as a production-grade,\u00a0cloud-native service on\u00a0<strong>Google Cloud<\/strong>,\u00a0following a containerised,\u00a0infrastructure-as-code workflow from local development through to automated deployment.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Infrastructure-as-Code (Terraform)<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>All infrastructure is defined as Terraform in\u00a0<code>deployment\/terraform\/<\/code>\u00a0with six modules:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:table {\"hasFixedLayout\":true} --><\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Module<\/strong><\/th>\n<th><strong>Resources Created<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong><code>apis<\/code><\/strong><\/td>\n<td>Enables 10 GCP APIs (Cloud Run, Artifact Registry, Storage, BigQuery, Vertex AI, Secret Manager, IAM, Cloud Build, etc.)<\/td>\n<\/tr>\n<tr>\n<td><strong><code>artifact_registry<\/code><\/strong><\/td>\n<td>Docker image repository with a keep-last-5 cleanup policy<\/td>\n<\/tr>\n<tr>\n<td><strong><code>gcs<\/code><\/strong><\/td>\n<td>Versioned GCS bucket for application data (platforms, fetches, users, banners) with lifecycle rules<\/td>\n<\/tr>\n<tr>\n<td><strong><code>iam<\/code><\/strong><\/td>\n<td>Service account with roles: Storage Object Admin, BigQuery Data Viewer, BigQuery Job User, Vertex AI User, Secret Accessor<\/td>\n<\/tr>\n<tr>\n<td><strong><code>secret_manager<\/code><\/strong><\/td>\n<td>Three secrets:\u00a0<code>SECRET_KEY<\/code>,\u00a0<code>GOOGLE_CLIENT_ID<\/code>,\u00a0<code>GOOGLE_CLIENT_SECRET<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong><code>cloud_run<\/code><\/strong><\/td>\n<td>Cloud Run v2 service with health probes, auto-scaling (0 to 3 instances), and secret-backed environment variables<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p><!-- \/wp:table --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Containerisation<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The application is packaged as a Docker image using a\u00a0<strong>multi-stage build<\/strong>:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:list {\"ordered\":true} --><\/p>\n<ol class=\"wp-block-list\">\n<!-- wp:list-item --><\/p>\n<li><strong>Builder stage<\/strong>\u00a0&#8211; Installs Python dependencies via Poetry, exports to\u00a0<code>requirements.txt<\/code>, and installs packages into a clean prefix.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Runtime stage<\/strong>\u00a0&#8211; Copies only the installed packages and application code into a slim Python 3.11 image. Runs as a non-root\u00a0<code>appuser<\/code>\u00a0for security. Seed data (platform configs, modality dictionaries, Adverity docs) is baked into the image; runtime data (fetches, users, inference results) lives in the GCS bucket.<\/li>\n<p><!-- \/wp:list-item -->\n<\/ol>\n<p><!-- \/wp:list --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Deployment pipeline<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>A full deployment is executed with a single command:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:preformatted --><\/p>\n<pre class=\"wp-block-preformatted\"><code>make deploy-all    # tf-init -&gt; tf-apply -&gt; docker-push -&gt; seed-data<br><\/code><\/pre>\n<p><!-- \/wp:preformatted --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>This runs Terraform to provision\/update all infrastructure,\u00a0builds and pushes the Docker image to Artifact Registry,\u00a0and syncs the local\u00a0<code>data\/<\/code>\u00a0directory to the GCS bucket.\u00a0Incremental deployments\u00a0(code-only changes)\u00a0use\u00a0<code>make deploy<\/code>\u00a0which skips the Terraform init step.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The\u00a0<code>seed-data<\/code>\u00a0target uses\u00a0<code>gsutil -m rsync<\/code>\u00a0to upload platform definitions,\u00a0modality dictionaries,\u00a0BQ filters,\u00a0Adverity documentation,\u00a0and other configuration data into the GCS bucket.\u00a0Without seeding,\u00a0a fresh deployment starts with an empty bucket and no platform or user configuration.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Storage abstraction layer<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>A critical architectural decision was the\u00a0<strong>storage abstraction layer<\/strong>\u00a0(<code>app\/core\/storage.py<\/code>).\u00a0All data I\/O goes through a unified\u00a0<code>StorageBackend<\/code>\u00a0interface with two implementations:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:list --><\/p>\n<ul class=\"wp-block-list\">\n<!-- wp:list-item --><\/p>\n<li><strong><code>LocalStorage<\/code><\/strong>\u00a0&#8211; reads\/writes to the local\u00a0<code>data\/<\/code>\u00a0directory. Used during development (<code>STORAGE_BACKEND=local<\/code>).<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong><code>GCSStorage<\/code><\/strong>\u00a0&#8211; reads\/writes to a GCS bucket under a configurable prefix. Used in production (<code>STORAGE_BACKEND=gcs<\/code>).<\/li>\n<p><!-- \/wp:list-item -->\n<\/ul>\n<p><!-- \/wp:list --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Switching between backends requires changing only the\u00a0<code>STORAGE_BACKEND<\/code>\u00a0environment variable.\u00a0Every service module calls\u00a0<code>get_storage()<\/code>\u00a0and operates through the same API regardless of the underlying storage\u00a0&#8211;\u00a0ensuring that code tested locally behaves identically in production.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Authentication and access control<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The application uses\u00a0<strong>Google OAuth 2.0<\/strong>\u00a0(OpenID Connect)\u00a0for user authentication:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:list --><\/p>\n<ul class=\"wp-block-list\">\n<!-- wp:list-item --><\/p>\n<li>Users log in with their Google account via the standard OAuth consent flow.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Domain allow-lists<\/strong>\u00a0restrict login to specific email domains (e.g.,\u00a0<code>satalia.com<\/code>,\u00a0<code>choreograph.com<\/code>).<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Email ban-lists<\/strong>\u00a0allow blocking specific users.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Role-based access<\/strong>\u00a0separates admin users (full control panel) from regular users (executive dashboard only).<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li>A\u00a0<strong>bypass mode<\/strong>\u00a0(<code>BYPASS_AUTH_AS_ADMIN=true<\/code>) enables local development without Google credentials.<\/li>\n<p><!-- \/wp:list-item -->\n<\/ul>\n<p><!-- \/wp:list --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>All user and auth settings are managed through the admin UI and persisted via the storage backend.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:separator --><\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator --><\/p>\n<p><!-- wp:heading {\"level\":1} --><\/p>\n<h1 class=\"wp-block-heading\">Results and impact<\/h1>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>When the pipeline completes a full run across all fourteen platforms,\u00a0the executive dashboard delivers a comprehensive genome report of the data warehouse.\u00a0The following results are drawn from the live executive summary generated by the dashboard.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Current warehouse state<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:table {\"hasFixedLayout\":true} --><\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Metric<\/strong><\/th>\n<th><strong>Value<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Platforms Monitored<\/td>\n<td>14<\/td>\n<\/tr>\n<tr>\n<td>Tables<\/td>\n<td>179<\/td>\n<\/tr>\n<tr>\n<td>Columns<\/td>\n<td>2,709<\/td>\n<\/tr>\n<tr>\n<td>Total Records<\/td>\n<td>15.8B<\/td>\n<\/tr>\n<tr>\n<td>Average Completeness<\/td>\n<td><strong>58%<\/strong><\/td>\n<\/tr>\n<tr>\n<td>Highest Completeness<\/td>\n<td>Snapchat &#8211;\u00a0<strong>75%<\/strong>\u00a0(18\/24)<\/td>\n<\/tr>\n<tr>\n<td>Lowest Completeness<\/td>\n<td>Integral Ad Science &#8211;\u00a0<strong>25%<\/strong>\u00a0(6\/24)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p><!-- \/wp:table --><\/p>\n<p><!-- wp:heading {\"level\":3} --><\/p>\n<h3 class=\"wp-block-heading\">Per-platform breakdown<\/h3>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:table {\"hasFixedLayout\":true} --><\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Platform<\/strong><\/th>\n<th><strong>Completeness<\/strong><\/th>\n<th><strong>Tables<\/strong><\/th>\n<th><strong>Columns<\/strong><\/th>\n<th><strong>Records<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Facebook Ads<\/td>\n<td>66% (16\/24)<\/td>\n<td>15<\/td>\n<td>364<\/td>\n<td>3,147.7M<\/td>\n<\/tr>\n<tr>\n<td>Google Ads<\/td>\n<td>70% (17\/24)<\/td>\n<td>21<\/td>\n<td>399<\/td>\n<td>2,284.6M<\/td>\n<\/tr>\n<tr>\n<td>TikTok<\/td>\n<td>62% (15\/24)<\/td>\n<td>14<\/td>\n<td>217<\/td>\n<td>15.1M<\/td>\n<\/tr>\n<tr>\n<td>LinkedIn<\/td>\n<td>70% (17\/24)<\/td>\n<td>12<\/td>\n<td>143<\/td>\n<td>366.4k<\/td>\n<\/tr>\n<tr>\n<td>Amazon DSP<\/td>\n<td>62% (15\/24)<\/td>\n<td>18<\/td>\n<td>213<\/td>\n<td>18.2M<\/td>\n<\/tr>\n<tr>\n<td>Integral Ad Science<\/td>\n<td>25% (6\/24)<\/td>\n<td>12<\/td>\n<td>130<\/td>\n<td>95.9M<\/td>\n<\/tr>\n<tr>\n<td>Xandr<\/td>\n<td>50% (12\/24)<\/td>\n<td>11<\/td>\n<td>156<\/td>\n<td>1.2M<\/td>\n<\/tr>\n<tr>\n<td>Google Ads (YouTube)<\/td>\n<td>45% (11\/24)<\/td>\n<td>9<\/td>\n<td>112<\/td>\n<td>10.9M<\/td>\n<\/tr>\n<tr>\n<td>Display &amp; Video 360<\/td>\n<td>70% (17\/24)<\/td>\n<td>16<\/td>\n<td>275<\/td>\n<td>10,148.3M<\/td>\n<\/tr>\n<tr>\n<td>Display &amp; Video 360 (YouTube)<\/td>\n<td>54% (13\/24)<\/td>\n<td>15<\/td>\n<td>187<\/td>\n<td>63.6M<\/td>\n<\/tr>\n<tr>\n<td>Pinterest<\/td>\n<td>70% (17\/24)<\/td>\n<td>11<\/td>\n<td>170<\/td>\n<td>6.7M<\/td>\n<\/tr>\n<tr>\n<td>Snapchat<\/td>\n<td>75% (18\/24)<\/td>\n<td>10<\/td>\n<td>150<\/td>\n<td>15M<\/td>\n<\/tr>\n<tr>\n<td>The Trade Desk<\/td>\n<td>45% (11\/24)<\/td>\n<td>6<\/td>\n<td>80<\/td>\n<td>4.1M<\/td>\n<\/tr>\n<tr>\n<td>Twitter Ads<\/td>\n<td>50% (12\/24)<\/td>\n<td>9<\/td>\n<td>113<\/td>\n<td>40.4k<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p><!-- \/wp:table --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Headline findings<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Four platforms\u00a0&#8211;\u00a0Google Ads,\u00a0LinkedIn,\u00a0Display\u00a0&amp;\u00a0Video 360,\u00a0and Pinterest\u00a0&#8211;\u00a0cluster at\u00a0<strong>70% completeness<\/strong>\u00a0(17\/24 sub-modalities).\u00a0Snapchat leads the pack at\u00a0<strong>75%<\/strong>.\u00a0Several platforms sit in the 50-66%\u00a0range,\u00a0and a handful fall below 50%.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p><strong>Performance metrics<\/strong>\u00a0(spend,\u00a0impressions,\u00a0clicks)\u00a0are well-represented across the board\u00a0&#8211;\u00a0these are the columns every platform exposes and every data team queries first.\u00a0But\u00a0<strong>Audience, Geo, and Brand modalities<\/strong>\u00a0show significant gaps,\u00a0particularly in measurement and programmatic categories.\u00a0Some findings were genuine surprises:\u00a0platforms assumed to lack audience data turned out to carry age range and gender columns that mapped cleanly.\u00a0The data was there all along\u00a0&#8211;\u00a0it just hadn&#8217;t been annotated.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>At 58%,\u00a0the warehouse is more than half-mapped\u00a0&#8211;\u00a0but meaningful blind spots remain.\u00a0That&#8217;s a useful headline.\u00a0It is unequivocally better to know the current state than to assume it&#8217;s healthy without running the test.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading --><\/p>\n<h2 class=\"wp-block-heading\">Operational impact<\/h2>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:table {\"hasFixedLayout\":true} --><\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Before (Manual)<\/strong><\/th>\n<th><strong>After (Agent)<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>2-4 weeks per full mapping pass<\/td>\n<td>Hours for a complete run<\/td>\n<\/tr>\n<tr>\n<td>Mapping decays immediately as schemas change<\/td>\n<td>Re-run on demand; schema drift detected automatically<\/td>\n<\/tr>\n<tr>\n<td>Tribal knowledge locked in spreadsheets<\/td>\n<td>Structured, versioned, searchable mappings with confidence scores<\/td>\n<\/tr>\n<tr>\n<td>Answering &#8220;do we have geo data from Pinterest?&#8221; required manual investigation<\/td>\n<td>Dashboard lookup: instant answer with completeness score<\/td>\n<\/tr>\n<tr>\n<td>Enrichment recommendations require manual cross-referencing<\/td>\n<td>Automated prescriptions: &#8220;Enable\u00a0<code>country_code<\/code>\u00a0on this connector&#8221;<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p><!-- \/wp:table --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The shift is from\u00a0<em>doing the mapping<\/em>\u00a0to\u00a0<em>reviewing the mapping<\/em>\u00a0&#8211;\u00a0a fundamentally different use of analyst time.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:separator --><\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator --><\/p>\n<p><!-- wp:heading {\"level\":1} --><\/p>\n<h1 class=\"wp-block-heading\">Admin workflow<\/h1>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The typical end-to-end admin workflow from raw data to a published dashboard follows this sequence:<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:image {\"id\":604,\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} --><\/p>\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"404\" height=\"1024\" src=\"https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-404x1024.png\" alt=\"\" class=\"wp-image-604\" srcset=\"https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-404x1024.png 404w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-118x300.png 118w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-768x1949.png 768w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-807x2048.png 807w, https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-scaled.png 1009w\" sizes=\"auto, (max-width: 404px) 100vw, 404px\" \/><figcaption class=\"wp-element-caption\"><em>Figure 2 &#8211; End-to-end admin workflow: eleven steps from platform configuration to a live dashboard, with a feedback loop for prompt iteration.<\/em><\/figcaption><\/figure>\n<p><!-- \/wp:image --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>Each step is performed through the web-based admin UI.\u00a0The pipeline is intentionally manual at the trigger level\u00a0&#8211;\u00a0an admin decides when to run each stage\u00a0&#8211;\u00a0while the execution of each stage is fully automated.\u00a0This design provides human oversight at decision points while eliminating manual drudgery within each step.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:separator --><\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator --><\/p>\n<p><!-- wp:heading {\"level\":1} --><\/p>\n<h1 class=\"wp-block-heading\">Conclusion<\/h1>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>This technical walkthrough has presented the Data Discovery Agent:\u00a0a production-grade,\u00a0AI-powered platform that transforms the laborious process of advertising data warehouse annotation from weeks of manual spreadsheet work into an automated,\u00a0repeatable,\u00a0and auditable pipeline.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:paragraph --><\/p>\n<p>The agent&#8217;s five-stage architecture\u00a0&#8211;\u00a0Discovery,\u00a0Sampling,\u00a0Documentation Crawling,\u00a0LLM Inference,\u00a0and Enrichment\u00a0&#8211;\u00a0systematically builds context at each step so that the LLM&#8217;s mapping decisions are grounded in real evidence rather than speculation.\u00a0The result is a fully annotated data warehouse with confidence-scored mappings,\u00a0quantitative completeness metrics,\u00a0and actionable enrichment recommendations.<\/p>\n<p><!-- \/wp:paragraph --><\/p>\n<p><!-- wp:heading {\"level\":3} --><\/p>\n<h3 class=\"wp-block-heading\">Lessons learned<\/h3>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:list --><\/p>\n<ul class=\"wp-block-list\">\n<!-- wp:list-item --><\/p>\n<li><strong>Grounding is Everything:<\/strong>\u00a0Providing the LLM with real sample data alongside schema metadata was the single most impactful design decision. Column names alone are ambiguous; actual values resolve that ambiguity decisively.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Dual-Direction Mapping Closes the Loop:<\/strong>\u00a0Mapping warehouse columns forward (what do we have?) and connector fields backward (what could we enable?) transforms the output from a passive inventory into an active roadmap.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Storage Abstraction Pays for Itself:<\/strong>\u00a0The\u00a0<code>LocalStorage<\/code>\u00a0\/\u00a0<code>GCSStorage<\/code>\u00a0abstraction &#8211; a seemingly minor architectural decision &#8211; eliminated an entire class of development-vs-production bugs and made the system genuinely portable from day one.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Versioned Prompts Enable Iteration:<\/strong>\u00a0Content-hashing every prompt template and recording the hash alongside inference results made prompt engineering a disciplined, reproducible process rather than an ad-hoc exercise.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Build on a Unified Cloud Ecosystem:<\/strong>\u00a0Building entirely on\u00a0<strong>Google Cloud services &#8211; BigQuery, Vertex AI, Cloud Run, Cloud Storage, Secret Manager<\/strong>\u00a0&#8211; eliminated integration friction between components and allowed the project to move from prototype to production deployment without stitching together tools from multiple vendors.<\/li>\n<p><!-- \/wp:list-item -->\n<\/ul>\n<p><!-- \/wp:list --><\/p>\n<p><!-- wp:heading {\"level\":3} --><\/p>\n<h3 class=\"wp-block-heading\">What we are building next<\/h3>\n<p><!-- \/wp:heading --><\/p>\n<p><!-- wp:list {\"ordered\":true} --><\/p>\n<ol class=\"wp-block-list\">\n<!-- wp:list-item --><\/p>\n<li><strong>Scheduled Re-Scans:<\/strong>\u00a0Automated daily or weekly re-runs with alerting when a platform&#8217;s schema mutates &#8211; detecting drift before it causes downstream problems.<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Automatic Warehouse Verification:<\/strong>\u00a0A verification layer that checks whether recommended Adverity fields are actually populated with non-null values, distinguishing between &#8220;this field exists&#8221; and &#8220;this field contains useful data.&#8221;<\/li>\n<p><!-- \/wp:list-item --><br \/>\n<!-- wp:list-item --><\/p>\n<li><strong>Text-to-SQL:<\/strong>\u00a0A natural-language-to-SQL layer that uses the column mappings to answer ad-hoc questions against the warehouse &#8211; turning the annotated genome into a conversational data interface.<\/li>\n<p><!-- \/wp:list-item -->\n<\/ol>\n<p><!-- \/wp:list --><\/p>\n<p><!-- wp:separator --><\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator --><\/p>\n","related_pods":[593],"content_quarter":"Q1 2026"},"research_categories":[],"raw_acf":{"content":"<!-- wp:paragraph -->\n<p><em>Turning raw ad platform schemas into actionable intelligence through AI-powered semantic mapping.<\/em><\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":1} -->\n<h1 class=\"wp-block-heading\">TL;DR<\/h1>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>Up to 90%\u00a0of enterprise data sits unused,\u00a0and our BigQuery advertising warehouse was no exception:\u00a014 platforms,\u00a0179 tables, 2,709 columns, 15.8 billion records, zero shared schema, and answering a question as simple as whether Pinterest carries geo data required a two-week manual investigation. Every platform invented its own vocabulary (Facebook says <code>amount_spent<\/code>, Google says <code>cost_micros<\/code>, TikTok says <code>spend<\/code>)\u00a0and manual reconciliation cost analysts two to four weeks per pass,\u00a0producing a spreadsheet that started decaying the moment it was finished.\u00a0<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>We built the Data Discovery Agent,\u00a0an AI-powered pipeline that connects to BigQuery,\u00a0samples real data for grounding,\u00a0crawls external connector documentation,\u00a0and runs Gemini LLM inference with versioned prompts to autonomously annotate every column with confidence-scored mappings.\u00a0The agent compressed weeks of manual work into hours,\u00a0took us from zero structured understanding of our warehouse to a 58%\u00a0average completeness\u00a0(column presence per canonical sub-modality;\u00a0signal strength and population validation planned for Q2)\u00a0baseline across all 14 platforms,\u00a0and replaced tribal-knowledge spreadsheets with a searchable,\u00a0versioned genome report that turns\u00a0\"do we have that data?\"\u00a0from a research project into a dashboard lookup.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:quote -->\n<blockquote class=\"wp-block-quote\"><!-- wp:paragraph -->\n<p><strong><strong>If you don't care about the technical details, read <a href=\"https:\/\/research.wpp.com\/blog\/why-your-data-genome-may-need-a-check-up-and-how-a-data-discovery-agent-can-help\">our blog post <\/a>instead. <\/strong><\/strong><\/p>\n<!-- \/wp:paragraph --><\/blockquote>\n<!-- \/wp:quote -->\n\n<!-- wp:separator -->\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator -->\n\n<!-- wp:heading {\"level\":1} -->\n<h1 class=\"wp-block-heading\">Technical walkthrough<\/h1>\n<!-- \/wp:heading -->\n\n<!-- wp:heading {\"level\":1} -->\n<h1 class=\"wp-block-heading\">Introduction<\/h1>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>Advertising data warehouses are,\u00a0by nature,\u00a0enormous and messy.\u00a0Every major ad platform\u00a0-\u00a0Facebook,\u00a0Google,\u00a0TikTok,\u00a0and a dozen others\u00a0-\u00a0exposes its own schema,\u00a0its own naming conventions,\u00a0and its own definition of what a\u00a0\"click\"\u00a0or a\u00a0\"spend\"\u00a0actually means.\u00a0When these platforms feed into a centralised\u00a0<a href=\"https:\/\/cloud.google.com\/bigquery\/docs\"><strong>Google BigQuery<\/strong><\/a>\u00a0data lake,\u00a0the result is a sprawling collection of tables and columns that no single analyst can hold in their head.\u00a0In our case:\u00a0<strong>14 advertising platforms, 179 tables, and 2,709 columns<\/strong>\u00a0-\u00a0with over 15.8 billion records behind them.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>Historically,\u00a0making sense of this warehouse required manual schema annotation:\u00a0a data analyst opening each table,\u00a0reading every column name,\u00a0pulling sample rows,\u00a0cross-referencing API documentation,\u00a0and deciding which canonical category each column belonged to.\u00a0Conservatively,\u00a0that process took\u00a0<strong>two to four weeks<\/strong>\u00a0of focused work for a single pass.\u00a0And the result started decaying immediately\u00a0-\u00a0platforms update schemas quarterly,\u00a0sometimes monthly.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>To eliminate this bottleneck,\u00a0we architected and deployed the\u00a0<strong>Data Discovery Agent<\/strong>:\u00a0an AI-powered platform that autonomously discovers,\u00a0maps,\u00a0and scores the completeness of advertising data across an entire BigQuery data lake.\u00a0The agent connects to BigQuery,\u00a0fetches every table and column,\u00a0extracts real sample data for grounding,\u00a0crawls external documentation for cross-reference,\u00a0invokes\u00a0<a href=\"https:\/\/cloud.google.com\/vertex-ai\"><strong>Vertex AI<\/strong><\/a>\u00a0LLM inference with structured prompts,\u00a0and delivers a fully annotated mapping with confidence scores\u00a0-\u00a0compressing weeks of manual work into hours.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>The system is built as a production-grade\u00a0<a href=\"https:\/\/fastapi.tiangolo.com\/\"><strong>FastAPI<\/strong><\/a>\u00a0web application,\u00a0deployed on\u00a0<a href=\"https:\/\/cloud.google.com\/run\/docs\"><strong>Google Cloud Run<\/strong><\/a>,\u00a0with a complete admin control panel,\u00a0role-based access via Google OAuth,\u00a0and a storage abstraction layer that operates identically on local filesystems and Google Cloud Storage.\u00a0This document provides a comprehensive technical walkthrough of its architecture,\u00a0pipeline,\u00a0deployment,\u00a0and results.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:separator -->\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator -->\n\n<!-- wp:heading {\"level\":1} -->\n<h1 class=\"wp-block-heading\">Agent architecture<\/h1>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The Data Discovery Agent is structured as a\u00a0<strong>five-stage sequential pipeline<\/strong>\u00a0orchestrated by a FastAPI backend.\u00a0Unlike a chatbot or a single-prompt wrapper,\u00a0the agent executes an end-to-end workflow of discovery,\u00a0sampling,\u00a0documentation crawling,\u00a0LLM inference,\u00a0and enrichment reporting\u00a0-\u00a0with minimal human intervention.\u00a0The human's role shifts from\u00a0<em>doing the mapping<\/em>\u00a0to\u00a0<em>reviewing the mapping<\/em>.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>The high-level data flow is illustrated below:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":602,\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-large\"><img src=\"https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/flow1-1-720x1024.png\" alt=\"\" class=\"wp-image-602\"\/><figcaption class=\"wp-element-caption\"><em>Figure 1 - Data Discovery Agent pipeline: five stages from BigQuery data lake to executive dashboard.<\/em><\/figcaption><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:paragraph -->\n<p><br>The architecture was inspired by a clean separation of concerns.\u00a0The application core follows a layered pattern with dedicated service modules.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Service module inventory<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The backend is organised into focused service modules,\u00a0each responsible for a distinct domain:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:table {\"hasFixedLayout\":true} -->\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Service Module<\/strong><\/th><th><strong>Core Responsibility<\/strong><\/th><\/tr><\/thead><tbody><tr><td><strong>BigQuery Service<\/strong><\/td><td>Connects to the BigQuery data lake, discovers datasets and tables, fetches column schemas and row counts, and saves timestamped fetch snapshots.<\/td><\/tr><tr><td><strong>Sample Service<\/strong><\/td><td>Extracts real sample rows from BigQuery tables using background-threaded parallel queries. Supports pause, resume, and abort controls.<\/td><\/tr><tr><td><strong>Adverity Service<\/strong><\/td><td>Crawls official Adverity connector documentation pages using Playwright, extracting structured field lists (name, description, dimension\/metric).<\/td><\/tr><tr><td><strong>Inference Service<\/strong><\/td><td>Builds versioned LLM prompts, manages sync and batch inference pipelines via Vertex AI, and parses structured JSON responses into column-to-submodality mappings.<\/td><\/tr><tr><td><strong>BQ Filter Service<\/strong><\/td><td>Manages regex-based include\/exclude rules that control which BigQuery datasets and tables appear in a fetch.<\/td><\/tr><tr><td><strong>User Service<\/strong><\/td><td>Handles user CRUD operations, domain allow-lists, email ban-lists, and role-based access control (admin\/user).<\/td><\/tr><tr><td><strong>LLM Config Service<\/strong><\/td><td>Loads and manages LLM model configurations, including model identifiers and API parameters.<\/td><\/tr><tr><td><strong>Vertex Model Service<\/strong><\/td><td>Discovers available Vertex AI regions and publisher models for inference.<\/td><\/tr><\/tbody><\/table><\/figure>\n<!-- \/wp:table -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Key system capabilities<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:list {\"ordered\":true} -->\n<ol class=\"wp-block-list\">\n<!-- wp:list-item -->\n<li><strong>Autonomous Schema Discovery:<\/strong>\u00a0The agent connects to BigQuery, applies configurable regex filters, and discovers all advertising-platform datasets, tables, and column schemas without manual intervention.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Evidence-Grounded LLM Inference:<\/strong>\u00a0Every mapping decision is grounded in real data - column names, data types, and actual sample values - not just schema metadata. This dramatically reduces hallucination risk.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Dual-Direction Mapping:<\/strong>\u00a0The system performs both forward mapping (BigQuery columns to canonical sub-modalities) and reverse mapping (Adverity connector fields to sub-modalities), closing the loop from \"what do we have?\" to \"what should we enable?\"<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Completeness Scoring:<\/strong>\u00a0Each platform receives a quantitative completeness score measuring how many of the 24 canonical sub-modalities are covered, turning vague data quality concerns into actionable metrics.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Versioned Prompt Engineering:<\/strong>\u00a0All LLM prompts are version-tracked with SHA-256 content hashing, ensuring that every inference result can be traced back to the exact prompt that produced it.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Transparent Storage Abstraction:<\/strong>\u00a0A unified storage layer allows the application to operate identically on local filesystems (development) and Google Cloud Storage (production) by changing a single environment variable.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Production-Grade Access Control:<\/strong>\u00a0Google OAuth 2.0 authentication with domain allow-lists, email ban-lists, and admin\/user role separation ensures secure, auditable access.<\/li>\n<!-- \/wp:list-item -->\n<\/ol>\n<!-- \/wp:list -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">The modality schema: a canonical taxonomy<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>At the heart of the agent's mapping logic is a canonical\u00a0<strong>modality schema<\/strong>\u00a0-\u00a0a structured taxonomy that defines what advertising data\u00a0<em>means<\/em>,\u00a0independent of any platform's naming conventions.\u00a0The schema is organised around five high-level\u00a0<strong>modalities<\/strong>,\u00a0each representing a fundamental category of advertising measurement:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:table {\"hasFixedLayout\":true} -->\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Modality<\/strong><\/th><th><strong>Sub-modalities<\/strong><\/th><th><strong>What It Captures<\/strong><\/th><\/tr><\/thead><tbody><tr><td><strong>Performance<\/strong><\/td><td>7<\/td><td>The numbers: spend, impressions, clicks, video plays, conversions, leads, CTR<\/td><\/tr><tr><td><strong>Creative<\/strong><\/td><td>4<\/td><td>What the ad looked like: creative IDs, asset paths, ad names, format types<\/td><\/tr><tr><td><strong>Audience<\/strong><\/td><td>6<\/td><td>Who saw it: gender, age group, interests, custom audiences, behavioural segments<\/td><\/tr><tr><td><strong>Geo<\/strong><\/td><td>5<\/td><td>Where they saw it: countries, regions, cities, postal codes, designated market areas<\/td><\/tr><tr><td><strong>Brand<\/strong><\/td><td>2<\/td><td>Who paid for it: brand name, advertiser identity<\/td><\/tr><\/tbody><\/table><\/figure>\n<!-- \/wp:table -->\n\n<!-- wp:paragraph -->\n<p>This yields\u00a0<strong>24 sub-modalities<\/strong>\u00a0in total\u00a0-\u00a0the atomic units of the mapping.\u00a0The LLM's task is to determine,\u00a0for each BigQuery column across all 179 tables,\u00a0which sub-modality\u00a0(if any)\u00a0it corresponds to,\u00a0and with what confidence.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>The modality dictionary is fully editable through the admin UI,\u00a0meaning the taxonomy can be extended or refined without touching code.\u00a0It is injected into every LLM prompt as structured context,\u00a0ensuring consistent mapping behaviour across inference runs.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:separator -->\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator -->\n\n<!-- wp:heading {\"level\":1} -->\n<h1 class=\"wp-block-heading\">Technology stack<\/h1>\n<!-- \/wp:heading -->\n\n<!-- wp:table {\"hasFixedLayout\":true} -->\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Component<\/strong><\/th><th><strong>Technology<\/strong><\/th><th><strong>Role<\/strong><\/th><\/tr><\/thead><tbody><tr><td><strong>Web Framework<\/strong><\/td><td><a href=\"https:\/\/fastapi.tiangolo.com\/\">FastAPI<\/a><\/td><td>Application backend, API routing, middleware, and session management<\/td><\/tr><tr><td><strong>LLM Engine<\/strong><\/td><td><a href=\"https:\/\/deepmind.google\/technologies\/gemini\/\">Google Gemini<\/a>\u00a0(via\u00a0<a href=\"https:\/\/cloud.google.com\/vertex-ai\">Vertex AI<\/a>)<\/td><td>Powers all schema-mapping and recommendation inference across the Gemini model family (including Gemini Flash for batch runs); chosen for low latency, strong instruction-following, and structured JSON output<\/td><\/tr><tr><td><strong>Data Warehouse<\/strong><\/td><td><a href=\"https:\/\/cloud.google.com\/bigquery\/docs\">Google BigQuery<\/a><\/td><td>Primary data source; the agent queries dataset schemas, table metadata, and sample rows<\/td><\/tr><tr><td><strong>Batch Inference<\/strong><\/td><td><a href=\"https:\/\/cloud.google.com\/vertex-ai\/docs\/predictions\/get-batch-predictions\">Vertex AI Batch Prediction<\/a><\/td><td>Processes bulk LLM requests asynchronously via JSONL files staged in GCS; more cost-effective for full runs across all platforms<\/td><\/tr><tr><td><strong>Documentation Crawling<\/strong><\/td><td><a href=\"https:\/\/playwright.dev\/python\/\">Playwright<\/a><\/td><td>Headless browser automation for crawling Adverity connector documentation pages and extracting structured field lists<\/td><\/tr><tr><td><strong>Frontend<\/strong><\/td><td><a href=\"https:\/\/jinja.palletsprojects.com\/\">Jinja2<\/a>\u00a0+\u00a0<a href=\"https:\/\/htmx.org\/\">HTMX<\/a><\/td><td>Server-side rendered HTML templates with HTMX for interactive, partial-page updates without a JavaScript framework<\/td><\/tr><tr><td><strong>Authentication<\/strong><\/td><td><a href=\"https:\/\/developers.google.com\/identity\/protocols\/oauth2\">Google OAuth 2.0<\/a>\u00a0(via\u00a0<a href=\"https:\/\/authlib.org\/\">Authlib<\/a>)<\/td><td>Secure user login with domain allow-lists, email ban-lists, and admin\/user role separation<\/td><\/tr><tr><td><strong>Application Data<\/strong><\/td><td><a href=\"https:\/\/cloud.google.com\/storage\/docs\">Google Cloud Storage<\/a><\/td><td>Persistent store for fetch results, inference outputs, platform configs, user records, and Adverity documentation<\/td><\/tr><tr><td><strong>Deployment<\/strong><\/td><td><a href=\"https:\/\/cloud.google.com\/run\/docs\">Google Cloud Run<\/a><\/td><td>Serverless container hosting with auto-scaling, health probes, and secret-backed environment variables<\/td><\/tr><tr><td><strong>Infrastructure<\/strong><\/td><td><a href=\"https:\/\/www.terraform.io\/\">Terraform<\/a><\/td><td>Infrastructure-as-code: GCS buckets, Artifact Registry, Cloud Run service, IAM roles, Secret Manager secrets<\/td><\/tr><tr><td><strong>Containerisation<\/strong><\/td><td><a href=\"https:\/\/www.docker.com\/\">Docker<\/a>\u00a0(multi-stage build)<\/td><td>Reproducible builds with a builder stage for dependency installation and a slim runtime stage running as non-root<\/td><\/tr><tr><td><strong>Secret Management<\/strong><\/td><td><a href=\"https:\/\/cloud.google.com\/secret-manager\/docs\">Google Secret Manager<\/a><\/td><td>Stores OAuth credentials and session-signing keys, injected into Cloud Run as environment variables<\/td><\/tr><tr><td><strong>Language<\/strong><\/td><td>Python 3.11<\/td><td>All application code, managed via\u00a0<a href=\"https:\/\/python-poetry.org\/\">Poetry<\/a>\u00a0for dependency resolution<\/td><\/tr><\/tbody><\/table><\/figure>\n<!-- \/wp:table -->\n\n<!-- wp:separator -->\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator -->\n\n<!-- wp:heading {\"level\":1} -->\n<h1 class=\"wp-block-heading\">Pipeline deep-dive<\/h1>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The following five stages take the system from a raw,\u00a0unannotated data warehouse to a fully mapped,\u00a0scored,\u00a0and actionable genome report.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Stage 1 - Discovery<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The agent connects to Google BigQuery and discovers all staging datasets matching a configurable naming pattern\u00a0(e.g.,\u00a0<code>^bq_cgh_mp_.*_staging$<\/code>).\u00a0For each dataset,\u00a0it fetches the full table inventory:\u00a0column names,\u00a0data types,\u00a0and row counts.\u00a0Intelligent regex filters\u00a0-\u00a0managed through the admin UI via\u00a0<code>bq_filters.json<\/code>\u00a0-\u00a0exclude temporary artefacts such as tables prefixed with\u00a0<code>stg_<\/code>,\u00a0suffixed with\u00a0<code>_tmp<\/code>\u00a0or\u00a0<code>_dbt_tmp<\/code>,\u00a0and any datasets that don't match the expected naming convention.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>Results are saved as a\u00a0<strong>timestamped fetch snapshot<\/strong>\u00a0(<code>fetches\/fetch_YYYYMMDD_HHMMSS\/results.json<\/code>).\u00a0Multiple fetches can coexist,\u00a0allowing admins to compare schema evolution over time.\u00a0Each fetch captures the complete state of the warehouse at a point in time\u00a0-\u00a0the starting point for all downstream inference work.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p><strong>Current warehouse dimensions:<\/strong><\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:table {\"hasFixedLayout\":true} -->\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Metric<\/strong><\/th><th><strong>Count<\/strong><\/th><\/tr><\/thead><tbody><tr><td>Advertising Platforms<\/td><td>14<\/td><\/tr><tr><td>BigQuery Datasets<\/td><td>17<\/td><\/tr><tr><td>Tables<\/td><td>179<\/td><\/tr><tr><td>Total Columns<\/td><td>2,709<\/td><\/tr><tr><td>Total Records<\/td><td>15.8 billion+<\/td><\/tr><\/tbody><\/table><\/figure>\n<!-- \/wp:table -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">Platforms covered<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:table {\"hasFixedLayout\":true} -->\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>#<\/th><th>Platform<\/th><th>#<\/th><th>Platform<\/th><\/tr><\/thead><tbody><tr><td>1<\/td><td>Amazon DSP<\/td><td>8<\/td><td>Pinterest<\/td><\/tr><tr><td>2<\/td><td>Facebook Ads<\/td><td>9<\/td><td>Snapchat<\/td><\/tr><tr><td>3<\/td><td>Google Ads<\/td><td>10<\/td><td>TikTok<\/td><\/tr><tr><td>4<\/td><td>Google Ads (YouTube)<\/td><td>11<\/td><td>The Trade Desk<\/td><\/tr><tr><td>5<\/td><td>Google DV360<\/td><td>12<\/td><td>Twitter\/X Ads<\/td><\/tr><tr><td>6<\/td><td>Google DV360 (YouTube)<\/td><td>13<\/td><td>Xandr<\/td><\/tr><tr><td>7<\/td><td>LinkedIn<\/td><td>14<\/td><td>IAS<\/td><\/tr><\/tbody><\/table><\/figure>\n<!-- \/wp:table -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Stage 2 - Sampling<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>Column names alone are often ambiguous.\u00a0A column called\u00a0<code>cost<\/code>\u00a0could mean total spend,\u00a0cost-per-click,\u00a0or an internal ID.\u00a0The agent resolves this ambiguity by extracting\u00a0<strong>actual sample data rows<\/strong>\u00a0from each table using background-threaded parallel queries against BigQuery.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>These real values become critical evidence for the LLM:\u00a0seeing\u00a0<code>14.50<\/code>,\u00a0<code>0.83<\/code>,\u00a0<code>127.99<\/code>\u00a0in a\u00a0<code>cost<\/code>\u00a0column strongly suggests monetary spend,\u00a0not an identifier.\u00a0Seeing\u00a0<code>M<\/code>,\u00a0<code>F<\/code>,\u00a0<code>Unknown<\/code>\u00a0in a\u00a0<code>gender<\/code>\u00a0column confirms an audience dimension more reliably than the column name alone.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>The extraction process supports\u00a0<strong>pause, resume, and abort<\/strong>\u00a0controls\u00a0-\u00a0essential when sampling across hundreds of tables with billions of rows.\u00a0Progress is tracked per-platform and persisted to storage,\u00a0so an interrupted extraction can resume exactly where it left off.\u00a0Samples are saved as\u00a0<code>samples_&lt;platform&gt;.json<\/code>\u00a0per fetch and are injected into the LLM prompt alongside the schema metadata.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Stage 3 - Documentation crawling<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The agent crawls the official connector documentation for each platform\u00a0-\u00a0specifically,\u00a0the authoritative\u00a0\"Most Used Fields\"\u00a0pages published by\u00a0<a href=\"https:\/\/www.adverity.com\/\">Adverity<\/a>,\u00a0the data integration platform that feeds data into the BigQuery warehouse.\u00a0Using\u00a0<a href=\"https:\/\/playwright.dev\/python\/\">Playwright<\/a>\u00a0for headless browser automation,\u00a0the crawler fetches each documentation page and extracts a structured field list:\u00a0name,\u00a0description,\u00a0and whether the field is a dimension or metric.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>Each crawled page is saved in three formats\u00a0-\u00a0HTML,\u00a0Markdown,\u00a0and structured JSON\u00a0-\u00a0under\u00a0<code>adverity_docs\/<\/code>\u00a0in the storage backend.\u00a0These documents serve as the agent's\u00a0\"reference manual\":\u00a0a ground-truth checklist of what fields each platform\u00a0<em>could<\/em>\u00a0provide,\u00a0independent of what currently exists in the warehouse.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>The crawl engine runs as a background task with progress tracking,\u00a0with a configurable delay between requests to avoid rate-limiting.\u00a0Platform URLs are managed through the admin UI,\u00a0making it straightforward to add new data sources.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Stage 4 - LLM inference<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This is the core intellectual step\u00a0-\u00a0where the agent reads every column and produces an annotated mapping.\u00a0The Inference Service builds\u00a0<strong>versioned prompts<\/strong>\u00a0containing three layers of context:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list {\"ordered\":true} -->\n<ol class=\"wp-block-list\">\n<!-- wp:list-item -->\n<li><strong>Table schemas<\/strong>\u00a0- column names and data types for every table in the platform's dataset.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Sample data<\/strong>\u00a0- real row values extracted in Stage 2, providing grounding evidence.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Modality definitions<\/strong>\u00a0- the target taxonomy of modalities and sub-modalities the LLM must map to.<\/li>\n<!-- \/wp:list-item -->\n<\/ol>\n<!-- \/wp:list -->\n\n<!-- wp:paragraph -->\n<p>These structured prompts are sent to models from the\u00a0<strong><a href=\"https:\/\/deepmind.google\/technologies\/gemini\/\">Google Gemini<\/a><\/strong>\u00a0family via Vertex AI.\u00a0The model returns structured JSON:\u00a0for each canonical sub-modality,\u00a0it identifies matching columns,\u00a0assigns a\u00a0<strong>confidence score<\/strong>\u00a0(0.0 to 1.0),\u00a0and provides natural-language reasoning for each match.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">Inference modes<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>Two inference modes are supported,\u00a0selectable through the admin UI:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:table {\"hasFixedLayout\":true} -->\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Mode<\/strong><\/th><th><strong>Mechanism<\/strong><\/th><th><strong>Best For<\/strong><\/th><\/tr><\/thead><tbody><tr><td><strong>Sync<\/strong><\/td><td>One LLM request per platform, processed sequentially via\u00a0<code>POST \/api\/inference\/sync<\/code><\/td><td>Quick testing, single-platform runs<\/td><\/tr><tr><td><strong>Batch<\/strong><\/td><td>Prompts written to JSONL files in GCS, submitted as Vertex AI batch prediction jobs, polled for completion<\/td><td>Full production runs across all 14 platforms; significantly more cost-effective<\/td><\/tr><\/tbody><\/table><\/figure>\n<!-- \/wp:table -->\n\n<!-- wp:paragraph -->\n<p>For batch mode,\u00a0the workflow is:\u00a0write prompts to JSONL,\u00a0submit batch jobs,\u00a0poll status until completion,\u00a0collect and parse results.\u00a0The UI provides real-time progress tracking for all active jobs.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">Prompt engineering<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>All prompt templates live in\u00a0<code>app\/prompts\/<\/code>\u00a0and are version-tracked with semantic versioning and SHA-256 content hashing via\u00a0<code>versions.json<\/code>.\u00a0This means every inference result records exactly which prompt version and content hash produced it\u00a0-\u00a0critical for reproducibility.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>The base modality mapping prompt instructs the LLM to:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\">\n<!-- wp:list-item -->\n<li>Examine every column name and data type across all provided tables.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li>Use sample data to confirm or reject potential matches.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li>Assign confidence scores on a defined scale (0.9-1.0 for clear matches, 0.5-0.69 for ambiguous, below 0.5 for weak).<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li>Return\u00a0<strong>only valid JSON<\/strong>\u00a0- no markdown, no prose.<\/li>\n<!-- \/wp:list-item -->\n<\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:paragraph -->\n<p>A dedicated\u00a0<strong>Adverity recommendation prompt<\/strong>\u00a0performs the reverse mapping:\u00a0given Adverity connector documentation,\u00a0recommend which Adverity fields map to which sub-modalities.\u00a0This prompt enforces a critical rule\u00a0-\u00a0field names must appear\u00a0<strong>verbatim<\/strong>\u00a0in the documentation,\u00a0preventing the LLM from inventing fields that don't exist.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">Robust JSON parsing<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>LLM outputs are rarely perfectly formatted.\u00a0The Inference Service includes a\u00a0<strong>robust JSON parser<\/strong>\u00a0(<code>_robust_parse_llm_json<\/code>)\u00a0that handles common LLM formatting mistakes:\u00a0markdown code fences,\u00a0trailing commas,\u00a0single-quoted strings,\u00a0BOM characters,\u00a0leading\/trailing prose around the JSON object,\u00a0and escaped newlines inside strings.\u00a0This defensive parsing layer ensures that minor formatting issues never cause a pipeline failure.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">Three inference pipelines<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The system supports three distinct inference types:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list {\"ordered\":true} -->\n<ol class=\"wp-block-list\">\n<!-- wp:list-item -->\n<li><strong>Modality Mapping Inference<\/strong>\u00a0- Maps BigQuery columns to the canonical sub-modality taxonomy. The primary pipeline.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Adverity Recommendation Inference<\/strong>\u00a0- Given Adverity documentation, recommends which connector fields should be enabled for each sub-modality.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Field Extraction Inference<\/strong>\u00a0- Extracts structured field definitions from raw Adverity HTML\/Markdown documentation using chunked batch inference, producing clean per-platform field maps.<\/li>\n<!-- \/wp:list-item -->\n<\/ol>\n<!-- \/wp:list -->\n\n<!-- wp:paragraph -->\n<p>Each run records metadata:\u00a0timestamp,\u00a0model used,\u00a0prompt version,\u00a0prompt content hash,\u00a0and platforms processed.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Stage 5 - Enrichment and dashboard<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The final stage closes the diagnostic loop by cross-referencing the warehouse mappings from Stage 4 against the connector field lists from Stage 3.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">Enrichment recommendations<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The Adverity Recommendation inference goes beyond simply flagging missing geo data. It prescribes a fix: <em>\"The connector for this platform offers a field called <code>country_code<\/code> (dimension); enable it, and your geo coverage improves from 2\/5 to 3\/5 sub-modalities.\"<\/em> Diagnosis and prescription in one step, turning gap analysis into concrete action items for the data engineering team.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">Completeness scoring<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>Each platform receives a\u00a0<strong>completeness score<\/strong>:\u00a0how much of the canonical 24-sub-modality schema is actually present.\u00a0The approach is deliberately simple\u00a0-\u00a0a presence\/absence assay.\u00a0For each platform,\u00a0the denominator is 24\u00a0(total sub-modalities).\u00a0The numerator is how many have at least one column mapped above a configurable confidence threshold.\u00a0A sub-modality is either present or it isn't\u00a0-\u00a0this handles deduplication naturally.\u00a0If three tables each have a\u00a0<code>spend<\/code>\u00a0column mapped to\u00a0<code>performance__spend_usd<\/code>,\u00a0the sub-modality counts once,\u00a0not three times.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">The executive dashboard<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>All results render in a\u00a0<strong>FastAPI + Jinja2\/HTMX dashboard<\/strong>\u00a0that provides:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\">\n<!-- wp:list-item -->\n<li><strong>Platform scorecards<\/strong>\u00a0- one card per advertising platform showing its name, banner image, and completeness percentage.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Modality drilldowns<\/strong>\u00a0- for each platform, expandable breakdowns into modalities and their sub-modalities.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Column mappings<\/strong>\u00a0- the specific BigQuery columns mapped to each sub-modality, with confidence scores and LLM reasoning.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Sample data preview<\/strong>\u00a0- actual row data from each table for manual verification.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Executive summary sidebar<\/strong>\u00a0- aggregated statistics across all platforms: total mappings, average completeness, record counts, highest and lowest performing platforms.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Interactive mapping visualiser<\/strong>\u00a0- a left-right mapping view, filterable by platform, modality, and confidence threshold.<\/li>\n<!-- \/wp:list-item -->\n<\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:paragraph -->\n<p>Visibility of platforms is controlled via\u00a0<strong>Dashboard Settings<\/strong>,\u00a0where admins choose which fetch and which platforms appear to end users.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">PDF executive report generation<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The dashboard includes a one-click\u00a0<strong>PDF executive report generator<\/strong>\u00a0for stakeholders who need a polished,\u00a0offline-readable document.\u00a0When triggered,\u00a0the system:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list {\"ordered\":true} -->\n<ol class=\"wp-block-list\">\n<!-- wp:list-item -->\n<li><strong>Aggregates all dashboard data<\/strong>\u00a0- platform stats, completeness scores, modality breakdowns, and mapped column details - into a single render context.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Renders a dedicated Jinja2 report template<\/strong>\u00a0(<code>exec_report.html<\/code>) - a professionally styled, multi-page A4 document with a cover page, executive overview, key metric summary cards, a platform comparison table, the full modality dictionary with descriptions, and per-platform detail sections showing completeness stats, modality coverage grids (with check\/miss indicators per sub-modality), and optionally the full column mapping list.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Converts the HTML to PDF<\/strong>\u00a0using\u00a0<a href=\"https:\/\/playwright.dev\/python\/\">Playwright<\/a>'s headless Chromium instance (<code>page.pdf(format=\"A4\", print_background=True)<\/code>), producing a pixel-perfect render with page breaks, headers, and footers showing the generation timestamp and page numbers.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Returns the PDF as a downloadable file<\/strong>\u00a0(e.g.,\u00a0<code>exec_report_20260327_112709.pdf<\/code>).<\/li>\n<!-- \/wp:list-item -->\n<\/ol>\n<!-- \/wp:list -->\n\n<!-- wp:paragraph -->\n<p>The report is designed for executive consumption:\u00a0the first page presents headline metrics\u00a0(total platforms,\u00a0tables,\u00a0columns,\u00a0records,\u00a0average completeness)\u00a0and identifies the best and worst performers.\u00a0Subsequent pages provide the modality dictionary for reference,\u00a0followed by detailed per-platform breakdowns with modality coverage grids showing exactly which sub-modalities are mapped\u00a0(with column counts)\u00a0and which remain gaps.\u00a0This gives leadership a complete,\u00a0self-contained snapshot of data integration maturity without requiring dashboard access.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:separator -->\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator -->\n\n<!-- wp:heading {\"level\":1} -->\n<h1 class=\"wp-block-heading\">Cloud deployment<\/h1>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The system is deployed as a production-grade,\u00a0cloud-native service on\u00a0<strong>Google Cloud<\/strong>,\u00a0following a containerised,\u00a0infrastructure-as-code workflow from local development through to automated deployment.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Infrastructure-as-Code (Terraform)<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>All infrastructure is defined as Terraform in\u00a0<code>deployment\/terraform\/<\/code>\u00a0with six modules:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:table {\"hasFixedLayout\":true} -->\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Module<\/strong><\/th><th><strong>Resources Created<\/strong><\/th><\/tr><\/thead><tbody><tr><td><strong><code>apis<\/code><\/strong><\/td><td>Enables 10 GCP APIs (Cloud Run, Artifact Registry, Storage, BigQuery, Vertex AI, Secret Manager, IAM, Cloud Build, etc.)<\/td><\/tr><tr><td><strong><code>artifact_registry<\/code><\/strong><\/td><td>Docker image repository with a keep-last-5 cleanup policy<\/td><\/tr><tr><td><strong><code>gcs<\/code><\/strong><\/td><td>Versioned GCS bucket for application data (platforms, fetches, users, banners) with lifecycle rules<\/td><\/tr><tr><td><strong><code>iam<\/code><\/strong><\/td><td>Service account with roles: Storage Object Admin, BigQuery Data Viewer, BigQuery Job User, Vertex AI User, Secret Accessor<\/td><\/tr><tr><td><strong><code>secret_manager<\/code><\/strong><\/td><td>Three secrets:\u00a0<code>SECRET_KEY<\/code>,\u00a0<code>GOOGLE_CLIENT_ID<\/code>,\u00a0<code>GOOGLE_CLIENT_SECRET<\/code><\/td><\/tr><tr><td><strong><code>cloud_run<\/code><\/strong><\/td><td>Cloud Run v2 service with health probes, auto-scaling (0 to 3 instances), and secret-backed environment variables<\/td><\/tr><\/tbody><\/table><\/figure>\n<!-- \/wp:table -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Containerisation<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The application is packaged as a Docker image using a\u00a0<strong>multi-stage build<\/strong>:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list {\"ordered\":true} -->\n<ol class=\"wp-block-list\">\n<!-- wp:list-item -->\n<li><strong>Builder stage<\/strong>\u00a0- Installs Python dependencies via Poetry, exports to\u00a0<code>requirements.txt<\/code>, and installs packages into a clean prefix.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Runtime stage<\/strong>\u00a0- Copies only the installed packages and application code into a slim Python 3.11 image. Runs as a non-root\u00a0<code>appuser<\/code>\u00a0for security. Seed data (platform configs, modality dictionaries, Adverity docs) is baked into the image; runtime data (fetches, users, inference results) lives in the GCS bucket.<\/li>\n<!-- \/wp:list-item -->\n<\/ol>\n<!-- \/wp:list -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Deployment pipeline<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>A full deployment is executed with a single command:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:preformatted -->\n<pre class=\"wp-block-preformatted\"><code>make deploy-all    # tf-init -&gt; tf-apply -&gt; docker-push -&gt; seed-data<br><\/code><\/pre>\n<!-- \/wp:preformatted -->\n\n<!-- wp:paragraph -->\n<p>This runs Terraform to provision\/update all infrastructure,\u00a0builds and pushes the Docker image to Artifact Registry,\u00a0and syncs the local\u00a0<code>data\/<\/code>\u00a0directory to the GCS bucket.\u00a0Incremental deployments\u00a0(code-only changes)\u00a0use\u00a0<code>make deploy<\/code>\u00a0which skips the Terraform init step.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>The\u00a0<code>seed-data<\/code>\u00a0target uses\u00a0<code>gsutil -m rsync<\/code>\u00a0to upload platform definitions,\u00a0modality dictionaries,\u00a0BQ filters,\u00a0Adverity documentation,\u00a0and other configuration data into the GCS bucket.\u00a0Without seeding,\u00a0a fresh deployment starts with an empty bucket and no platform or user configuration.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Storage abstraction layer<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>A critical architectural decision was the\u00a0<strong>storage abstraction layer<\/strong>\u00a0(<code>app\/core\/storage.py<\/code>).\u00a0All data I\/O goes through a unified\u00a0<code>StorageBackend<\/code>\u00a0interface with two implementations:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\">\n<!-- wp:list-item -->\n<li><strong><code>LocalStorage<\/code><\/strong>\u00a0- reads\/writes to the local\u00a0<code>data\/<\/code>\u00a0directory. Used during development (<code>STORAGE_BACKEND=local<\/code>).<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong><code>GCSStorage<\/code><\/strong>\u00a0- reads\/writes to a GCS bucket under a configurable prefix. Used in production (<code>STORAGE_BACKEND=gcs<\/code>).<\/li>\n<!-- \/wp:list-item -->\n<\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:paragraph -->\n<p>Switching between backends requires changing only the\u00a0<code>STORAGE_BACKEND<\/code>\u00a0environment variable.\u00a0Every service module calls\u00a0<code>get_storage()<\/code>\u00a0and operates through the same API regardless of the underlying storage\u00a0-\u00a0ensuring that code tested locally behaves identically in production.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Authentication and access control<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The application uses\u00a0<strong>Google OAuth 2.0<\/strong>\u00a0(OpenID Connect)\u00a0for user authentication:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\">\n<!-- wp:list-item -->\n<li>Users log in with their Google account via the standard OAuth consent flow.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Domain allow-lists<\/strong>\u00a0restrict login to specific email domains (e.g.,\u00a0<code>satalia.com<\/code>,\u00a0<code>choreograph.com<\/code>).<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Email ban-lists<\/strong>\u00a0allow blocking specific users.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Role-based access<\/strong>\u00a0separates admin users (full control panel) from regular users (executive dashboard only).<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li>A\u00a0<strong>bypass mode<\/strong>\u00a0(<code>BYPASS_AUTH_AS_ADMIN=true<\/code>) enables local development without Google credentials.<\/li>\n<!-- \/wp:list-item -->\n<\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:paragraph -->\n<p>All user and auth settings are managed through the admin UI and persisted via the storage backend.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:separator -->\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator -->\n\n<!-- wp:heading {\"level\":1} -->\n<h1 class=\"wp-block-heading\">Results and impact<\/h1>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>When the pipeline completes a full run across all fourteen platforms,\u00a0the executive dashboard delivers a comprehensive genome report of the data warehouse.\u00a0The following results are drawn from the live executive summary generated by the dashboard.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Current warehouse state<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:table {\"hasFixedLayout\":true} -->\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Metric<\/strong><\/th><th><strong>Value<\/strong><\/th><\/tr><\/thead><tbody><tr><td>Platforms Monitored<\/td><td>14<\/td><\/tr><tr><td>Tables<\/td><td>179<\/td><\/tr><tr><td>Columns<\/td><td>2,709<\/td><\/tr><tr><td>Total Records<\/td><td>15.8B<\/td><\/tr><tr><td>Average Completeness<\/td><td><strong>58%<\/strong><\/td><\/tr><tr><td>Highest Completeness<\/td><td>Snapchat -\u00a0<strong>75%<\/strong>\u00a0(18\/24)<\/td><\/tr><tr><td>Lowest Completeness<\/td><td>Integral Ad Science -\u00a0<strong>25%<\/strong>\u00a0(6\/24)<\/td><\/tr><\/tbody><\/table><\/figure>\n<!-- \/wp:table -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">Per-platform breakdown<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:table {\"hasFixedLayout\":true} -->\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Platform<\/strong><\/th><th><strong>Completeness<\/strong><\/th><th><strong>Tables<\/strong><\/th><th><strong>Columns<\/strong><\/th><th><strong>Records<\/strong><\/th><\/tr><\/thead><tbody><tr><td>Facebook Ads<\/td><td>66% (16\/24)<\/td><td>15<\/td><td>364<\/td><td>3,147.7M<\/td><\/tr><tr><td>Google Ads<\/td><td>70% (17\/24)<\/td><td>21<\/td><td>399<\/td><td>2,284.6M<\/td><\/tr><tr><td>TikTok<\/td><td>62% (15\/24)<\/td><td>14<\/td><td>217<\/td><td>15.1M<\/td><\/tr><tr><td>LinkedIn<\/td><td>70% (17\/24)<\/td><td>12<\/td><td>143<\/td><td>366.4k<\/td><\/tr><tr><td>Amazon DSP<\/td><td>62% (15\/24)<\/td><td>18<\/td><td>213<\/td><td>18.2M<\/td><\/tr><tr><td>Integral Ad Science<\/td><td>25% (6\/24)<\/td><td>12<\/td><td>130<\/td><td>95.9M<\/td><\/tr><tr><td>Xandr<\/td><td>50% (12\/24)<\/td><td>11<\/td><td>156<\/td><td>1.2M<\/td><\/tr><tr><td>Google Ads (YouTube)<\/td><td>45% (11\/24)<\/td><td>9<\/td><td>112<\/td><td>10.9M<\/td><\/tr><tr><td>Display &amp; Video 360<\/td><td>70% (17\/24)<\/td><td>16<\/td><td>275<\/td><td>10,148.3M<\/td><\/tr><tr><td>Display &amp; Video 360 (YouTube)<\/td><td>54% (13\/24)<\/td><td>15<\/td><td>187<\/td><td>63.6M<\/td><\/tr><tr><td>Pinterest<\/td><td>70% (17\/24)<\/td><td>11<\/td><td>170<\/td><td>6.7M<\/td><\/tr><tr><td>Snapchat<\/td><td>75% (18\/24)<\/td><td>10<\/td><td>150<\/td><td>15M<\/td><\/tr><tr><td>The Trade Desk<\/td><td>45% (11\/24)<\/td><td>6<\/td><td>80<\/td><td>4.1M<\/td><\/tr><tr><td>Twitter Ads<\/td><td>50% (12\/24)<\/td><td>9<\/td><td>113<\/td><td>40.4k<\/td><\/tr><\/tbody><\/table><\/figure>\n<!-- \/wp:table -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Headline findings<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>Four platforms\u00a0-\u00a0Google Ads,\u00a0LinkedIn,\u00a0Display\u00a0&amp;\u00a0Video 360,\u00a0and Pinterest\u00a0-\u00a0cluster at\u00a0<strong>70% completeness<\/strong>\u00a0(17\/24 sub-modalities).\u00a0Snapchat leads the pack at\u00a0<strong>75%<\/strong>.\u00a0Several platforms sit in the 50-66%\u00a0range,\u00a0and a handful fall below 50%.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p><strong>Performance metrics<\/strong>\u00a0(spend,\u00a0impressions,\u00a0clicks)\u00a0are well-represented across the board\u00a0-\u00a0these are the columns every platform exposes and every data team queries first.\u00a0But\u00a0<strong>Audience, Geo, and Brand modalities<\/strong>\u00a0show significant gaps,\u00a0particularly in measurement and programmatic categories.\u00a0Some findings were genuine surprises:\u00a0platforms assumed to lack audience data turned out to carry age range and gender columns that mapped cleanly.\u00a0The data was there all along\u00a0-\u00a0it just hadn't been annotated.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>At 58%,\u00a0the warehouse is more than half-mapped\u00a0-\u00a0but meaningful blind spots remain.\u00a0That's a useful headline.\u00a0It is unequivocally better to know the current state than to assume it's healthy without running the test.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Operational impact<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:table {\"hasFixedLayout\":true} -->\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><strong>Before (Manual)<\/strong><\/th><th><strong>After (Agent)<\/strong><\/th><\/tr><\/thead><tbody><tr><td>2-4 weeks per full mapping pass<\/td><td>Hours for a complete run<\/td><\/tr><tr><td>Mapping decays immediately as schemas change<\/td><td>Re-run on demand; schema drift detected automatically<\/td><\/tr><tr><td>Tribal knowledge locked in spreadsheets<\/td><td>Structured, versioned, searchable mappings with confidence scores<\/td><\/tr><tr><td>Answering \"do we have geo data from Pinterest?\" required manual investigation<\/td><td>Dashboard lookup: instant answer with completeness score<\/td><\/tr><tr><td>Enrichment recommendations require manual cross-referencing<\/td><td>Automated prescriptions: \"Enable\u00a0<code>country_code<\/code>\u00a0on this connector\"<\/td><\/tr><\/tbody><\/table><\/figure>\n<!-- \/wp:table -->\n\n<!-- wp:paragraph -->\n<p>The shift is from\u00a0<em>doing the mapping<\/em>\u00a0to\u00a0<em>reviewing the mapping<\/em>\u00a0-\u00a0a fundamentally different use of analyst time.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:separator -->\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator -->\n\n<!-- wp:heading {\"level\":1} -->\n<h1 class=\"wp-block-heading\">Admin workflow<\/h1>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The typical end-to-end admin workflow from raw data to a published dashboard follows this sequence:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":604,\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-large\"><img src=\"https:\/\/cms.research.wpp.com\/wp-content\/uploads\/2026\/03\/admin-flow-404x1024.png\" alt=\"\" class=\"wp-image-604\"\/><figcaption class=\"wp-element-caption\"><em>Figure 2 - End-to-end admin workflow: eleven steps from platform configuration to a live dashboard, with a feedback loop for prompt iteration.<\/em><\/figcaption><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:paragraph -->\n<p>Each step is performed through the web-based admin UI.\u00a0The pipeline is intentionally manual at the trigger level\u00a0-\u00a0an admin decides when to run each stage\u00a0-\u00a0while the execution of each stage is fully automated.\u00a0This design provides human oversight at decision points while eliminating manual drudgery within each step.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:separator -->\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator -->\n\n<!-- wp:heading {\"level\":1} -->\n<h1 class=\"wp-block-heading\">Conclusion<\/h1>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This technical walkthrough has presented the Data Discovery Agent:\u00a0a production-grade,\u00a0AI-powered platform that transforms the laborious process of advertising data warehouse annotation from weeks of manual spreadsheet work into an automated,\u00a0repeatable,\u00a0and auditable pipeline.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>The agent's five-stage architecture\u00a0-\u00a0Discovery,\u00a0Sampling,\u00a0Documentation Crawling,\u00a0LLM Inference,\u00a0and Enrichment\u00a0-\u00a0systematically builds context at each step so that the LLM's mapping decisions are grounded in real evidence rather than speculation.\u00a0The result is a fully annotated data warehouse with confidence-scored mappings,\u00a0quantitative completeness metrics,\u00a0and actionable enrichment recommendations.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">Lessons learned<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\">\n<!-- wp:list-item -->\n<li><strong>Grounding is Everything:<\/strong>\u00a0Providing the LLM with real sample data alongside schema metadata was the single most impactful design decision. Column names alone are ambiguous; actual values resolve that ambiguity decisively.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Dual-Direction Mapping Closes the Loop:<\/strong>\u00a0Mapping warehouse columns forward (what do we have?) and connector fields backward (what could we enable?) transforms the output from a passive inventory into an active roadmap.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Storage Abstraction Pays for Itself:<\/strong>\u00a0The\u00a0<code>LocalStorage<\/code>\u00a0\/\u00a0<code>GCSStorage<\/code>\u00a0abstraction - a seemingly minor architectural decision - eliminated an entire class of development-vs-production bugs and made the system genuinely portable from day one.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Versioned Prompts Enable Iteration:<\/strong>\u00a0Content-hashing every prompt template and recording the hash alongside inference results made prompt engineering a disciplined, reproducible process rather than an ad-hoc exercise.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Build on a Unified Cloud Ecosystem:<\/strong>\u00a0Building entirely on\u00a0<strong>Google Cloud services - BigQuery, Vertex AI, Cloud Run, Cloud Storage, Secret Manager<\/strong>\u00a0- eliminated integration friction between components and allowed the project to move from prototype to production deployment without stitching together tools from multiple vendors.<\/li>\n<!-- \/wp:list-item -->\n<\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">What we are building next<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:list {\"ordered\":true} -->\n<ol class=\"wp-block-list\">\n<!-- wp:list-item -->\n<li><strong>Scheduled Re-Scans:<\/strong>\u00a0Automated daily or weekly re-runs with alerting when a platform's schema mutates - detecting drift before it causes downstream problems.<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Automatic Warehouse Verification:<\/strong>\u00a0A verification layer that checks whether recommended Adverity fields are actually populated with non-null values, distinguishing between \"this field exists\" and \"this field contains useful data.\"<\/li>\n<!-- \/wp:list-item -->\n<!-- wp:list-item -->\n<li><strong>Text-to-SQL:<\/strong>\u00a0A natural-language-to-SQL layer that uses the column mappings to answer ad-hoc questions against the warehouse - turning the annotated genome into a conversational data interface.<\/li>\n<!-- \/wp:list-item -->\n<\/ol>\n<!-- \/wp:list -->\n\n<!-- wp:separator -->\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<!-- \/wp:separator -->","content_quarter":"Q1 2026","related_pods":["593"],"featured":"","legacy_perspective_source_id":""},"_links":{"self":[{"href":"https:\/\/cms.research.wpp.com\/index.php?rest_route=\/wp\/v2\/research_feed\/1655","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cms.research.wpp.com\/index.php?rest_route=\/wp\/v2\/research_feed"}],"about":[{"href":"https:\/\/cms.research.wpp.com\/index.php?rest_route=\/wp\/v2\/types\/research_feed"}],"author":[{"embeddable":true,"href":"https:\/\/cms.research.wpp.com\/index.php?rest_route=\/wp\/v2\/users\/13"}],"acf:post":[{"embeddable":true,"href":"https:\/\/cms.research.wpp.com\/index.php?rest_route=\/wp\/v2\/research_pods\/593"}],"wp:attachment":[{"href":"https:\/\/cms.research.wpp.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1655"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cms.research.wpp.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1655"},{"taxonomy":"content_type","embeddable":true,"href":"https:\/\/cms.research.wpp.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcontent_types&post=1655"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/cms.research.wpp.com\/index.php?rest_route=%2Fwp%2Fv2%2Fppma_author&post=1655"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}