<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anthony Lee</title>
    <description>The latest articles on DEV Community by Anthony Lee (@anthony_lee_63e96408d7573).</description>
    <link>https://dev.to/anthony_lee_63e96408d7573</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3605723%2F08a5ac12-368f-4e52-ab95-1f55ec66e4ea.png</url>
      <title>DEV Community: Anthony Lee</title>
      <link>https://dev.to/anthony_lee_63e96408d7573</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anthony_lee_63e96408d7573"/>
    <language>en</language>
    <item>
      <title>How to Set Up Google Analytics as a Claude Code Skill</title>
      <dc:creator>Anthony Lee</dc:creator>
      <pubDate>Wed, 11 Feb 2026 11:43:31 +0000</pubDate>
      <link>https://dev.to/anthony_lee_63e96408d7573/how-to-set-up-google-analytics-as-a-claude-code-skill-1</link>
      <guid>https://dev.to/anthony_lee_63e96408d7573/how-to-set-up-google-analytics-as-a-claude-code-skill-1</guid>
      <description>&lt;p&gt;A step-by-step guide for connecting your Google Analytics data to Claude Code, so you can ask questions about your website traffic in plain English.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You'll End Up With
&lt;/h2&gt;

&lt;p&gt;Once this is set up, you'll be able to open Claude Code (in VS Code or the terminal) and ask things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How much traffic did my site get this week?"&lt;/li&gt;
&lt;li&gt;"What are my top pages in the last 30 days?"&lt;/li&gt;
&lt;li&gt;"Where is my traffic coming from?"&lt;/li&gt;
&lt;li&gt;"Show me a daily breakdown of visitors this month"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude will run a script behind the scenes, pull your real Google Analytics data, and give you the answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You'll Need Before Starting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Google Analytics 4 (GA4) property (the newer version of Google Analytics)&lt;/li&gt;
&lt;li&gt;A Google account with access to that GA4 property&lt;/li&gt;
&lt;li&gt;Claude Code installed (either the VS Code extension or the CLI)&lt;/li&gt;
&lt;li&gt;Python installed on your computer&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 1: Set Up Google Cloud
&lt;/h2&gt;

&lt;p&gt;Google Analytics data is accessed through Google's cloud platform. You need to create a "service account" - think of it as a robot employee that has read-only access to your analytics data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Go to Google Cloud Console
&lt;/h3&gt;

&lt;p&gt;Open your browser and go to: &lt;strong&gt;&lt;a href="https://console.cloud.google.com" rel="noopener noreferrer"&gt;https://console.cloud.google.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sign in with the same Google account that has access to your Google Analytics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create a Project (or select an existing one)
&lt;/h3&gt;

&lt;p&gt;If you've never used Google Cloud before:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click the project dropdown at the top of the page (it might say "Select a project")&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New Project&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name (something like "My Analytics" or your business name)&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Wait a moment, then select your new project from the dropdown&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you already have a project, just make sure it's selected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Enable the Google Analytics API
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the search bar at the top of Google Cloud Console, type: &lt;strong&gt;Google Analytics Data API&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;Google Analytics Data API&lt;/strong&gt; in the results&lt;/li&gt;
&lt;li&gt;Click the big blue &lt;strong&gt;Enable&lt;/strong&gt; button&lt;/li&gt;
&lt;li&gt;Wait for it to activate (takes a few seconds)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 4: Create a Service Account
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the left sidebar, click &lt;strong&gt;IAM &amp;amp; Admin&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Service Accounts&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+ Create Service Account&lt;/strong&gt; at the top&lt;/li&gt;
&lt;li&gt;Fill in the details:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service account name&lt;/strong&gt;: &lt;code&gt;claude-ga4-reader&lt;/code&gt; (or whatever you like)&lt;/li&gt;
&lt;li&gt;The email will auto-generate - it will look something like: &lt;code&gt;claude-ga4-reader@your-project.iam.gserviceaccount.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create and Continue&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Skip the "Grant this service account access" step - click &lt;strong&gt;Continue&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Skip the "Grant users access" step - click &lt;strong&gt;Done&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 5: Download the Credentials Key
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;You should now see your new service account in the list - click on it&lt;/li&gt;
&lt;li&gt;Go to the &lt;strong&gt;Keys&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Key&lt;/strong&gt; → &lt;strong&gt;Create new key&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;JSON&lt;/strong&gt; and click &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;.json&lt;/code&gt; file will download to your computer - &lt;strong&gt;this is important, don't lose it&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Move this file somewhere safe and permanent on your computer (for example: &lt;code&gt;C:\Users\YourName\keys\&lt;/code&gt; or your Desktop)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Write down the full file path&lt;/strong&gt; - you'll need it later. It will look something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\YourName\Downloads\your-project-name-abc123.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also &lt;strong&gt;write down the service account email&lt;/strong&gt; from step 4 - you'll need it in the next section.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Give the Service Account Access to Your Analytics
&lt;/h2&gt;

&lt;p&gt;The service account exists, but it doesn't have permission to see your analytics data yet. You need to invite it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Open Google Analytics
&lt;/h3&gt;

&lt;p&gt;Go to: &lt;strong&gt;&lt;a href="https://analytics.google.com" rel="noopener noreferrer"&gt;https://analytics.google.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Add the Service Account as a Viewer
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;gear icon&lt;/strong&gt; (⚙️) in the bottom-left corner to open Admin&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Property&lt;/strong&gt; column (the middle column), click &lt;strong&gt;Property Access Management&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;+&lt;/strong&gt; button in the top-right → &lt;strong&gt;Add users&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;In the email field, paste your &lt;strong&gt;service account email&lt;/strong&gt; (the one that looks like &lt;code&gt;claude-ga4-reader@your-project.iam.gserviceaccount.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Set the role to &lt;strong&gt;Viewer&lt;/strong&gt; (this is read-only — it can't change anything)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncheck&lt;/strong&gt; "Notify new users by email" (it's not a real email address)&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 8: Find Your GA4 Property ID
&lt;/h3&gt;

&lt;p&gt;While you're still in the Admin area:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;strong&gt;Property&lt;/strong&gt; column, click &lt;strong&gt;Property Settings&lt;/strong&gt; (or &lt;strong&gt;Property details&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Look for &lt;strong&gt;Property ID&lt;/strong&gt; - it's a number like &lt;code&gt;363186564&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write this number down&lt;/strong&gt; - you'll need it soon&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Part 3: Install the Python Package
&lt;/h2&gt;

&lt;p&gt;The skill uses a Python library to talk to Google's servers. You need to install it once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 9: Open a Terminal
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On Windows&lt;/strong&gt;: Press &lt;code&gt;Win + R&lt;/code&gt;, type &lt;code&gt;cmd&lt;/code&gt;, press Enter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On Mac&lt;/strong&gt;: Open the Terminal app (search for "Terminal" in Spotlight)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 10: Install the Package
&lt;/h3&gt;

&lt;p&gt;Type this command and press Enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install google-analytics-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for it to finish. You should see "Successfully installed" at the end.&lt;/p&gt;

&lt;p&gt;If you get an error about &lt;code&gt;pip&lt;/code&gt; not being found, try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip3 install google-analytics-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 4: Create the Skill Files
&lt;/h2&gt;

&lt;p&gt;A Claude Code skill is just a folder with specific files in it. You need three files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 11: Create the Skill Folder
&lt;/h3&gt;

&lt;p&gt;Navigate to your Claude configuration folder and create the skill directory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On Windows&lt;/strong&gt;: &lt;code&gt;C:\Users\YourName\.claude\skills\google-analytics\&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On Mac/Linux&lt;/strong&gt;: &lt;code&gt;~/.claude/skills/google-analytics/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the &lt;code&gt;skills&lt;/code&gt; folder doesn't exist yet, create it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 12: Create SKILL.md
&lt;/h3&gt;

&lt;p&gt;Inside the &lt;code&gt;google-analytics&lt;/code&gt; folder, create a file called &lt;code&gt;SKILL.md&lt;/code&gt; and paste the following content. This file tells Claude what the skill does and how to use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-analytics&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Query Google Analytics 4 data. Use when the user asks about website traffic, page views, sessions, user counts, conversions, top pages, traffic sources, or any analytics/metrics questions. Trigger on keywords like "analytics", "traffic", "visitors", "page views", "sessions", "GA4", "bounce rate", "conversions", "top pages", "referrals".&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Google Analytics 4 Skill&lt;/span&gt;

Query GA4 property data using the Google Analytics Data API v1.

&lt;span class="gu"&gt;## Setup&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Credentials**&lt;/span&gt;: Service account JSON key at &lt;span class="sb"&gt;`YOUR_CREDENTIALS_PATH_HERE`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Property ID**&lt;/span&gt;: &lt;span class="sb"&gt;`YOUR_PROPERTY_ID_HERE`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Python dependency**&lt;/span&gt;: &lt;span class="sb"&gt;`google-analytics-data`&lt;/span&gt; (install if needed: &lt;span class="sb"&gt;`pip install google-analytics-data`&lt;/span&gt;)

&lt;span class="gu"&gt;## How to Use&lt;/span&gt;

Run the query script from this skill's directory:

&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python PATH_TO_SKILL/ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; &amp;lt;report_type&amp;gt; &lt;span class="o"&gt;[&lt;/span&gt;options]
&lt;span class="p"&gt;~~~&lt;/span&gt;

&lt;span class="gu"&gt;## Available Reports&lt;/span&gt;

&lt;span class="gu"&gt;### 1. `overview` - High-level summary&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; overview &lt;span class="nt"&gt;--days&lt;/span&gt; 30
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: total users, sessions, page views, avg session duration, bounce rate, new vs returning users.

&lt;span class="gu"&gt;### 2. `pages` - Top pages by views&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; pages &lt;span class="nt"&gt;--days&lt;/span&gt; 30 &lt;span class="nt"&gt;--limit&lt;/span&gt; 20
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: page path, title, views, users, avg engagement time.

&lt;span class="gu"&gt;### 3. `sources` - Traffic sources&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; sources &lt;span class="nt"&gt;--days&lt;/span&gt; 30 &lt;span class="nt"&gt;--limit&lt;/span&gt; 20
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: source, medium, sessions, users, conversions.

&lt;span class="gu"&gt;### 4. `countries` - Geographic breakdown&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; countries &lt;span class="nt"&gt;--days&lt;/span&gt; 30 &lt;span class="nt"&gt;--limit&lt;/span&gt; 20
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: country, sessions, users, engagement rate.

&lt;span class="gu"&gt;### 5. `devices` - Device category breakdown&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; devices &lt;span class="nt"&gt;--days&lt;/span&gt; 30
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: device category (desktop/mobile/tablet), sessions, users.

&lt;span class="gu"&gt;### 6. `daily` - Day-by-day trend&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; daily &lt;span class="nt"&gt;--days&lt;/span&gt; 30
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: date, users, sessions, page views per day.

&lt;span class="gu"&gt;### 7. `realtime` - Active users right now&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; realtime
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: active users in last 30 minutes by source.

&lt;span class="gu"&gt;### 8. `custom` - Custom query (advanced)&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; custom &lt;span class="nt"&gt;--metrics&lt;/span&gt; &lt;span class="s2"&gt;"sessions,totalUsers"&lt;/span&gt; &lt;span class="nt"&gt;--dimensions&lt;/span&gt; &lt;span class="s2"&gt;"city"&lt;/span&gt; &lt;span class="nt"&gt;--days&lt;/span&gt; 7 &lt;span class="nt"&gt;--limit&lt;/span&gt; 10
&lt;span class="p"&gt;~~~&lt;/span&gt;
Pass any valid GA4 API metric/dimension names as comma-separated values.

&lt;span class="gu"&gt;## Common Options&lt;/span&gt;

| Option | Default | Description |
|--------|---------|-------------|
| &lt;span class="sb"&gt;`--days`&lt;/span&gt; | &lt;span class="sb"&gt;`30`&lt;/span&gt; | Lookback period in days |
| &lt;span class="sb"&gt;`--limit`&lt;/span&gt; | &lt;span class="sb"&gt;`10`&lt;/span&gt; | Max rows returned |
| &lt;span class="sb"&gt;`--start`&lt;/span&gt; | — | Explicit start date (YYYY-MM-DD), overrides --days |
| &lt;span class="sb"&gt;`--end`&lt;/span&gt; | — | Explicit end date (YYYY-MM-DD), defaults to today |
| &lt;span class="sb"&gt;`--output`&lt;/span&gt; | &lt;span class="sb"&gt;`table`&lt;/span&gt; | Output format: &lt;span class="sb"&gt;`table`&lt;/span&gt;, &lt;span class="sb"&gt;`json`&lt;/span&gt;, or &lt;span class="sb"&gt;`csv`&lt;/span&gt; |

&lt;span class="gu"&gt;## GA4 Metric and Dimension Reference (for custom queries)&lt;/span&gt;

&lt;span class="gs"&gt;**Common Metrics**&lt;/span&gt;: totalUsers, newUsers, sessions, screenPageViews, averageSessionDuration, bounceRate, engagementRate, conversions, eventCount, activeUsers

&lt;span class="gs"&gt;**Common Dimensions**&lt;/span&gt;: date, pagePath, pageTitle, sessionSource, sessionMedium, country, city, deviceCategory, browser, operatingSystem, landingPage, sessionDefaultChannelGroup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;IMPORTANT - Replace the placeholders:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;YOUR_CREDENTIALS_PATH_HERE&lt;/code&gt; with the full path to your JSON key file from Step 5&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;YOUR_PROPERTY_ID_HERE&lt;/code&gt; with your GA4 Property ID from Step 8&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;PATH_TO_SKILL&lt;/code&gt; with the full path to your skill folder&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 13: Create ga_query.py
&lt;/h3&gt;

&lt;p&gt;In the same &lt;code&gt;google-analytics&lt;/code&gt; folder, create a file called &lt;code&gt;ga_query.py&lt;/code&gt; and paste the following Python script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before pasting&lt;/strong&gt;, you need to update two values at the top of the file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CREDENTIALS_PATH&lt;/code&gt; - the full path to your JSON key file from Step 5&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PROPERTY_ID&lt;/code&gt; - your GA4 Property ID from Step 8
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Google Analytics 4 Data API query tool.
Queries GA4 property data using a service account.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="c1"&gt;# ============================================================
# CONFIGURATION - UPDATE THESE TWO VALUES
# ============================================================
&lt;/span&gt;&lt;span class="n"&gt;CREDENTIALS_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C:\Users\YourName\path\to\your-credentials.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;PROPERTY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;000000000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# ============================================================
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_client&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create and return a GA4 BetaAnalyticsDataClient.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CREDENTIALS_PATH&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.analytics.data_v1beta&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BetaAnalyticsDataClient&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;BetaAnalyticsDataClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Build a RunReportRequest.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.analytics.data_v1beta.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;RunReportRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dimension&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderBy&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;end_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;start_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;start_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RunReportRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROPERTY_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Dimension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="n"&gt;date_ranges&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;DateRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
        &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_bys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nc"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MetricOrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metric_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Format the API response into the desired output format.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimension_headers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric_headers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimension_values&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;mv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric_values&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# table
&lt;/span&gt;        &lt;span class="n"&gt;col_widths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;col_widths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col_widths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

        &lt;span class="n"&gt;separator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;col_widths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;header_row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_widths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header_row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_widths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Total rows: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_overview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;High-level site overview.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;newUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screenPageViews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;averageSessionDuration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagementRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bounceRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Top pages by views.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screenPageViews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;averageSessionDuration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pagePath&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pageTitle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screenPageViews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_sources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Traffic sources.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagementRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessionSource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessionMedium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_countries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Geographic breakdown.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagementRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_devices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Device category breakdown.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagementRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deviceCategory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_daily&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Day-by-day trend.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screenPageViews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.analytics.data_v1beta.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OrderBy&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_bys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimension&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DimensionOrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimension_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_realtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Realtime active users.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.analytics.data_v1beta.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;RunRealtimeReportRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dimension&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RunRealtimeReportRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROPERTY_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;activeUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Dimension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unifiedScreenName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
        &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_realtime_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimension_headers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric_headers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimension_values&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;mv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric_values&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No active users right now.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;col_widths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;col_widths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col_widths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;separator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;col_widths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;header_row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_widths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header_row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_widths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_custom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Custom query with user-specified metrics and dimensions.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: --metrics required for custom report (comma-separated)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;REPORTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_overview&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_sources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_countries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_devices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_daily&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;realtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_realtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;custom&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_custom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Query Google Analytics 4 data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REPORTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Report type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lookback period in days (default: 30)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Start date (YYYY-MM-DD), overrides --days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;End date (YYYY-MM-DD), defaults to today&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Max rows (default: 10)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Output format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Comma-separated metrics (for custom report)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Comma-separated dimensions (for custom report)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REPORTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 14: Create requirements.txt
&lt;/h3&gt;

&lt;p&gt;In the same folder, create a file called &lt;code&gt;requirements.txt&lt;/code&gt; with just this one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;google-analytics-data&amp;gt;=0.18.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file is just for reference - it documents what Python package the skill needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: Verify Your Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 15: Check Your Folder Structure
&lt;/h3&gt;

&lt;p&gt;Your skill folder should now look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.claude/
  skills/
    google-analytics/
      SKILL.md
      ga_query.py
      requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows, the full path would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\YourName\.claude\skills\google-analytics\SKILL.md
C:\Users\YourName\.claude\skills\google-analytics\ga_query.py
C:\Users\YourName\.claude\skills\google-analytics\requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 16: Test the Script Manually (Optional but Recommended)
&lt;/h3&gt;

&lt;p&gt;Before trying it in Claude Code, you can test the script directly to make sure everything is connected:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a terminal&lt;/li&gt;
&lt;li&gt;Navigate to the skill folder:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   cd C:\Users\YourName\.claude\skills\google-analytics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Run:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   python ga_query.py --report overview --days 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is set up correctly, you should see a table with your analytics data. If you get an error, check that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The credentials path in &lt;code&gt;ga_query.py&lt;/code&gt; is correct&lt;/li&gt;
&lt;li&gt;The property ID is correct&lt;/li&gt;
&lt;li&gt;You added the service account email as a Viewer in Google Analytics (Step 7)&lt;/li&gt;
&lt;li&gt;You installed the Python package (Step 10)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 17: Test in Claude Code
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open Claude Code (restart it if it was already running)&lt;/li&gt;
&lt;li&gt;Ask something like: &lt;strong&gt;"How much traffic did my site get in the last 7 days?"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Claude should recognize the analytics-related question, load the skill, and run the script&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If Claude doesn't pick it up automatically, try being explicit: &lt;strong&gt;"Use the google-analytics skill to show me a traffic overview for the last 30 days."&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"ModuleNotFoundError: No module named 'google'"&lt;/strong&gt;&lt;br&gt;
→ The Python package isn't installed. Run: &lt;code&gt;pip install google-analytics-data&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Permission denied" or "403" errors&lt;/strong&gt;&lt;br&gt;
→ The service account doesn't have access to your GA4 property. Go back to Step 7 and make sure you added the service account email as a Viewer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"File not found" error for credentials&lt;/strong&gt;&lt;br&gt;
→ The path to your JSON key file is wrong in &lt;code&gt;ga_query.py&lt;/code&gt;. Double-check the &lt;code&gt;CREDENTIALS_PATH&lt;/code&gt; value. On Windows, use a raw string: &lt;code&gt;r"C:\Users\..."&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"API not enabled" error&lt;/strong&gt;&lt;br&gt;
→ The Google Analytics Data API isn't turned on. Go back to Step 3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code doesn't use the skill&lt;/strong&gt;&lt;br&gt;
→ Try invoking it directly with &lt;code&gt;/google-analytics&lt;/code&gt;. If that doesn't work, add this line to your global CLAUDE.md file (at &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;): "When asked about analytics or website traffic, use the google-analytics skill."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Property not found" error&lt;/strong&gt;&lt;br&gt;
→ Double-check your Property ID. It should be just the number (like &lt;code&gt;363186564&lt;/code&gt;), not the full "properties/363186564" string.&lt;/p&gt;




&lt;h2&gt;
  
  
  Important Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This skill only works in &lt;strong&gt;local&lt;/strong&gt; Claude Code (VS Code extension or terminal CLI). It does &lt;strong&gt;not&lt;/strong&gt; work in the browser-based Claude Code on claude.ai, because that runs in a cloud sandbox without access to your local files.&lt;/li&gt;
&lt;li&gt;The service account has &lt;strong&gt;read-only&lt;/strong&gt; access — it cannot modify your Google Analytics settings.&lt;/li&gt;
&lt;li&gt;Keep your credentials JSON file safe. Don't share it or commit it to a public code repository.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;pip install&lt;/code&gt; only needs to be done once. The package stays installed on your computer.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>python</category>
      <category>googleanalytics</category>
    </item>
    <item>
      <title>Context Mesh Lite: Hybrid Vector Search + SQL Search + Graph Search Fused (for Super Accurate RAG)</title>
      <dc:creator>Anthony Lee</dc:creator>
      <pubDate>Tue, 23 Dec 2025 23:39:10 +0000</pubDate>
      <link>https://dev.to/anthony_lee_63e96408d7573/context-mesh-lite-hybrid-vector-search-sql-search-graph-search-fused-for-super-accurate-rag-25kn</link>
      <guid>https://dev.to/anthony_lee_63e96408d7573/context-mesh-lite-hybrid-vector-search-sql-search-graph-search-fused-for-super-accurate-rag-25kn</guid>
      <description>&lt;p&gt;I spent WAYYY too long trying to build a more accurate RAG retrieval system.&lt;br&gt;&lt;br&gt;
With Context Mesh Lite, I managed to combine hybrid vector search with SQL search (agentic text-to-sql) with graph search (shallow graph using dependent tables).&lt;/p&gt;

&lt;p&gt;The results were a significantly more accurate (albeit slower) RAG system.&lt;/p&gt;

&lt;p&gt;How does it work?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL Functions do most of the heavy lifting, creating tables and table dependencies.&lt;/li&gt;
&lt;li&gt;Then Edge Functions call Gemini (embeddings 001 and 2.5 flash) to create vector embeddings and graph entity/predicate extraction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;REQUIREMENTS: This system was built to exist within a Supabase instance. It also requires a Gemini API key (set in your Edge Functions window).&lt;/p&gt;

&lt;p&gt;I also connected the system to n8n workflows and it works like a charm. Anyway, I'm gonna give it to you. Maybe it'll be useful. Maybe you can improve on it.&lt;/p&gt;

&lt;p&gt;So, first, go to your Supabase (the entire end-to-end system exists there...only the interface for document upsert and chat are external).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;. Go to the SQL editor and paste this master query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
    -- ===============================================================
    -- CONTEXT MESH V9.0: GOLDEN MASTER (COMPOSITE RETRIEVAL)
    -- UPDATED: Nov 25, 2025
    -- ===============================================================
    -- PART 1: &amp;nbsp;EXTENSIONS
    -- PART 2: &amp;nbsp;STORAGE CONFIGURATION
    -- PART 3: &amp;nbsp;CORE TABLES (Docs, Graph, Queue, Config, Registry)
    -- PART 4: &amp;nbsp;INDEXES
    -- PART 5: &amp;nbsp;HELPER FUNCTIONS &amp;amp; TRIGGERS
    -- PART 6: &amp;nbsp;INGESTION FUNCTIONS (Universal V8 Logic)
    -- PART 7: &amp;nbsp;SEARCH FUNCTIONS (V9: FTS, Enrichment, Detection, Peek)
    -- PART 8: &amp;nbsp;CONFIGURATION LOGIC (V9 Weights)
    -- PART 9: &amp;nbsp;GRAPH CONTEXT RERANKER
    -- PART 10: WORKER SETUP
    -- PART 11: PERMISSIONS &amp;amp; REPAIRS
    -- ===============================================================


    -- ===============================================================
    -- PART 1: EXTENSIONS
    -- ===============================================================
    CREATE EXTENSION IF NOT EXISTS vector;
    CREATE EXTENSION IF NOT EXISTS pg_trgm;
    CREATE EXTENSION IF NOT EXISTS pg_net; -- Required for Async Worker


    -- ===============================================================
    -- PART 2: STORAGE CONFIGURATION
    -- ===============================================================
    INSERT INTO storage.buckets (id, name, public)
    VALUES ('raw_uploads', 'raw_uploads', false)
    ON CONFLICT (id) DO NOTHING;


    DROP POLICY IF EXISTS "Service Role Full Access" ON storage.objects;


    CREATE POLICY "Service Role Full Access"
    ON storage.objects FOR ALL
    USING ( auth.role() = 'service_role' )
    WITH CHECK ( auth.role() = 'service_role' );


    -- ===============================================================
    -- PART 3: CORE TABLES
    -- ===============================================================


    -- 3a. The Async Queue
    CREATE TABLE IF NOT EXISTS public.ingestion_queue (
    &amp;nbsp; &amp;nbsp; id uuid default gen_random_uuid() primary key,
    &amp;nbsp; &amp;nbsp; uri text not null,
    &amp;nbsp; &amp;nbsp; title text not null,
    &amp;nbsp; &amp;nbsp; chunk_index int not null,
    &amp;nbsp; &amp;nbsp; chunk_text text not null,
    &amp;nbsp; &amp;nbsp; status text default 'pending',
    &amp;nbsp; &amp;nbsp; error_log text,
    &amp;nbsp; &amp;nbsp; created_at timestamptz default now()
    );


    -- 3b. RAG Tables
    CREATE TABLE IF NOT EXISTS public.document (
    &amp;nbsp; &amp;nbsp; id &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; BIGSERIAL PRIMARY KEY,
    &amp;nbsp; &amp;nbsp; uri &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;TEXT NOT NULL UNIQUE,
    &amp;nbsp; &amp;nbsp; title &amp;nbsp; &amp;nbsp; &amp;nbsp;TEXT NOT NULL,
    &amp;nbsp; &amp;nbsp; doc_type &amp;nbsp; TEXT NOT NULL DEFAULT 'document',
    &amp;nbsp; &amp;nbsp; meta &amp;nbsp; &amp;nbsp; &amp;nbsp; JSONB NOT NULL DEFAULT '{}'::jsonb,
    &amp;nbsp; &amp;nbsp; created_at TIMESTAMPTZ NOT NULL DEFAULT now()
    );


    CREATE TABLE IF NOT EXISTS public.chunk (
    &amp;nbsp; &amp;nbsp; id &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;BIGSERIAL PRIMARY KEY,
    &amp;nbsp; &amp;nbsp; document_id BIGINT NOT NULL REFERENCES public.document(id) ON DELETE CASCADE,
    &amp;nbsp; &amp;nbsp; ordinal &amp;nbsp; &amp;nbsp; INT &amp;nbsp; &amp;nbsp;NOT NULL,
    &amp;nbsp; &amp;nbsp; text &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;TEXT &amp;nbsp; NOT NULL,
    &amp;nbsp; &amp;nbsp; tsv &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; TSVECTOR,
    &amp;nbsp; &amp;nbsp; UNIQUE (document_id, ordinal)
    );


    -- PERFORMANCE: Using halfvec(768) for Gemini embeddings
    CREATE TABLE IF NOT EXISTS public.chunk_embedding (
    &amp;nbsp; &amp;nbsp; chunk_id &amp;nbsp;BIGINT PRIMARY KEY REFERENCES public.chunk(id) ON DELETE CASCADE,
    &amp;nbsp; &amp;nbsp; embedding halfvec(768) NOT NULL 
    );


    -- 3c. Graph Tables
    CREATE TABLE IF NOT EXISTS public.node (
    &amp;nbsp; &amp;nbsp; id &amp;nbsp; &amp;nbsp; BIGSERIAL PRIMARY KEY,
    &amp;nbsp; &amp;nbsp; key &amp;nbsp; &amp;nbsp;TEXT UNIQUE NOT NULL,
    &amp;nbsp; &amp;nbsp; labels TEXT[] NOT NULL DEFAULT '{}',
    &amp;nbsp; &amp;nbsp; props &amp;nbsp;JSONB &amp;nbsp;NOT NULL DEFAULT '{}'::jsonb
    );


    CREATE TABLE IF NOT EXISTS public.edge (
    &amp;nbsp; &amp;nbsp; src &amp;nbsp; BIGINT NOT NULL REFERENCES public.node(id) ON DELETE CASCADE,
    &amp;nbsp; &amp;nbsp; dst &amp;nbsp; BIGINT NOT NULL REFERENCES public.node(id) ON DELETE CASCADE,
    &amp;nbsp; &amp;nbsp; type &amp;nbsp;TEXT &amp;nbsp; NOT NULL,
    &amp;nbsp; &amp;nbsp; props JSONB &amp;nbsp;NOT NULL DEFAULT '{}'::jsonb,
    &amp;nbsp; &amp;nbsp; PRIMARY KEY (src, dst, type)
    );


    CREATE TABLE IF NOT EXISTS public.chunk_node (
    &amp;nbsp; &amp;nbsp; chunk_id BIGINT NOT NULL REFERENCES public.chunk(id) ON DELETE CASCADE,
    &amp;nbsp; &amp;nbsp; node_id &amp;nbsp;BIGINT NOT NULL REFERENCES public.node(id) &amp;nbsp;ON DELETE CASCADE,
    &amp;nbsp; &amp;nbsp; rel &amp;nbsp; &amp;nbsp; &amp;nbsp;TEXT &amp;nbsp; NOT NULL DEFAULT 'MENTIONS',
    &amp;nbsp; &amp;nbsp; PRIMARY KEY (chunk_id, node_id, rel)
    );


    -- 3d. Structured Data Registry (V8 Updated)
    CREATE TABLE IF NOT EXISTS public.structured_table (
    &amp;nbsp; &amp;nbsp; id &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;BIGSERIAL PRIMARY KEY,
    &amp;nbsp; &amp;nbsp; table_name &amp;nbsp; &amp;nbsp;TEXT NOT NULL UNIQUE,
    &amp;nbsp; &amp;nbsp; document_id &amp;nbsp; BIGINT REFERENCES public.document(id) ON DELETE CASCADE,
    &amp;nbsp; &amp;nbsp; schema_def &amp;nbsp; &amp;nbsp;JSONB NOT NULL,
    &amp;nbsp; &amp;nbsp; row_count &amp;nbsp; &amp;nbsp; INT DEFAULT 0,
    &amp;nbsp; &amp;nbsp; -- V8 Metadata Columns
    &amp;nbsp; &amp;nbsp; description &amp;nbsp; TEXT,
    &amp;nbsp; &amp;nbsp; column_semantics JSONB DEFAULT '{}'::jsonb,
    &amp;nbsp; &amp;nbsp; graph_hints &amp;nbsp; JSONB DEFAULT '[]'::jsonb,
    &amp;nbsp; &amp;nbsp; sample_row &amp;nbsp; &amp;nbsp;JSONB,
    &amp;nbsp; &amp;nbsp; created_at &amp;nbsp; &amp;nbsp;TIMESTAMPTZ NOT NULL DEFAULT now(),
    &amp;nbsp; &amp;nbsp; updated_at &amp;nbsp; &amp;nbsp;TIMESTAMPTZ NOT NULL DEFAULT now()
    );


    -- 3e. Configuration Table
    CREATE TABLE IF NOT EXISTS public.app_config (
    &amp;nbsp; &amp;nbsp; id INT PRIMARY KEY DEFAULT 1,
    &amp;nbsp; &amp;nbsp; settings JSONB NOT NULL,
    &amp;nbsp; &amp;nbsp; updated_at TIMESTAMPTZ DEFAULT now(),
    &amp;nbsp; &amp;nbsp; CONSTRAINT single_row CHECK (id = 1)
    );


    -- ===============================================================
    -- PART 4: INDEXES
    -- ===============================================================
    CREATE INDEX IF NOT EXISTS idx_queue_status ON public.ingestion_queue(status);
    CREATE INDEX IF NOT EXISTS document_type_idx ON public.document(doc_type);
    CREATE INDEX IF NOT EXISTS document_uri_idx ON public.document(uri);
    CREATE INDEX IF NOT EXISTS chunk_tsv_gin ON public.chunk USING GIN (tsv);
    CREATE INDEX IF NOT EXISTS chunk_doc_idx ON public.chunk(document_id);


    -- Embedding Index (HNSW)
    CREATE INDEX IF NOT EXISTS emb_hnsw_cos ON public.chunk_embedding USING HNSW (embedding halfvec_cosine_ops);


    -- Graph Indexes
    CREATE INDEX IF NOT EXISTS edge_src_idx ON public.edge (src);
    CREATE INDEX IF NOT EXISTS edge_dst_idx ON public.edge (dst);
    CREATE INDEX IF NOT EXISTS node_labels_gin ON public.node USING GIN (labels);
    CREATE INDEX IF NOT EXISTS node_props_gin ON public.node USING GIN (props);
    CREATE INDEX IF NOT EXISTS chunknode_node_idx ON public.chunk_node (node_id);
    CREATE INDEX IF NOT EXISTS chunknode_chunk_idx ON public.chunk_node (chunk_id);


    -- Registry Index
    CREATE INDEX IF NOT EXISTS idx_structured_table_active ON public.structured_table(table_name) WHERE row_count &amp;gt; 0;


    -- ===============================================================
    -- PART 5: HELPER FUNCTIONS &amp;amp; TRIGGERS
    -- ===============================================================


    -- 5a. Full Text Search Update Trigger
    CREATE OR REPLACE FUNCTION public.chunk_tsv_update()
    RETURNS trigger LANGUAGE plpgsql AS $$
    DECLARE doc_title text;
    BEGIN
    &amp;nbsp; SELECT d.title INTO doc_title FROM public.document d WHERE d.id = NEW.document_id;
    &amp;nbsp; NEW.tsv := 
    &amp;nbsp; &amp;nbsp; setweight(to_tsvector('english', coalesce(doc_title, '')), 'D') || 
    &amp;nbsp; &amp;nbsp; setweight(to_tsvector('english', coalesce(NEW.text, '')), 'A');
    &amp;nbsp; RETURN NEW;
    END $$;


    DROP TRIGGER IF EXISTS chunk_tsv_trg ON public.chunk;
    CREATE TRIGGER chunk_tsv_trg
    BEFORE INSERT OR UPDATE OF text, document_id ON public.chunk
    FOR EACH ROW EXECUTE FUNCTION public.chunk_tsv_update();


    -- 5b. Sanitize Table Names
    CREATE OR REPLACE FUNCTION public.sanitize_table_name(name TEXT)
    RETURNS TEXT LANGUAGE sql IMMUTABLE AS $$
    &amp;nbsp; SELECT 'tbl_' || regexp_replace(lower(trim(name)), '[^a-z0-9_]', '_', 'g');
    $$;


    -- 5c. Data Extraction Helpers
    CREATE OR REPLACE FUNCTION public.extract_numeric(text TEXT, key TEXT)
    RETURNS NUMERIC LANGUAGE sql IMMUTABLE AS $$
    &amp;nbsp; SELECT (regexp_match(text, key || '\s*:\s*\$?([0-9,]+\.?[0-9]*)', 'i'))[1]::text::numeric;
    $$;


    -- 5d. Polymorphic Date Extraction (Supports Excel, ISO, US formats)
    -- 1. Text
    CREATE OR REPLACE FUNCTION public.extract_date(text TEXT)
    RETURNS DATE LANGUAGE plpgsql IMMUTABLE AS $$
    BEGIN
    &amp;nbsp; IF text ~ '^\d{5}$' THEN RETURN '1899-12-30'::date + (text::int); END IF;
    &amp;nbsp; IF text ~ '\d{4}-\d{2}-\d{2}' THEN RETURN (regexp_match(text, '(\d{4}-\d{2}-\d{2})'))[1]::date; END IF;
    &amp;nbsp; IF text ~ '\d{1,2}/\d{1,2}/\d{4}' THEN RETURN to_date((regexp_match(text, '(\d{1,2}/\d{1,2}/\d{4})'))[1], 'MM/DD/YYYY'); END IF;
    &amp;nbsp; RETURN NULL;
    EXCEPTION WHEN OTHERS THEN RETURN NULL;
    END $$;


    -- 2. Numeric (Excel Serial)
    CREATE OR REPLACE FUNCTION public.extract_date(val NUMERIC)
    RETURNS DATE LANGUAGE plpgsql IMMUTABLE AS $$
    BEGIN RETURN '1899-12-30'::date + (val::int); EXCEPTION WHEN OTHERS THEN RETURN NULL; END $$;


    -- 3. Integer
    CREATE OR REPLACE FUNCTION public.extract_date(val INTEGER)
    RETURNS DATE LANGUAGE plpgsql IMMUTABLE AS $$
    BEGIN RETURN '1899-12-30'::date + val; EXCEPTION WHEN OTHERS THEN RETURN NULL; END $$;


    -- 4. Date (Pass-through)
    CREATE OR REPLACE FUNCTION public.extract_date(val DATE)
    RETURNS DATE LANGUAGE sql IMMUTABLE AS $$ SELECT val; $$;


    CREATE OR REPLACE FUNCTION public.extract_keywords(p_text TEXT)
    RETURNS TEXT[] LANGUAGE sql IMMUTABLE AS $$
    &amp;nbsp; SELECT array_agg(DISTINCT word) FROM (
    &amp;nbsp; &amp;nbsp; SELECT unnest(tsvector_to_array(to_tsvector('english', p_text))) AS word
    &amp;nbsp; ) words WHERE length(word) &amp;gt; 2 LIMIT 10;
    $$;


    -- 5e. Column Type Inference
    CREATE OR REPLACE FUNCTION public.infer_column_type(sample_values TEXT[])
    RETURNS TEXT LANGUAGE plpgsql IMMUTABLE AS $$
    DECLARE
    &amp;nbsp; val TEXT;
    &amp;nbsp; numeric_count INT := 0;
    &amp;nbsp; date_count INT := 0;
    &amp;nbsp; boolean_count INT := 0;
    &amp;nbsp; total_non_null INT := 0;
    BEGIN
    &amp;nbsp; FOR val IN SELECT unnest(sample_values) LOOP
    &amp;nbsp; &amp;nbsp; IF val IS NOT NULL AND val != '' THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; total_non_null := total_non_null + 1;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; IF lower(val) IN ('true', 'false', 'yes', 'no', 't', 'f', 'y', 'n', '1', '0') THEN boolean_count := boolean_count + 1; END IF;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; IF val ~ '\d+\s*x\s*\d+' THEN RETURN 'TEXT'; END IF;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; IF val ~ '\d+\s*(cm|mm|m|km|in|ft|yd|kg|g|mg|lb|oz|ml|l|gal)' THEN RETURN 'TEXT'; END IF;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; IF val ~ '^\$?-?[0-9,]+\.?[0-9]*$' THEN numeric_count := numeric_count + 1; END IF;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; IF val ~ '^\d{4}-\d{2}-\d{2}' OR val ~ '^\d{1,2}/\d{1,2}/\d{4}' OR val ~ '^\d{5}$' THEN date_count := date_count + 1; END IF;
    &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; END LOOP;
    &amp;nbsp; 
    &amp;nbsp; IF total_non_null = 0 THEN RETURN 'TEXT'; END IF;
    &amp;nbsp; IF boolean_count::float / total_non_null &amp;gt; 0.8 THEN RETURN 'BOOLEAN'; END IF;
    &amp;nbsp; IF numeric_count::float / total_non_null &amp;gt; 0.8 THEN RETURN 'NUMERIC'; END IF;
    &amp;nbsp; IF date_count::float / total_non_null &amp;gt; 0.8 THEN RETURN 'DATE'; END IF;
    &amp;nbsp; RETURN 'TEXT';
    END $$;


    -- ===============================================================
    -- PART 6: INGESTION FUNCTIONS (RPC)
    -- ===============================================================


    -- 6a. Document Ingest (Standard)
    CREATE OR REPLACE FUNCTION public.ingest_document_chunk(
    &amp;nbsp; p_uri TEXT, p_title TEXT, p_doc_meta JSONB,
    &amp;nbsp; p_chunk JSONB, p_nodes JSONB, p_edges JSONB, p_mentions JSONB
    )
    RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$
    DECLARE
    &amp;nbsp; v_doc_id BIGINT; v_chunk_id BIGINT; v_node JSONB; v_edge JSONB; v_mention JSONB;
    &amp;nbsp; v_src_id BIGINT; v_dst_id BIGINT;
    BEGIN
    &amp;nbsp; INSERT INTO public.document(uri, title, doc_type, meta)
    &amp;nbsp; VALUES (p_uri, p_title, 'document', COALESCE(p_doc_meta, '{}'::jsonb))
    &amp;nbsp; ON CONFLICT (uri) DO UPDATE SET title = EXCLUDED.title, meta = public.document.meta || EXCLUDED.meta
    &amp;nbsp; RETURNING id INTO v_doc_id;


    &amp;nbsp; INSERT INTO public.chunk(document_id, ordinal, text)
    &amp;nbsp; VALUES (v_doc_id, (p_chunk-&amp;gt;&amp;gt;'ordinal')::INT, p_chunk-&amp;gt;&amp;gt;'text')
    &amp;nbsp; ON CONFLICT (document_id, ordinal) DO UPDATE SET text = EXCLUDED.text
    &amp;nbsp; RETURNING id INTO v_chunk_id;


    &amp;nbsp; IF (p_chunk ? 'embedding') THEN
    &amp;nbsp; &amp;nbsp; INSERT INTO public.chunk_embedding(chunk_id, embedding)
    &amp;nbsp; &amp;nbsp; VALUES (v_chunk_id, (SELECT array_agg((e)::float4 ORDER BY ord) FROM jsonb_array_elements_text(p_chunk-&amp;gt;'embedding') WITH ORDINALITY t(e, ord))::halfvec(768))
    &amp;nbsp; &amp;nbsp; ON CONFLICT (chunk_id) DO UPDATE SET embedding = EXCLUDED.embedding;
    &amp;nbsp; END IF;


    &amp;nbsp; FOR v_node IN SELECT * FROM jsonb_array_elements(COALESCE(p_nodes, '[]'::jsonb)) LOOP
    &amp;nbsp; &amp;nbsp; INSERT INTO public.node(key, labels, props)
    &amp;nbsp; &amp;nbsp; VALUES (v_node-&amp;gt;&amp;gt;'key', COALESCE((SELECT array_agg(l::TEXT) FROM jsonb_array_elements_text(v_node-&amp;gt;'labels') l), '{}'), COALESCE(v_node-&amp;gt;'props', '{}'::jsonb))
    &amp;nbsp; &amp;nbsp; ON CONFLICT (key) DO UPDATE SET props = public.node.props || EXCLUDED.props;
    &amp;nbsp; END LOOP;


    &amp;nbsp; FOR v_edge IN SELECT * FROM jsonb_array_elements(COALESCE(p_edges, '[]'::jsonb)) LOOP
    &amp;nbsp; &amp;nbsp; SELECT id INTO v_src_id FROM public.node WHERE key = v_edge-&amp;gt;&amp;gt;'src_key';
    &amp;nbsp; &amp;nbsp; SELECT id INTO v_dst_id FROM public.node WHERE key = v_edge-&amp;gt;&amp;gt;'dst_key';
    &amp;nbsp; &amp;nbsp; IF v_src_id IS NOT NULL AND v_dst_id IS NOT NULL THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; INSERT INTO public.edge(src, dst, type, props)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; VALUES (v_src_id, v_dst_id, v_edge-&amp;gt;&amp;gt;'type', COALESCE(v_edge-&amp;gt;'props', '{}'::jsonb))
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ON CONFLICT (src, dst, type) DO UPDATE SET props = public.edge.props || EXCLUDED.props;
    &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; END LOOP;


    &amp;nbsp; FOR v_mention IN SELECT * FROM jsonb_array_elements(COALESCE(p_mentions, '[]'::jsonb)) LOOP
    &amp;nbsp; &amp;nbsp; SELECT id INTO v_src_id FROM public.node WHERE key = v_mention-&amp;gt;&amp;gt;'node_key';
    &amp;nbsp; &amp;nbsp; IF v_chunk_id IS NOT NULL AND v_src_id IS NOT NULL THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; INSERT INTO public.chunk_node(chunk_id, node_id, rel)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; VALUES (v_chunk_id, v_src_id, COALESCE(v_mention-&amp;gt;&amp;gt;'rel', 'MENTIONS'))
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ON CONFLICT (chunk_id, node_id, rel) DO NOTHING;
    &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; END LOOP;


    &amp;nbsp; RETURN jsonb_build_object('ok', true);
    END $$;


    -- 6b. Universal Spreadsheet Ingest (V8 Updated: Description Support)
    DROP FUNCTION IF EXISTS public.ingest_spreadsheet(text, text, text, jsonb, jsonb, jsonb, jsonb);


    CREATE OR REPLACE FUNCTION public.ingest_spreadsheet(
    &amp;nbsp; p_uri TEXT, p_title TEXT, p_table_name TEXT, 
    &amp;nbsp; p_description TEXT, -- V8 Addition
    &amp;nbsp; p_rows JSONB, p_schema JSONB, p_nodes JSONB, p_edges JSONB
    )
    RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$
    DECLARE
    &amp;nbsp; v_doc_id BIGINT; v_safe_name TEXT; v_col_name TEXT; v_inferred_type TEXT;
    &amp;nbsp; v_cols TEXT[]; v_sample_values TEXT[]; v_row JSONB; v_node JSONB; v_edge JSONB;
    &amp;nbsp; v_src_id BIGINT; v_dst_id BIGINT; v_table_exists BOOLEAN; v_all_columns TEXT[];
    &amp;nbsp; v_schema_def JSONB;
    BEGIN
    &amp;nbsp; INSERT INTO public.document(uri, title, doc_type, meta)
    &amp;nbsp; VALUES (p_uri, p_title, 'spreadsheet', jsonb_build_object('table_name', p_table_name))
    &amp;nbsp; ON CONFLICT (uri) DO UPDATE SET title = EXCLUDED.title RETURNING id INTO v_doc_id;


    &amp;nbsp; v_safe_name := public.sanitize_table_name(p_table_name);
    &amp;nbsp; SELECT EXISTS (SELECT FROM pg_tables WHERE schemaname = 'public' AND tablename = v_safe_name) INTO v_table_exists;


    &amp;nbsp; IF NOT v_table_exists THEN
    &amp;nbsp; &amp;nbsp; -- Table Creation Logic
    &amp;nbsp; &amp;nbsp; SELECT array_agg(DISTINCT key ORDER BY key) INTO v_all_columns FROM jsonb_array_elements(p_rows) AS r, jsonb_object_keys(r) AS key;
    &amp;nbsp; &amp;nbsp; v_cols := ARRAY['id BIGSERIAL PRIMARY KEY'];
    &amp;nbsp; &amp;nbsp; FOREACH v_col_name IN ARRAY v_all_columns LOOP
    &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT array_agg(kv.value::text) INTO v_sample_values FROM jsonb_array_elements(p_rows) r, jsonb_each_text(r) kv WHERE kv.key = v_col_name LIMIT 100;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; v_inferred_type := public.infer_column_type(COALESCE(v_sample_values, ARRAY[]::TEXT[]));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; v_cols := v_cols || format('%I %s', v_col_name, v_inferred_type);
    &amp;nbsp; &amp;nbsp; END LOOP;
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; EXECUTE format('CREATE TABLE public.%I (%s)', v_safe_name, array_to_string(v_cols, ', '));
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; -- Permissions
    &amp;nbsp; &amp;nbsp; EXECUTE format('GRANT ALL ON TABLE public.%I TO service_role', v_safe_name);
    &amp;nbsp; &amp;nbsp; EXECUTE format('GRANT SELECT ON TABLE public.%I TO authenticated, anon', v_safe_name);
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; SELECT jsonb_object_agg(col_name, 'TEXT') INTO v_schema_def FROM unnest(v_all_columns) AS col_name;
    &amp;nbsp; END IF;


    &amp;nbsp; -- Insert Rows
    &amp;nbsp; FOR v_row IN SELECT * FROM jsonb_array_elements(p_rows) LOOP
    &amp;nbsp; &amp;nbsp; DECLARE v_k TEXT; v_v TEXT; v_cl TEXT[] := ARRAY[]::TEXT[]; v_vl TEXT[] := ARRAY[]::TEXT[];
    &amp;nbsp; &amp;nbsp; BEGIN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; FOR v_k, v_v IN SELECT * FROM jsonb_each_text(v_row) LOOP v_cl := v_cl || quote_ident(v_k); v_vl := v_vl || quote_literal(v_v); END LOOP;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; IF array_length(v_cl, 1) &amp;gt; 0 THEN EXECUTE format('INSERT INTO public.%I (%s) VALUES (%s)', v_safe_name, array_to_string(v_cl, ', '), array_to_string(v_vl, ', ')); END IF;
    &amp;nbsp; &amp;nbsp; END;
    &amp;nbsp; END LOOP;


    &amp;nbsp; -- Upsert Registry (V8 with Description)
    &amp;nbsp; INSERT INTO public.structured_table(table_name, document_id, schema_def, row_count, description)
    &amp;nbsp; VALUES (v_safe_name, v_doc_id, COALESCE(v_schema_def, '{}'::jsonb), jsonb_array_length(p_rows), p_description)
    &amp;nbsp; ON CONFLICT (table_name) DO UPDATE SET updated_at = now(), description = EXCLUDED.description;


    &amp;nbsp; -- Upsert Graph Nodes
    &amp;nbsp; FOR v_node IN SELECT * FROM jsonb_array_elements(COALESCE(p_nodes, '[]'::jsonb)) LOOP
    &amp;nbsp; &amp;nbsp; INSERT INTO public.node(key, labels, props)
    &amp;nbsp; &amp;nbsp; VALUES (v_node-&amp;gt;&amp;gt;'key', COALESCE((SELECT array_agg(l::TEXT) FROM jsonb_array_elements_text(v_node-&amp;gt;'labels') l), '{}'), COALESCE(v_node-&amp;gt;'props', '{}'::jsonb))
    &amp;nbsp; &amp;nbsp; ON CONFLICT (key) DO UPDATE SET props = public.node.props || EXCLUDED.props;
    &amp;nbsp; END LOOP;


    &amp;nbsp; -- Upsert Graph Edges
    &amp;nbsp; FOR v_edge IN SELECT * FROM jsonb_array_elements(COALESCE(p_edges, '[]'::jsonb)) LOOP
    &amp;nbsp; &amp;nbsp; SELECT id INTO v_src_id FROM public.node WHERE key = v_edge-&amp;gt;&amp;gt;'src_key';
    &amp;nbsp; &amp;nbsp; SELECT id INTO v_dst_id FROM public.node WHERE key = v_edge-&amp;gt;&amp;gt;'dst_key';
    &amp;nbsp; &amp;nbsp; IF v_src_id IS NOT NULL AND v_dst_id IS NOT NULL THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; INSERT INTO public.edge(src, dst, type, props)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; VALUES (v_src_id, v_dst_id, v_edge-&amp;gt;&amp;gt;'type', COALESCE(v_edge-&amp;gt;'props', '{}'::jsonb))
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ON CONFLICT (src, dst, type) DO UPDATE SET props = public.edge.props || EXCLUDED.props;
    &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; END LOOP;


    &amp;nbsp; RETURN jsonb_build_object('ok', true, 'table_name', v_safe_name);
    END $$;


    -- ===============================================================
    -- PART 7: SEARCH FUNCTIONS (UPDATED V9 - COMPOSITE RETRIEVAL)
    -- ===============================================================


    -- 7a. Vector Search
    CREATE OR REPLACE FUNCTION public.search_vector(
    &amp;nbsp; &amp;nbsp; p_embedding VECTOR(768), 
    &amp;nbsp; &amp;nbsp; p_limit INT,
    &amp;nbsp; &amp;nbsp; p_threshold FLOAT8 DEFAULT 0.65
    )
    RETURNS TABLE(chunk_id BIGINT, content TEXT, score FLOAT8)
    LANGUAGE sql STABLE AS $$
    &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; ce.chunk_id, 
    &amp;nbsp; &amp;nbsp; c.text as content,
    &amp;nbsp; &amp;nbsp; 1.0 / (1.0 + (ce.embedding &amp;lt;=&amp;gt; p_embedding)) AS score
    &amp;nbsp; FROM public.chunk_embedding ce
    &amp;nbsp; JOIN public.chunk c ON c.id = ce.chunk_id
    &amp;nbsp; WHERE (1.0 / (1.0 + (ce.embedding &amp;lt;=&amp;gt; p_embedding))) &amp;gt;= p_threshold
    &amp;nbsp; ORDER BY score DESC
    &amp;nbsp; LIMIT p_limit;
    $$;


    -- 7b. Multi-Strategy Full-Text Search (V9)
    CREATE OR REPLACE FUNCTION public.search_fulltext(
    &amp;nbsp; p_query text, 
    &amp;nbsp; p_limit integer
    )
    RETURNS TABLE(
    &amp;nbsp; chunk_id bigint, 
    &amp;nbsp; content text, 
    &amp;nbsp; score double precision
    )
    LANGUAGE sql STABLE AS $$
    &amp;nbsp; WITH query_variants AS (
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; websearch_to_tsquery('english', p_query) AS tsq_websearch,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; plainto_tsquery('english', p_query) AS tsq_plain,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; to_tsquery('english', regexp_replace(p_query, '\s+', ' | ', 'g')) AS tsq_or
    &amp;nbsp; ),
    &amp;nbsp; results AS (
    &amp;nbsp; &amp;nbsp; -- Strategy 1: Websearch (most precise)
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; c.id AS chunk_id, 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; c.text AS content,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ts_rank_cd(c.tsv, q.tsq_websearch)::float8 AS score,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 1 as strategy
    &amp;nbsp; &amp;nbsp; FROM public.chunk c 
    &amp;nbsp; &amp;nbsp; CROSS JOIN query_variants q 
    &amp;nbsp; &amp;nbsp; WHERE c.tsv @@ q.tsq_websearch
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; UNION ALL
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; -- Strategy 2: Plain text (more flexible)
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; c.id AS chunk_id, 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; c.text AS content,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ts_rank_cd(c.tsv, q.tsq_plain)::float8 * 0.8 AS score,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 2 as strategy
    &amp;nbsp; &amp;nbsp; FROM public.chunk c 
    &amp;nbsp; &amp;nbsp; CROSS JOIN query_variants q 
    &amp;nbsp; &amp;nbsp; WHERE c.tsv @@ q.tsq_plain
    &amp;nbsp; &amp;nbsp; &amp;nbsp; AND NOT EXISTS (
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT 1 FROM public.chunk c2 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; CROSS JOIN query_variants q2
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE c2.id = c.id AND c2.tsv @@ q2.tsq_websearch
    &amp;nbsp; &amp;nbsp; &amp;nbsp; )
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; UNION ALL
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; -- Strategy 3: OR query (most flexible)
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; c.id AS chunk_id, 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; c.text AS content,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ts_rank_cd(c.tsv, q.tsq_or)::float8 * 0.6 AS score,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 3 as strategy
    &amp;nbsp; &amp;nbsp; FROM public.chunk c 
    &amp;nbsp; &amp;nbsp; CROSS JOIN query_variants q 
    &amp;nbsp; &amp;nbsp; WHERE c.tsv @@ q.tsq_or
    &amp;nbsp; &amp;nbsp; &amp;nbsp; AND NOT EXISTS (
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT 1 FROM public.chunk c2 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; CROSS JOIN query_variants q2
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE c2.id = c.id 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; AND (c2.tsv @@ q2.tsq_websearch OR c2.tsv @@ q2.tsq_plain)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; )
    &amp;nbsp; )
    &amp;nbsp; SELECT chunk_id, content, score
    &amp;nbsp; FROM results
    &amp;nbsp; ORDER BY score DESC
    &amp;nbsp; LIMIT p_limit;
    $$;
    GRANT EXECUTE ON FUNCTION public.search_fulltext TO anon, authenticated, service_role;


    -- 7c. Table Peeking (V9: GENERIC FK DETECTION + REVERSE LOOKUP)


    CREATE OR REPLACE FUNCTION public.get_table_context(p_table_name TEXT)
    RETURNS JSONB
    LANGUAGE plpgsql 
    SECURITY DEFINER 
    SET search_path = public, pg_temp
    AS $$
    DECLARE
    &amp;nbsp; v_safe_name TEXT;
    &amp;nbsp; v_columns TEXT;
    &amp;nbsp; v_sample_row JSONB;
    &amp;nbsp; v_description TEXT;
    &amp;nbsp; v_semantics JSONB;
    &amp;nbsp; v_categorical_values JSONB := '{}'::jsonb;
    &amp;nbsp; v_related_tables JSONB := '[]'::jsonb;
    &amp;nbsp; v_cat_col TEXT;
    &amp;nbsp; v_cat_values JSONB;
    &amp;nbsp; v_distinct_count INT;
    &amp;nbsp; v_fk_col TEXT;
    &amp;nbsp; v_ref_table TEXT;
    &amp;nbsp; v_ref_table_exists BOOLEAN;
    &amp;nbsp; v_join_col TEXT;
    &amp;nbsp; v_ref_has_id BOOLEAN;
    &amp;nbsp; v_ref_has_name BOOLEAN;
    &amp;nbsp; v_reverse_rec RECORD;
    BEGIN
    &amp;nbsp; v_safe_name := quote_ident(p_table_name);
    &amp;nbsp; 
    &amp;nbsp; -- Get schema
    &amp;nbsp; SELECT string_agg(column_name || ' (' || data_type || ')', ', ')
    &amp;nbsp; INTO v_columns
    &amp;nbsp; FROM information_schema.columns
    &amp;nbsp; WHERE table_name = p_table_name AND table_schema = 'public';


    &amp;nbsp; -- Get sample row
    &amp;nbsp; EXECUTE format('SELECT to_jsonb(t) FROM (SELECT * FROM public.%I LIMIT 1) t', v_safe_name)
    &amp;nbsp; INTO v_sample_row;
    &amp;nbsp; 
    &amp;nbsp; -- Get semantic metadata from structured_table
    &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; description,
    &amp;nbsp; &amp;nbsp; column_semantics
    &amp;nbsp; INTO v_description, v_semantics
    &amp;nbsp; FROM public.structured_table
    &amp;nbsp; WHERE table_name = p_table_name;
    &amp;nbsp; 
    &amp;nbsp; -- Get categorical values for columns with status/type/category in name
    &amp;nbsp; FOR v_cat_col IN 
    &amp;nbsp; &amp;nbsp; SELECT column_name
    &amp;nbsp; &amp;nbsp; FROM information_schema.columns
    &amp;nbsp; &amp;nbsp; WHERE table_schema = 'public' 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; AND table_name = p_table_name
    &amp;nbsp; &amp;nbsp; &amp;nbsp; AND (
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name LIKE '%status%' OR
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name LIKE '%type%' OR
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name LIKE '%category%' OR
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name LIKE '%state%' OR
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name LIKE '%priority%' OR
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name LIKE '%level%'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; )
    &amp;nbsp; LOOP
    &amp;nbsp; &amp;nbsp; BEGIN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Check if column has reasonable number of distinct values
    &amp;nbsp; &amp;nbsp; &amp;nbsp; EXECUTE format('SELECT COUNT(DISTINCT %I) FROM public.%I', v_cat_col, v_safe_name)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; INTO v_distinct_count;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Only include if 20 or fewer distinct values
    &amp;nbsp; &amp;nbsp; &amp;nbsp; IF v_distinct_count &amp;lt;= 20 THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; EXECUTE format('SELECT jsonb_agg(DISTINCT %I ORDER BY %I) FROM public.%I', 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; v_cat_col, v_cat_col, v_safe_name)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; INTO v_cat_values;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Add to categorical_values object
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; v_categorical_values := v_categorical_values || jsonb_build_object(v_cat_col, v_cat_values);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; EXCEPTION WHEN OTHERS THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Skip this column if any error
    &amp;nbsp; &amp;nbsp; &amp;nbsp; CONTINUE;
    &amp;nbsp; &amp;nbsp; END;
    &amp;nbsp; END LOOP;
    &amp;nbsp; 
    &amp;nbsp; -- ========================================================================
    &amp;nbsp; -- PART 1: FORWARD FK DETECTION (this table references other tables)
    &amp;nbsp; -- ========================================================================
    &amp;nbsp; FOR v_fk_col IN 
    &amp;nbsp; &amp;nbsp; SELECT column_name
    &amp;nbsp; &amp;nbsp; FROM information_schema.columns
    &amp;nbsp; &amp;nbsp; WHERE table_schema = 'public' 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; AND table_name = p_table_name
    &amp;nbsp; &amp;nbsp; &amp;nbsp; AND (
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name LIKE '%\_id' OR &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- customer_id, warehouse_id, manager_id
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name LIKE '%\_name' &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;-- manager_name, customer_name
    &amp;nbsp; &amp;nbsp; &amp;nbsp; )
    &amp;nbsp; &amp;nbsp; &amp;nbsp; AND column_name != 'id' &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Skip primary key
    &amp;nbsp; LOOP
    &amp;nbsp; &amp;nbsp; -- Infer referenced table name
    &amp;nbsp; &amp;nbsp; v_ref_table := regexp_replace(v_fk_col, '_(id|name)$', '');
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; -- Handle pluralization
    &amp;nbsp; &amp;nbsp; -- carriers → carrier, employees → employee, warehouses → warehouse
    &amp;nbsp; &amp;nbsp; IF v_ref_table ~ '(ss|us|ch|sh|x|z)es$' THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; v_ref_table := regexp_replace(v_ref_table, 'es$', '');
    &amp;nbsp; &amp;nbsp; ELSIF v_ref_table ~ 'ies$' THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; v_ref_table := regexp_replace(v_ref_table, 'ies$', 'y');
    &amp;nbsp; &amp;nbsp; ELSIF v_ref_table ~ 's$' THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; v_ref_table := regexp_replace(v_ref_table, 's$', '');
    &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; -- Add tbl_ prefix
    &amp;nbsp; &amp;nbsp; v_ref_table := 'tbl_' || v_ref_table;
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; -- Check if referenced table exists
    &amp;nbsp; &amp;nbsp; SELECT EXISTS (
    &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT FROM pg_tables 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE schemaname = 'public' AND tablename = v_ref_table
    &amp;nbsp; &amp;nbsp; ) INTO v_ref_table_exists;
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; IF v_ref_table_exists THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Determine join column in referenced table
    &amp;nbsp; &amp;nbsp; &amp;nbsp; v_join_col := NULL;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; v_ref_has_id := FALSE;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; v_ref_has_name := FALSE;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Check what columns the referenced table has
    &amp;nbsp; &amp;nbsp; &amp;nbsp; IF v_fk_col LIKE '%\_id' THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- For FK columns ending in _id, look for matching ID column in ref table
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- customer_id → look for customer_id in tbl_customers
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- manager_id → look for employee_id in tbl_employees (special case)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT bool_or(
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name = v_fk_col OR 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name = regexp_replace(v_ref_table, '^tbl_', '') || '_id'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; INTO v_ref_has_id
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; FROM information_schema.columns
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE table_schema = 'public' AND table_name = v_ref_table;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; IF v_ref_has_id THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Find the actual ID column name
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT column_name INTO v_join_col
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; FROM information_schema.columns
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE table_schema = 'public' AND table_name = v_ref_table
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; AND (column_name = v_fk_col OR 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;column_name = regexp_replace(v_ref_table, '^tbl_', '') || '_id')
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; LIMIT 1;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; IF v_fk_col LIKE '%\_name' THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- For FK columns ending in _name, look for 'name' column in ref table
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT bool_or(column_name = 'name')
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; INTO v_ref_has_name
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; FROM information_schema.columns
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE table_schema = 'public' AND table_name = v_ref_table;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; IF v_ref_has_name THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; v_join_col := 'name';
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- If we found a valid join column, add to related tables
    &amp;nbsp; &amp;nbsp; &amp;nbsp; IF v_join_col IS NOT NULL THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; v_related_tables := v_related_tables || jsonb_build_array(
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; jsonb_build_object(
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'table', v_ref_table,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'fk_column', v_fk_col,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'join_on', format('%I.%I = %I.%I', 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;p_table_name, v_fk_col,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;v_ref_table, v_join_col),
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'useful_columns', 'Details from ' || v_ref_table,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'use_when', format('Query mentions %s or asks about %s details', 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; regexp_replace(v_ref_table, '^tbl_', ''),
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; regexp_replace(v_fk_col, '_(id|name)$', ''))
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; );
    &amp;nbsp; &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; END LOOP;


    &amp;nbsp; -- ========================================================================
    &amp;nbsp; -- PART 2: REVERSE FK DETECTION (other tables reference this table)
    &amp;nbsp; -- ========================================================================
    &amp;nbsp; -- Example: tbl_employees should know that tbl_warehouses.manager_name references it
    &amp;nbsp; 
    &amp;nbsp; FOR v_reverse_rec IN
    &amp;nbsp; &amp;nbsp; SELECT DISTINCT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; c.table_name,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; c.column_name,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; c.data_type
    &amp;nbsp; &amp;nbsp; FROM information_schema.columns c
    &amp;nbsp; &amp;nbsp; WHERE c.table_schema = 'public' 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; AND c.table_name LIKE 'tbl_%'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; AND c.table_name != p_table_name
    &amp;nbsp; &amp;nbsp; &amp;nbsp; AND (
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; c.column_name LIKE '%\_id' OR
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; c.column_name LIKE '%\_name'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; )
    &amp;nbsp; LOOP
    &amp;nbsp; &amp;nbsp; v_ref_table := v_reverse_rec.table_name;
    &amp;nbsp; &amp;nbsp; v_fk_col := v_reverse_rec.column_name;
    &amp;nbsp; &amp;nbsp; v_join_col := NULL;
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; -- Extract the base entity name from the foreign key column
    &amp;nbsp; &amp;nbsp; -- manager_id → manager, customer_name → customer
    &amp;nbsp; &amp;nbsp; DECLARE
    &amp;nbsp; &amp;nbsp; &amp;nbsp; v_base_entity TEXT;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; v_current_table_entity TEXT;
    &amp;nbsp; &amp;nbsp; BEGIN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; v_base_entity := regexp_replace(v_fk_col, '_(id|name)$', '');
    &amp;nbsp; &amp;nbsp; &amp;nbsp; v_current_table_entity := regexp_replace(p_table_name, '^tbl_', '');
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Normalize pluralization for comparison
    &amp;nbsp; &amp;nbsp; &amp;nbsp; IF v_current_table_entity ~ 's$' THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; v_current_table_entity := regexp_replace(v_current_table_entity, 's$', '');
    &amp;nbsp; &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Check if this FK might reference the current table
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Examples:
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- &amp;nbsp; manager → employee (tbl_warehouses.manager_name → tbl_employees.name)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- &amp;nbsp; customer → customer (tbl_orders.customer_id → tbl_customers.customer_id)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- &amp;nbsp; employee → employee (tbl_employees.manager_id → tbl_employees.employee_id)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; IF v_base_entity = v_current_table_entity OR
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(v_base_entity = 'manager' AND v_current_table_entity = 'employee') OR
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(v_base_entity = 'employee' AND v_current_table_entity = 'employee') THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Determine what column in current table this FK should join to
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; IF v_fk_col LIKE '%\_id' THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Look for matching ID column in current table
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT column_name INTO v_join_col
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; FROM information_schema.columns
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE table_schema = 'public' 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; AND table_name = p_table_name
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; AND (
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name = v_fk_col OR
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name = p_table_name || '_id' OR
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column_name = regexp_replace(p_table_name, '^tbl_', '') || '_id'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; LIMIT 1;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ELSIF v_fk_col LIKE '%\_name' THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Look for 'name' column in current table
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT column_name INTO v_join_col
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; FROM information_schema.columns
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE table_schema = 'public' 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; AND table_name = p_table_name
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; AND column_name = 'name'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; LIMIT 1;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- If we found a matching join column, add to related tables
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; IF v_join_col IS NOT NULL THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Check if this relationship already exists (avoid duplicates from forward pass)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; IF NOT EXISTS (
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT 1 FROM jsonb_array_elements(v_related_tables) elem
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE elem-&amp;gt;&amp;gt;'table' = v_ref_table
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; AND elem-&amp;gt;&amp;gt;'fk_column' = v_fk_col
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ) THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; v_related_tables := v_related_tables || jsonb_build_array(
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; jsonb_build_object(
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'table', v_ref_table,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'fk_column', v_fk_col,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'join_on', format('%I.%I = %I.%I', 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;p_table_name, v_join_col,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;v_ref_table, v_fk_col),
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'useful_columns', 'Details from ' || v_ref_table,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'use_when', format('Query asks about %s that reference %s', 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; regexp_replace(v_ref_table, '^tbl_', ''),
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; regexp_replace(p_table_name, '^tbl_', ''))
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; );
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; END;
    &amp;nbsp; END LOOP;


    &amp;nbsp; RETURN jsonb_build_object(
    &amp;nbsp; &amp;nbsp; 'table', p_table_name,
    &amp;nbsp; &amp;nbsp; 'schema', v_columns,
    &amp;nbsp; &amp;nbsp; 'sample', COALESCE(v_sample_row, '{}'::jsonb),
    &amp;nbsp; &amp;nbsp; 'description', v_description,
    &amp;nbsp; &amp;nbsp; 'column_semantics', COALESCE(v_semantics, '{}'::jsonb),
    &amp;nbsp; &amp;nbsp; 'categorical_values', v_categorical_values,
    &amp;nbsp; &amp;nbsp; 'related_tables', v_related_tables
    &amp;nbsp; );
    &amp;nbsp; 
    EXCEPTION WHEN OTHERS THEN
    &amp;nbsp; RETURN jsonb_build_object('error', SQLERRM);
    END $$;


    GRANT EXECUTE ON FUNCTION public.get_table_context(text) TO service_role, authenticated, anon;


    -- 7d. Hybrid Graph Search
    CREATE OR REPLACE FUNCTION public.search_graph_hybrid(
    &amp;nbsp; p_entities TEXT[],
    &amp;nbsp; p_actions TEXT[],
    &amp;nbsp; p_limit INT DEFAULT 20
    )
    RETURNS TABLE(chunk_id BIGINT, content TEXT, score FLOAT8, strategy TEXT)
    LANGUAGE plpgsql STABLE AS $$
    DECLARE
    &amp;nbsp; v_has_actions BOOLEAN;
    BEGIN
    &amp;nbsp; v_has_actions := (array_length(p_actions, 1) &amp;gt; 0);


    &amp;nbsp; RETURN QUERY
    &amp;nbsp; WITH relevant_nodes AS (
    &amp;nbsp; &amp;nbsp; SELECT id FROM public.node
    &amp;nbsp; &amp;nbsp; WHERE EXISTS (
    &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT 1 FROM unnest(p_entities) entity
    &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE public.node.key ILIKE '%' || entity || '%'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;OR public.node.props-&amp;gt;&amp;gt;'name' ILIKE '%' || entity || '%'
    &amp;nbsp; &amp;nbsp; )
    &amp;nbsp; ),
    &amp;nbsp; relevant_edges AS (
    &amp;nbsp; &amp;nbsp; SELECT e.src, e.dst, e.type, 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;CASE 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;WHEN s.id IS NOT NULL AND d.id IS NOT NULL THEN 'entity-entity'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;ELSE 'entity-action'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;END as match_strategy,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;CASE 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;WHEN s.id IS NOT NULL AND d.id IS NOT NULL THEN 2.0
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;ELSE 1.5
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;END as base_score
    &amp;nbsp; &amp;nbsp; FROM public.edge e
    &amp;nbsp; &amp;nbsp; LEFT JOIN relevant_nodes s ON e.src = s.id
    &amp;nbsp; &amp;nbsp; LEFT JOIN relevant_nodes d ON e.dst = d.id
    &amp;nbsp; &amp;nbsp; WHERE 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; (s.id IS NOT NULL AND d.id IS NOT NULL)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; OR
    &amp;nbsp; &amp;nbsp; &amp;nbsp; (v_has_actions AND (s.id IS NOT NULL OR d.id IS NOT NULL) AND 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; EXISTS (SELECT 1 FROM unnest(p_actions) act WHERE e.type ILIKE act || '%')
    &amp;nbsp; &amp;nbsp; &amp;nbsp; )
    &amp;nbsp; ),
    &amp;nbsp; hits AS (
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; cn.chunk_id,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; count(*) as mention_count,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; max(base_score) as max_strategy_score,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; string_agg(DISTINCT match_strategy, ', ') as strategies
    &amp;nbsp; &amp;nbsp; FROM relevant_edges re
    &amp;nbsp; &amp;nbsp; JOIN public.chunk_node cn ON cn.node_id = re.src
    &amp;nbsp; &amp;nbsp; GROUP BY cn.chunk_id
    &amp;nbsp; )
    &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; h.chunk_id, 
    &amp;nbsp; &amp;nbsp; c.text as content,
    &amp;nbsp; &amp;nbsp; (log(h.mention_count + 1) * h.max_strategy_score)::float8 AS score,
    &amp;nbsp; &amp;nbsp; h.strategies::text as strategy
    &amp;nbsp; FROM hits h
    &amp;nbsp; JOIN public.chunk c ON c.id = h.chunk_id
    &amp;nbsp; ORDER BY score DESC
    &amp;nbsp; LIMIT p_limit;
    END $$;


    -- 7e. Legacy Targeted Graph Search (RESTORED FOR COMPLETENESS)
    CREATE OR REPLACE FUNCTION public.search_graph_targeted(
    &amp;nbsp; p_entities TEXT[],
    &amp;nbsp; p_actions TEXT[],
    &amp;nbsp; p_limit INT DEFAULT 20
    )
    RETURNS TABLE(chunk_id BIGINT, content TEXT, score FLOAT8)
    LANGUAGE plpgsql STABLE AS $$
    DECLARE
    &amp;nbsp; v_has_actions BOOLEAN;
    BEGIN
    &amp;nbsp; v_has_actions := (array_length(p_actions, 1) &amp;gt; 0);


    &amp;nbsp; RETURN QUERY
    &amp;nbsp; WITH relevant_nodes AS (
    &amp;nbsp; &amp;nbsp; SELECT id, props-&amp;gt;&amp;gt;'name' as name
    &amp;nbsp; &amp;nbsp; FROM public.node
    &amp;nbsp; &amp;nbsp; WHERE EXISTS (
    &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT 1 FROM unnest(p_entities) entity
    &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE public.node.key ILIKE '%' || entity || '%'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;OR public.node.props-&amp;gt;&amp;gt;'name' ILIKE '%' || entity || '%'
    &amp;nbsp; &amp;nbsp; )
    &amp;nbsp; ),
    &amp;nbsp; relevant_edges AS (
    &amp;nbsp; &amp;nbsp; SELECT e.src, e.dst, e.type
    &amp;nbsp; &amp;nbsp; FROM public.edge e
    &amp;nbsp; &amp;nbsp; JOIN relevant_nodes rn ON e.src = rn.id
    &amp;nbsp; ),
    &amp;nbsp; hits AS (
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; cn.chunk_id,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; count(*) as mention_count
    &amp;nbsp; &amp;nbsp; FROM relevant_edges re
    &amp;nbsp; &amp;nbsp; JOIN public.chunk_node cn ON cn.node_id = re.src
    &amp;nbsp; &amp;nbsp; GROUP BY cn.chunk_id
    &amp;nbsp; )
    &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; h.chunk_id, 
    &amp;nbsp; &amp;nbsp; c.text as content,
    &amp;nbsp; &amp;nbsp; (log(h.mention_count + 1) * 1.5)::float8 AS score
    &amp;nbsp; FROM hits h
    &amp;nbsp; JOIN public.chunk c ON c.id = h.chunk_id
    &amp;nbsp; ORDER BY score DESC
    &amp;nbsp; LIMIT p_limit;
    END $$;


    -- 7f. Graph Neighborhood (Update 4: Security Definer / Case Insensitive)
    DROP FUNCTION IF EXISTS public.get_graph_neighborhood(text[]);
    CREATE OR REPLACE FUNCTION public.get_graph_neighborhood(
    &amp;nbsp; p_entity_names TEXT[]
    )
    RETURNS TABLE(subject TEXT, action TEXT, object TEXT, context JSONB)
    LANGUAGE plpgsql 
    SECURITY DEFINER 
    SET search_path = public, pg_temp
    AS $$
    BEGIN
    &amp;nbsp; RETURN QUERY
    &amp;nbsp; WITH target_nodes AS (
    &amp;nbsp; &amp;nbsp; SELECT id, key, props-&amp;gt;&amp;gt;'name' as name 
    &amp;nbsp; &amp;nbsp; FROM public.node
    &amp;nbsp; &amp;nbsp; WHERE 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;-- 1. Direct Key Match
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;key = ANY(p_entity_names)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;-- 2. Name Match (Case Insensitive, Trimmed)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;OR lower(trim(props-&amp;gt;&amp;gt;'name')) = ANY(
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT lower(trim(x)) FROM unnest(p_entity_names) x
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;-- 3. Fuzzy Match (Substring)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;OR EXISTS (
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;SELECT 1 FROM unnest(p_entity_names) term
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;WHERE length(term) &amp;gt; 3 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;AND public.node.props-&amp;gt;&amp;gt;'name' ILIKE '%' || term || '%'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;)
    &amp;nbsp; )
    &amp;nbsp; -- Outgoing Edges
    &amp;nbsp; SELECT n1.props-&amp;gt;&amp;gt;'name', e.type, n2.props-&amp;gt;&amp;gt;'name', e.props
    &amp;nbsp; FROM target_nodes tn
    &amp;nbsp; JOIN public.edge e ON tn.id = e.src
    &amp;nbsp; JOIN public.node n1 ON e.src = n1.id
    &amp;nbsp; JOIN public.node n2 ON e.dst = n2.id
    &amp;nbsp; 
    &amp;nbsp; UNION ALL
    &amp;nbsp; 
    &amp;nbsp; -- Incoming Edges
    &amp;nbsp; SELECT n1.props-&amp;gt;&amp;gt;'name', e.type, n2.props-&amp;gt;&amp;gt;'name', e.props
    &amp;nbsp; FROM target_nodes tn
    &amp;nbsp; JOIN public.edge e ON tn.id = e.dst
    &amp;nbsp; JOIN public.node n1 ON e.src = n1.id
    &amp;nbsp; JOIN public.node n2 ON e.dst = n2.id;
    END $$;
    GRANT EXECUTE ON FUNCTION public.get_graph_neighborhood(text[]) TO service_role, authenticated, anon;


    -- 7g. Structured Search (Safe V6.1)
    DROP FUNCTION IF EXISTS public.search_structured(text, int);
    CREATE OR REPLACE FUNCTION public.search_structured(p_query_sql TEXT, p_limit INT DEFAULT 20)
    RETURNS TABLE(table_name TEXT, row_data JSONB, score FLOAT8, rank INT)
    LANGUAGE plpgsql 
    SECURITY DEFINER 
    SET search_path = public, pg_temp
    AS $$
    DECLARE 
    &amp;nbsp; v_sql TEXT;
    BEGIN
    &amp;nbsp; IF p_query_sql IS NULL OR length(trim(p_query_sql)) = 0 THEN RETURN; END IF;
    &amp;nbsp; v_sql := p_query_sql;
    &amp;nbsp; 
    &amp;nbsp; -- Sanitization
    &amp;nbsp; v_sql := regexp_replace(v_sql, '(\W)to\.([a-zA-Z0-9_]+)', '\1t_orders.\2', 'g');
    &amp;nbsp; v_sql := regexp_replace(v_sql, '\s+to\s+ON\s+', ' t_orders ON ', 'gi');
    &amp;nbsp; v_sql := regexp_replace(v_sql, '\s+AS\s+to\s+', ' AS t_orders ', 'gi');
    &amp;nbsp; v_sql := regexp_replace(v_sql, 'tbl_orders\s+to\s+', 'tbl_orders t_orders ', 'gi');
    &amp;nbsp; v_sql := regexp_replace(v_sql, '[;\s]+$', '');


    &amp;nbsp; RETURN QUERY EXECUTE format(
    &amp;nbsp; &amp;nbsp; 'WITH user_query AS (%s) 
    &amp;nbsp; &amp;nbsp; &amp;nbsp;SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;''result''::text AS table_name, 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;to_jsonb(user_query.*) AS row_data, 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;1.0::float8 AS score, 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(row_number() OVER ())::int AS rank 
    &amp;nbsp; &amp;nbsp; &amp;nbsp;FROM user_query LIMIT %s',
    &amp;nbsp; &amp;nbsp; v_sql, p_limit
    &amp;nbsp; );


    EXCEPTION WHEN OTHERS THEN
    &amp;nbsp; RETURN QUERY SELECT 'ERROR'::text, jsonb_build_object('msg', SQLERRM, 'sql', v_sql), 1.0, 1;
    END $$;
    GRANT EXECUTE ON FUNCTION public.search_structured(text, int) TO service_role, authenticated, anon;


    -- 7h. Smart Entity Detection (V9 - Composite Retrieval)
    CREATE OR REPLACE FUNCTION public.detect_query_entities(
    &amp;nbsp; p_query TEXT
    )
    RETURNS TABLE(
    &amp;nbsp; entity_type TEXT,
    &amp;nbsp; table_name TEXT,
    &amp;nbsp; key_column TEXT,
    &amp;nbsp; key_value TEXT
    )
    LANGUAGE plpgsql
    AS $$
    BEGIN
    &amp;nbsp; -- Detect ORDER IDs (O followed by 5 digits)
    &amp;nbsp; IF p_query ~* 'O\d{5}' THEN
    &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'order'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'tbl_orders'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'order_id'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; (regexp_match(p_query, '(O\d{5})', 'i'))[1]::TEXT;
    &amp;nbsp; END IF;
    &amp;nbsp; 
    &amp;nbsp; -- Detect CUSTOMER IDs (CU followed by 3 digits)
    &amp;nbsp; IF p_query ~* 'CU\d{3}' THEN
    &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'customer'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'tbl_customers'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'customer_id'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; (regexp_match(p_query, '(CU\d{3})', 'i'))[1]::TEXT;
    &amp;nbsp; END IF;
    &amp;nbsp; 
    &amp;nbsp; -- Detect EMPLOYEE IDs (E followed by 3 digits)
    &amp;nbsp; IF p_query ~* 'E\d{3}' THEN
    &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'employee'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'tbl_employees'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'employee_id'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; (regexp_match(p_query, '(E\d{3})', 'i'))[1]::TEXT;
    &amp;nbsp; END IF;
    &amp;nbsp; 
    &amp;nbsp; -- Detect WAREHOUSE IDs (WH followed by 3 digits)
    &amp;nbsp; IF p_query ~* 'WH\d{3}' THEN
    &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'warehouse'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'tbl_warehouses'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'warehouse_id'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; (regexp_match(p_query, '(WH\d{3})', 'i'))[1]::TEXT;
    &amp;nbsp; END IF;
    &amp;nbsp; 
    &amp;nbsp; -- Detect CARRIER IDs (CR followed by 3 digits)
    &amp;nbsp; IF p_query ~* 'CR\d{3}' THEN
    &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'carrier'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'tbl_carriers'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'carrier_id'::TEXT,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; (regexp_match(p_query, '(CR\d{3})', 'i'))[1]::TEXT;
    &amp;nbsp; END IF;
    &amp;nbsp; 
    &amp;nbsp; RETURN;
    END;
    $$;
    GRANT EXECUTE ON FUNCTION public.detect_query_entities TO anon, authenticated, service_role;


    -- 7i. Context Enrichment (V9 - Composite Retrieval)
    CREATE OR REPLACE FUNCTION public.enrich_query_context(
    &amp;nbsp; p_primary_table TEXT,
    &amp;nbsp; p_primary_key TEXT,
    &amp;nbsp; p_primary_value TEXT
    )
    RETURNS TABLE(
    &amp;nbsp; enrichment_type TEXT,
    &amp;nbsp; table_name TEXT,
    &amp;nbsp; row_data JSONB,
    &amp;nbsp; relationship TEXT
    )
    LANGUAGE plpgsql
    AS $$
    DECLARE
    &amp;nbsp; v_customer_id TEXT;
    &amp;nbsp; v_warehouse_id TEXT;
    &amp;nbsp; v_employee_id TEXT;
    &amp;nbsp; v_carrier_id TEXT;
    &amp;nbsp; v_primary_row JSONB;
    BEGIN
    &amp;nbsp; -- ====================================================
    &amp;nbsp; -- STEP 1: GET PRIMARY ROW
    &amp;nbsp; -- ====================================================
    &amp;nbsp; EXECUTE format(
    &amp;nbsp; &amp;nbsp; 'SELECT to_jsonb(t.*) FROM public.%I t WHERE %I = $1 LIMIT 1',
    &amp;nbsp; &amp;nbsp; p_primary_table,
    &amp;nbsp; &amp;nbsp; p_primary_key
    &amp;nbsp; ) INTO v_primary_row USING p_primary_value;
    &amp;nbsp; 
    &amp;nbsp; IF v_primary_row IS NULL THEN
    &amp;nbsp; &amp;nbsp; RETURN;
    &amp;nbsp; END IF;
    &amp;nbsp; 
    &amp;nbsp; RETURN QUERY SELECT 
    &amp;nbsp; &amp;nbsp; 'primary'::TEXT,
    &amp;nbsp; &amp;nbsp; p_primary_table,
    &amp;nbsp; &amp;nbsp; v_primary_row,
    &amp;nbsp; &amp;nbsp; 'direct_match'::TEXT;
    &amp;nbsp; 
    &amp;nbsp; -- ====================================================
    &amp;nbsp; -- STEP 2: FOLLOW FOREIGN KEYS (ENRICH!)
    &amp;nbsp; -- ====================================================
    &amp;nbsp; 
    &amp;nbsp; -- ORDERS TABLE
    &amp;nbsp; IF p_primary_table = 'tbl_orders' THEN
    &amp;nbsp; &amp;nbsp; v_customer_id := v_primary_row-&amp;gt;&amp;gt;'customer_id';
    &amp;nbsp; &amp;nbsp; v_warehouse_id := v_primary_row-&amp;gt;&amp;gt;'warehouse_id';
    &amp;nbsp; &amp;nbsp; v_carrier_id := v_primary_row-&amp;gt;&amp;gt;'carrier_id';
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; -- Customer
    &amp;nbsp; &amp;nbsp; IF v_customer_id IS NOT NULL THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT 'enrichment'::TEXT, 'tbl_customers'::TEXT, to_jsonb(c.*), 'order_customer'::TEXT
    &amp;nbsp; &amp;nbsp; &amp;nbsp; FROM public.tbl_customers c WHERE c.customer_id = v_customer_id;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; -- Customer History
    &amp;nbsp; &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT 'related_orders'::TEXT, 'tbl_orders'::TEXT, to_jsonb(o.*), 'customer_history'::TEXT
    &amp;nbsp; &amp;nbsp; &amp;nbsp; FROM public.tbl_orders o WHERE o.customer_id = v_customer_id AND o.order_id != p_primary_value
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ORDER BY o.order_date DESC LIMIT 5;
    &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; -- Warehouse
    &amp;nbsp; &amp;nbsp; IF v_warehouse_id IS NOT NULL THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT 'enrichment'::TEXT, 'tbl_warehouses'::TEXT, to_jsonb(w.*), 'order_warehouse'::TEXT
    &amp;nbsp; &amp;nbsp; &amp;nbsp; FROM public.tbl_warehouses w WHERE w.warehouse_id = v_warehouse_id;
    &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; -- Carrier
    &amp;nbsp; &amp;nbsp; IF v_carrier_id IS NOT NULL THEN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT 'enrichment'::TEXT, 'tbl_carriers'::TEXT, to_jsonb(cr.*), 'order_carrier'::TEXT
    &amp;nbsp; &amp;nbsp; &amp;nbsp; FROM public.tbl_carriers cr WHERE cr.carrier_id = v_carrier_id;
    &amp;nbsp; &amp;nbsp; END IF;
    &amp;nbsp; END IF;
    &amp;nbsp; 
    &amp;nbsp; -- CUSTOMERS TABLE
    &amp;nbsp; IF p_primary_table = 'tbl_customers' THEN
    &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; SELECT 'enrichment'::TEXT, 'tbl_orders'::TEXT, to_jsonb(o.*), 'customer_orders'::TEXT
    &amp;nbsp; &amp;nbsp; FROM public.tbl_orders o WHERE o.customer_id = p_primary_value
    &amp;nbsp; &amp;nbsp; ORDER BY o.order_date DESC LIMIT 10;
    &amp;nbsp; END IF;
    &amp;nbsp; 
    &amp;nbsp; -- EMPLOYEES TABLE
    &amp;nbsp; IF p_primary_table = 'tbl_employees' THEN
    &amp;nbsp; &amp;nbsp; v_employee_id := v_primary_row-&amp;gt;&amp;gt;'employee_id';
    &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; SELECT 'enrichment'::TEXT, 'tbl_employees'::TEXT, to_jsonb(e.*), 'direct_reports'::TEXT
    &amp;nbsp; &amp;nbsp; FROM public.tbl_employees e WHERE e.manager_id = v_employee_id;
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; SELECT 'enrichment'::TEXT, 'tbl_employees'::TEXT, to_jsonb(m.*), 'manager'::TEXT
    &amp;nbsp; &amp;nbsp; FROM public.tbl_employees m WHERE m.employee_id = (v_primary_row-&amp;gt;&amp;gt;'manager_id');
    &amp;nbsp; END IF;
    &amp;nbsp; 
    &amp;nbsp; -- WAREHOUSES TABLE
    &amp;nbsp; IF p_primary_table = 'tbl_warehouses' THEN
    &amp;nbsp; &amp;nbsp; RETURN QUERY
    &amp;nbsp; &amp;nbsp; SELECT 'enrichment'::TEXT, 'tbl_orders'::TEXT, to_jsonb(o.*), 'warehouse_orders'::TEXT
    &amp;nbsp; &amp;nbsp; FROM public.tbl_orders o WHERE o.warehouse_id = p_primary_value
    &amp;nbsp; &amp;nbsp; ORDER BY o.order_date DESC LIMIT 10;
    &amp;nbsp; END IF;
    &amp;nbsp; 
    &amp;nbsp; RETURN;
    END;
    $$;
    GRANT EXECUTE ON FUNCTION public.enrich_query_context TO anon, authenticated, service_role;


    -- ===============================================================
    -- PART 8: CONFIGURATION SYSTEM
    -- ===============================================================


    INSERT INTO public.app_config (id, settings)
    VALUES (1, '{
    &amp;nbsp; &amp;nbsp; "chunk_size": 500,
    &amp;nbsp; &amp;nbsp; "chunk_overlap": 100,
    &amp;nbsp; &amp;nbsp; "graph_sample_rate": 5,
    &amp;nbsp; &amp;nbsp; "worker_batch_size": 5,
    &amp;nbsp; &amp;nbsp; "model_router": "gemini-2.5-flash",
    &amp;nbsp; &amp;nbsp; "model_reranker": "gemini-2.5-flash-lite",
    &amp;nbsp; &amp;nbsp; "model_sql": "gemini-2.5-flash",
    &amp;nbsp; &amp;nbsp; "model_extraction": "gemini-2.5-flash",
    &amp;nbsp; &amp;nbsp; "rrf_weight_enrichment": 15.0,
    &amp;nbsp; &amp;nbsp; "rrf_weight_sql": 10.0,
    &amp;nbsp; &amp;nbsp; "rrf_weight_graph": 5.0,
    &amp;nbsp; &amp;nbsp; "rrf_weight_fts": 3.0,
    &amp;nbsp; &amp;nbsp; "rrf_weight_vector": 1.0,
    &amp;nbsp; &amp;nbsp; "rerank_depth": 15,
    &amp;nbsp; &amp;nbsp; "min_vector_score": 0.01,
    &amp;nbsp; &amp;nbsp; "search_limit": 10
    }'::jsonb)
    ON CONFLICT (id) DO NOTHING;


    CREATE OR REPLACE FUNCTION public.configure_system(
    &amp;nbsp; &amp;nbsp; p_chunk_size INT DEFAULT NULL,
    &amp;nbsp; &amp;nbsp; p_chunk_overlap INT DEFAULT NULL,
    &amp;nbsp; &amp;nbsp; p_graph_sample_rate INT DEFAULT NULL,
    &amp;nbsp; &amp;nbsp; p_worker_batch_size INT DEFAULT NULL,
    &amp;nbsp; &amp;nbsp; p_model_router TEXT DEFAULT NULL,
    &amp;nbsp; &amp;nbsp; p_model_reranker TEXT DEFAULT NULL,
    &amp;nbsp; &amp;nbsp; p_model_extraction TEXT DEFAULT NULL,
    &amp;nbsp; &amp;nbsp; p_search_limit INT DEFAULT NULL,
    &amp;nbsp; &amp;nbsp; p_rerank_depth INT DEFAULT NULL,
    &amp;nbsp; &amp;nbsp; p_rrf_weight_vector NUMERIC DEFAULT NULL,
    &amp;nbsp; &amp;nbsp; p_rrf_weight_graph NUMERIC DEFAULT NULL,
    &amp;nbsp; &amp;nbsp; p_rrf_weight_sql NUMERIC DEFAULT NULL
    )
    RETURNS TEXT LANGUAGE plpgsql SECURITY DEFINER AS $$
    DECLARE
    &amp;nbsp; &amp;nbsp; current_settings JSONB;
    &amp;nbsp; &amp;nbsp; new_settings JSONB;
    BEGIN
    &amp;nbsp; &amp;nbsp; SELECT settings INTO current_settings FROM public.app_config WHERE id = 1;
    &amp;nbsp; &amp;nbsp; new_settings := current_settings;
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; IF p_chunk_size IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{chunk_size}', to_jsonb(p_chunk_size)); END IF;
    &amp;nbsp; &amp;nbsp; IF p_chunk_overlap IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{chunk_overlap}', to_jsonb(p_chunk_overlap)); END IF;
    &amp;nbsp; &amp;nbsp; IF p_graph_sample_rate IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{graph_sample_rate}', to_jsonb(p_graph_sample_rate)); END IF;
    &amp;nbsp; &amp;nbsp; IF p_worker_batch_size IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{worker_batch_size}', to_jsonb(p_worker_batch_size)); END IF;
    &amp;nbsp; &amp;nbsp; IF p_model_router IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{model_router}', to_jsonb(p_model_router)); END IF;
    &amp;nbsp; &amp;nbsp; IF p_model_reranker IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{model_reranker}', to_jsonb(p_model_reranker)); END IF;
    &amp;nbsp; &amp;nbsp; IF p_model_extraction IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{model_extraction}', to_jsonb(p_model_extraction)); END IF;
    &amp;nbsp; &amp;nbsp; IF p_search_limit IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{search_limit}', to_jsonb(p_search_limit)); END IF;
    &amp;nbsp; &amp;nbsp; IF p_rerank_depth IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{rerank_depth}', to_jsonb(p_rerank_depth)); END IF;
    &amp;nbsp; &amp;nbsp; IF p_rrf_weight_vector IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{rrf_weight_vector}', to_jsonb(p_rrf_weight_vector)); END IF;
    &amp;nbsp; &amp;nbsp; IF p_rrf_weight_graph IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{rrf_weight_graph}', to_jsonb(p_rrf_weight_graph)); END IF;
    &amp;nbsp; &amp;nbsp; IF p_rrf_weight_sql IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{rrf_weight_sql}', to_jsonb(p_rrf_weight_sql)); END IF;


    &amp;nbsp; &amp;nbsp; UPDATE public.app_config SET settings = new_settings, updated_at = now() WHERE id = 1;
    &amp;nbsp; &amp;nbsp; RETURN 'System configuration updated successfully.';
    END;
    $$;


    -- ===============================================================
    -- PART 9: GRAPH CONTEXT RERANKER
    -- ===============================================================
    DROP FUNCTION IF EXISTS public.get_graph_context(bigint[], text[]);


    CREATE OR REPLACE FUNCTION public.get_graph_context(
    &amp;nbsp; p_chunk_ids BIGINT[],
    &amp;nbsp; p_keywords TEXT[] DEFAULT '{}',
    &amp;nbsp; p_actions TEXT[] DEFAULT '{}'
    )
    RETURNS TABLE (chunk_id BIGINT, graph_data JSONB)
    LANGUAGE sql STABLE AS $$
    &amp;nbsp; WITH raw_edges AS (
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; cn_src.chunk_id,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; jsonb_build_object(
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'subject', n1.props-&amp;gt;&amp;gt;'name',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'action', e.type,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'object', n2.props-&amp;gt;&amp;gt;'name',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'context', e.props
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ) as edge_json,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; (
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 0 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; + (SELECT COALESCE(MAX(CASE WHEN n1.props-&amp;gt;&amp;gt;'name' ILIKE '%' || kw || '%' THEN 10 ELSE 0 END),0) FROM unnest(p_keywords) kw)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; + (SELECT COALESCE(MAX(CASE WHEN n2.props-&amp;gt;&amp;gt;'name' ILIKE '%' || kw || '%' THEN 10 ELSE 0 END),0) FROM unnest(p_keywords) kw)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; + (SELECT COALESCE(MAX(CASE WHEN array_length(p_actions, 1) &amp;gt; 0 AND e.type ILIKE act || '%' THEN 20 ELSE 0 END),0) FROM unnest(p_actions) act)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ) as relevance_score
    &amp;nbsp; &amp;nbsp; FROM public.chunk_node cn_src
    &amp;nbsp; &amp;nbsp; JOIN public.edge e ON cn_src.node_id = e.src
    &amp;nbsp; &amp;nbsp; JOIN public.chunk_node cn_tgt ON e.dst = cn_tgt.node_id AND cn_src.chunk_id = cn_tgt.chunk_id
    &amp;nbsp; &amp;nbsp; JOIN public.node n1 ON e.src = n1.id
    &amp;nbsp; &amp;nbsp; JOIN public.node n2 ON e.dst = n2.id
    &amp;nbsp; &amp;nbsp; WHERE cn_src.chunk_id = ANY(p_chunk_ids)
    &amp;nbsp; ),
    &amp;nbsp; ranked_edges AS (
    &amp;nbsp; &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; chunk_id, 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; edge_json,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; relevance_score,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ROW_NUMBER() OVER (PARTITION BY chunk_id ORDER BY relevance_score DESC, length(edge_json::text) ASC) as rn
    &amp;nbsp; &amp;nbsp; FROM raw_edges
    &amp;nbsp; )
    &amp;nbsp; SELECT 
    &amp;nbsp; &amp;nbsp; chunk_id,
    &amp;nbsp; &amp;nbsp; jsonb_agg(edge_json) as graph_data
    &amp;nbsp; FROM ranked_edges
    &amp;nbsp; WHERE rn &amp;lt;= 5
    &amp;nbsp; GROUP BY chunk_id;
    $$;


    -- ===============================================================
    -- PART 10: WORKER SETUP
    -- ===============================================================


    CREATE OR REPLACE FUNCTION public.setup_worker(project_url TEXT, service_role_key TEXT)
    RETURNS TEXT LANGUAGE plpgsql SECURITY DEFINER AS $$
    BEGIN
    &amp;nbsp; EXECUTE format(
    &amp;nbsp; &amp;nbsp; $f$
    &amp;nbsp; &amp;nbsp; CREATE OR REPLACE FUNCTION public.trigger_ingest_worker()
    &amp;nbsp; &amp;nbsp; RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $func$
    &amp;nbsp; &amp;nbsp; BEGIN
    &amp;nbsp; &amp;nbsp; &amp;nbsp; PERFORM net.http_post(
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; url := '%s/functions/v1/ingest-worker',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; headers := jsonb_build_object('Content-Type', 'application/json', 'Authorization', 'Bearer %s'),
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; body := jsonb_build_object('type', 'INSERT', 'table', 'objects', 'schema', 'storage', 'record', row_to_json(NEW))
    &amp;nbsp; &amp;nbsp; &amp;nbsp; );
    &amp;nbsp; &amp;nbsp; &amp;nbsp; RETURN NEW;
    &amp;nbsp; &amp;nbsp; END;
    &amp;nbsp; &amp;nbsp; $func$;
    &amp;nbsp; &amp;nbsp; $f$,
    &amp;nbsp; &amp;nbsp; project_url, service_role_key
    &amp;nbsp; );


    &amp;nbsp; DROP TRIGGER IF EXISTS "trigger-ingest-worker" ON storage.objects;
    &amp;nbsp; CREATE TRIGGER "trigger-ingest-worker"
    &amp;nbsp; AFTER INSERT ON storage.objects
    &amp;nbsp; FOR EACH ROW
    &amp;nbsp; WHEN (NEW.bucket_id = 'raw_uploads')
    &amp;nbsp; EXECUTE FUNCTION public.trigger_ingest_worker();


    &amp;nbsp; RETURN 'Worker configured successfully!';
    END;
    $$;


    -- ===============================================================
    -- PART 11: PERMISSIONS &amp;amp; REPAIRS
    -- ===============================================================
    GRANT USAGE ON SCHEMA public TO service_role, authenticated, anon;
    GRANT ALL ON ALL TABLES IN SCHEMA public TO service_role, authenticated;
    GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO service_role, authenticated;
    GRANT ALL ON TABLE public.ingestion_queue TO service_role, postgres, anon, authenticated;
    GRANT SELECT ON TABLE public.app_config TO anon, authenticated, service_role;
    GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO authenticated, service_role;


    -- 🚨 REPAIR SCRIPT: Fix permissions for any EXISTING "tbl_" spreadsheets
    DO $$ 
    DECLARE 
    &amp;nbsp; &amp;nbsp; r RECORD;
    BEGIN 
    &amp;nbsp; &amp;nbsp; FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE 'tbl_%') 
    &amp;nbsp; &amp;nbsp; LOOP 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.%I TO service_role', r.tablename);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; EXECUTE format('GRANT SELECT ON TABLE public.%I TO anon, authenticated', r.tablename);
    &amp;nbsp; &amp;nbsp; END LOOP; 
    END $$;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;. This is minor but important. If you upload a BIG file, it needs to be able to process in the background. So in order to enable background functions you need to set up a worker. In a new query, paste this and then fill in your information before running it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    -- insert your url and service worker secret
    SELECT setup_worker(
    &amp;nbsp; 'https://YOUR URL.supabase.co', 
    &amp;nbsp; 'ey**YOUR SERVICE WORKER SERET***Tc'
    );

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;. If you want to adjust weighting and other parameters, run any of these queries with your desired adjustments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    -- ===============================================================
    -- CONTEXT MESH: SIMPLE CONFIGURATION SCRIPT
    -- ===============================================================
    -- Instructions: Replace the default values below and run any line
    -- Each parameter can be updated independently
    -- ===============================================================


    -- ============================================================
    -- INGESTION PARAMETERS
    -- ============================================================


    SELECT public.configure_system(p_chunk_size =&amp;gt; 500);
    -- What it does: Number of characters per document chunk
    -- Suggestions: 
    -- &amp;nbsp; Higher (800-1000) = Better context, slower processing
    -- &amp;nbsp; Lower (300-400) = Faster processing, more precise retrieval
    -- &amp;nbsp; Default (500) = Balanced performance


    SELECT public.configure_system(p_chunk_overlap =&amp;gt; 100);
    -- What it does: Character overlap between adjacent chunks
    -- Suggestions:
    -- &amp;nbsp; Higher (150-200) = Better continuity, more redundancy
    -- &amp;nbsp; Lower (50-75) = Less redundancy, faster processing
    -- &amp;nbsp; Default (100) = Balanced overlap


    SELECT public.configure_system(p_graph_sample_rate =&amp;gt; 5);
    -- What it does: Extract graph relationships from every Nth chunk
    -- Suggestions:
    -- &amp;nbsp; Higher (10-15) = Faster ingestion, sparser graph
    -- &amp;nbsp; Lower (1-3) = Dense graph, slower ingestion
    -- &amp;nbsp; Default (5) = Balanced graph density


    SELECT public.configure_system(p_worker_batch_size =&amp;gt; 5);
    -- What it does: Queue items processed per worker batch
    -- Suggestions:
    -- &amp;nbsp; Higher (10-20) = Faster bulk ingestion, higher memory
    -- &amp;nbsp; Lower (1-3) = Slower ingestion, lower memory
    -- &amp;nbsp; Default (5) = Balanced throughput


    -- ============================================================
    -- MODEL SELECTION
    -- ============================================================


    SELECT public.configure_system(p_model_router =&amp;gt; 'gemini-2.5-flash');
    -- What it does: LLM for query routing and entity extraction
    -- Suggestions:
    -- &amp;nbsp; 'gemini-2.5-flash' = Fast, cost-effective (default)
    -- &amp;nbsp; 'gemini-2.0-flash-exp' = Experimental, free tier
    -- &amp;nbsp; 'gemini-2.5-pro' = Most accurate, expensive


    SELECT public.configure_system(p_model_reranker =&amp;gt; 'gemini-2.5-flash-lite');
    -- What it does: LLM for reranking documents by relevance
    -- Suggestions:
    -- &amp;nbsp; 'gemini-2.5-flash-lite' = Ultra-fast, cheap (default)
    -- &amp;nbsp; 'gemini-2.5-flash' = More accurate, slightly slower
    -- &amp;nbsp; 'gemini-2.0-flash-exp' = Free tier option


    SELECT public.configure_system(p_model_sql =&amp;gt; 'gemini-2.5-flash');
    -- What it does: LLM for generating SQL queries
    -- Suggestions:
    -- &amp;nbsp; 'gemini-2.5-flash' = Good balance (default)
    -- &amp;nbsp; 'gemini-2.5-pro' = Complex queries, better accuracy
    -- &amp;nbsp; 'gemini-2.0-flash-exp' = Free tier option


    -- ============================================================
    -- SEARCH WEIGHTS (RRF Fusion)
    -- ============================================================
    -- Higher weight = More influence in final ranking
    -- ============================================================


    SELECT public.configure_system(p_rrf_weight_enrichment =&amp;gt; 15.0);
    -- What it does: Weight for composite enrichment (SQL + FK relationships)
    -- Suggestions:
    -- &amp;nbsp; Higher (20-30) = Prioritize structured data context
    -- &amp;nbsp; Lower (10-12) = Balance with documents
    -- &amp;nbsp; Default (15.0) = Highest priority (recommended)


    SELECT public.configure_system(p_rrf_weight_sql =&amp;gt; 10.0);
    -- What it does: Weight for direct SQL query results
    -- Suggestions:
    -- &amp;nbsp; Higher (15-20) = Prioritize exact matches
    -- &amp;nbsp; Lower (5-8) = More exploratory results
    -- &amp;nbsp; Default (10.0) = Strong influence


    SELECT public.configure_system(p_rrf_weight_graph =&amp;gt; 5.0);
    -- What it does: Weight for knowledge graph relationships
    -- Suggestions:
    -- &amp;nbsp; Higher (8-12) = More relationship context
    -- &amp;nbsp; Lower (2-4) = Focus on direct matches
    -- &amp;nbsp; Default (5.0) = Moderate context


    SELECT public.configure_system(p_rrf_weight_fts =&amp;gt; 3.0);
    -- What it does: Weight for full-text keyword search
    -- Suggestions:
    -- &amp;nbsp; Higher (5-7) = Better keyword matching
    -- &amp;nbsp; Lower (1-2) = Favor semantic search
    -- &amp;nbsp; Default (3.0) = Balanced keyword influence


    SELECT public.configure_system(p_rrf_weight_vector =&amp;gt; 1.0);
    -- What it does: Weight for vector similarity search
    -- Suggestions:
    -- &amp;nbsp; Higher (2-5) = More semantic similarity
    -- &amp;nbsp; Lower (0.5-0.8) = Favor exact matches
    -- &amp;nbsp; Default (1.0) = Lowest priority (as designed)


    -- ============================================================
    -- SEARCH THRESHOLDS
    -- ============================================================


    SELECT public.configure_system(p_rerank_depth =&amp;gt; 15);
    -- What it does: Number of documents sent to LLM reranker
    -- Suggestions:
    -- &amp;nbsp; Higher (20-30) = Better accuracy, slower, more expensive
    -- &amp;nbsp; Lower (5-10) = Faster, cheaper, less accurate
    -- &amp;nbsp; Default (15) = Good balance


    SELECT public.configure_system(p_search_limit =&amp;gt; 10);
    -- What it does: Maximum results returned to user
    -- Suggestions:
    -- &amp;nbsp; Higher (20-30) = More comprehensive results
    -- &amp;nbsp; Lower (5-8) = Faster response, focused results
    -- &amp;nbsp; Default (10) = Standard result count


    -- ============================================================
    -- BATCH UPDATE (Update multiple at once)
    -- ============================================================


    -- Example: Update all weights at once
    SELECT public.configure_system(
    &amp;nbsp; &amp;nbsp; p_rrf_weight_enrichment =&amp;gt; 15.0,
    &amp;nbsp; &amp;nbsp; p_rrf_weight_sql =&amp;gt; 10.0,
    &amp;nbsp; &amp;nbsp; p_rrf_weight_graph =&amp;gt; 5.0,
    &amp;nbsp; &amp;nbsp; p_rrf_weight_fts =&amp;gt; 3.0,
    &amp;nbsp; &amp;nbsp; p_rrf_weight_vector =&amp;gt; 1.0
    );


    -- Example: Update all models at once
    SELECT public.configure_system(
    &amp;nbsp; &amp;nbsp; p_model_router =&amp;gt; 'gemini-2.5-flash',
    &amp;nbsp; &amp;nbsp; p_model_reranker =&amp;gt; 'gemini-2.5-flash-lite',
    &amp;nbsp; &amp;nbsp; p_model_sql =&amp;gt; 'gemini-2.5-flash',
    &amp;nbsp; &amp;nbsp; p_model_extraction =&amp;gt; 'gemini-2.5-flash'
    );


    -- ============================================================
    -- VIEW CURRENT SETTINGS
    -- ============================================================


    SELECT jsonb_pretty(settings) FROM public.app_config WHERE id = 1;


    -- ============================================================
    -- RESET TO DEFAULTS
    -- ============================================================


    UPDATE public.app_config 
    SET settings = '{
    &amp;nbsp; &amp;nbsp; "chunk_size": 500,
    &amp;nbsp; &amp;nbsp; "chunk_overlap": 100,
    &amp;nbsp; &amp;nbsp; "graph_sample_rate": 5,
    &amp;nbsp; &amp;nbsp; "worker_batch_size": 5,
    &amp;nbsp; &amp;nbsp; "model_router": "gemini-2.5-flash",
    &amp;nbsp; &amp;nbsp; "model_reranker": "gemini-2.5-flash-lite",
    &amp;nbsp; &amp;nbsp; "model_sql": "gemini-2.5-flash",
    &amp;nbsp; &amp;nbsp; "model_extraction": "gemini-2.5-flash",
    &amp;nbsp; &amp;nbsp; "rrf_weight_enrichment": 15.0,
    &amp;nbsp; &amp;nbsp; "rrf_weight_sql": 10.0,
    &amp;nbsp; &amp;nbsp; "rrf_weight_graph": 5.0,
    &amp;nbsp; &amp;nbsp; "rrf_weight_fts": 3.0,
    &amp;nbsp; &amp;nbsp; "rrf_weight_vector": 1.0,
    &amp;nbsp; &amp;nbsp; "rerank_depth": 15,
    &amp;nbsp; &amp;nbsp; "min_vector_score": 0.01,
    &amp;nbsp; &amp;nbsp; "search_limit": 10
    }'::jsonb
    WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt;. Now go over to Edge Functions. Create the first one. Make sure you name it 'ingest-intelligent'...PLEASE NOTE, YOU MUST SET A SECRET FOR GOOGLE_API_KEY WITH YOUR GEMINI API KEY FOR THESE EDGE FUNCTIONS TO WORK :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
    import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
    const corsHeaders = {
    &amp;nbsp; 'Access-Control-Allow-Origin': '*',
    &amp;nbsp; 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
    };
    // --- CONFIG LOADER ---
    async function getConfig(supabase) {
    &amp;nbsp; const { data } = await supabase.from('app_config').select('settings').single();
    &amp;nbsp; const defaults = {
    &amp;nbsp; &amp;nbsp; chunk_size: 600,
    &amp;nbsp; &amp;nbsp; chunk_overlap: 100
    &amp;nbsp; };
    &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; ...defaults,
    &amp;nbsp; &amp;nbsp; ...data &amp;amp;&amp;amp; data.settings ? data.settings : {}
    &amp;nbsp; };
    }
    // --- HELPER 1: SEMANTIC CHUNKER ---
    function semanticChunker(text, maxSize = 600, overlap = 50) {
    &amp;nbsp; const cleanText = text.replace(/\r\n/g, "\n");
    &amp;nbsp; const separators = [
    &amp;nbsp; &amp;nbsp; "\n\n",
    &amp;nbsp; &amp;nbsp; "\n",
    &amp;nbsp; &amp;nbsp; ". ",
    &amp;nbsp; &amp;nbsp; "? ",
    &amp;nbsp; &amp;nbsp; "! ",
    &amp;nbsp; &amp;nbsp; " "
    &amp;nbsp; ];
    &amp;nbsp; function splitRecursive(input) {
    &amp;nbsp; &amp;nbsp; if (input.length &amp;lt;= maxSize) return [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; input
    &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; let splitBy = "";
    &amp;nbsp; &amp;nbsp; for (const sep of separators) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (input.includes(sep)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; splitBy = sep;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; break;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; if (!splitBy) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const chunks = [];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; for (let i = 0; i &amp;lt; input.length; i += maxSize) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; chunks.push(input.slice(i, i + maxSize));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; return chunks;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; const parts = input.split(splitBy);
    &amp;nbsp; &amp;nbsp; const finalChunks = [];
    &amp;nbsp; &amp;nbsp; let current = "";
    &amp;nbsp; &amp;nbsp; for (const part of parts) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const p = splitBy.trim() === "" ? part : part + splitBy;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (current.length + p.length &amp;gt; maxSize) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (current.trim()) finalChunks.push(current.trim());
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (p.length &amp;gt; maxSize) finalChunks.push(...splitRecursive(p));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else current = p;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; current += p;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; if (current.trim()) finalChunks.push(current.trim());
    &amp;nbsp; &amp;nbsp; return finalChunks;
    &amp;nbsp; }
    &amp;nbsp; let chunks = splitRecursive(cleanText);
    &amp;nbsp; if (overlap &amp;gt; 0 &amp;amp;&amp;amp; chunks.length &amp;gt; 1) {
    &amp;nbsp; &amp;nbsp; const overlapped = [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; chunks[0]
    &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; for (let i = 1; i &amp;lt; chunks.length; i++) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const prev = chunks[i - 1];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const tail = prev.length &amp;gt; overlap ? prev.slice(-overlap) : prev;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const snap = tail.indexOf(" ");
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const cleanTail = snap &amp;gt; -1 ? tail.slice(snap + 1) : tail;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; overlapped.push(cleanTail + " ... " + chunks[i]);
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; return overlapped;
    &amp;nbsp; }
    &amp;nbsp; return chunks;
    }
    // --- GEMINI CALLER ---
    async function callGemini(prompt, apiKey, model = "gemini-2.5-flash") {
    &amp;nbsp; try {
    &amp;nbsp; &amp;nbsp; const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; method: "POST",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; "Content-Type": "application/json"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; &amp;nbsp; body: JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; contents: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; parts: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; text: prompt
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ]
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; generationConfig: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; temperature: 0.1,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; responseMimeType: "application/json"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; })
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; const data = await response.json();
    &amp;nbsp; &amp;nbsp; const text = data.candidates &amp;amp;&amp;amp; data.candidates[0] &amp;amp;&amp;amp; data.candidates[0].content &amp;amp;&amp;amp; data.candidates[0].content.parts &amp;amp;&amp;amp; data.candidates[0].content.parts[0] &amp;amp;&amp;amp; data.candidates[0].content.parts[0].text || "{}";
    &amp;nbsp; &amp;nbsp; return JSON.parse(text);
    &amp;nbsp; } catch (e) {
    &amp;nbsp; &amp;nbsp; console.error("Gemini Error:", e);
    &amp;nbsp; &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; nodes: [],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; edges: []
    &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; }
    }
    // --- SMART GRAPH BUILDER ---
    async function buildGraphGeneric(rows, tableName, apiKey) {
    &amp;nbsp; console.log(`[Graph] Building graph for ${tableName} (${rows.length} rows)`);
    &amp;nbsp; const hints = await analyzeRelationships(rows, tableName, apiKey);
    &amp;nbsp; if (hints &amp;amp;&amp;amp; hints.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; console.log(`[Graph] Using AI hints (${hints.length} relationships found)`);
    &amp;nbsp; &amp;nbsp; return buildFromHints(rows, hints);
    &amp;nbsp; }
    &amp;nbsp; console.log(`[Graph] No AI hints, using generic heuristics`);
    &amp;nbsp; return buildFromHeuristics(rows, tableName);
    }
    // --- AI Analysis ---
    async function analyzeRelationships(rows, tableName, apiKey) {
    &amp;nbsp; const sample = rows.slice(0, 5);
    &amp;nbsp; const headers = Object.keys(sample[0] || {});
    &amp;nbsp; const prompt = `You are a data relationship analyzer.


    &amp;nbsp; Table: ${tableName}
    &amp;nbsp; Columns: ${headers.join(', ')}
    &amp;nbsp; Sample Data: ${JSON.stringify(sample, null, 2)}


    &amp;nbsp; TASK: Identify HIGH-VALUE relationships ONLY.


    &amp;nbsp; ✅ PRIORITIZE (High-Value Relationships):
    &amp;nbsp; 1. **Person-to-Person**: Manager-employee, mentor-mentee, colleague relationships
    &amp;nbsp; &amp;nbsp; - employee_id → manager_id = "REPORTS_TO"
    &amp;nbsp; &amp;nbsp; - manager_id → employee_id = "MANAGES"
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; 2. **Business Process**: Order-customer, shipment-warehouse, payment-account
    &amp;nbsp; &amp;nbsp; - order_id → customer_id = "PLACED_BY"
    &amp;nbsp; &amp;nbsp; - order_id → warehouse_id = "FULFILLED_FROM"
    &amp;nbsp; &amp;nbsp; - shipment_id → carrier_id = "SHIPPED_BY"
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; 3. **Ownership/Assignment**: Asset-owner, project-lead, task-assignee
    &amp;nbsp; &amp;nbsp; - warehouse_id → manager_name = "MANAGED_BY"
    &amp;nbsp; &amp;nbsp; - project_id → owner_id = "OWNED_BY"


    &amp;nbsp; ❌ IGNORE (Low-Value Relationships):
    &amp;nbsp; 1. **Generic Attributes**: HAS_STATUS, HAS_TYPE, HAS_CATEGORY
    &amp;nbsp; &amp;nbsp; - order_id → order_status (this is an attribute, not a relationship)
    &amp;nbsp; &amp;nbsp; - item_id → item_type (this is classification, not a relationship)
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; 2. **Carrier/Infrastructure**: Unless directly person-related
    &amp;nbsp; &amp;nbsp; - order_id → carrier_id (weak relationship, often just logistics)
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; 3. **Self-References**: Same entity on both sides
    &amp;nbsp; &amp;nbsp; - employee_id → employee_id (invalid)


    &amp;nbsp; RELATIONSHIP QUALITY CRITERIA:
    &amp;nbsp; - **High**: Connects two different entities with meaningful business relationship
    &amp;nbsp; - **Medium**: Connects entities but relationship is transactional
    &amp;nbsp; - **Low**: Just describes an attribute or status


    &amp;nbsp; OUTPUT RULES:
    &amp;nbsp; - Only return relationships with confidence "high" or "medium"
    &amp;nbsp; - Skip any relationship that just describes an attribute
    &amp;nbsp; - Focus on relationships between ENTITIES, not entity-to-attribute


    &amp;nbsp; Output ONLY valid JSON array:
    &amp;nbsp; [
    &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; "from_col": "source_column",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; "to_col": "target_column", 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; "relationship": "VERB_DESCRIBING_RELATIONSHIP",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; "confidence": "high",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; "explanation": "Why this relationship is valuable"
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; ]


    &amp;nbsp; If no HIGH-VALUE relationships exist, return empty array [].`;
    &amp;nbsp; try {
    &amp;nbsp; &amp;nbsp; const result = await callGemini(prompt, apiKey, "gemini-2.5-flash");
    &amp;nbsp; &amp;nbsp; if (Array.isArray(result)) return result;
    &amp;nbsp; &amp;nbsp; if (result.relationships) return result.relationships;
    &amp;nbsp; &amp;nbsp; return [];
    &amp;nbsp; } catch (e) {
    &amp;nbsp; &amp;nbsp; console.error("[Graph] AI analysis failed:", e);
    &amp;nbsp; &amp;nbsp; return [];
    &amp;nbsp; }
    }
    // --- V10: GRAPH EDGE QUALITY VALIDATOR ---
    function validateGraphEdge(srcNode, dstNode, edgeType, context) {
    &amp;nbsp; const validation = {
    &amp;nbsp; &amp;nbsp; valid: true,
    &amp;nbsp; &amp;nbsp; reason: null,
    &amp;nbsp; &amp;nbsp; priority: 'normal'
    &amp;nbsp; };
    &amp;nbsp; // Get clean names for comparison
    &amp;nbsp; const srcName = srcNode.props?.name || srcNode.key || '';
    &amp;nbsp; const dstName = dstNode.props?.name || dstNode.key || '';
    &amp;nbsp; // Rule 1: Reject self-referential edges
    &amp;nbsp; if (srcName === dstName) {
    &amp;nbsp; &amp;nbsp; validation.valid = false;
    &amp;nbsp; &amp;nbsp; validation.reason = `Self-referential: ${srcName} → ${srcName}`;
    &amp;nbsp; &amp;nbsp; return validation;
    &amp;nbsp; }
    &amp;nbsp; // Rule 2: Reject if both nodes have same key prefix (duplicates)
    &amp;nbsp; const srcPrefix = srcNode.key?.split(':')[0];
    &amp;nbsp; const dstPrefix = dstNode.key?.split(':')[0];
    &amp;nbsp; if (srcPrefix === dstPrefix &amp;amp;&amp;amp; srcName === dstName) {
    &amp;nbsp; &amp;nbsp; validation.valid = false;
    &amp;nbsp; &amp;nbsp; validation.reason = `Duplicate nodes: ${srcNode.key} → ${dstNode.key}`;
    &amp;nbsp; &amp;nbsp; return validation;
    &amp;nbsp; }
    &amp;nbsp; // Rule 3: Define relationship priorities
    &amp;nbsp; const VALUABLE_RELATIONSHIPS = new Set([
    &amp;nbsp; &amp;nbsp; 'REPORTS_TO',
    &amp;nbsp; &amp;nbsp; 'MANAGES',
    &amp;nbsp; &amp;nbsp; 'WORKS_WITH',
    &amp;nbsp; &amp;nbsp; 'ASSIGNED_TO',
    &amp;nbsp; &amp;nbsp; 'PLACED_BY',
    &amp;nbsp; &amp;nbsp; 'FULFILLED_FROM',
    &amp;nbsp; &amp;nbsp; 'SHIPPED_BY'
    &amp;nbsp; ]);
    &amp;nbsp; const LOW_VALUE_RELATIONSHIPS = new Set([
    &amp;nbsp; &amp;nbsp; 'HAS_CARRIER',
    &amp;nbsp; &amp;nbsp; 'HAS_STATUS',
    &amp;nbsp; &amp;nbsp; 'HAS_TYPE',
    &amp;nbsp; &amp;nbsp; 'HAS_CATEGORY'
    &amp;nbsp; ]);
    &amp;nbsp; // Rule 4: Reject generic "HAS_*" relationships unless high priority
    &amp;nbsp; if (edgeType.startsWith('HAS_') &amp;amp;&amp;amp; !VALUABLE_RELATIONSHIPS.has(edgeType)) {
    &amp;nbsp; &amp;nbsp; if (LOW_VALUE_RELATIONSHIPS.has(edgeType)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; validation.valid = false;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; validation.reason = `Low-value relationship: ${edgeType}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; return validation;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; }
    &amp;nbsp; // Rule 5: Reject if context suggests it's inferred from ID column only
    &amp;nbsp; const contextStr = context?.context || context?.explanation || '';
    &amp;nbsp; if (contextStr.includes('Inferred from carrier_id') || contextStr.includes('Inferred from status') || contextStr.includes('Inferred from type')) {
    &amp;nbsp; &amp;nbsp; validation.valid = false;
    &amp;nbsp; &amp;nbsp; validation.reason = `Low-confidence inference: ${contextStr}`;
    &amp;nbsp; &amp;nbsp; return validation;
    &amp;nbsp; }
    &amp;nbsp; // Rule 6: Boost person-to-person relationships
    &amp;nbsp; const isPersonToPerson = (srcNode.labels?.includes('Person') || srcNode.labels?.includes('Employee')) &amp;amp;&amp;amp; (dstNode.labels?.includes('Person') || dstNode.labels?.includes('Employee'));
    &amp;nbsp; if (isPersonToPerson &amp;amp;&amp;amp; VALUABLE_RELATIONSHIPS.has(edgeType)) {
    &amp;nbsp; &amp;nbsp; validation.priority = 'high';
    &amp;nbsp; }
    &amp;nbsp; // Rule 7: Reject edges with missing names
    &amp;nbsp; if (!srcName || !dstName || srcName === 'undefined' || dstName === 'undefined') {
    &amp;nbsp; &amp;nbsp; validation.valid = false;
    &amp;nbsp; &amp;nbsp; validation.reason = `Missing names: src="${srcName}", dst="${dstName}"`;
    &amp;nbsp; &amp;nbsp; return validation;
    &amp;nbsp; }
    &amp;nbsp; return validation;
    }
    // --- BUILD FROM HINTS (DEDUPLICATED) ---
    function buildFromHints(rows, hints) {
    &amp;nbsp; const nodeMap = new Map();
    &amp;nbsp; const edgeMap = new Map();
    &amp;nbsp; const clean = (s) =&amp;gt; String(s).toLowerCase().trim().replace(/[^a-z0-9]/g, "_");
    &amp;nbsp; // Build ID maps
    &amp;nbsp; const idMaps = {};
    &amp;nbsp; for (const hint of hints) {
    &amp;nbsp; &amp;nbsp; if (hint.from_col.includes('_id') || hint.to_col.includes('_id')) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const idCol = hint.from_col.includes('_id') ? hint.from_col : hint.to_col;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const nameCol = findNameColumn(rows[0], idCol);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (nameCol) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; idMaps[idCol] = {};
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; rows.forEach((r) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (r[idCol] &amp;amp;&amp;amp; r[nameCol]) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; idMaps[idCol][r[idCol]] = r[nameCol];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; }
    &amp;nbsp; // Process each row
    &amp;nbsp; for (const row of rows) {
    &amp;nbsp; &amp;nbsp; for (const hint of hints) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const fromVal = row[hint.from_col];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const toVal = row[hint.to_col];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!fromVal || !toVal) continue;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ FIXED: Removed optional chaining
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const fromIdMap = idMaps[hint.from_col];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const toIdMap = idMaps[hint.to_col];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const resolvedFrom = fromIdMap &amp;amp;&amp;amp; fromIdMap[fromVal] || fromVal;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const resolvedTo = toIdMap &amp;amp;&amp;amp; toIdMap[toVal] || toVal;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const fromKey = `entity:${clean(String(resolvedFrom))}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const toKey = `entity:${clean(String(resolvedTo))}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!nodeMap.has(fromKey)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; nodeMap.set(fromKey, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; key: fromKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; labels: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; inferEntityType(hint.from_col)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; props: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name: String(resolvedFrom)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!nodeMap.has(toKey)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; nodeMap.set(toKey, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; key: toKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; labels: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; inferEntityType(hint.to_col)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; props: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name: String(resolvedTo)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ V10: VALIDATE EDGE BEFORE ADDING
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const srcNode = nodeMap.get(fromKey);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const dstNode = nodeMap.get(toKey);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const edgeType = hint.relationship || 'RELATES_TO';
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const edgeContext = {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; context: hint.explanation || `${hint.from_col} → ${hint.to_col}`,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; confidence: hint.confidence || 'medium'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const validation = validateGraphEdge(srcNode, dstNode, edgeType, edgeContext);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (validation.valid) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const edgeKey = `${fromKey}-${edgeType}-${toKey}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!edgeMap.has(edgeKey)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; edgeMap.set(edgeKey, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; src_key: fromKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; dst_key: toKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; type: edgeType,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; props: edgeContext
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Graph Quality] REJECTED edge: ${validation.reason}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; }
    &amp;nbsp; const nodes = Array.from(nodeMap.values());
    &amp;nbsp; const edges = Array.from(edgeMap.values());
    &amp;nbsp; console.log(`[Graph] Deduplicated: ${nodes.length} unique nodes, ${edges.length} unique edges`);
    &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; nodes,
    &amp;nbsp; &amp;nbsp; edges
    &amp;nbsp; };
    }
    // --- V10: NODE KEY NORMALIZER (PREVENTS DUPLICATES) ---
    function normalizeNodeKey(tableName, entityName) {
    &amp;nbsp; const clean = (s) =&amp;gt; String(s).toLowerCase().trim().replace(/[^a-z0-9]/g, "_");
    &amp;nbsp; // Always use tbl_ prefix for consistency
    &amp;nbsp; let normalizedTable = tableName;
    &amp;nbsp; if (!normalizedTable.startsWith('tbl_')) {
    &amp;nbsp; &amp;nbsp; normalizedTable = `tbl_${tableName}`;
    &amp;nbsp; }
    &amp;nbsp; return `${normalizedTable}:${clean(String(entityName))}`;
    }
    // --- BUILD FROM HEURISTICS (DEDUPLICATED) ---
    function buildFromHeuristics(rows, tableName) {
    &amp;nbsp; const nodeMap = new Map();
    &amp;nbsp; const edgeMap = new Map();
    &amp;nbsp; const entityRegistry = new Map();
    &amp;nbsp; const clean = (s) =&amp;gt; String(s).toLowerCase().trim().replace(/[^a-z0-9]/g, "_");
    &amp;nbsp; const firstRow = rows[0] || {};
    &amp;nbsp; const columns = Object.keys(firstRow);
    &amp;nbsp; const idColumns = columns.filter((c) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; return c.endsWith('_id') || c === 'id' || c.includes('identifier');
    &amp;nbsp; });
    &amp;nbsp; const nameColumns = columns.filter((c) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; return c.includes('name') || c === 'title' || c === 'label';
    &amp;nbsp; });
    &amp;nbsp; if (nameColumns.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; const primaryName = nameColumns[0];
    &amp;nbsp; &amp;nbsp; rows.forEach((row) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const entityName = row[primaryName];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (entityName) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ V10: Use normalized key and check entity registry
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const normalizedName = clean(String(entityName));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const key = normalizeNodeKey(tableName, entityName);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!entityRegistry.has(normalizedName)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; entityRegistry.set(normalizedName, key); // Track this entity
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; nodeMap.set(key, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; key,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; labels: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; tableName
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; props: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name: String(entityName),
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; source: tableName
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Graph Dedup] Created primary node: ${key}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Graph Dedup] Skipped duplicate primary: ${entityName} (already exists as ${entityRegistry.get(normalizedName)})`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; }
    &amp;nbsp; for (const col of idColumns) {
    &amp;nbsp; &amp;nbsp; if (col === 'id') continue;
    &amp;nbsp; &amp;nbsp; const referencedTable = col.replace(/_id$/, '');
    &amp;nbsp; &amp;nbsp; const correspondingName = findNameColumn(firstRow, col);
    &amp;nbsp; &amp;nbsp; if (correspondingName) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; rows.forEach((row) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const fkValue = row[col];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const fkName = row[correspondingName];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (fkValue &amp;amp;&amp;amp; fkName) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ V10: Check if entity already exists before creating node
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const normalizedFkName = clean(String(fkName));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; let fkKey;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (entityRegistry.has(normalizedFkName)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Entity already exists, use existing key
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fkKey = entityRegistry.get(normalizedFkName);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Graph Dedup] Reusing existing node: ${fkKey} for FK ${fkName}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Create new node with normalized key
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fkKey = normalizeNodeKey(referencedTable, fkName);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; entityRegistry.set(normalizedFkName, fkKey);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; nodeMap.set(fkKey, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; key: fkKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; labels: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; referencedTable
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; props: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name: String(fkName)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Graph Dedup] Created FK node: ${fkKey}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (nameColumns.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const primaryName = row[nameColumns[0]];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (primaryName) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const primaryKey = `${tableName}:${clean(String(primaryName))}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ V10: VALIDATE HEURISTIC EDGE
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const edgeType = `HAS_${referencedTable.toUpperCase()}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const srcNode = nodeMap.get(primaryKey);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const dstNode = nodeMap.get(fkKey);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const edgeContext = {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; context: `Inferred from ${col}`
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const validation = validateGraphEdge(srcNode, dstNode, edgeType, edgeContext);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (validation.valid) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const edgeKey = `${primaryKey}-${edgeType}-${fkKey}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!edgeMap.has(edgeKey)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; edgeMap.set(edgeKey, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; src_key: primaryKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; dst_key: fkKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; type: edgeType,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; props: edgeContext
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Graph Quality] REJECTED heuristic edge: ${validation.reason}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; }
    &amp;nbsp; const nodes = Array.from(nodeMap.values());
    &amp;nbsp; const edges = Array.from(edgeMap.values());
    &amp;nbsp; console.log(`[Graph] Heuristic mode: ${nodes.length} unique nodes, ${edges.length} unique edges`);
    &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; nodes,
    &amp;nbsp; &amp;nbsp; edges
    &amp;nbsp; };
    }
    // --- HELPER FUNCTIONS ---
    function findNameColumn(row, idColumn) {
    &amp;nbsp; const keys = Object.keys(row);
    &amp;nbsp; const baseName = idColumn.replace(/_id$/, '');
    &amp;nbsp; const candidates = [
    &amp;nbsp; &amp;nbsp; `${baseName}_name`,
    &amp;nbsp; &amp;nbsp; `${baseName}_title`,
    &amp;nbsp; &amp;nbsp; `${baseName}`,
    &amp;nbsp; &amp;nbsp; 'name',
    &amp;nbsp; &amp;nbsp; 'title',
    &amp;nbsp; &amp;nbsp; 'label'
    &amp;nbsp; ];
    &amp;nbsp; for (const candidate of candidates) {
    &amp;nbsp; &amp;nbsp; if (keys.includes(candidate)) return candidate;
    &amp;nbsp; }
    &amp;nbsp; return null;
    }
    function inferEntityType(columnName) {
    &amp;nbsp; if (columnName.includes('employee') || columnName.includes('person')) return 'Person';
    &amp;nbsp; if (columnName.includes('customer') || columnName.includes('client')) return 'Customer';
    &amp;nbsp; if (columnName.includes('warehouse') || columnName.includes('location')) return 'Location';
    &amp;nbsp; if (columnName.includes('product') || columnName.includes('item')) return 'Product';
    &amp;nbsp; if (columnName.includes('order')) return 'Order';
    &amp;nbsp; if (columnName.includes('company') || columnName.includes('organization')) return 'Organization';
    &amp;nbsp; return 'Entity';
    }
    async function analyzeTableSchema(tableName, rows, apiKey) {
    &amp;nbsp; const sample = rows.slice(0, 3);
    &amp;nbsp; const headers = Object.keys(sample[0] || {});
    &amp;nbsp; const prompt = `You are a data schema analyst. Analyze this table:


    Table Name: ${tableName}
    Columns: ${headers.join(', ')}
    Sample Data: ${JSON.stringify(sample, null, 2)}


    Provide:
    1. description: One sentence explaining what this data represents
    2. semantics: For each column, identify its semantic type. Options:
    &amp;nbsp; &amp;nbsp;- person_name, company_name, location_name
    &amp;nbsp; &amp;nbsp;- currency_amount, percentage, count
    &amp;nbsp; &amp;nbsp;- date, datetime, duration
    &amp;nbsp; &amp;nbsp;- identifier, category, description, status
    3. graph_hints: Relationships that could form knowledge graph edges. Format:
    &amp;nbsp; &amp;nbsp;[{"from_col": "manager_id", "to_col": "employee_id", "edge_type": "MANAGES", "confidence": "high"}]


    Output ONLY valid JSON:
    {
    &amp;nbsp; "description": "...",
    &amp;nbsp; "semantics": {"col1": "type", ...},
    &amp;nbsp; "graph_hints": [...]
    }`;
    &amp;nbsp; try {
    &amp;nbsp; &amp;nbsp; const result = await callGemini(prompt, apiKey, "gemini-2.5-flash");
    &amp;nbsp; &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; description: result.description || `Data table: ${tableName}`,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; semantics: result.semantics || {},
    &amp;nbsp; &amp;nbsp; &amp;nbsp; graphHints: result.graph_hints || []
    &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; } catch (e) {
    &amp;nbsp; &amp;nbsp; console.error("Schema analysis failed:", e);
    &amp;nbsp; &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; description: `Data table: ${tableName}`,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; semantics: {},
    &amp;nbsp; &amp;nbsp; &amp;nbsp; graphHints: []
    &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; }
    }
    // --- MAIN LOGIC: SPREADSHEET ---
    async function processSpreadsheetCore(uri, title, rows, env) {
    &amp;nbsp; const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
    &amp;nbsp; console.log(`[Spreadsheet] Processing ${rows.length} rows for: ${title}`);
    &amp;nbsp; // 1. Clean Rows
    &amp;nbsp; const cleanRows = rows.map((row) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; const newRow = {};
    &amp;nbsp; &amp;nbsp; Object.keys(row).forEach((k) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const cleanKey = k.toLowerCase().trim().replace(/[^a-z0-9]/g, '_');
    &amp;nbsp; &amp;nbsp; &amp;nbsp; newRow[cleanKey] = row[k];
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; return newRow;
    &amp;nbsp; });
    &amp;nbsp; // 2. Schema Inference
    &amp;nbsp; const firstRow = cleanRows[0] || {};
    &amp;nbsp; const schema = {};
    &amp;nbsp; Object.keys(firstRow).forEach((k) =&amp;gt; schema[k] = typeof firstRow[k]);
    &amp;nbsp; const safeName = title.toLowerCase().replace(/[^a-z0-9]/g, '_');
    &amp;nbsp; const tableName = `tbl_${safeName}`;
    &amp;nbsp; // 3. BUILD ID MAP
    &amp;nbsp; const idMap = {};
    &amp;nbsp; cleanRows.forEach((r) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; if (r.employee_id &amp;amp;&amp;amp; r.name) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; idMap[r.employee_id] = r.name;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; });
    &amp;nbsp; // 4. ANALYZE SCHEMA
    &amp;nbsp; console.log(`[V8] Analyzing schema for: ${title}`);
    &amp;nbsp; const schemaAnalysis = await analyzeTableSchema(title, cleanRows, env.GOOGLE_API_KEY);
    &amp;nbsp; console.log(`[V8] Analysis complete:`, schemaAnalysis);
    &amp;nbsp; // 5. GENERATE GRAPH
    &amp;nbsp; console.log(`[Graph] Building graph using generic approach...`);
    &amp;nbsp; const { nodes: allNodes, edges: allEdges } = await buildGraphGeneric(cleanRows, tableName, env.GOOGLE_API_KEY);
    &amp;nbsp; console.log(`[Graph] Generated ${allNodes.length} nodes and ${allEdges.length} edges.`);
    &amp;nbsp; // 6. DB Insert
    &amp;nbsp; const BATCH_SIZE = 500;
    &amp;nbsp; for (let i = 0; i &amp;lt; cleanRows.length; i += BATCH_SIZE) {
    &amp;nbsp; &amp;nbsp; const rowBatch = cleanRows.slice(i, i + BATCH_SIZE);
    &amp;nbsp; &amp;nbsp; const nodesBatch = i === 0 ? allNodes : [];
    &amp;nbsp; &amp;nbsp; const edgesBatch = i === 0 ? allEdges : [];
    &amp;nbsp; &amp;nbsp; const { error } = await supabase.rpc('ingest_spreadsheet', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_uri: uri,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_title: title,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_table_name: safeName,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_description: null,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_rows: rowBatch,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_schema: schema,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_nodes: nodesBatch,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_edges: edgesBatch
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; if (error) throw new Error(`Batch ${i} error: ${error.message}`);
    &amp;nbsp; }
    &amp;nbsp; // 7. SAVE METADATA
    &amp;nbsp; console.log(`[V8] Saving metadata for ${tableName}...`);
    &amp;nbsp; const { error: metaError } = await supabase.from('structured_table').update({
    &amp;nbsp; &amp;nbsp; description: schemaAnalysis.description,
    &amp;nbsp; &amp;nbsp; column_semantics: schemaAnalysis.semantics,
    &amp;nbsp; &amp;nbsp; graph_hints: schemaAnalysis.graphHints,
    &amp;nbsp; &amp;nbsp; sample_row: cleanRows[0]
    &amp;nbsp; }).eq('table_name', tableName);
    &amp;nbsp; if (metaError) {
    &amp;nbsp; &amp;nbsp; console.error(`[V8] Metadata save failed:`, metaError);
    &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; console.log(`[V8] Metadata saved successfully`);
    &amp;nbsp; }
    &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; success: true,
    &amp;nbsp; &amp;nbsp; rows: cleanRows.length,
    &amp;nbsp; &amp;nbsp; graph_nodes: allNodes.length,
    &amp;nbsp; &amp;nbsp; metadata: schemaAnalysis
    &amp;nbsp; };
    }
    // --- MAIN HANDLER ---
    serve(async (req) =&amp;gt; {
    &amp;nbsp; if (req.method === 'OPTIONS') {
    &amp;nbsp; &amp;nbsp; return new Response('ok', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: corsHeaders
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; }
    &amp;nbsp; try {
    &amp;nbsp; &amp;nbsp; const env = Deno.env.toObject();
    &amp;nbsp; &amp;nbsp; const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
    &amp;nbsp; &amp;nbsp; const body = await req.json();
    &amp;nbsp; &amp;nbsp; const { uri, title, text, data } = body;
    &amp;nbsp; &amp;nbsp; if (!uri || !title) throw new Error("Missing 'uri' or 'title'");
    &amp;nbsp; &amp;nbsp; if (!text &amp;amp;&amp;amp; !data) throw new Error("Must provide 'text' or 'data'");
    &amp;nbsp; &amp;nbsp; // DOC PATH
    &amp;nbsp; &amp;nbsp; if (text) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const config = await getConfig(supabase);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const chunks = semanticChunker(text, config.chunk_size, config.chunk_overlap);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const rows = chunks.map((chunk, idx) =&amp;gt; ({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; uri,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; chunk_index: idx,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; chunk_text: chunk,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; status: 'pending'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; for (let i = 0; i &amp;lt; rows.length; i += 100) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const { error } = await supabase.from('ingestion_queue').insert(rows.slice(i, i + 100));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (error) throw error;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; fetch(`${env.SUPABASE_URL}/functions/v1/ingest-worker`, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; method: 'POST',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Authorization': `Bearer ${env.SUPABASE_SERVICE_ROLE_KEY}`,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Content-Type': 'application/json'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; body: JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; action: 'start_processing'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; })
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }).catch((e) =&amp;gt; console.error("Worker trigger failed", e));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; return new Response(JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; success: true,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; message: `Queued ${chunks.length} chunks.`
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }), {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...corsHeaders,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Content-Type': 'application/json'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // DATA PATH
    &amp;nbsp; &amp;nbsp; if (data) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const payloadSize = JSON.stringify(data).length;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (payloadSize &amp;lt; 40000) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const result = await processSpreadsheetCore(uri, title, data, env);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return new Response(JSON.stringify(result), {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...corsHeaders,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Content-Type': 'application/json'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const fileName = `${Date.now()}_${uri.replace(/[^a-z0-9]/gi, '_')}.json`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const { error } = await supabase.storage.from('raw_uploads').upload(fileName, JSON.stringify(body), {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; contentType: 'application/json'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (error) throw error;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return new Response(JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; status: "queued",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; message: "Large file uploaded to background queue."
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }), {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; status: 202,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...corsHeaders,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Content-Type': 'application/json'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; } catch (error) {
    &amp;nbsp; &amp;nbsp; return new Response(JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; error: error.message
    &amp;nbsp; &amp;nbsp; }), {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; status: 500,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...corsHeaders,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Content-Type': 'application/json'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; }
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5&lt;/strong&gt;. Create another Edge Function. Name this one 'ingest-worker':&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
    import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
    const corsHeaders = {
    &amp;nbsp; 'Access-Control-Allow-Origin': '*',
    &amp;nbsp; 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
    };
    // --- CONFIG LOADER ---
    async function getConfig(supabase) {
    &amp;nbsp; const { data } = await supabase.from('app_config').select('settings').single();
    &amp;nbsp; const defaults = {
    &amp;nbsp; &amp;nbsp; graph_sample_rate: 5,
    &amp;nbsp; &amp;nbsp; worker_batch_size: 5,
    &amp;nbsp; &amp;nbsp; model_extraction: "gemini-2.5-flash"
    &amp;nbsp; };
    &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; ...defaults,
    &amp;nbsp; &amp;nbsp; ...data?.settings || {}
    &amp;nbsp; };
    }
    // --- GEMINI CALLER ---
    async function callGemini(prompt, apiKey, model) {
    &amp;nbsp; console.log(`[Gemini] Calling ${model}...`);
    &amp;nbsp; try {
    &amp;nbsp; &amp;nbsp; const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; method: "POST",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; "Content-Type": "application/json"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; &amp;nbsp; body: JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; contents: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; parts: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; text: prompt
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ]
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; generationConfig: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; temperature: 0.1,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; responseMimeType: "application/json"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; })
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; if (!response.ok) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.error(`[Gemini] HTTP ${response.status}: ${response.statusText}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; description: "Analysis unavailable",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; semantics: {},
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; graph_hints: []
    &amp;nbsp; &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; const data = await response.json();
    &amp;nbsp; &amp;nbsp; const text = data.candidates?.[0]?.content?.parts?.[0]?.text || "{}";
    &amp;nbsp; &amp;nbsp; console.log(`[Gemini] Response received: ${text.substring(0, 100)}...`);
    &amp;nbsp; &amp;nbsp; return JSON.parse(text);
    &amp;nbsp; } catch (e) {
    &amp;nbsp; &amp;nbsp; console.error("[Gemini] Error:", e);
    &amp;nbsp; &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; description: "Analysis failed",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; semantics: {},
    &amp;nbsp; &amp;nbsp; &amp;nbsp; graph_hints: []
    &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; }
    }
    // --- EMBEDDING HELPER ---
    async function getEmbedding(text, apiKey) {
    &amp;nbsp; const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=${apiKey}`, {
    &amp;nbsp; &amp;nbsp; method: "POST",
    &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; "Content-Type": "application/json"
    &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; body: JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; model: "models/gemini-embedding-001",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; content: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; parts: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; text
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ]
    &amp;nbsp; &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; &amp;nbsp; outputDimensionality: 768
    &amp;nbsp; &amp;nbsp; })
    &amp;nbsp; });
    &amp;nbsp; const data = await response.json();
    &amp;nbsp; return data.embedding?.values || [];
    }
    // --- HELPER: SMART GRAPH BUILDER (LEGACY - FALLBACK) ---
    async function buildGraphGeneric(rows, tableName, apiKey) {
    &amp;nbsp; console.log(`[Graph] Building graph for ${tableName} (${rows.length} rows)`);
    &amp;nbsp; // Step 1: Try AI Analysis
    &amp;nbsp; const hints = await analyzeRelationships(rows, tableName, apiKey);
    &amp;nbsp; if (hints &amp;amp;&amp;amp; hints.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; console.log(`[Graph] Using AI hints (${hints.length} relationships found)`);
    &amp;nbsp; &amp;nbsp; return buildFromHints(rows, hints);
    &amp;nbsp; }
    &amp;nbsp; // Step 2: Generic Heuristic Fallback
    &amp;nbsp; console.log(`[Graph] No AI hints, using generic heuristics`);
    &amp;nbsp; return buildFromHeuristics(rows, tableName);
    }
    // --- AI Analysis (Enhanced Prompt) ---
    async function analyzeRelationships(rows, tableName, apiKey) {
    &amp;nbsp; const sample = rows.slice(0, 5); // More samples = better analysis
    &amp;nbsp; const headers = Object.keys(sample[0] || {});
    &amp;nbsp; const prompt = `You are a data relationship analyzer.


    &amp;nbsp; Table: ${tableName}
    &amp;nbsp; Columns: ${headers.join(', ')}
    &amp;nbsp; Sample Data: ${JSON.stringify(sample, null, 2)}


    &amp;nbsp; TASK: Identify HIGH-VALUE relationships ONLY.


    &amp;nbsp; ✅ PRIORITIZE (High-Value Relationships):
    &amp;nbsp; 1. **Person-to-Person**: Manager-employee, mentor-mentee, colleague relationships
    &amp;nbsp; &amp;nbsp; - employee_id → manager_id = "REPORTS_TO"
    &amp;nbsp; &amp;nbsp; - manager_id → employee_id = "MANAGES"
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; 2. **Business Process**: Order-customer, shipment-warehouse, payment-account
    &amp;nbsp; &amp;nbsp; - order_id → customer_id = "PLACED_BY"
    &amp;nbsp; &amp;nbsp; - order_id → warehouse_id = "FULFILLED_FROM"
    &amp;nbsp; &amp;nbsp; - shipment_id → carrier_id = "SHIPPED_BY"
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; 3. **Ownership/Assignment**: Asset-owner, project-lead, task-assignee
    &amp;nbsp; &amp;nbsp; - warehouse_id → manager_name = "MANAGED_BY"
    &amp;nbsp; &amp;nbsp; - project_id → owner_id = "OWNED_BY"


    &amp;nbsp; ❌ IGNORE (Low-Value Relationships):
    &amp;nbsp; 1. **Generic Attributes**: HAS_STATUS, HAS_TYPE, HAS_CATEGORY
    &amp;nbsp; &amp;nbsp; - order_id → order_status (this is an attribute, not a relationship)
    &amp;nbsp; &amp;nbsp; - item_id → item_type (this is classification, not a relationship)
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; 2. **Carrier/Infrastructure**: Unless directly person-related
    &amp;nbsp; &amp;nbsp; - order_id → carrier_id (weak relationship, often just logistics)
    &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; 3. **Self-References**: Same entity on both sides
    &amp;nbsp; &amp;nbsp; - employee_id → employee_id (invalid)


    &amp;nbsp; RELATIONSHIP QUALITY CRITERIA:
    &amp;nbsp; - **High**: Connects two different entities with meaningful business relationship
    &amp;nbsp; - **Medium**: Connects entities but relationship is transactional
    &amp;nbsp; - **Low**: Just describes an attribute or status


    &amp;nbsp; OUTPUT RULES:
    &amp;nbsp; - Only return relationships with confidence "high" or "medium"
    &amp;nbsp; - Skip any relationship that just describes an attribute
    &amp;nbsp; - Focus on relationships between ENTITIES, not entity-to-attribute


    &amp;nbsp; Output ONLY valid JSON array:
    &amp;nbsp; [
    &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; "from_col": "source_column",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; "to_col": "target_column", 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; "relationship": "VERB_DESCRIBING_RELATIONSHIP",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; "confidence": "high",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; "explanation": "Why this relationship is valuable"
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; ]


    &amp;nbsp; If no HIGH-VALUE relationships exist, return empty array [].`;
    &amp;nbsp; try {
    &amp;nbsp; &amp;nbsp; const result = await callGemini(prompt, apiKey, "gemini-2.5-flash");
    &amp;nbsp; &amp;nbsp; // Handle both array and object responses
    &amp;nbsp; &amp;nbsp; if (Array.isArray(result)) return result;
    &amp;nbsp; &amp;nbsp; if (result.relationships) return result.relationships;
    &amp;nbsp; &amp;nbsp; return [];
    &amp;nbsp; } catch (e) {
    &amp;nbsp; &amp;nbsp; console.error("[Graph] AI analysis failed:", e);
    &amp;nbsp; &amp;nbsp; return [];
    &amp;nbsp; }
    }
    // --- V10: GRAPH EDGE QUALITY VALIDATOR ---
    function validateGraphEdge(srcNode, dstNode, edgeType, context) {
    &amp;nbsp; const validation = {
    &amp;nbsp; &amp;nbsp; valid: true,
    &amp;nbsp; &amp;nbsp; reason: null,
    &amp;nbsp; &amp;nbsp; priority: 'normal'
    &amp;nbsp; };
    &amp;nbsp; // Get clean names for comparison
    &amp;nbsp; const srcName = srcNode.props?.name || srcNode.key || '';
    &amp;nbsp; const dstName = dstNode.props?.name || dstNode.key || '';
    &amp;nbsp; // Rule 1: Reject self-referential edges
    &amp;nbsp; if (srcName === dstName) {
    &amp;nbsp; &amp;nbsp; validation.valid = false;
    &amp;nbsp; &amp;nbsp; validation.reason = `Self-referential: ${srcName} → ${srcName}`;
    &amp;nbsp; &amp;nbsp; return validation;
    &amp;nbsp; }
    &amp;nbsp; // Rule 2: Reject if both nodes have same key prefix (duplicates)
    &amp;nbsp; const srcPrefix = srcNode.key?.split(':')[0];
    &amp;nbsp; const dstPrefix = dstNode.key?.split(':')[0];
    &amp;nbsp; if (srcPrefix === dstPrefix &amp;amp;&amp;amp; srcName === dstName) {
    &amp;nbsp; &amp;nbsp; validation.valid = false;
    &amp;nbsp; &amp;nbsp; validation.reason = `Duplicate nodes: ${srcNode.key} → ${dstNode.key}`;
    &amp;nbsp; &amp;nbsp; return validation;
    &amp;nbsp; }
    &amp;nbsp; // Rule 3: Define relationship priorities
    &amp;nbsp; const VALUABLE_RELATIONSHIPS = new Set([
    &amp;nbsp; &amp;nbsp; 'REPORTS_TO',
    &amp;nbsp; &amp;nbsp; 'MANAGES',
    &amp;nbsp; &amp;nbsp; 'WORKS_WITH',
    &amp;nbsp; &amp;nbsp; 'ASSIGNED_TO',
    &amp;nbsp; &amp;nbsp; 'PLACED_BY',
    &amp;nbsp; &amp;nbsp; 'FULFILLED_FROM',
    &amp;nbsp; &amp;nbsp; 'SHIPPED_BY'
    &amp;nbsp; ]);
    &amp;nbsp; const LOW_VALUE_RELATIONSHIPS = new Set([
    &amp;nbsp; &amp;nbsp; 'HAS_CARRIER',
    &amp;nbsp; &amp;nbsp; 'HAS_STATUS',
    &amp;nbsp; &amp;nbsp; 'HAS_TYPE',
    &amp;nbsp; &amp;nbsp; 'HAS_CATEGORY'
    &amp;nbsp; ]);
    &amp;nbsp; // Rule 4: Reject generic "HAS_*" relationships unless high priority
    &amp;nbsp; if (edgeType.startsWith('HAS_') &amp;amp;&amp;amp; !VALUABLE_RELATIONSHIPS.has(edgeType)) {
    &amp;nbsp; &amp;nbsp; if (LOW_VALUE_RELATIONSHIPS.has(edgeType)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; validation.valid = false;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; validation.reason = `Low-value relationship: ${edgeType}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; return validation;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; }
    &amp;nbsp; // Rule 5: Reject if context suggests it's inferred from ID column only
    &amp;nbsp; const contextStr = context?.context || context?.explanation || '';
    &amp;nbsp; if (contextStr.includes('Inferred from carrier_id') || contextStr.includes('Inferred from status') || contextStr.includes('Inferred from type')) {
    &amp;nbsp; &amp;nbsp; validation.valid = false;
    &amp;nbsp; &amp;nbsp; validation.reason = `Low-confidence inference: ${contextStr}`;
    &amp;nbsp; &amp;nbsp; return validation;
    &amp;nbsp; }
    &amp;nbsp; // Rule 6: Boost person-to-person relationships
    &amp;nbsp; const isPersonToPerson = (srcNode.labels?.includes('Person') || srcNode.labels?.includes('Employee')) &amp;amp;&amp;amp; (dstNode.labels?.includes('Person') || dstNode.labels?.includes('Employee'));
    &amp;nbsp; if (isPersonToPerson &amp;amp;&amp;amp; VALUABLE_RELATIONSHIPS.has(edgeType)) {
    &amp;nbsp; &amp;nbsp; validation.priority = 'high';
    &amp;nbsp; }
    &amp;nbsp; // Rule 7: Reject edges with missing names
    &amp;nbsp; if (!srcName || !dstName || srcName === 'undefined' || dstName === 'undefined') {
    &amp;nbsp; &amp;nbsp; validation.valid = false;
    &amp;nbsp; &amp;nbsp; validation.reason = `Missing names: src="${srcName}", dst="${dstName}"`;
    &amp;nbsp; &amp;nbsp; return validation;
    &amp;nbsp; }
    &amp;nbsp; return validation;
    }
    // --- BUILD FROM HINTS (DEDUPLICATED) ---
    function buildFromHints(rows, hints) {
    &amp;nbsp; const nodeMap = new Map();
    &amp;nbsp; const edgeMap = new Map();
    &amp;nbsp; const clean = (s) =&amp;gt; String(s).toLowerCase().trim().replace(/[^a-z0-9]/g, "_");
    &amp;nbsp; // Build ID maps
    &amp;nbsp; const idMaps = {};
    &amp;nbsp; for (const hint of hints) {
    &amp;nbsp; &amp;nbsp; if (hint.from_col.includes('_id') || hint.to_col.includes('_id')) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const idCol = hint.from_col.includes('_id') ? hint.from_col : hint.to_col;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const nameCol = findNameColumn(rows[0], idCol);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (nameCol) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; idMaps[idCol] = {};
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; rows.forEach((r) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (r[idCol] &amp;amp;&amp;amp; r[nameCol]) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; idMaps[idCol][r[idCol]] = r[nameCol];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; }
    &amp;nbsp; // Process each row
    &amp;nbsp; for (const row of rows) {
    &amp;nbsp; &amp;nbsp; for (const hint of hints) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const fromVal = row[hint.from_col];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const toVal = row[hint.to_col];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!fromVal || !toVal) continue;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ FIXED: Removed optional chaining
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const fromIdMap = idMaps[hint.from_col];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const toIdMap = idMaps[hint.to_col];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const resolvedFrom = fromIdMap &amp;amp;&amp;amp; fromIdMap[fromVal] || fromVal;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const resolvedTo = toIdMap &amp;amp;&amp;amp; toIdMap[toVal] || toVal;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const fromKey = `entity:${clean(String(resolvedFrom))}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const toKey = `entity:${clean(String(resolvedTo))}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!nodeMap.has(fromKey)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; nodeMap.set(fromKey, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; key: fromKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; labels: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; inferEntityType(hint.from_col)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; props: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name: String(resolvedFrom)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!nodeMap.has(toKey)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; nodeMap.set(toKey, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; key: toKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; labels: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; inferEntityType(hint.to_col)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; props: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name: String(resolvedTo)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ V10: VALIDATE EDGE BEFORE ADDING
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const srcNode = nodeMap.get(fromKey);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const dstNode = nodeMap.get(toKey);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const edgeType = hint.relationship || 'RELATES_TO';
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const edgeContext = {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; context: hint.explanation || `${hint.from_col} → ${hint.to_col}`,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; confidence: hint.confidence || 'medium'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const validation = validateGraphEdge(srcNode, dstNode, edgeType, edgeContext);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (validation.valid) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const edgeKey = `${fromKey}-${edgeType}-${toKey}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!edgeMap.has(edgeKey)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; edgeMap.set(edgeKey, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; src_key: fromKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; dst_key: toKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; type: edgeType,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; props: edgeContext
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Graph Quality] REJECTED edge: ${validation.reason}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; }
    &amp;nbsp; const nodes = Array.from(nodeMap.values());
    &amp;nbsp; const edges = Array.from(edgeMap.values());
    &amp;nbsp; console.log(`[Graph] Deduplicated: ${nodes.length} unique nodes, ${edges.length} unique edges`);
    &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; nodes,
    &amp;nbsp; &amp;nbsp; edges
    &amp;nbsp; };
    }
    // --- V10: NODE KEY NORMALIZER (PREVENTS DUPLICATES) ---
    function normalizeNodeKey(tableName, entityName) {
    &amp;nbsp; const clean = (s) =&amp;gt; String(s).toLowerCase().trim().replace(/[^a-z0-9]/g, "_");
    &amp;nbsp; // Always use tbl_ prefix for consistency
    &amp;nbsp; let normalizedTable = tableName;
    &amp;nbsp; if (!normalizedTable.startsWith('tbl_')) {
    &amp;nbsp; &amp;nbsp; normalizedTable = `tbl_${tableName}`;
    &amp;nbsp; }
    &amp;nbsp; return `${normalizedTable}:${clean(String(entityName))}`;
    }
    // --- BUILD FROM HEURISTICS (DEDUPLICATED) ---
    function buildFromHeuristics(rows, tableName) {
    &amp;nbsp; const nodeMap = new Map();
    &amp;nbsp; const edgeMap = new Map();
    &amp;nbsp; const entityRegistry = new Map();
    &amp;nbsp; const clean = (s) =&amp;gt; String(s).toLowerCase().trim().replace(/[^a-z0-9]/g, "_");
    &amp;nbsp; const firstRow = rows[0] || {};
    &amp;nbsp; const columns = Object.keys(firstRow);
    &amp;nbsp; const idColumns = columns.filter((c) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; return c.endsWith('_id') || c === 'id' || c.includes('identifier');
    &amp;nbsp; });
    &amp;nbsp; const nameColumns = columns.filter((c) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; return c.includes('name') || c === 'title' || c === 'label';
    &amp;nbsp; });
    &amp;nbsp; if (nameColumns.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; const primaryName = nameColumns[0];
    &amp;nbsp; &amp;nbsp; rows.forEach((row) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const entityName = row[primaryName];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (entityName) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ V10: Use normalized key and check entity registry
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const normalizedName = clean(String(entityName));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const key = normalizeNodeKey(tableName, entityName);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!entityRegistry.has(normalizedName)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; entityRegistry.set(normalizedName, key); // Track this entity
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; nodeMap.set(key, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; key,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; labels: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; tableName
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; props: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name: String(entityName),
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; source: tableName
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Graph Dedup] Created primary node: ${key}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Graph Dedup] Skipped duplicate primary: ${entityName} (already exists as ${entityRegistry.get(normalizedName)})`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; }
    &amp;nbsp; for (const col of idColumns) {
    &amp;nbsp; &amp;nbsp; if (col === 'id') continue;
    &amp;nbsp; &amp;nbsp; const referencedTable = col.replace(/_id$/, '');
    &amp;nbsp; &amp;nbsp; const correspondingName = findNameColumn(firstRow, col);
    &amp;nbsp; &amp;nbsp; if (correspondingName) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; rows.forEach((row) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const fkValue = row[col];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const fkName = row[correspondingName];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (fkValue &amp;amp;&amp;amp; fkName) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ V10: Check if entity already exists before creating node
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const normalizedFkName = clean(String(fkName));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; let fkKey;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (entityRegistry.has(normalizedFkName)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Entity already exists, use existing key
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fkKey = entityRegistry.get(normalizedFkName);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Graph Dedup] Reusing existing node: ${fkKey} for FK ${fkName}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Create new node with normalized key
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fkKey = normalizeNodeKey(referencedTable, fkName);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; entityRegistry.set(normalizedFkName, fkKey);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; nodeMap.set(fkKey, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; key: fkKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; labels: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; referencedTable
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; props: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name: String(fkName)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Graph Dedup] Created FK node: ${fkKey}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (nameColumns.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const primaryName = row[nameColumns[0]];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (primaryName) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const primaryKey = `${tableName}:${clean(String(primaryName))}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ V10: VALIDATE HEURISTIC EDGE
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const edgeType = `HAS_${referencedTable.toUpperCase()}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const srcNode = nodeMap.get(primaryKey);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const dstNode = nodeMap.get(fkKey);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const edgeContext = {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; context: `Inferred from ${col}`
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const validation = validateGraphEdge(srcNode, dstNode, edgeType, edgeContext);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (validation.valid) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const edgeKey = `${primaryKey}-${edgeType}-${fkKey}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!edgeMap.has(edgeKey)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; edgeMap.set(edgeKey, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; src_key: primaryKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; dst_key: fkKey,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; type: edgeType,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; props: edgeContext
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Graph Quality] REJECTED heuristic edge: ${validation.reason}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; }
    &amp;nbsp; const nodes = Array.from(nodeMap.values());
    &amp;nbsp; const edges = Array.from(edgeMap.values());
    &amp;nbsp; console.log(`[Graph] Heuristic mode: ${nodes.length} unique nodes, ${edges.length} unique edges`);
    &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; nodes,
    &amp;nbsp; &amp;nbsp; edges
    &amp;nbsp; };
    }
    // --- Helper Functions ---
    function findNameColumn(row, idColumn) {
    &amp;nbsp; const keys = Object.keys(row);
    &amp;nbsp; // Try exact match: employee_id → employee_name
    &amp;nbsp; const baseName = idColumn.replace(/_id$/, '');
    &amp;nbsp; const candidates = [
    &amp;nbsp; &amp;nbsp; `${baseName}_name`,
    &amp;nbsp; &amp;nbsp; `${baseName}_title`,
    &amp;nbsp; &amp;nbsp; `${baseName}`,
    &amp;nbsp; &amp;nbsp; 'name',
    &amp;nbsp; &amp;nbsp; 'title',
    &amp;nbsp; &amp;nbsp; 'label'
    &amp;nbsp; ];
    &amp;nbsp; for (const candidate of candidates) {
    &amp;nbsp; &amp;nbsp; if (keys.includes(candidate)) return candidate;
    &amp;nbsp; }
    &amp;nbsp; return null;
    }
    function inferEntityType(columnName) {
    &amp;nbsp; // Infer entity type from column name
    &amp;nbsp; if (columnName.includes('employee') || columnName.includes('person')) return 'Person';
    &amp;nbsp; if (columnName.includes('customer') || columnName.includes('client')) return 'Customer';
    &amp;nbsp; if (columnName.includes('warehouse') || columnName.includes('location')) return 'Location';
    &amp;nbsp; if (columnName.includes('product') || columnName.includes('item')) return 'Product';
    &amp;nbsp; if (columnName.includes('order')) return 'Order';
    &amp;nbsp; if (columnName.includes('company') || columnName.includes('organization')) return 'Organization';
    &amp;nbsp; return 'Entity'; // Generic fallback
    }
    async function analyzeTableSchema(tableName, rows, apiKey) {
    &amp;nbsp; console.log(`[V8] Starting schema analysis for: ${tableName}`);
    &amp;nbsp; const sample = rows.slice(0, 3);
    &amp;nbsp; const headers = Object.keys(sample[0] || {});
    &amp;nbsp; const prompt = `You are a data schema analyst. Analyze this table:


    Table Name: ${tableName}
    Columns: ${headers.join(', ')}
    Sample Data: ${JSON.stringify(sample, null, 2)}


    Provide:
    1. description: One sentence explaining what this data represents
    2. semantics: For each column, identify its semantic type. Options:
    &amp;nbsp; &amp;nbsp;- person_name, company_name, location_name
    &amp;nbsp; &amp;nbsp;- currency_amount, percentage, count
    &amp;nbsp; &amp;nbsp;- date, datetime, duration
    &amp;nbsp; &amp;nbsp;- identifier, category, description, status
    3. graph_hints: Relationships that could form knowledge graph edges. Format:
    &amp;nbsp; &amp;nbsp;[{"from_col": "manager_id", "to_col": "employee_id", "edge_type": "MANAGES", "confidence": "high"}]


    Output ONLY valid JSON:
    {
    &amp;nbsp; "description": "...",
    &amp;nbsp; "semantics": {"col1": "type", ...},
    &amp;nbsp; "graph_hints": [...]
    }`;
    &amp;nbsp; try {
    &amp;nbsp; &amp;nbsp; const result = await callGemini(prompt, apiKey, "gemini-2.5-flash");
    &amp;nbsp; &amp;nbsp; console.log(`[V8] Analysis complete for ${tableName}`);
    &amp;nbsp; &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; description: result.description || `Data table: ${tableName}`,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; semantics: result.semantics || {},
    &amp;nbsp; &amp;nbsp; &amp;nbsp; graphHints: result.graph_hints || []
    &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; } catch (e) {
    &amp;nbsp; &amp;nbsp; console.error("[V8] Schema analysis failed:", e);
    &amp;nbsp; &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; description: `Data table: ${tableName}`,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; semantics: {},
    &amp;nbsp; &amp;nbsp; &amp;nbsp; graphHints: []
    &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; }
    }
    // --- SPREADSHEET PROCESSOR ---
    async function processSpreadsheetCore(uri, title, rows, env) {
    &amp;nbsp; console.log(`[Spreadsheet] Starting processing: ${rows.length} rows for ${title}`);
    &amp;nbsp; const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
    &amp;nbsp; // 1. Clean Keys
    &amp;nbsp; console.log(`[Spreadsheet] Cleaning row keys...`);
    &amp;nbsp; const cleanRows = rows.map((row) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; const newRow = {};
    &amp;nbsp; &amp;nbsp; Object.keys(row).forEach((k) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const cleanKey = k.toLowerCase().trim().replace(/[^a-z0-9]/g, '_');
    &amp;nbsp; &amp;nbsp; &amp;nbsp; newRow[cleanKey] = row[k];
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; return newRow;
    &amp;nbsp; });
    &amp;nbsp; // 2. Infer Schema
    &amp;nbsp; const firstRow = cleanRows[0] || {};
    &amp;nbsp; const schema = {};
    &amp;nbsp; Object.keys(firstRow).forEach((k) =&amp;gt; schema[k] = typeof firstRow[k]);
    &amp;nbsp; console.log(`[Spreadsheet] Schema inferred: ${Object.keys(schema).length} columns`);
    &amp;nbsp; // *** V8: Generate table name early ***
    &amp;nbsp; const safeName = title.toLowerCase().replace(/[^a-z0-9]/g, '_');
    &amp;nbsp; const tableName = `tbl_${safeName}`;
    &amp;nbsp; console.log(`[Spreadsheet] Table name: ${tableName}`);
    &amp;nbsp; // 3. PRE-SCAN FOR ID MAP
    &amp;nbsp; const idMap = {};
    &amp;nbsp; cleanRows.forEach((r) =&amp;gt; {
    &amp;nbsp; &amp;nbsp; if (r.employee_id &amp;amp;&amp;amp; r.name) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; idMap[r.employee_id] = r.name;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; });
    &amp;nbsp; console.log(`[Spreadsheet] ID map built: ${Object.keys(idMap).length} entries`);
    &amp;nbsp; // *** V8: ANALYZE SCHEMA ***
    &amp;nbsp; console.log(`[V8] Analyzing schema for: ${title}`);
    &amp;nbsp; const schemaAnalysis = await analyzeTableSchema(title, cleanRows, env.GOOGLE_API_KEY);
    &amp;nbsp; console.log(`[V8] Analysis complete:`, schemaAnalysis);
    &amp;nbsp; // 4. GENERATE GRAPH (V8: Use Generic Builder)
    &amp;nbsp; console.log(`[Graph] Building graph using generic approach...`);
    &amp;nbsp; const { nodes: allNodes, edges: allEdges } = await buildGraphGeneric(cleanRows, tableName, env.GOOGLE_API_KEY);
    &amp;nbsp; console.log(`[Graph] Generated ${allNodes.length} nodes and ${allEdges.length} edges.`);
    &amp;nbsp; // 5. BATCH INSERT
    &amp;nbsp; console.log(`[DB] Starting batch insert...`);
    &amp;nbsp; const BATCH_SIZE = 500;
    &amp;nbsp; for (let i = 0; i &amp;lt; cleanRows.length; i += BATCH_SIZE) {
    &amp;nbsp; &amp;nbsp; const rowBatch = cleanRows.slice(i, i + BATCH_SIZE);
    &amp;nbsp; &amp;nbsp; const nodesBatch = i === 0 ? allNodes : [];
    &amp;nbsp; &amp;nbsp; const edgesBatch = i === 0 ? allEdges : [];
    &amp;nbsp; &amp;nbsp; console.log(`[DB] Inserting batch ${i / BATCH_SIZE + 1}: ${rowBatch.length} rows`);
    &amp;nbsp; &amp;nbsp; const { error } = await supabase.rpc('ingest_spreadsheet', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_uri: uri,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_title: title,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_table_name: safeName,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_description: null,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_rows: rowBatch,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_schema: schema,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_nodes: nodesBatch,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_edges: edgesBatch
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; if (error) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.error(`[DB] Batch ${i} ERROR:`, error);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; throw new Error(`Batch ${i} error: ${error.message}`);
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; console.log(`[DB] Batch ${i / BATCH_SIZE + 1} completed successfully`);
    &amp;nbsp; }
    &amp;nbsp; // *** V8: SAVE METADATA (SKIP FOR NOW) ***
    &amp;nbsp; console.log(`[V8] SKIPPING metadata save to test basic ingestion`);
    &amp;nbsp; console.log(`[Spreadsheet] Processing complete!`);
    &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; success: true,
    &amp;nbsp; &amp;nbsp; rows: cleanRows.length,
    &amp;nbsp; &amp;nbsp; graph_nodes: allNodes.length,
    &amp;nbsp; &amp;nbsp; metadata: schemaAnalysis
    &amp;nbsp; };
    }
    // --- MAIN WORKER HANDLER ---
    serve(async (req) =&amp;gt; {
    &amp;nbsp; if (req.method === 'OPTIONS') return new Response('ok', {
    &amp;nbsp; &amp;nbsp; headers: corsHeaders
    &amp;nbsp; });
    &amp;nbsp; console.log(`[Worker] Request received`);
    &amp;nbsp; try {
    &amp;nbsp; &amp;nbsp; const env = Deno.env.toObject();
    &amp;nbsp; &amp;nbsp; const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
    &amp;nbsp; &amp;nbsp; const payload = await req.json();
    &amp;nbsp; &amp;nbsp; console.log(`[Worker] Payload parsed`);
    &amp;nbsp; &amp;nbsp; // PATH A: STORAGE TRIGGER (Large Spreadsheets)
    &amp;nbsp; &amp;nbsp; if (payload.record &amp;amp;&amp;amp; payload.record.bucket_id === 'raw_uploads') {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Worker] Storage Trigger: ${payload.record.name}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Worker] Downloading file...`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const { data: fileData, error: dlError } = await supabase.storage.from('raw_uploads').download(payload.record.name);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (dlError) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.error(`[Worker] Download error:`, dlError);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; throw dlError;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Worker] File downloaded, parsing JSON...`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const contentStr = await fileData.text();
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Worker] Content length: ${contentStr.length} bytes`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const parsed = JSON.parse(contentStr);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const { uri, title, data } = parsed;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Worker] Parsed: uri=${uri}, title=${title}, rows=${data?.length || 0}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!data || data.length === 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.error(`[Worker] ERROR: No data in payload!`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; throw new Error("No data found in uploaded file");
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Worker] Calling processSpreadsheetCore...`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; await processSpreadsheetCore(uri, title, data, env);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Worker] Processing complete, deleting file...`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; await supabase.storage.from('raw_uploads').remove([
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; payload.record.name
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ]);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Worker] SUCCESS!`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; return new Response(JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; success: true
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }), {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...corsHeaders,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Content-Type': 'application/json'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // PATH B: QUEUE TRIGGER (Text Documents)
    &amp;nbsp; &amp;nbsp; console.log(`[Worker] Queue trigger path...`);
    &amp;nbsp; &amp;nbsp; const config = await getConfig(supabase);
    &amp;nbsp; &amp;nbsp; const { data: batch, error } = await supabase.from('ingestion_queue').select('*').eq('status', 'pending').limit(config.worker_batch_size);
    &amp;nbsp; &amp;nbsp; if (error || !batch || batch.length === 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Worker] Queue empty or error:`, error);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; return new Response(JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg: "Queue empty"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }), {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...corsHeaders,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Content-Type': 'application/json'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; console.log(`[Worker] Processing ${batch.length} chunks...`);
    &amp;nbsp; &amp;nbsp; await supabase.from('ingestion_queue').update({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; status: 'processing'
    &amp;nbsp; &amp;nbsp; }).in('id', batch.map((r) =&amp;gt; r.id));
    &amp;nbsp; &amp;nbsp; for (const row of batch) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; try {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const embedding = await getEmbedding(row.chunk_text, env.GOOGLE_API_KEY);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // GRAPH EXTRACTION FOR TEXT (Every Nth chunk)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; let nodes = [], edges = [], mentions = [];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (row.chunk_index % config.graph_sample_rate === 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const extractPrompt = `
    You are a Knowledge Graph Extractor.
    Analyze this text. Identify:
    1. **ROLES**: Job titles (e.g., "Customer Support Manager").
    2. **RESPONSIBILITIES**: Key duties (e.g., "Refunds", "OSHA").
    3. **SYSTEMS**: Tools (e.g., "OMS", "Chatbots").


    Output JSON: { 
    &amp;nbsp; &amp;nbsp; "nodes": [{"key": "Role:Name", "labels": ["Role"], "props": {"name": "Name"}}], 
    &amp;nbsp; &amp;nbsp; "edges": [{"src_key": "...", "dst_key": "...", "type": "OWNS", "props": {"context": "..."}}] 
    }


    Text: ${row.chunk_text}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const graphData = await callGemini(extractPrompt, env.GOOGLE_API_KEY, config.model_extraction);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; nodes = graphData.nodes || [];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; edges = graphData.edges || [];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; mentions = nodes.map((n) =&amp;gt; ({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; node_key: n.key,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; rel: "MENTIONS"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; await supabase.rpc('ingest_document_chunk', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_uri: row.uri,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_title: row.title,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_doc_meta: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; processed_at: new Date().toISOString()
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_chunk: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ordinal: row.chunk_index,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; text: row.chunk_text,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; embedding
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_nodes: nodes,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_edges: edges,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_mentions: mentions
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; await supabase.from('ingestion_queue').delete().eq('id', row.id);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; } catch (err) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.error(`Error processing chunk ${row.id}:`, err);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; await supabase.from('ingestion_queue').update({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; status: 'failed',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; error_log: err.message
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }).eq('id', row.id);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // RECURSION (Process next batch)
    &amp;nbsp; &amp;nbsp; const { count } = await supabase.from('ingestion_queue').select('*', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; count: 'exact',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; head: true
    &amp;nbsp; &amp;nbsp; }).eq('status', 'pending');
    &amp;nbsp; &amp;nbsp; if (count &amp;amp;&amp;amp; count &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; fetch(`${env.SUPABASE_URL}/functions/v1/ingest-worker`, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; method: 'POST',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Authorization': `Bearer ${env.SUPABASE_SERVICE_ROLE_KEY}`,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Content-Type': 'application/json'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; body: JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; action: 'continue'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; })
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }).catch((e) =&amp;gt; console.error("Daisy chain failed", e));
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; return new Response(JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; success: true,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; processed: batch.length
    &amp;nbsp; &amp;nbsp; }), {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...corsHeaders,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Content-Type': 'application/json'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; } catch (e) {
    &amp;nbsp; &amp;nbsp; console.error("[Worker] FATAL ERROR:", e);
    &amp;nbsp; &amp;nbsp; return new Response(JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; error: e.message,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; stack: e.stack
    &amp;nbsp; &amp;nbsp; }), {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; status: 500,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...corsHeaders,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Content-Type': 'application/json'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; }
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 6&lt;/strong&gt;. Create one last Edge Function. Name this one, simply 'search':&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
    import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
    const CORS = {
    &amp;nbsp; "Access-Control-Allow-Origin": "*",
    &amp;nbsp; "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type"
    };
    // --- CONFIG LOADER ---
    async function getConfig(supabase) {
    &amp;nbsp; const { data } = await supabase.from('app_config').select('settings').single();
    &amp;nbsp; return {
    &amp;nbsp; &amp;nbsp; model_router: "gemini-2.5-flash",
    &amp;nbsp; &amp;nbsp; model_reranker: "gemini-2.5-flash-lite",
    &amp;nbsp; &amp;nbsp; model_sql: "gemini-2.5-flash",
    &amp;nbsp; &amp;nbsp; rrf_weight_enrichment: 15.0,
    &amp;nbsp; &amp;nbsp; rrf_weight_sql: 10.0,
    &amp;nbsp; &amp;nbsp; rrf_weight_graph: 2.0,
    &amp;nbsp; &amp;nbsp; rrf_weight_fts: 4.0,
    &amp;nbsp; &amp;nbsp; rrf_weight_vector: 2.5,
    &amp;nbsp; &amp;nbsp; rerank_depth: 15,
    &amp;nbsp; &amp;nbsp; min_vector_score: 0.01,
    &amp;nbsp; &amp;nbsp; ...data &amp;amp;&amp;amp; data.settings || {}
    &amp;nbsp; };
    }
    // --- V8: DYNAMIC SCHEMA LOADER ---
    async function getAvailableSchemas(supabase) {
    &amp;nbsp; const { data, error } = await supabase.from('structured_table').select('table_name, description, schema_def, column_semantics').gt('row_count', 0);
    &amp;nbsp; if (error || !data || data.length === 0) {
    &amp;nbsp; &amp;nbsp; return "No structured data available.";
    &amp;nbsp; }
    &amp;nbsp; return data.map((t)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; const cols = Object.keys(t.schema_def || {}).join(', ');
    &amp;nbsp; &amp;nbsp; const desc = t.description || 'No description';
    &amp;nbsp; &amp;nbsp; return `- ${t.table_name}: ${desc}\n &amp;nbsp;Columns: ${cols}`;
    &amp;nbsp; }).join('\n');
    }
    // --- V9: COMPOSITE ENRICHMENT ---
    async function enrichQueryContext(query, supabase, log) {
    &amp;nbsp; console.log("[V9] Detecting entities in query...");
    &amp;nbsp; const { data: detected, error: detectError } = await supabase.rpc('detect_query_entities', {
    &amp;nbsp; &amp;nbsp; p_query: query
    &amp;nbsp; });
    &amp;nbsp; if (detectError || !detected || detected.length === 0) {
    &amp;nbsp; &amp;nbsp; log("ENTITY_DETECTION", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; found: false
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; return [];
    &amp;nbsp; }
    &amp;nbsp; log("ENTITY_DETECTION", {
    &amp;nbsp; &amp;nbsp; found: true,
    &amp;nbsp; &amp;nbsp; entities: detected
    &amp;nbsp; });
    &amp;nbsp; const enrichments = [];
    &amp;nbsp; for (const entity of detected){
    &amp;nbsp; &amp;nbsp; console.log(`[V9] Enriching ${entity.entity_type}: ${entity.key_value}`);
    &amp;nbsp; &amp;nbsp; const { data: enriched, error: enrichError } = await supabase.rpc('enrich_query_context', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_primary_table: entity.table_name,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_primary_key: entity.key_column,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_primary_value: entity.key_value
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; if (enrichError) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; log("ENRICHMENT_ERROR", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; entity,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; error: enrichError
    &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; if (enriched &amp;amp;&amp;amp; enriched.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; enrichments.push(...enriched.map((e)=&amp;gt;({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...e,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; _source: `enrichment (${e.enrichment_type})`,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; content: `[${e.enrichment_type.toUpperCase()}] ${e.table_name}: ${JSON.stringify(e.row_data)}`
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; })));
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; }
    &amp;nbsp; log("ENRICHMENT_RESULTS", {
    &amp;nbsp; &amp;nbsp; count: enrichments.length
    &amp;nbsp; });
    &amp;nbsp; return enrichments;
    }
    // --- GEMINI LLM RERANKER (FROM OLD SYSTEM) ---
    async function rerankWithGemini(query, docs, apiKey, model, depth) {
    &amp;nbsp; if (!docs || docs.length === 0) return [];
    &amp;nbsp; const candidates = docs.slice(0, depth);
    &amp;nbsp; const docList = candidates.map((d)=&amp;gt;({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; id: d.chunk_id,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; text: (d.content || "").substring(0, 350)
    &amp;nbsp; &amp;nbsp; }));
    &amp;nbsp; const prompt = `Role: Relevance Filter.
    &amp;nbsp; Task: Evaluate if chunks are RELEVANT to the User Query.


    &amp;nbsp; User Query: "${query}"


    &amp;nbsp; KEEP RULES:
    &amp;nbsp; ✅ KEEP if chunk directly answers the query
    &amp;nbsp; ✅ KEEP if chunk provides important context (definitions, procedures, policies)
    &amp;nbsp; ✅ KEEP if chunk mentions key entities from the query (names, IDs, locations)
    &amp;nbsp; ✅ KEEP if unsure - err on the side of inclusion


    &amp;nbsp; DISCARD RULES:
    &amp;nbsp; ❌ ONLY discard if completely unrelated (different topic entirely)
    &amp;nbsp; ❌ Discard "Table of Contents", "Index", or navigation elements
    &amp;nbsp; ❌ Discard if chunk is just metadata without substance


    &amp;nbsp; EXAMPLES:
    &amp;nbsp; - Query: "return policy" → KEEP: "Customer Support: returns processing", "30-day return window", "refund procedures"
    &amp;nbsp; - Query: "Order O00062" → KEEP: order details, customer info, warehouse data, shipping info
    &amp;nbsp; - Query: "Who founded company?" → KEEP: company history, founder bio, origin story


    &amp;nbsp; Return JSON: { "kept_ids": [list of chunk IDs to keep] }


    &amp;nbsp; Docs: ${JSON.stringify(docList)}`;
    &amp;nbsp; try {
    &amp;nbsp; &amp;nbsp; const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; method: "POST",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; "Content-Type": "application/json"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; &amp;nbsp; body: JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; contents: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; parts: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; text: prompt
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ]
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; generationConfig: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; responseMimeType: "application/json"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; })
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; const data = await response.json();
    &amp;nbsp; &amp;nbsp; const text = data.candidates &amp;amp;&amp;amp; data.candidates[0] &amp;amp;&amp;amp; data.candidates[0].content &amp;amp;&amp;amp; data.candidates[0].content.parts &amp;amp;&amp;amp; data.candidates[0].content.parts[0] &amp;amp;&amp;amp; data.candidates[0].content.parts[0].text || "{}";
    &amp;nbsp; &amp;nbsp; const result = JSON.parse(text);
    &amp;nbsp; &amp;nbsp; if (result.kept_ids &amp;amp;&amp;amp; Array.isArray(result.kept_ids)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const keptDocs = [];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; result.kept_ids.forEach((id)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const doc = docs.find((d)=&amp;gt;d.chunk_id === id);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (doc) keptDocs.push(doc);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Safety net: If LLM discards everything, return top 1
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (keptDocs.length === 0 &amp;amp;&amp;amp; docs.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; docs[0]
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const MIN_KEPT = 10;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (keptDocs.length &amp;lt; MIN_KEPT &amp;amp;&amp;amp; docs.length &amp;gt;= MIN_KEPT) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Rerank] Only kept ${keptDocs.length}, adding top ${MIN_KEPT - keptDocs.length} from original set`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const keptIds = new Set(keptDocs.map((d)=&amp;gt;d.chunk_id));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const remaining = docs.filter((d)=&amp;gt;!keptIds.has(d.chunk_id));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const toAdd = remaining.slice(0, MIN_KEPT - keptDocs.length);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...keptDocs,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...toAdd
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; return keptDocs;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; return candidates;
    &amp;nbsp; } catch (e) {
    &amp;nbsp; &amp;nbsp; console.error("[Rerank] Error:", e);
    &amp;nbsp; &amp;nbsp; return candidates;
    &amp;nbsp; }
    }
    // --- V10: GRAPH RELEVANCE FILTER ---
    function filterGraphByRelevance(graphEdges, query) {
    &amp;nbsp; if (!graphEdges || graphEdges.length === 0) return [];
    &amp;nbsp; const queryLower = query.toLowerCase();
    &amp;nbsp; const queryWords = queryLower.split(/\s+/).filter((w)=&amp;gt;w.length &amp;gt; 2);
    &amp;nbsp; const priorityRelations = [
    &amp;nbsp; &amp;nbsp; 'REPORTS_TO',
    &amp;nbsp; &amp;nbsp; 'MANAGES',
    &amp;nbsp; &amp;nbsp; 'PLACED_BY',
    &amp;nbsp; &amp;nbsp; 'FULFILLED_FROM',
    &amp;nbsp; &amp;nbsp; 'SHIPPED_BY',
    &amp;nbsp; &amp;nbsp; 'ASSIGNED_TO',
    &amp;nbsp; &amp;nbsp; 'WORKS_WITH'
    &amp;nbsp; ];
    &amp;nbsp; const relevantEdges = [];
    &amp;nbsp; for (const edge of graphEdges){
    &amp;nbsp; &amp;nbsp; const edgeText = `${edge.subject || ''} ${edge.action || ''} ${edge.object || ''}`.toLowerCase();
    &amp;nbsp; &amp;nbsp; // Calculate keyword overlap
    &amp;nbsp; &amp;nbsp; const matchedWords = queryWords.filter((w)=&amp;gt;edgeText.includes(w));
    &amp;nbsp; &amp;nbsp; const overlapRatio = matchedWords.length / queryWords.length;
    &amp;nbsp; &amp;nbsp; // Check for exact phrase matches (e.g., "customer onboarding" as a phrase)
    &amp;nbsp; &amp;nbsp; const hasPhraseMatch = queryWords.some((word, idx)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (idx &amp;lt; queryWords.length - 1) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const phrase = `${word} ${queryWords[idx + 1]}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return edgeText.includes(phrase);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; return false;
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; // Keep if:
    &amp;nbsp; &amp;nbsp; // 1. High keyword overlap (&amp;gt;40% of query words present in edge)
    &amp;nbsp; &amp;nbsp; // 2. OR at least 2 keywords match
    &amp;nbsp; &amp;nbsp; // 3. OR exact phrase match found
    &amp;nbsp; &amp;nbsp; // 4. OR it's a high-priority relationship type
    &amp;nbsp; &amp;nbsp; if (overlapRatio &amp;gt; 0.4 || matchedWords.length &amp;gt;= 2 || hasPhraseMatch || priorityRelations.includes(edge.action)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; relevantEdges.push(edge);
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; }
    &amp;nbsp; console.log(`[Graph Filter] ${graphEdges.length} → ${relevantEdges.length} relevant edges`);
    &amp;nbsp; return relevantEdges;
    }
    // --- RRF FUSION (SIMPLIFIED) ---
    async function performRRFFusion(rerankedDocs, graphResults, sqlResults, config) {
    &amp;nbsp; const K = 60;
    &amp;nbsp; const scores = {};
    &amp;nbsp; const addScore = (item, rank, weight, type)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; let key = item.chunk_id ? `chunk_${item.chunk_id}` : `sql_${JSON.stringify(item.row_data)}`;
    &amp;nbsp; &amp;nbsp; if (!scores[key]) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; scores[key] = {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...item,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; _source: type
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; score: 0
    &amp;nbsp; &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; scores[key].score += 1.0 / (K + rank) * weight;
    &amp;nbsp; &amp;nbsp; if (!scores[key].item._source.includes(type)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; scores[key].item._source += `, ${type}`;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; };
    &amp;nbsp; // Apply weights
    &amp;nbsp; rerankedDocs.forEach((item, idx)=&amp;gt;addScore(item, idx, config.rrf_weight_vector, 'vector/fts'));
    &amp;nbsp; if (graphResults) graphResults.forEach((item, idx)=&amp;gt;addScore(item, idx, config.rrf_weight_graph, 'graph'));
    &amp;nbsp; if (sqlResults) sqlResults.forEach((item, idx)=&amp;gt;addScore(item, idx, config.rrf_weight_sql, 'structured'));
    &amp;nbsp; // Convert to array &amp;amp; sort
    &amp;nbsp; let results = Object.values(scores).sort((a, b)=&amp;gt;b.score - a.score);
    &amp;nbsp; // ✅ NO CUTOFF - Let ranking + limit handle quality
    &amp;nbsp; // Reranking already filtered noise, cutoff was too aggressive
    &amp;nbsp; console.log(`[RRF_FUSION] Total results: ${results.length}`);
    &amp;nbsp; return results.map((s)=&amp;gt;s.item);
    }
    // --- V10: STRICTER ROUTER PROMPT (LINE ~238) ---
    function buildRouterPrompt(schemas, query) {
    &amp;nbsp; return `You are the Context Mesh Router.
    User Query: "${query}"


    Available Tables:
    ${schemas}


    DECISION PROTOCOL:


    1. **ENTITY EXTRACTION** - Extract ONLY specific identifiers and proper names:


    &amp;nbsp; &amp;nbsp;âœ… ALWAYS EXTRACT (ID Patterns):
    &amp;nbsp; &amp;nbsp;- Order IDs: O00001, O00062, O\\d{5}
    &amp;nbsp; &amp;nbsp;- Customer IDs: CU006, CU008, CU\\d{3}
    &amp;nbsp; &amp;nbsp;- Employee IDs: E001, E002, E\\d{3}
    &amp;nbsp; &amp;nbsp;- Warehouse IDs: WH001, WH002, WH\\d{3}
    &amp;nbsp; &amp;nbsp;- Carrier IDs: CR001, CR005, CR\\d{3}
    &amp;nbsp; &amp;nbsp;
    &amp;nbsp; &amp;nbsp;âœ… ALWAYS EXTRACT (Proper Names):
    &amp;nbsp; &amp;nbsp;- Full person names: "Nicholas Cooper", "Sarah Brooks", "Emily Chen"
    &amp;nbsp; &amp;nbsp;- Company names: "CommerceFlow Solutions", "Brand 06"
    &amp;nbsp; &amp;nbsp;- Specific location names: "California", "Texas", "New Jersey"
    &amp;nbsp; &amp;nbsp;
    &amp;nbsp; &amp;nbsp;âŒ NEVER EXTRACT:
    &amp;nbsp; &amp;nbsp;- Action verbs: list, show, get, find, tell, display, give, provide
    &amp;nbsp; &amp;nbsp;- Question words: what, who, where, when, how, why, which
    &amp;nbsp; &amp;nbsp;- Generic nouns: employees, customers, orders, warehouses, people, items
    &amp;nbsp; &amp;nbsp;- Plural table names: customers, orders, employees (these trigger SQL, not graph)
    &amp;nbsp; &amp;nbsp;- Departments: Operations, Sales, Marketing (these are WHERE clauses, not entities)
    &amp;nbsp; &amp;nbsp;- Roles/titles: manager, CEO, analyst (these are WHERE clauses, not entities)
    &amp;nbsp; &amp;nbsp;- Determiners: the, a, an, this, that, these, those
    &amp;nbsp; &amp;nbsp;- Prepositions: in, at, from, to, with, by
    &amp;nbsp; &amp;nbsp;- Status words: active, inactive, pending, shipped
    &amp;nbsp; &amp;nbsp;
    &amp;nbsp; &amp;nbsp;âš ï¸ CRITICAL VALIDATION:
    &amp;nbsp; &amp;nbsp;- If word appears in common English dictionary → NOT an entity
    &amp;nbsp; &amp;nbsp;- If word is lowercase in query → NOT an entity (unless it's an ID)
    &amp;nbsp; &amp;nbsp;- If word describes a category/group → NOT an entity
    &amp;nbsp; &amp;nbsp;- If word is a verb in any tense → NOT an entity


    &amp;nbsp; &amp;nbsp;EXAMPLES:
    &amp;nbsp; &amp;nbsp;Query: "List orders placed this year"
    &amp;nbsp; &amp;nbsp;❌ BAD: entities: ["list", "orders", "placed", "year"]
    &amp;nbsp; &amp;nbsp;âœ… GOOD: entities: []
    &amp;nbsp; &amp;nbsp;Reason: All are generic terms, no specific identifiers
    &amp;nbsp; &amp;nbsp;
    &amp;nbsp; &amp;nbsp;Query: "Show me order O00062"
    &amp;nbsp; &amp;nbsp;❌ BAD: entities: ["show", "me", "order", "O00062"]
    &amp;nbsp; &amp;nbsp;âœ… GOOD: entities: ["O00062"]
    &amp;nbsp; &amp;nbsp;Reason: Only O00062 is a specific identifier
    &amp;nbsp; &amp;nbsp;
    &amp;nbsp; &amp;nbsp;Query: "Who does Sarah Brooks report to?"
    &amp;nbsp; &amp;nbsp;❌ BAD: entities: ["who", "Sarah", "Brooks", "report", "to"]
    &amp;nbsp; &amp;nbsp;âœ… GOOD: entities: ["Sarah Brooks"]
    &amp;nbsp; &amp;nbsp;Reason: Full name is proper noun, rest are grammar/verbs
    &amp;nbsp; &amp;nbsp;
    &amp;nbsp; &amp;nbsp;Query: "List employees in Operations department"
    &amp;nbsp; &amp;nbsp;❌ BAD: entities: ["employees", "Operations", "department"]
    &amp;nbsp; &amp;nbsp;âœ… GOOD: entities: []
    &amp;nbsp; &amp;nbsp;Reason: All are generic category terms, no specific names
    &amp;nbsp; &amp;nbsp;
    &amp;nbsp; &amp;nbsp;Query: "Show orders for Brand 06"
    &amp;nbsp; &amp;nbsp;❌ BAD: entities: ["show", "orders", "Brand", "06"]
    &amp;nbsp; &amp;nbsp;âœ… GOOD: entities: ["Brand 06"]
    &amp;nbsp; &amp;nbsp;Reason: "Brand 06" is a specific customer name
    &amp;nbsp; &amp;nbsp;
    &amp;nbsp; &amp;nbsp;Query: "Which carrier shipped order O00123?"
    &amp;nbsp; &amp;nbsp;❌ BAD: entities: ["which", "carrier", "shipped", "order", "O00123"]
    &amp;nbsp; &amp;nbsp;âœ… GOOD: entities: ["O00123"]
    &amp;nbsp; &amp;nbsp;Reason: Only O00123 is a specific identifier


    2. **SQL TABLE DETECTION**:
    &amp;nbsp; &amp;nbsp;- Use sql_tables for: counts, sums, averages, lists, filters
    &amp;nbsp; &amp;nbsp;- Keywords: "how many", "total", "average", "list", "show all"
    &amp;nbsp; &amp;nbsp;
    3. **KEYWORD EXTRACTION** (for FTS):
    &amp;nbsp; &amp;nbsp;- Extract nouns ONLY (no verbs, no determiners)
    &amp;nbsp; &amp;nbsp;- Max 3-5 keywords
    &amp;nbsp; &amp;nbsp;- Focus on domain-specific terms


    VALIDATION CHECKLIST (before returning entities):
    1. Is it an ID pattern (letters + numbers)? → YES = keep, NO = next check
    2. Is it a capitalized proper name (2+ words)? → YES = keep, NO = next check &amp;nbsp;
    3. Is it a common English word? → YES = REMOVE, NO = keep
    4. Is it a verb (ends in -ing, -ed, -s)? → YES = REMOVE, NO = keep
    5. Is it a plural noun (employees, orders)? → YES = REMOVE, NO = keep


    DEFAULT BEHAVIOR:
    - When in doubt → DO NOT extract as entity
    - Empty entities array is CORRECT for most queries
    - Entities should be rare (only 20-30% of queries have them)


    Output JSON: { "sql_tables": [], "entities": [], "keywords": [] }`;
    }
    // --- V10: ENTITY VALIDATION FILTER (ADD AFTER buildRouterPrompt) ---
    function validateRouterEntities(entities, query) {
    &amp;nbsp; if (!entities || entities.length === 0) return [];
    &amp;nbsp; const validated = [];
    &amp;nbsp; const queryLower = query.toLowerCase();
    &amp;nbsp; // Common English verbs and nouns to reject
    &amp;nbsp; const REJECT_PATTERNS = {
    &amp;nbsp; &amp;nbsp; // Action verbs
    &amp;nbsp; &amp;nbsp; verbs: new Set([
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'list',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'show',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'get',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'find',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'tell',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'display',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'give',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'provide',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'placed',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'hired',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'shipped',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'ordered',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'delivered',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'returned',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'create',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'update',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'delete',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'search',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'filter',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'sort'
    &amp;nbsp; &amp;nbsp; ]),
    &amp;nbsp; &amp;nbsp; // Question words
    &amp;nbsp; &amp;nbsp; questions: new Set([
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'what',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'who',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'where',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'when',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'why',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'how',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'which',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'whose'
    &amp;nbsp; &amp;nbsp; ]),
    &amp;nbsp; &amp;nbsp; // Generic nouns (plural forms)
    &amp;nbsp; &amp;nbsp; plurals: new Set([
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'employees',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'customers',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'orders',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'warehouses',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'carriers',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'products',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'items',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'people',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'users',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'companies',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'brands'
    &amp;nbsp; &amp;nbsp; ]),
    &amp;nbsp; &amp;nbsp; // Generic nouns (singular forms)
    &amp;nbsp; &amp;nbsp; singulars: new Set([
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'employee',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'customer',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'order',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'warehouse',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'carrier',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'product',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'item',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'person',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'user',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'company',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'brand',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'manager',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'analyst',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'director',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'supervisor',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'coordinator'
    &amp;nbsp; &amp;nbsp; ]),
    &amp;nbsp; &amp;nbsp; // Departments and categories
    &amp;nbsp; &amp;nbsp; departments: new Set([
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'operations',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'sales',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'marketing',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'finance',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'logistics',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'hr',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'it',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'support',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'management',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'administration'
    &amp;nbsp; &amp;nbsp; ]),
    &amp;nbsp; &amp;nbsp; // Status and descriptors
    &amp;nbsp; &amp;nbsp; descriptors: new Set([
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'active',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'inactive',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'pending',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'shipped',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'delivered',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'returned',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'new',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'old',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'current',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'previous',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'next',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'last',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'first'
    &amp;nbsp; &amp;nbsp; ])
    &amp;nbsp; };
    &amp;nbsp; for (const entity of entities){
    &amp;nbsp; &amp;nbsp; const entityLower = entity.toLowerCase().trim();
    &amp;nbsp; &amp;nbsp; // Rule 1: Reject if empty or too short
    &amp;nbsp; &amp;nbsp; if (!entityLower || entityLower.length &amp;lt; 2) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] REJECTED (too short): "${entity}"`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Rule 2: Keep if matches ID pattern (letters + numbers)
    &amp;nbsp; &amp;nbsp; // O00062, CU006, E001, WH003, CR005
    &amp;nbsp; &amp;nbsp; if (entity.match(/^[A-Z]{1,3}\d{3,5}$/)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] ACCEPTED (ID pattern): "${entity}"`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; validated.push(entity);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Rule 3: Keep if proper name (2+ capitalized words)
    &amp;nbsp; &amp;nbsp; // "Nicholas Cooper", "Sarah Brooks", "Brand 06"
    &amp;nbsp; &amp;nbsp; if (entity.match(/^[A-Z][a-z]+(\s+[A-Z0-9][a-z0-9]*)+$/)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] ACCEPTED (proper name): "${entity}"`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; validated.push(entity);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Rule 4: Keep if single capitalized word (potential company/location name)
    &amp;nbsp; &amp;nbsp; // "CommerceFlow", "California", "Texas"
    &amp;nbsp; &amp;nbsp; if (entity.match(/^[A-Z][a-z]{2,}$/) &amp;amp;&amp;amp; entityLower.length &amp;gt; 4) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // But reject if it's a known category word
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (REJECT_PATTERNS.departments.has(entityLower) || REJECT_PATTERNS.singulars.has(entityLower) || REJECT_PATTERNS.descriptors.has(entityLower)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] REJECTED (category word): "${entity}"`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] ACCEPTED (capitalized term): "${entity}"`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; validated.push(entity);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Rule 5: Reject if it's a known verb
    &amp;nbsp; &amp;nbsp; if (REJECT_PATTERNS.verbs.has(entityLower)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] REJECTED (verb): "${entity}"`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Rule 6: Reject if it's a question word
    &amp;nbsp; &amp;nbsp; if (REJECT_PATTERNS.questions.has(entityLower)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] REJECTED (question word): "${entity}"`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Rule 7: Reject if it's a plural generic noun
    &amp;nbsp; &amp;nbsp; if (REJECT_PATTERNS.plurals.has(entityLower)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] REJECTED (plural noun): "${entity}"`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Rule 8: Reject if it's a singular generic noun
    &amp;nbsp; &amp;nbsp; if (REJECT_PATTERNS.singulars.has(entityLower)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] REJECTED (singular noun): "${entity}"`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Rule 9: Reject if it's a department/category
    &amp;nbsp; &amp;nbsp; if (REJECT_PATTERNS.departments.has(entityLower)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] REJECTED (department): "${entity}"`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Rule 10: Reject if it's a status/descriptor
    &amp;nbsp; &amp;nbsp; if (REJECT_PATTERNS.descriptors.has(entityLower)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] REJECTED (descriptor): "${entity}"`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Rule 11: Reject if word appears as-is in query (likely a query word)
    &amp;nbsp; &amp;nbsp; // Exception: proper nouns (capitalized in both)
    &amp;nbsp; &amp;nbsp; if (queryLower.includes(entityLower) &amp;amp;&amp;amp; entity === entityLower) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] REJECTED (uncapitalized query word): "${entity}"`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; continue;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // If we got here, it passed all checks - keep it with warning
    &amp;nbsp; &amp;nbsp; console.log(`[Router Validation] ACCEPTED (passed all checks): "${entity}"`);
    &amp;nbsp; &amp;nbsp; validated.push(entity);
    &amp;nbsp; }
    &amp;nbsp; console.log(`[Router Validation] Final: ${entities.length} → ${validated.length}`);
    &amp;nbsp; return validated;
    }
    // --- HELPERS ---
    async function callGemini(prompt, apiKey, model = "gemini-2.5-flash") {
    &amp;nbsp; try {
    &amp;nbsp; &amp;nbsp; const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; method: "POST",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; "Content-Type": "application/json"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; &amp;nbsp; body: JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; contents: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; parts: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; text: prompt
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ]
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ]
    &amp;nbsp; &amp;nbsp; &amp;nbsp; })
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; const data = await response.json();
    &amp;nbsp; &amp;nbsp; if (!data.candidates) return {};
    &amp;nbsp; &amp;nbsp; const txt = data.candidates[0].content.parts[0].text;
    &amp;nbsp; &amp;nbsp; return JSON.parse(txt.replace(/```

json/g, "").replace(/

```/g, "").trim());
    &amp;nbsp; } catch (e) {
    &amp;nbsp; &amp;nbsp; console.error("Gemini error:", e);
    &amp;nbsp; &amp;nbsp; return {};
    &amp;nbsp; }
    }
    async function getEmbedding(text, apiKey) {
    &amp;nbsp; const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=${apiKey}`, {
    &amp;nbsp; &amp;nbsp; method: "POST",
    &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; "Content-Type": "application/json"
    &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; body: JSON.stringify({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; model: "models/gemini-embedding-001",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; content: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; parts: [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; text
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ]
    &amp;nbsp; &amp;nbsp; &amp;nbsp; },
    &amp;nbsp; &amp;nbsp; &amp;nbsp; outputDimensionality: 768
    &amp;nbsp; &amp;nbsp; })
    &amp;nbsp; });
    &amp;nbsp; const data = await response.json();
    &amp;nbsp; return data.embedding &amp;amp;&amp;amp; data.embedding.values || [];
    }
    function extractFastKeywords(query) {
    &amp;nbsp; const stopWords = new Set([
    &amp;nbsp; &amp;nbsp; 'the',
    &amp;nbsp; &amp;nbsp; 'and',
    &amp;nbsp; &amp;nbsp; 'for',
    &amp;nbsp; &amp;nbsp; 'with',
    &amp;nbsp; &amp;nbsp; 'that',
    &amp;nbsp; &amp;nbsp; 'this',
    &amp;nbsp; &amp;nbsp; 'what',
    &amp;nbsp; &amp;nbsp; 'where',
    &amp;nbsp; &amp;nbsp; 'when',
    &amp;nbsp; &amp;nbsp; 'who',
    &amp;nbsp; &amp;nbsp; 'how',
    &amp;nbsp; &amp;nbsp; 'show',
    &amp;nbsp; &amp;nbsp; 'list',
    &amp;nbsp; &amp;nbsp; 'tell'
    &amp;nbsp; ]);
    &amp;nbsp; return query.replace(/[^\w\s]/g, '').split(/\s+/).filter((w)=&amp;gt;w.length &amp;gt; 3 &amp;amp;&amp;amp; !stopWords.has(w.toLowerCase()));
    }
    // --- MAIN HANDLER ---
    serve(async (req)=&amp;gt;{
    &amp;nbsp; if (req.method === 'OPTIONS') {
    &amp;nbsp; &amp;nbsp; return new Response('ok', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: CORS
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; }
    &amp;nbsp; // NEW: toggle debug output from request body
    &amp;nbsp; let debugEnabled = false;
    &amp;nbsp; let debugInfo = {
    &amp;nbsp; &amp;nbsp; logs: []
    &amp;nbsp; };
    &amp;nbsp; const log = (msg, data)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; if (!debugEnabled) return; // no-op when debug is off
    &amp;nbsp; &amp;nbsp; debugInfo.logs.push({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; msg,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; data
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; };
    &amp;nbsp; let resultLimit = 20;
    &amp;nbsp; try {
    &amp;nbsp; &amp;nbsp; const body = await req.json();
    &amp;nbsp; &amp;nbsp; const query = body.query;
    &amp;nbsp; &amp;nbsp; // You can make this stricter/looser if you want
    &amp;nbsp; &amp;nbsp; debugEnabled = body.debug === true;
    &amp;nbsp; &amp;nbsp; if (body.limit) resultLimit = body.limit;
    &amp;nbsp; &amp;nbsp; const env = Deno.env.toObject();
    &amp;nbsp; &amp;nbsp; const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
    &amp;nbsp; &amp;nbsp; const config = await getConfig(supabase);
    &amp;nbsp; &amp;nbsp; // V8: DYNAMIC SCHEMA LOADING
    &amp;nbsp; &amp;nbsp; console.log("[V8] Loading available schemas...");
    &amp;nbsp; &amp;nbsp; const schemas = await getAvailableSchemas(supabase);
    &amp;nbsp; &amp;nbsp; log("AVAILABLE_SCHEMAS", schemas);
    &amp;nbsp; &amp;nbsp; // V9: COMPOSITE ENRICHMENT
    &amp;nbsp; &amp;nbsp; const enrichmentPromise = enrichQueryContext(query, supabase, log);
    &amp;nbsp; &amp;nbsp; // ROUTER
    &amp;nbsp; &amp;nbsp; const routerPrompt = buildRouterPrompt(schemas, query);
    &amp;nbsp; &amp;nbsp; const embeddingPromise = getEmbedding(query, env.GOOGLE_API_KEY);
    &amp;nbsp; &amp;nbsp; const routerPromise = callGemini(routerPrompt, env.GOOGLE_API_KEY, config.model_router);
    &amp;nbsp; &amp;nbsp; const fastKeywords = extractFastKeywords(query);
    &amp;nbsp; &amp;nbsp; const [embedding, routerRes, enrichedData] = await Promise.all([
    &amp;nbsp; &amp;nbsp; &amp;nbsp; embeddingPromise,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; routerPromise,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; enrichmentPromise
    &amp;nbsp; &amp;nbsp; ]);
    &amp;nbsp; &amp;nbsp; // ✅ V10: VALIDATE ROUTER ENTITIES
    &amp;nbsp; &amp;nbsp; const originalEntities = routerRes.entities || [];
    &amp;nbsp; &amp;nbsp; const validatedEntities = validateRouterEntities(originalEntities, query);
    &amp;nbsp; &amp;nbsp; // Update router response with validated entities
    &amp;nbsp; &amp;nbsp; routerRes.entities = validatedEntities;
    &amp;nbsp; &amp;nbsp; debugInfo.router = {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ...routerRes,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; entity_validation: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; before: originalEntities,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; after: validatedEntities,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; rejected: originalEntities.filter((e)=&amp;gt;!validatedEntities.includes(e))
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; &amp;nbsp; log("ROUTER_DECISION", debugInfo.router);
    &amp;nbsp; &amp;nbsp; // ✅ MERGE: Combine entity detection + router entities
    &amp;nbsp; &amp;nbsp; const allEntities = [];
    &amp;nbsp; &amp;nbsp; // Add entities from detection (O00062, CU006, etc.)
    &amp;nbsp; &amp;nbsp; if (enrichedData &amp;amp;&amp;amp; enrichedData.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Entity detection already found these
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const detectedIds = enrichedData.filter((e)=&amp;gt;e.enrichment_type === 'primary').map((e)=&amp;gt;e.row_data.order_id || e.row_data.customer_id || e.row_data.employee_id).filter(Boolean);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; allEntities.push(...detectedIds);
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Add entities from router (person names, roles, etc.)
    &amp;nbsp; &amp;nbsp; if (routerRes.entities &amp;amp;&amp;amp; routerRes.entities.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; allEntities.push(...routerRes.entities);
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Add keywords as fallback (for queries like "employees in warehouse")
    &amp;nbsp; &amp;nbsp; if (allEntities.length === 0 &amp;amp;&amp;amp; routerRes.keywords &amp;amp;&amp;amp; routerRes.keywords.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; allEntities.push(...routerRes.keywords.slice(0, 3)); // Top 3 keywords
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; log("MERGED_ENTITIES", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; count: allEntities.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; entities: allEntities
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; // ✅ ENTITY VALIDATION: Filter out generic terms
    &amp;nbsp; &amp;nbsp; const specificEntities = allEntities.filter((entity)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const entityLower = entity.toLowerCase().trim();
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // 1. Keep if matches ID pattern (O00062, CU006, E001, WH001, CR001)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (entity.match(/^[A-Z]{1,3}\d{3,5}$/)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return true;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // 2. Keep if proper name (has space + capitalized words)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // "Sarah Brooks", "Nicholas Cooper", "Brand 06"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (entity.match(/^[A-Z][a-z]+(\s+[A-Z][a-z0-9]+)+$/)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return true;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // 3. Reject if generic table name
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const genericTableNames = [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'employee',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'employees',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'customer',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'customers',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'order',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'orders',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'warehouse',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'warehouses',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'carrier',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'carriers',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'product',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'products',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'item',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'items',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'user',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'users'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (genericTableNames.includes(entityLower)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return false;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // 4. Reject if generic concept
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const genericConcepts = [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'process',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'procedure',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'system',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'policy',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'onboarding',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'training',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'support',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'service',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'department',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'role',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'location',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'region',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'manager',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'staff',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'team',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'people'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (genericConcepts.includes(entityLower)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return false;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // 5. Keep if single capitalized word (might be company name)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // "CommerceFlow", "California", "Texas"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (entity.match(/^[A-Z][a-z]+$/) &amp;amp;&amp;amp; entity.length &amp;gt; 3) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return true;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // 6. Default: reject
    &amp;nbsp; &amp;nbsp; &amp;nbsp; return false;
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; log("ENTITY_VALIDATION", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; before: allEntities,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; after: specificEntities,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; filtered_out: allEntities.filter((e)=&amp;gt;!specificEntities.includes(e)),
    &amp;nbsp; &amp;nbsp; &amp;nbsp; count_before: allEntities.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; count_after: specificEntities.length
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; const searchTasks = [];
    &amp;nbsp; &amp;nbsp; // TASK A: VECTOR
    &amp;nbsp; &amp;nbsp; if (embedding &amp;amp;&amp;amp; embedding.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; searchTasks.push(supabase.rpc('search_vector', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_embedding: embedding,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_limit: 20,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_threshold: config.min_vector_score
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }));
    &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; searchTasks.push(Promise.resolve([]));
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // TASK B: FTS
    &amp;nbsp; &amp;nbsp; const ftsQuery = routerRes.keywords &amp;amp;&amp;amp; routerRes.keywords.length &amp;gt; 0 ? routerRes.keywords.join(' ') : query;
    &amp;nbsp; &amp;nbsp; console.log(`[V8] FTS search for: "${ftsQuery}"`);
    &amp;nbsp; &amp;nbsp; searchTasks.push(supabase.rpc('search_fulltext', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_query: ftsQuery,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; p_limit: 20
    &amp;nbsp; &amp;nbsp; }));
    &amp;nbsp; &amp;nbsp; // TASK C: GRAPH (Entity Neighborhood)
    &amp;nbsp; &amp;nbsp; if (specificEntities.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; searchTasks.push((async ()=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[V8] Searching graph for entities:`, allEntities);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const { data, error } = await supabase.rpc('get_graph_neighborhood', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_entity_names: specificEntities
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("GRAPH_RAW", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; count: data &amp;amp;&amp;amp; data.length || 0,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; sample: data &amp;amp;&amp;amp; data[0],
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; error: error &amp;amp;&amp;amp; error.message
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (error) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("GRAPH_ERROR", error);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return [];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ FILTER OUT GARBAGE EDGES
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const cleanEdges = (data || []).filter((r)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Remove self-referential edges (subject == object)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (r.subject &amp;amp;&amp;amp; r.object &amp;amp;&amp;amp; r.subject === r.object) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[GRAPH_FILTER] Removed self-referential: ${r.subject} ${r.action} ${r.object}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return false;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Remove edges with "Inferred from carrier_id" context (low quality)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (r.context &amp;amp;&amp;amp; r.context.context &amp;amp;&amp;amp; r.context.context.includes('Inferred from carrier_id')) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[GRAPH_FILTER] Removed low-quality inferred edge: ${r.subject} ${r.action} ${r.object}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return false;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return true;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("GRAPH_FILTERED", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; before: data &amp;amp;&amp;amp; data.length || 0,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; after: cleanEdges.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; removed: (data &amp;amp;&amp;amp; data.length || 0) - cleanEdges.length
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return cleanEdges.map((r)=&amp;gt;({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...r,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; _source: `graph (${r.action})`,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; content: `[GRAPH] ${r.subject} ${r.action} ${r.object || ''}. Context: ${r.context &amp;amp;&amp;amp; r.context.context || ''}`
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; })());
    &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; searchTasks.push(Promise.resolve([]));
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // TASK D: SQL
    &amp;nbsp; &amp;nbsp; let sqlEntityNames = []; // Track entity names for graph (generic)
    &amp;nbsp; &amp;nbsp; let sqlEntityType = ''; // Track what type of entities
    &amp;nbsp; &amp;nbsp; if (routerRes.sql_tables &amp;amp;&amp;amp; routerRes.sql_tables.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; searchTasks.push((async ()=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const targetTable = routerRes.sql_tables[0];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[V8] Peeking at table: ${targetTable}`);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const allTableNames = [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; targetTable,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...routerRes.sql_tables.filter((t)=&amp;gt;t !== targetTable)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const allContexts = await Promise.all(allTableNames.map((tableName)=&amp;gt;supabase.rpc('get_table_context', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_table_name: tableName
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; })));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const context = allContexts[0].data; // Primary table
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const relatedSchemas = allContexts.slice(1).map((c)=&amp;gt;c.data).filter(Boolean);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!context || context.error) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("PEEK_ERROR", context?.error || "Failed to fetch table context");
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return [];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("TABLE_CONTEXT", context);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const sqlPrompt = `PostgreSQL Query Generator


    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Query: "${query}"


    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; PRIMARY TABLE: ${context.table}
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Schema: ${context.schema}
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ${context.description ? `Purpose: ${context.description}` : ''}


    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ${relatedSchemas.length &amp;gt; 0 ? `
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; RELATED TABLES:
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ${relatedSchemas.map((s)=&amp;gt;`${s.table}: ${s.schema}`).join('\n')}
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ` : ''}


    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ${context.related_tables &amp;amp;&amp;amp; context.related_tables.length &amp;gt; 0 ? `
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; JOINS (copy exact syntax):
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ${context.related_tables.map((rt)=&amp;gt;`${rt.join_on}`).join('\n')}
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ` : ''}


    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 🚨 MANDATORY RULES:


    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 1. DATE CONVERSION (start_date, order_date are Excel serials):
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ✅ CORRECT: WHERE EXTRACT(YEAR FROM (DATE '1899-12-30' + start_date::int)) = 2022
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ❌ WRONG: WHERE LEFT(start_date::text, 4) = '2022'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ❌ WRONG: WHERE TO_TIMESTAMP(order_date) &amp;gt;= '2024-01-01'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ❌ WRONG: WHERE start_date &amp;lt; 2015


    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 2. TYPE CASTING (all aggregates):
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - SUM(col::numeric)::double precision
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - AVG(col::numeric)::double precision &amp;nbsp;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - COUNT(*)::double precision
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - MIN/MAX(col::numeric)::double precision


    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 3. EXACT COLUMN NAMES (from schemas above):
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - order_status, warehouse_id, order_value_usd
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - NOT: status, id, value


    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 4. LIST QUERIES (contains "list", "show", "find", "display"):
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ✅ CORRECT: SELECT name, role, department, location, email, employee_id FROM...
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ❌ WRONG: SELECT name FROM...
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - Return ALL identifying + descriptive columns
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - For employees: name, role, department, location, email, employee_id
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - For customers: customer_id, customer_name, status, industry, location
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - For orders: order_id, customer_name, order_status, order_value_usd, order_date


    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 5. INCLUDE FILTER COLUMNS (CRITICAL):
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; If query filters by a column, you MUST include it (or computed version) in SELECT.
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ✅ CORRECT Examples:
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Query: "employees who started before 2015"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT name, role, department, location, email, employee_id,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;EXTRACT(YEAR FROM (DATE '1899-12-30' + start_date::int)) as start_year
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; FROM tbl_employees
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE EXTRACT(YEAR FROM (DATE '1899-12-30' + start_date::int)) &amp;lt; 2015
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Query: "orders over $1000"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT order_id, customer_name, order_status, order_value_usd
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; FROM tbl_orders
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE order_value_usd &amp;gt; 1000
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Query: "customers in California"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SELECT customer_id, customer_name, status, location
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; FROM tbl_customers
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WHERE location ILIKE '%California%'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WHY: Users need to verify results match the filter criteria.
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; RULE: WHERE clause column → must appear in SELECT


    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Output: {"sql": "..."}`;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const sqlGen = await callGemini(sqlPrompt, env.GOOGLE_API_KEY, config.model_sql);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("SQL_GENERATED", sqlGen);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("SQL_PROMPT_LENGTH", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; chars: sqlPrompt.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; lines: sqlPrompt.split('\n').length
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!sqlGen || !sqlGen.sql) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("SQL_GENERATION_FAILED", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reason: "Gemini returned empty or invalid response",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; response: sqlGen
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (sqlGen.sql) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const cleanSql = sqlGen.sql.replace(/```

sql/g, "").replace(/

```/g, "").trim();
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const { data, error } = await supabase.rpc('search_structured', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_query_sql: cleanSql,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_limit: 20
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (error || data &amp;amp;&amp;amp; data[0] &amp;amp;&amp;amp; data[0].table_name === 'ERROR') {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("SQL_ERROR", error || data &amp;amp;&amp;amp; data[0]);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; _source: "structured_error",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; error: error || data &amp;amp;&amp;amp; data[0]
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ V10: Validate SQL results have expected fields
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (data &amp;amp;&amp;amp; data.length &amp;gt; 0 &amp;amp;&amp;amp; data[0].row_data) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const firstRow = data[0].row_data;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const columns = Object.keys(firstRow);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const queryLower = query.toLowerCase();
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Check for date fields in temporal queries
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (queryLower.match(/\b(hired|year|month|date|when|2022|2023|2024)\b/)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const hasDateField = columns.some((col)=&amp;gt;col.includes('date') || col.includes('hire') || col.includes('start'));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!hasDateField) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("SQL_VALIDATION_WARNING", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; issue: "temporal_query_missing_date",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; query: query,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; columns: columns,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; suggestion: "SQL should include converted date field"
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Check for name fields in employee queries
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (targetTable.includes('employee') &amp;amp;&amp;amp; !columns.includes('name')) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("SQL_VALIDATION_WARNING", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; issue: "employee_query_missing_name",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; query: query,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; columns: columns
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Check for status/value fields in order queries
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (targetTable.includes('order') &amp;amp;&amp;amp; queryLower.match(/\b(status|value|price|amount)\b/)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const hasStatusOrValue = columns.some((col)=&amp;gt;col.includes('status') || col.includes('value') || col.includes('amount'));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!hasStatusOrValue) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("SQL_VALIDATION_WARNING", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; issue: "order_query_missing_status_or_value",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; query: query,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; columns: columns
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("SQL_VALIDATION_PASSED", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; columns: columns,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; row_count: data.length
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // ✅ GENERIC ENTITY EXTRACTION (works for ANY table/columns)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (data &amp;amp;&amp;amp; data.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const firstRow = data[0].row_data || {};
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const columns = Object.keys(firstRow);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Try common identifier columns in priority order
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const identifierColumns = [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'name',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'customer_name',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'brand_name',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'order_id',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'customer_id',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'employee_id',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'product_id',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'warehouse_id',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'title',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'project_name' // Generic identifiers
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Find first column that exists
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const identifierColumn = identifierColumns.find((col)=&amp;gt;columns.includes(col));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (identifierColumn) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; sqlEntityNames = data.map((row)=&amp;gt;row.row_data &amp;amp;&amp;amp; row.row_data[identifierColumn]).filter(Boolean);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; sqlEntityType = identifierColumn.replace(/_id$/, '').replace(/_name$/, '');
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("SQL_ENTITY_NAMES", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; column: identifierColumn,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; type: sqlEntityType,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; count: sqlEntityNames.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; sample: sqlEntityNames.slice(0, 3)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return data || [];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return [];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; })());
    &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; searchTasks.push(Promise.resolve([]));
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // WAIT FOR ALL SEARCHES
    &amp;nbsp; &amp;nbsp; const [vectorRes, ftsRes, graphRes, sqlResults] = await Promise.all(searchTasks);
    &amp;nbsp; &amp;nbsp; // ✅ SECOND GRAPH PASS: Use SQL-derived entity names
    &amp;nbsp; &amp;nbsp; let entityGraphRes = [];
    &amp;nbsp; &amp;nbsp; if (sqlEntityNames.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Validate these are real names, not generic terms
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const validSqlNames = sqlEntityNames.filter((name)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return name &amp;amp;&amp;amp; name.length &amp;gt; 2 &amp;amp;&amp;amp; ![
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'employee',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'customer',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'order',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'warehouse'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ].includes(name.toLowerCase());
    &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (validSqlNames.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; console.log(`[V8] Second graph pass with SQL ${sqlEntityType}:`, validSqlNames);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const { data: entityGraphData, error: entityGraphError } = await supabase.rpc('get_graph_neighborhood', {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; p_entity_names: validSqlNames
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!entityGraphError &amp;amp;&amp;amp; entityGraphData &amp;amp;&amp;amp; entityGraphData.length &amp;gt; 0) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Filter out garbage edges
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const cleanEntityEdges = entityGraphData.filter((r)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (r.subject &amp;amp;&amp;amp; r.object &amp;amp;&amp;amp; r.subject === r.object) return false;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (r.context &amp;amp;&amp;amp; r.context.context &amp;amp;&amp;amp; r.context.context.includes('Inferred from carrier_id')) return false;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return true;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; entityGraphRes = cleanEntityEdges.map((r)=&amp;gt;({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...r,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; _source: `graph (${r.action})`,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; content: `[GRAPH] ${r.subject} ${r.action} ${r.object || ''}. Context: ${r.context &amp;amp;&amp;amp; r.context.context || ''}`
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("ENTITY_GRAPH", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; type: sqlEntityType,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; count: entityGraphRes.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; sample: entityGraphRes[0]
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; log("ENTITY_GRAPH_SKIPPED", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reason: "no_valid_sql_names",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; filtered_out: sqlEntityNames
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Log all search results
    &amp;nbsp; &amp;nbsp; log("VECTOR_RAW", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; count: Array.isArray(vectorRes) ? vectorRes.length : vectorRes &amp;amp;&amp;amp; vectorRes.data &amp;amp;&amp;amp; vectorRes.data.length || 0
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; log("FTS_RAW", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; count: Array.isArray(ftsRes) ? ftsRes.length : ftsRes &amp;amp;&amp;amp; ftsRes.data &amp;amp;&amp;amp; ftsRes.data.length || 0
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; log("GRAPH_EDGES", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; count: Array.isArray(graphRes) ? graphRes.length : graphRes &amp;amp;&amp;amp; graphRes.data &amp;amp;&amp;amp; graphRes.data.length || 0,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; sample: Array.isArray(graphRes) ? graphRes[0] : graphRes &amp;amp;&amp;amp; graphRes.data &amp;amp;&amp;amp; graphRes.data[0]
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; log("SQL_RAW", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; count: Array.isArray(sqlResults) ? sqlResults.length : 0
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; // ✅ COMBINE VECTOR + FTS &amp;amp; DEDUPLICATE
    &amp;nbsp; &amp;nbsp; const vectorItems = Array.isArray(vectorRes) ? vectorRes : vectorRes &amp;amp;&amp;amp; vectorRes.data || [];
    &amp;nbsp; &amp;nbsp; const ftsItems = Array.isArray(ftsRes) ? ftsRes : ftsRes &amp;amp;&amp;amp; ftsRes.data || [];
    &amp;nbsp; &amp;nbsp; const combinedDocs = [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ...vectorItems,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ...ftsItems
    &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; const uniqueDocsMap = new Map();
    &amp;nbsp; &amp;nbsp; combinedDocs.forEach((doc)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (doc.chunk_id &amp;amp;&amp;amp; !uniqueDocsMap.has(doc.chunk_id)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; uniqueDocsMap.set(doc.chunk_id, doc);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; log("BEFORE_RERANK", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; count: uniqueDocsMap.size
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; log("BEFORE_RERANK_IDS", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; chunk_ids: Array.from(uniqueDocsMap.keys())
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; // ✅ V10: SELECTIVE RERANKING - Only rerank for policy/document queries
    &amp;nbsp; &amp;nbsp; // Skip reranking when we have structured results (SQL/entities/enrichment)
    &amp;nbsp; &amp;nbsp; const hasStructuredResults = routerRes.sql_tables &amp;amp;&amp;amp; routerRes.sql_tables.length &amp;gt; 0 || enrichedData.length &amp;gt; 0 || Array.isArray(sqlResults) &amp;amp;&amp;amp; sqlResults.length &amp;gt; 0;
    &amp;nbsp; &amp;nbsp; let rerankedDocs;
    &amp;nbsp; &amp;nbsp; if (hasStructuredResults) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Structured query: Keep top 15 docs without reranking (reranker often removes critical context)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; rerankedDocs = Array.from(uniqueDocsMap.values()).slice(0, 15);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; log("RERANK_SKIPPED", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reason: "structured_query_detected",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; keeping: rerankedDocs.length
    &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; } else {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Policy/document query: Use reranker with safety net
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // SAFETY NET: Always preserve top 5 by original score
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const allDocs = Array.from(uniqueDocsMap.values());
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const topByScore = allDocs.slice(0, 5);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Send up to 30 docs to reranker
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const rerankerResults = await rerankWithGemini(query, allDocs, env.GOOGLE_API_KEY, config.model_reranker, Math.min(uniqueDocsMap.size, 30));
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Merge: reranker results + top 5 safety net (deduplicated)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const merged = [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...rerankerResults
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; &amp;nbsp; let safetyNetAdded = 0;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; topByScore.forEach((doc)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!merged.find((d)=&amp;gt;d.chunk_id === doc.chunk_id)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; merged.push(doc);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; safetyNetAdded++;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Keep top 15 from merged results
    &amp;nbsp; &amp;nbsp; &amp;nbsp; rerankedDocs = merged.slice(0, 15);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; log("RERANK_EXECUTED", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reason: "policy_query_detected",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reranker_kept: rerankerResults.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; safety_net_added: safetyNetAdded,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; final_count: rerankedDocs.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fallback_used: rerankerResults.length === 0
    &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; log("AFTER_RERANK", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; count: rerankedDocs.length
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; // ✅ CONDITIONAL GRAPH: Use entity-specific OR generic, NOT both
    &amp;nbsp; &amp;nbsp; const graphItems = Array.isArray(graphRes) ? graphRes : graphRes &amp;amp;&amp;amp; graphRes.data || [];
    &amp;nbsp; &amp;nbsp; // If we have entity-specific edges, DROP generic graph entirely
    &amp;nbsp; &amp;nbsp; const allGraphItems = entityGraphRes.length &amp;gt; 0 ? entityGraphRes // ✅ Use ONLY entity-specific edges (high quality)
    &amp;nbsp; &amp;nbsp; &amp;nbsp;: graphItems; // Fallback to generic (if no entities)
    &amp;nbsp; &amp;nbsp; log("GRAPH_STRATEGY", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; strategy: entityGraphRes.length &amp;gt; 0 ? "entity-specific" : "generic",
    &amp;nbsp; &amp;nbsp; &amp;nbsp; entity_type: sqlEntityType || 'none',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; entity_count: entityGraphRes.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; generic_count: graphItems.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; using: allGraphItems.length
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; // ✅ V10: Filter graph edges for relevance BEFORE fusion
    &amp;nbsp; &amp;nbsp; const filteredGraphItems = allGraphItems;
    &amp;nbsp; &amp;nbsp; log("GRAPH_RELEVANCE_FILTER", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; before: allGraphItems.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; after: filteredGraphItems.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; removed: allGraphItems.length - filteredGraphItems.length
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; const fusedResults = await performRRFFusion(rerankedDocs, filteredGraphItems, sqlResults, config);
    &amp;nbsp; &amp;nbsp; log("AFTER_FUSION", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; count: fusedResults.length
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; // ✅ FILTER &amp;amp; DIVERSIFY GRAPH EDGES
    &amp;nbsp; &amp;nbsp; // 1. Filter redundant attributes (don't repeat what SQL already shows)
    &amp;nbsp; &amp;nbsp; // 2. Deduplicate by entity (show diverse entities, not 4 facts about 1 person)
    &amp;nbsp; &amp;nbsp; const RELATIONSHIP_ACTIONS = [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'REPORTS_TO',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'MANAGES',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'ASSIGNED_TO',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'WORKS_WITH',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'PLACED_BY',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'FULFILLED_FROM',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'SHIPPED_BY'
    &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; const ATTRIBUTE_ACTIONS = [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'HAS_ROLE',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'BELONGS_TO',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'WORKS_AT',
    &amp;nbsp; &amp;nbsp; &amp;nbsp; 'HAS_STATUS'
    &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; // Check what SQL already has
    &amp;nbsp; &amp;nbsp; const sqlHasRole = sqlResults.some((r)=&amp;gt;r.row_data &amp;amp;&amp;amp; r.row_data.role);
    &amp;nbsp; &amp;nbsp; const sqlHasDepartment = sqlResults.some((r)=&amp;gt;r.row_data &amp;amp;&amp;amp; (r.row_data.department || r.row_data.dept));
    &amp;nbsp; &amp;nbsp; const sqlHasLocation = sqlResults.some((r)=&amp;gt;r.row_data &amp;amp;&amp;amp; r.row_data.location);
    &amp;nbsp; &amp;nbsp; // Filter redundant edges
    &amp;nbsp; &amp;nbsp; const valuableEdges = filteredGraphItems.filter((edge)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Keep all relationship edges (these ADD new info)
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (RELATIONSHIP_ACTIONS.includes(edge.action)) return true;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Filter redundant attributes
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (edge.action === 'HAS_ROLE' &amp;amp;&amp;amp; sqlHasRole) return false;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (edge.action === 'BELONGS_TO' &amp;amp;&amp;amp; sqlHasDepartment) return false;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (edge.action === 'WORKS_AT' &amp;amp;&amp;amp; sqlHasLocation) return false;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Keep everything else
    &amp;nbsp; &amp;nbsp; &amp;nbsp; return true;
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; log("FILTERED_REDUNDANT", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; before: filteredGraphItems.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; after: valuableEdges.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; removed: filteredGraphItems.length - valuableEdges.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; sql_has: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; role: sqlHasRole,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; department: sqlHasDepartment,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; location: sqlHasLocation
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; // Group by entity (subject)
    &amp;nbsp; &amp;nbsp; const edgesByEntity = new Map();
    &amp;nbsp; &amp;nbsp; valuableEdges.forEach((edge)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const subject = edge.subject || 'unknown';
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (!edgesByEntity.has(subject)) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; edgesByEntity.set(subject, []);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; &amp;nbsp; edgesByEntity.get(subject).push(edge);
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; // Take 1 edge per entity (prioritize relationships)
    &amp;nbsp; &amp;nbsp; const diverseGraphEdges = [];
    &amp;nbsp; &amp;nbsp; for (const [subject, edges] of edgesByEntity){
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Prioritize relationship edges over attributes
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const sorted = edges.sort((a, b)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; const relPriority = {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'REPORTS_TO': 10,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'MANAGES': 9,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'ASSIGNED_TO': 8,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'PLACED_BY': 7,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'FULFILLED_FROM': 6,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'SHIPPED_BY': 5,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'BELONGS_TO': 3,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'WORKS_AT': 2,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'HAS_ROLE': 1
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return (relPriority[b.action] || 0) - (relPriority[a.action] || 0);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; &amp;nbsp; diverseGraphEdges.push(sorted[0]); // Take best edge for this entity
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (diverseGraphEdges.length &amp;gt;= 10) break; // Max 10 diverse entities
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; // Score them for proper ranking
    &amp;nbsp; &amp;nbsp; const scoredGraphEdges = diverseGraphEdges.map((e, idx)=&amp;gt;({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...e,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; score: config.rrf_weight_graph + (10 - idx) * 0.05
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }));
    &amp;nbsp; &amp;nbsp; log("DIVERSIFIED_GRAPH", {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; total_edges: filteredGraphItems.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; after_filter: valuableEdges.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; unique_entities: edgesByEntity.size,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; selected: scoredGraphEdges.length,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; sample: scoredGraphEdges[0]
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; // ✅ ADD ENRICHED DATA AT TOP
    &amp;nbsp; &amp;nbsp; const finalResults = [
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ...enrichedData.map((e)=&amp;gt;({
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...e,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; score: config.rrf_weight_enrichment
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; })),
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ...scoredGraphEdges,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; ...fusedResults
    &amp;nbsp; &amp;nbsp; ];
    &amp;nbsp; &amp;nbsp; // ✅ DEDUPLICATE by content (avoid showing same edge/doc twice)
    &amp;nbsp; &amp;nbsp; const seenKeys = new Set();
    &amp;nbsp; &amp;nbsp; const dedupedResults = finalResults.filter((item)=&amp;gt;{
    &amp;nbsp; &amp;nbsp; &amp;nbsp; // Generate unique key
    &amp;nbsp; &amp;nbsp; &amp;nbsp; const key = item.chunk_id ? `chunk_${item.chunk_id}` : item.subject &amp;amp;&amp;amp; item.action &amp;amp;&amp;amp; item.object ? `graph_${item.subject}_${item.action}_${item.object}` : JSON.stringify(item.row_data || item);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; if (seenKeys.has(key)) return false;
    &amp;nbsp; &amp;nbsp; &amp;nbsp; seenKeys.add(key);
    &amp;nbsp; &amp;nbsp; &amp;nbsp; return true;
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; &amp;nbsp; dedupedResults.sort((a, b)=&amp;gt;b.score - a.score);
    &amp;nbsp; &amp;nbsp; const payload = {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; success: true,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; results: dedupedResults.slice(0, resultLimit),
    &amp;nbsp; &amp;nbsp; &amp;nbsp; enrichment_count: enrichedData.length
    &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; &amp;nbsp; // Only include diagnostics when explicitly requested
    &amp;nbsp; &amp;nbsp; if (debugEnabled) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; payload.debug = debugInfo;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; return new Response(JSON.stringify(payload), {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...CORS,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 'Content-Type': 'application/json'
    &amp;nbsp; &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; } catch (error) {
    &amp;nbsp; &amp;nbsp; console.error("[V9] Search error:", error);
    &amp;nbsp; &amp;nbsp; const errorPayload = {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; error: error?.message || "Unknown error"
    &amp;nbsp; &amp;nbsp; };
    &amp;nbsp; &amp;nbsp; if (debugEnabled) {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; errorPayload.debug = debugInfo;
    &amp;nbsp; &amp;nbsp; }
    &amp;nbsp; &amp;nbsp; return new Response(JSON.stringify(errorPayload), {
    &amp;nbsp; &amp;nbsp; &amp;nbsp; status: 500,
    &amp;nbsp; &amp;nbsp; &amp;nbsp; headers: CORS
    &amp;nbsp; &amp;nbsp; });
    &amp;nbsp; }
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 7&lt;/strong&gt;. Now you can interact with your Supabase functions using the api endpoints ingest-intelligent and search (ingest-worker is called by ingest-intelligent). You can do that with any REST API call. I've created a couple of n8n workflows that facilitate this. First, here's two workflows for ingestion (one for documents and one for spreadsheets). If you have n8n, copy and paste this json into a canvas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    {
      "name": "Context Mesh V2 - File Uploader",
      "nodes": [
        {
          "parameters": {
            "path": "doc-upload-v2-robust",
            "formTitle": "Upload Document",
            "formDescription": "Upload text files (PDF, TXT, or MD) to add to the knowledge base",
            "formFields": {
              "values": [
                {
                  "fieldLabel": "data",
                  "fieldType": "file",
                  "requiredField": true
                },
                {
                  "fieldLabel": "Title (use logical title name)"
                }
              ]
            },
            "options": {}
          },
          "type": "n8n-nodes-base.formTrigger",
          "typeVersion": 2,
          "position": [
            1232,
            -80
          ],
          "id": "da526682-dde5-4245-ac50-2a337c71dad6",
          "name": "Document Upload Form",
          "webhookId": "doc-upload-v2-robust"
        },
        {
          "parameters": {
            "operation": "text",
            "options": {}
          },
          "type": "n8n-nodes-base.extractFromFile",
          "typeVersion": 1,
          "position": [
            1456,
            -80
          ],
          "id": "8b526c4c-6773-43cd-b19a-ce70fb75f518",
          "name": "Extract Text"
        },
        {
          "parameters": {
            "method": "POST",
            "url": "=https://zbtqpvkaycnonaslwqfq.supabase.co/functions/v1/ingest-intelligent",
            "authentication": "predefinedCredentialType",
            "nodeCredentialType": "supabaseApi",
            "sendBody": true,
            "bodyParameters": {
              "parameters": [
                {
                  "name": "uri",
                  "value": "={{ $('Document Upload Form').item.json.data[0].filename }}"
                },
                {
                  "name": "title",
                  "value": "={{ $('Document Upload Form').item.json['Title (optional)'] }}"
                },
                {
                  "name": "text",
                  "value": "={{ $json.data }}"
                }
              ]
            },
            "options": {}
          },
          "type": "n8n-nodes-base.httpRequest",
          "typeVersion": 4,
          "position": [
            1680,
            -80
          ],
          "id": "5e2c69f6-4312-4b23-8881-37ee9ad0c360",
          "name": "Ingest Document Chunk",
          "retryOnFail": true,
          "waitBetweenTries": 5000,
          "credentials": {
            "supabaseApi": {
              "id": "L1c6TGVJOHc8wt9H",
              "name": "infoSupa_contentMesh"
            }
          }
        },
        {
          "parameters": {
            "content": "## This is for documents (txt, pdf, md) \n",
            "width": 192
          },
          "type": "n8n-nodes-base.stickyNote",
          "position": [
            1120,
            -192
          ],
          "typeVersion": 1,
          "id": "3e1eeb6f-fe73-4676-b714-e35008d2b27a",
          "name": "Sticky Note"
        },
        {
          "parameters": {
            "content": "## This node extracts from txt files\n**Change this to extract from whatever file type you wish to upload. 'Extract from Text File' for .txt or .md. 'Extract from PDF' for .pdf.**\n",
            "height": 208,
            "color": 3
          },
          "type": "n8n-nodes-base.stickyNote",
          "position": [
            1376,
            -256
          ],
          "typeVersion": 1,
          "id": "b82b51c2-aa31-4ec3-b262-4c175da20e26",
          "name": "Sticky Note1"
        },
        {
          "parameters": {
            "content": "## Make sure your Supabase Credentials are saved in n8n\n",
            "height": 176,
            "color": 6
          },
          "type": "n8n-nodes-base.stickyNote",
          "position": [
            1856,
            48
          ],
          "typeVersion": 1,
          "id": "58314655-fd7a-48d5-921d-11aba1b11333",
          "name": "Sticky Note2"
        },
        {
          "parameters": {
            "content": "## This is for spreadsheets (csv, xls, xlsx) \n",
            "width": 192
          },
          "type": "n8n-nodes-base.stickyNote",
          "position": [
            1088,
            192
          ],
          "typeVersion": 1,
          "id": "04447f73-111d-45c7-a37b-d81e08cb5189",
          "name": "Sticky Note3"
        },
        {
          "parameters": {
            "content": "## This node extracts from spreadsheet files\n**Change this to extract from whatever file type you wish to upload. 'Extract from CSV', 'Extract from XLS', etc**\n",
            "height": 208,
            "color": 3
          },
          "type": "n8n-nodes-base.stickyNote",
          "position": [
            1376,
            112
          ],
          "typeVersion": 1,
          "id": "d23b51ce-557b-4811-82f4-137b6592f0ff",
          "name": "Sticky Note4"
        },
        {
          "parameters": {
            "path": "sheet-upload-v3",
            "formTitle": "Upload Spreadsheet (V3)",
            "formDescription": "Upload CSV or Excel files. The system handles large files automatically.",
            "formFields": {
              "values": [
                {
                  "fieldLabel": "data",
                  "fieldType": "file",
                  "requiredField": true
                },
                {
                  "fieldLabel": "Table Name (e.g. sales_data)",
                  "requiredField": true
                }
              ]
            },
            "options": {}
          },
          "type": "n8n-nodes-base.formTrigger",
          "typeVersion": 2,
          "position": [
            1216,
            304
          ],
          "id": "d6e7a240-e165-4ed4-9310-07d1a59944e1",
          "name": "Spreadsheet Upload Form",
          "webhookId": "sheet-upload-v3"
        },
        {
          "parameters": {
            "operation": "xlsx",
            "options": {}
          },
          "type": "n8n-nodes-base.extractFromFile",
          "typeVersion": 1,
          "position": [
            1440,
            304
          ],
          "id": "8e4447c8-22db-4cc3-a6a5-8faa8f245f4f",
          "name": "Extract Spreadsheet"
        },
        {
          "parameters": {
            "method": "POST",
            "url": "https://zbtqpvkaycnonaslwqfq.supabase.co/functions/v1/ingest-intelligent",
            "authentication": "predefinedCredentialType",
            "nodeCredentialType": "supabaseApi",
            "sendBody": true,
            "bodyParameters": {
              "parameters": [
                {
                  "name": "uri",
                  "value": "={{ $('Spreadsheet Upload Form').item.json.data[0].filename }}"
                },
                {
                  "name": "title",
                  "value": "={{ $('Spreadsheet Upload Form').item.json['Table Name (e.g. sales_data)'] }}"
                },
                {
                  "name": "data",
                  "value": "={{ $json.data }}"
                }
              ]
            },
            "options": {}
          },
          "type": "n8n-nodes-base.httpRequest",
          "typeVersion": 4,
          "position": [
            1888,
            304
          ],
          "id": "c4130f58-b1a7-4d58-a338-7f481b89a695",
          "name": "Send to Context Mesh",
          "retryOnFail": true,
          "waitBetweenTries": 5000,
          "credentials": {
            "supabaseApi": {
              "id": "L1c6TGVJOHc8wt9H",
              "name": "infoSupa_contentMesh"
            }
          }
        },
        {
          "parameters": {
            "aggregate": "aggregateAllItemData",
            "options": {}
          },
          "type": "n8n-nodes-base.aggregate",
          "typeVersion": 1,
          "position": [
            1664,
            304
          ],
          "id": "5840c145-0511-4cec-8b70-3d5d373e2556",
          "name": "Aggregate"
        }
      ],
      "pinData": {},
      "connections": {
        "Document Upload Form": {
          "main": [
            [
              {
                "node": "Extract Text",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "Extract Text": {
          "main": [
            [
              {
                "node": "Ingest Document Chunk",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "Ingest Document Chunk": {
          "main": [
            []
          ]
        },
        "Spreadsheet Upload Form": {
          "main": [
            [
              {
                "node": "Extract Spreadsheet",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "Extract Spreadsheet": {
          "main": [
            [
              {
                "node": "Aggregate",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "Aggregate": {
          "main": [
            [
              {
                "node": "Send to Context Mesh",
                "type": "main",
                "index": 0
              }
            ]
          ]
        }
      },
      "active": false,
      "settings": {
        "executionOrder": "v1"
      },
      "versionId": "dc8d82a5-eb54-446f-ba8a-9274469bb70e",
      "meta": {
        "templateCredsSetupCompleted": true,
        "instanceId": "1dbf32ab27f7926a258ac270fe5e9e15871cfb01059a55b25aa401186050b9b5"
      },
      "id": "P9zYEohLKCCgjkym",
      "tags": []
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 8&lt;/strong&gt;. Here's a workflow for the 'search' endpoint. This one is the retrieval. I connected it as a tool to an A.I. agent, so you can just start chatting and reference your data directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    {
      "name": "Context Mesh V2 - Chat Interface",
      "nodes": [
        {
          "parameters": {
            "options": {}
          },
          "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
          "typeVersion": 1,
          "position": [
            304,
            528
          ],
          "id": "9da9a603-da31-460f-9f1a-96e0bb3e9e23",
          "name": "Google Gemini Chat Model",
          "credentials": {
            "googlePalmApi": {
              "id": "YEyGAyg7bHXHutrf",
              "name": "sb_projects"
            }
          }
        },
        {
          "parameters": {
            "toolDescription": "composite_query: query Supabase using edge function that retrieves hybrid vector search, SQL, and knowledge graph all at once.",
            "method": "POST",
            "url": "https://zbtqpvkaycnonaslwqfq.supabase.co/functions/v1/search",
            "authentication": "predefinedCredentialType",
            "nodeCredentialType": "supabaseApi",
            "sendHeaders": true,
            "headerParameters": {
              "parameters": [
                {
                  "name": "Content-Type",
                  "value": "application/json"
                }
              ]
            },
            "sendBody": true,
            "bodyParameters": {
              "parameters": [
                {
                  "name": "query",
                  "value": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('parameters0_Value', ``, 'string') }}"
                },
                {
                  "name": "limit",
                  "value": "20"
                }
              ]
            },
            "options": {}
          },
          "type": "n8n-nodes-base.httpRequestTool",
          "typeVersion": 4.2,
          "position": [
            560,
            528
          ],
          "id": "f02b140b-aaad-48b2-be6c-42ae55a1209f",
          "name": "composite_query",
          "credentials": {
            "supabaseApi": {
              "id": "L1c6TGVJOHc8wt9H",
              "name": "infoSupa_contentMesh"
            }
          }
        },
        {
          "parameters": {
            "options": {
              "systemMessage": "=You have access to a powerful search tool called `composite_query` that searches through a knowledge base using three search methods simultaneously:\n1. **Vector search** - semantic/meaning-based search\n2. **Graph search** - entity and relationship traversal  \n3. **Structured search** - full-text filtering\n\n**When to use this tool:**\n- Whenever the user asks any question\n- When you need factual information to answer questions accurately\n- When the user requests specific filtering or analysis\n\n**How to use this tool:**\nOutput a query_text:\n\n**Required:**\n- `query_text` (string) - The user's question or search terms in natural language\n\n\n**What you'll receive:**\n- `context_block` - Formatted text with source, content, entities, and relationships\n- `entities` - JSON array of relevant entities (people, products, companies, etc.)\n- `relationships` - JSON array showing how entities are connected\n- `relevance` - Indicates which search methods found this result\n\n**Important:** Always use this tool before answering questions. Use the returned context to provide accurate, grounded answers. Reference entities and relationships when relevant.\n"
            }
          },
          "type": "@n8n/n8n-nodes-langchain.agent",
          "typeVersion": 2.2,
          "position": [
            352,
            304
          ],
          "id": "c32fa3a4-d62f-47dc-b0a2-a4c418a30a35",
          "name": "AI Agent"
        },
        {
          "parameters": {
            "options": {}
          },
          "type": "@n8n/n8n-nodes-langchain.chatTrigger",
          "typeVersion": 1.3,
          "position": [
            128,
            304
          ],
          "id": "350f2dcc-4f8b-4dc7-861a-fe661b06348f",
          "name": "When chat message received",
          "webhookId": "873bddf5-f2ee-4ead-afa3-0a09463389ea"
        }
      ],
      "pinData": {},
      "connections": {
        "Google Gemini Chat Model": {
          "ai_languageModel": [
            [
              {
                "node": "AI Agent",
                "type": "ai_languageModel",
                "index": 0
              }
            ]
          ]
        },
        "composite_query": {
          "ai_tool": [
            [
              {
                "node": "AI Agent",
                "type": "ai_tool",
                "index": 0
              }
            ]
          ]
        },
        "When chat message received": {
          "main": [
            [
              {
                "node": "AI Agent",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "AI Agent": {
          "main": [
            []
          ]
        }
      },
      "active": false,
      "settings": {
        "executionOrder": "v1"
      },
      "versionId": "16316f80-7cac-4bd4-b05e-b0c4230a9e85",
      "meta": {
        "templateCredsSetupCompleted": true,
        "instanceId": "1dbf32ab27f7926a258ac270fe5e9e15871cfb01059a55b25aa401186050b9b5"
      },
      "id": "4lKXwzK514XEOuiY",
      "tags": []
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the full Context Mesh Lite. Cheers!&lt;/p&gt;

&lt;p&gt;P.S. if you want these in managable files, use this form here to give me your email and I'll send them to you in a nice zip file:&lt;br&gt;
&lt;a href="https://vmat.fillout.com/context-mesh-lite" rel="noopener noreferrer"&gt;https://vmat.fillout.com/context-mesh-lite&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gemini</category>
      <category>serverless</category>
      <category>database</category>
      <category>ai</category>
    </item>
    <item>
      <title>Beyond Basic RAG: 3 Advanced Architectures I Built to Fix AI Retrieval</title>
      <dc:creator>Anthony Lee</dc:creator>
      <pubDate>Sun, 07 Dec 2025 10:07:11 +0000</pubDate>
      <link>https://dev.to/anthony_lee_63e96408d7573/beyond-basic-rag-3-advanced-architectures-i-built-to-fix-ai-retrieval-4e5b</link>
      <guid>https://dev.to/anthony_lee_63e96408d7573/beyond-basic-rag-3-advanced-architectures-i-built-to-fix-ai-retrieval-4e5b</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Everyone builds a "Chat with your Data" bot eventually. But standard RAG fails when data is static (latency), exact (SQL table names), or noisy (Slack logs). Here are the three specific architectural patterns I used to solve those problems across three different products: &lt;strong&gt;Client-side Vector Search&lt;/strong&gt;, &lt;strong&gt;Temporal Graphs&lt;/strong&gt;, and &lt;strong&gt;Heuristic Signal Filtering&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Story
&lt;/h2&gt;

&lt;p&gt;I’ve been building AI-driven tools for a while now. I started in the no-code space, building “A.I. Agents” in n8n. Over the last several months I pivoted to coding solutions, many of which involve or revolve around RAG.&lt;/p&gt;

&lt;p&gt;And like many, I hit the wall.&lt;/p&gt;

&lt;p&gt;The "Hello World" of RAG is easy. But when you try to put it into production—where users want instant answers inside Excel, or need complex context about "when" something happened, or want to query a messy Slack history—the standard pattern breaks down.&lt;/p&gt;

&lt;p&gt;I’ve built three distinct projects recently, each with unique constraints that forced me to abandon the "default" RAG architecture. Here is exactly how I architected them and the specific strategies I used to make them work.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Formula AI (The "Mini" RAG)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Build:&lt;/strong&gt; An add-in for Google Sheets/Excel. The user opens a chat widget, describes what they want to do with their data, and the AI tells them which formula to use and where, writes it for them, and places the formula at the click of a button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; Latency and Privacy. Sending every user query to a cloud vector database (like Pinecone or Weaviate) to search a static dictionary of Excel functions is overkill. It introduces network lag and unnecessary costs for a dataset that rarely changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Strategy: Client-Side Vector Search&lt;/strong&gt; I realized the "knowledge base" (the dictionary of Excel/Google functions) is finite. It’s not petabytes of data; it’s a few hundred rows.&lt;/p&gt;

&lt;p&gt;Instead of a remote database, I turned the dataset into a &lt;strong&gt;portable vector search engine&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I took the entire function dictionary.
&lt;/li&gt;
&lt;li&gt;I generated vector embeddings and full-text indexes (tsvector) for every function description.
&lt;/li&gt;
&lt;li&gt;I exported this as a static JSON/binary object.
&lt;/li&gt;
&lt;li&gt;I host that file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When the add-in loads, it fetches this "Mini-DB" once. Now, when the user types, the retrieval happens &lt;strong&gt;locally in the browser&lt;/strong&gt; (or via a super-lightweight edge worker). The LLM receives the relevant formula context instantly without a heavy database query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 60-second mental model:&lt;/strong&gt; &lt;code&gt;[Static Data] -&amp;gt; [Pre-computed Embeddings] -&amp;gt; [JSON File] -&amp;gt; [Client Memory]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Takeaway:&lt;/strong&gt; &lt;strong&gt;You don't always need a Vector Database.&lt;/strong&gt; If your domain data is under 50MB and static (like documentation, syntax, or FAQs), compute your embeddings beforehand and ship them as a file. It’s faster, cheaper, and privacy-friendly.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Context Mesh (The "Hybrid" Graph)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Build:&lt;/strong&gt; A hybrid retrieval system that combines vector search, full-text retrieval, SQL, and graph search into a single answer. It allows LLMs to query databases intelligently while understanding the relationships between data points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; Vector search is terrible at &lt;strong&gt;exactness&lt;/strong&gt; and &lt;strong&gt;time&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If you search for "Order table", vectors might give you "shipping logs" (semantically similar) rather than the actual SQL table &lt;code&gt;tbl_orders_001&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;If you search "Why did the server crash?", vectors give you the &lt;em&gt;fact&lt;/em&gt; of the crash, but not the &lt;em&gt;sequence&lt;/em&gt; of events leading up to it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Strategy: Trigrams + Temporal Graphs&lt;/strong&gt; I approached this with a two-pronged solution:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part A: Trigrams for Structure&lt;/strong&gt; To solve the SQL schema problem, I use &lt;strong&gt;Trigram Similarity&lt;/strong&gt; (specifically &lt;code&gt;pg_trgm&lt;/code&gt; in Postgres). Vectors understand &lt;em&gt;meaning&lt;/em&gt;, but Trigrams understand &lt;em&gt;spelling&lt;/em&gt;. If the LLM needs a table name, we use Trigrams/&lt;code&gt;ilike&lt;/code&gt; to find the exact match, and only use vectors to find the relevant SQL syntax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part B: The Temporal Graph&lt;/strong&gt; Data isn't just &lt;em&gt;what&lt;/em&gt; happened, but &lt;em&gt;when&lt;/em&gt; and &lt;em&gt;in relation to what&lt;/em&gt;. In a standard vector store, "Server Crash" from 2020 looks the same as "Server Crash" from today. I implemented a lightweight graph where &lt;strong&gt;Time&lt;/strong&gt; and &lt;strong&gt;Events&lt;/strong&gt; are nodes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[User] --(commented)--&amp;gt; [Ticket] --(happened_at)--&amp;gt; [Event Node: Tuesday 10am]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When retrieving, even if the vector match is imperfect, the graph provides "relevant adjacency." We can see that the crash coincided with "Deployment 001" because they share a temporal node in the graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Takeaway:&lt;/strong&gt; &lt;strong&gt;Context is relational.&lt;/strong&gt; Don't just chuck text into a vector store. Even a shallow graph (linking Users, Orders, and Time) provides the "connective tissue" that pure vector search misses.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Slack Brain (The "Noise" Filter)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Build:&lt;/strong&gt; A connected knowledge hub inside Slack. It ingests files (PDFs, Videos, CSVs) and chat history, turning them into a queryable brain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; &lt;strong&gt;Signal to Noise Ratio.&lt;/strong&gt; Slack is 90% noise. "Good morning," "Lunch?", "lol." If you blindly feed all this into an LLM or vector store, you dilute your signal and bankrupt your API credits. Additionally, unstructured data (videos) and structured data (CSVs) need different treatment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Strategy: Heuristic Filtering &amp;amp; Normalization&lt;/strong&gt; I realized we can't rely on the AI to decide what is important—that's too expensive. We need to filter &lt;em&gt;before&lt;/em&gt; we embed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step A: The Heuristic Gate&lt;/strong&gt; We identify "Important Threads" programmatically using a set of rigid rules—&lt;strong&gt;No AI involved yet.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the thread inactive for X hours? (It's finished).
&lt;/li&gt;
&lt;li&gt;Does it have &amp;gt; 1 participant? (It's a conversation, not a monologue).
&lt;/li&gt;
&lt;li&gt;Does it follow a Q&amp;amp;A pattern? (e.g., ends with "Thanks" or "Fixed").
&lt;/li&gt;
&lt;li&gt;Does it contain specific keywords indicating a solution?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only if a thread passes these gates do we pass it to the LLM to summarize and embed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step B: Aggressive Normalization&lt;/strong&gt; To make the LLM's life easier, we reduce all file types to the lowest common denominator:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documents/Transcripts&lt;/strong&gt; → &lt;code&gt;.md&lt;/code&gt; files (ideal for dense retrieval).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured Data&lt;/strong&gt; → &lt;code&gt;.csv&lt;/code&gt; rows (ideal for code interpreter/analysis).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Takeaway:&lt;/strong&gt; &lt;strong&gt;Don't use AI to filter noise.&lt;/strong&gt; Use code. Simple logical heuristics are free, fast, and surprisingly effective at curating high-quality training data from messy chat logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;We are moving past the phase of "I sent a prompt to OpenAI and got an answer." The next generation of AI apps requires composite architectures.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Formula AI&lt;/strong&gt; taught me that sometimes the best database is a JSON file in memory.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Mesh&lt;/strong&gt; taught me that "time" and "spelling" are just as important as semantic meaning.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack Brain&lt;/strong&gt; taught me that heuristics save your wallet, and strict normalization saves your context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't be afraid to mix and match. The best retrieval systems aren't pure; they are pragmatic.&lt;/p&gt;

&lt;p&gt;Be well and build good systems.  &lt;/p&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>rag</category>
    </item>
    <item>
      <title>How I Created Superior RAG Retrieval With 3 Files in Supabase</title>
      <dc:creator>Anthony Lee</dc:creator>
      <pubDate>Wed, 12 Nov 2025 11:30:35 +0000</pubDate>
      <link>https://dev.to/anthony_lee_63e96408d7573/how-i-created-superior-rag-retrieval-with-3-files-in-supabase-4o25</link>
      <guid>https://dev.to/anthony_lee_63e96408d7573/how-i-created-superior-rag-retrieval-with-3-files-in-supabase-4o25</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;br&gt;
Plain RAG (vector + full-text) is great at fetching &lt;em&gt;facts in passages&lt;/em&gt;, but it struggles with &lt;em&gt;relationship answers&lt;/em&gt; (e.g., “How many times has this customer ordered?”). &lt;strong&gt;Context Mesh&lt;/strong&gt; adds a lightweight knowledge graph inside Supabase—so semantic, lexical, and &lt;strong&gt;relational&lt;/strong&gt; context get fused into one ranked result set (via RRF). It’s an opinionated pattern that lives mostly in SQL + Supabase RPCs. If hybrid search hasn’t closed the gap for you, add the graph.&lt;/p&gt;


&lt;h2&gt;
  
  
  The story
&lt;/h2&gt;

&lt;p&gt;I've been somewhat obsessed with RAG and A.I. powered document retrieval for some time. When I first figured out how to set up a vector DB using no-code, I did. When I learned how to set up hybrid retrieval I did. When I taught my A.I. agents how to generate SQL queries, I added that too. Despite those being INCREDIBLY USEFUL when combined, for most business cases it was still missing...something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
Let's say you have a pipeline into your RAG system that updates new order and logistics info (if not...you really should). Now let's say your customer support rep wants to query order #889. What they'll get back is likely all the information for that line-item; person who ordered, their contact info, product, shipping details, etc. &lt;/p&gt;

&lt;p&gt;What you &lt;strong&gt;don’t&lt;/strong&gt; get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;total number of orders by that buyer,&lt;/li&gt;
&lt;li&gt;when they first became a customer,&lt;/li&gt;
&lt;li&gt;lifetime value,&lt;/li&gt;
&lt;li&gt;number of support interactions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can SQL-join your way there—but that’s brittle and time-consuming. A &lt;strong&gt;knowledge graph&lt;/strong&gt; naturally keeps those relationships.&lt;/p&gt;

&lt;p&gt;That's why I've been building what I call the Context Mesh. On the journey I've created a lite version, which exists almost entirely in Supabase and requires only three files to implement (within Supabase, plus additional UI means of interacting with the system).&lt;/p&gt;

&lt;p&gt;Those elements are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an &lt;strong&gt;ingestion path&lt;/strong&gt; that standardizes content and writes to SQL + graph,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;retrieval path&lt;/strong&gt; that runs vector + FTS + graph and fuses results,&lt;/li&gt;
&lt;li&gt;a single &lt;strong&gt;SQL migration&lt;/strong&gt; that creates tables, functions, and indexes.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Before vs. after
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;User asks:&lt;/strong&gt; “Show me order #889 and customer context.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain RAG (before):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;889&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alexis Chen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alexis@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Ethiopia Natural 2x"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ship_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Delivered 2024-03-11"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Context Mesh (after):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;889&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alexis Chen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lifetime_orders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"first_order_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-08-19"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lifetime_value_eur"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;642.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"support_tickets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_ticket_disposition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Carrier delay - resolved"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this happens:&lt;/strong&gt; the system links &lt;code&gt;node(customer: Alexis Chen)&lt;/code&gt; ⇄ &lt;code&gt;orders&lt;/code&gt; ⇄ &lt;code&gt;tickets&lt;/code&gt; and stores those edges. Retrieval calls &lt;code&gt;search_vector&lt;/code&gt;, &lt;code&gt;search_fulltext&lt;/code&gt;, &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;search_graph&lt;/code&gt;, then unifies with RRF so top answers include the &lt;em&gt;relational&lt;/em&gt; context.&lt;/p&gt;




&lt;h2&gt;
  
  
  60-second mental model
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Files / CSVs] ──&amp;gt; [document] ──&amp;gt; [chunk] ─┬─&amp;gt; [chunk_embedding]  (vector)
                                          │
                                          ├─&amp;gt; [chunk.tsv]        (FTS)
                                          │
                                          └─&amp;gt; [chunk_node] ─&amp;gt; [node] &amp;lt;─&amp;gt; [edge]  (graph)

vector/full-text/graph ──&amp;gt; search_unified (RRF) ──&amp;gt; ranked, mixed results (chunks + rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What’s inside Context Mesh Lite (Supabase)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documents &amp;amp; chunks&lt;/strong&gt; with embeddings and FTS (&lt;code&gt;tsvector&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight graph&lt;/strong&gt;: &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;edge&lt;/code&gt;, plus &lt;code&gt;chunk_node&lt;/code&gt; mentions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured registry&lt;/strong&gt; for spreadsheet-to-SQL tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search functions&lt;/strong&gt;: vector, FTS, graph, and &lt;strong&gt;unified fusion&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guarded SQL execution&lt;/strong&gt; for safe read-only structured queries&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The SQL migration (collapsed for readability)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1) Extensions&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- EXTENSIONS&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_trgm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Enables vector embeddings and trigram text similarity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Core tables&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="n"&gt;TSVECTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_embedding&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_node&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;structured_table&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt; &lt;span class="n"&gt;schema_def&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Documents + chunks; embeddings; a minimal graph; and a registry for spreadsheet-derived tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) Indexes for speed&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;chunk_tsv_gin&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tsv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;emb_hnsw_cos&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_embedding&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;HNSW&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;edge_src_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;edge_dst_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;node_labels_gin&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;node_props_gin&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;FTS GIN + vector HNSW + graph helpers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4) Triggers &amp;amp; helpers&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_tsv_update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;doc_title&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s1"&gt;'B'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;chunk_tsv_trg&lt;/span&gt;
&lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;OF&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_tsv_update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sanitize_table_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'tbl_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s1"&gt;'[^a-z0-9_]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'_'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'g'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infer_column_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_values&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="c1"&gt;-- counts booleans/numerics/dates and returns BOOLEAN/NUMERIC/DATE/TEXT&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Keeps FTS up-to-date; normalizes spreadsheet table names; infers column types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5) Ingest documents (chunks + embeddings + graph)&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ingest_document_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;p_uri&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_doc_meta&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p_chunk&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_nodes&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_edges&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_mentions&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_doc_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ordinal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ordinal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_chunk_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_chunk&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'embedding'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;-- Upsert nodes/edges and link mentions chunk↔node&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ok'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'document_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'chunk_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_chunk_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;6) Ingest spreadsheets → SQL tables&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ingest_spreadsheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;p_uri&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_table_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p_rows&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_schema&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_nodes&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_edges&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="s1"&gt;'spreadsheet'&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="n"&gt;v_safe_name&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sanitize_table_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_table_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;-- CREATE MODE: infer columns &amp;amp;amp; types, then CREATE TABLE public.%I (...)&lt;/span&gt;
  &lt;span class="c1"&gt;-- APPEND MODE: reuse existing columns and INSERT rows&lt;/span&gt;
  &lt;span class="c1"&gt;-- Update structured_table(schema_def,row_count)&lt;/span&gt;
  &lt;span class="c1"&gt;-- Optional: upsert nodes/edges from the data&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ok'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'table_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_safe_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rows_inserted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_row_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;7) Search primitives (vector, FTS, graph)&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_embedding&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;p_limit&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;FLOAT8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;p_embedding&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row_number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;p_embedding&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_embedding&lt;/span&gt; &lt;span class="n"&gt;ce&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;p_limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_fulltext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_query&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_limit&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;FLOAT8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;websearch_to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tsq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts_rank_cd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tsv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tsq&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;float8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row_number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tsq&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;p_limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_graph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_keywords&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;p_limit&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;FLOAT8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;seeds&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(...),&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(...),&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;min_depth&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;float8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mention_count&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;float8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;row_number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;p_limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;8) Safe read-only SQL for structured data&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_query_sql&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_limit&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row_data&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;FLOAT8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;STABLE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- Reject dangerous statements and trailing semicolons&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;p_query_sql&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;p_query_sql&lt;/span&gt; &lt;span class="o"&gt;~*&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s1"&gt;(insert|update|delete|drop|alter|grant|revoke|truncate)&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;RETURN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;v_sql&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'WITH user_query AS (%s)
     SELECT &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;result&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; AS table_name, to_jsonb(user_query.*) AS row_data, 1.0::float8 AS score,
            (row_number() OVER ())::int AS rank FROM user_query LIMIT %s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p_query_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_limit&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;v_sql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;RETURN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;9) Unified search with RRF fusion&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_unified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;p_query_text&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_query_embedding&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;p_keywords&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;p_query_sql&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_limit&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_rrf_constant&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;final_score&lt;/span&gt; &lt;span class="n"&gt;FLOAT8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_rank&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fts_rank&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;graph_rank&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;struct_rank&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="n"&gt;vector_results&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_vector&lt;/span&gt;&lt;span class="p"&gt;(...)),&lt;/span&gt;
  &lt;span class="n"&gt;fts_results&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_fulltext&lt;/span&gt;&lt;span class="p"&gt;(...)),&lt;/span&gt;
  &lt;span class="n"&gt;graph_results&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_graph&lt;/span&gt;&lt;span class="p"&gt;(...)),&lt;/span&gt;
  &lt;span class="n"&gt;unstructured_fusion&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_rrf_constant&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;vr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
               &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_rrf_constant&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
               &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_rrf_constant&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rrf_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;vector_rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;fts_rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;graph_rank&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;vector_results&lt;/span&gt; &lt;span class="n"&gt;vr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;vr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;fts_results&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;graph_results&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;vr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;structured_results&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_query_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_limit&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="c1"&gt;-- graph-aware boost for structured rows by matching entity names&lt;/span&gt;
  &lt;span class="n"&gt;structured_with_graph&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(...),&lt;/span&gt;
  &lt;span class="n"&gt;structured_ranked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(...),&lt;/span&gt;
  &lt;span class="n"&gt;structured_normalized&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(...),&lt;/span&gt;
  &lt;span class="n"&gt;combined&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'chunk'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;result_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;structured_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rrf_score&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;final_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;unstructured_fusion&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'structured'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rrf_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;graph_rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;struct_rank&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;structured_normalized&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;combined&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;final_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;p_limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;10) Grants&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;SEQUENCES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;FUNCTIONS&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Security &amp;amp; cost notes (the honest bits)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails&lt;/strong&gt;: &lt;code&gt;search_structured&lt;/code&gt; blocks DDL/DML—keep it that way. If you expose custom SQL, add allowlists and parse checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII&lt;/strong&gt;: if nodes contain emails/phones, consider hashing or using RLS policies keyed by tenant/account.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cost drivers&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;embedding generation (per chunk),&lt;/li&gt;
&lt;li&gt;HNSW maintenance (inserts/updates),&lt;/li&gt;
&lt;li&gt;storage growth for &lt;code&gt;chunk&lt;/code&gt;, &lt;code&gt;chunk_embedding&lt;/code&gt;, and the graph.
Track these; consider tiered retention (hot vs warm).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Limitations &amp;amp; edge cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Graph drift&lt;/strong&gt;: entity IDs and names change—keep stable IDs, use alias nodes for renames.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal truth&lt;/strong&gt;: add &lt;code&gt;effective_from&lt;/code&gt;/&lt;code&gt;to&lt;/code&gt; on edges if you need time-aware answers (“as of March 2024”).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema evolution&lt;/strong&gt;: spreadsheet ingestion may need migrations (or shadow tables) when types change.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A tiny, honest benchmark (illustrative)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query type&lt;/th&gt;
&lt;th&gt;Plain RAG&lt;/th&gt;
&lt;th&gt;Context Mesh&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact order lookup&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer 360 roll-up&lt;/td&gt;
&lt;td&gt;😬&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“First purchase when?”&lt;/td&gt;
&lt;td&gt;😬&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Top related tickets?”&lt;/td&gt;
&lt;td&gt;😬&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The win isn’t fancy math; it’s &lt;em&gt;capturing relationships&lt;/em&gt; and letting retrieval use them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create a Supabase project; enable &lt;code&gt;vector&lt;/code&gt; and &lt;code&gt;pg_trgm&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run the single SQL migration (tables, functions, indexes, grants).&lt;/li&gt;
&lt;li&gt;Wire up your ingestion path to call the &lt;strong&gt;document&lt;/strong&gt; and &lt;strong&gt;spreadsheet&lt;/strong&gt; RPCs.&lt;/li&gt;
&lt;li&gt;Wire up retrieval to call &lt;strong&gt;unified search&lt;/strong&gt; with:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;natural-language text,&lt;/li&gt;
&lt;li&gt;an embedding (optional but recommended),&lt;/li&gt;
&lt;li&gt;a keyword set (for graph seeding),&lt;/li&gt;
&lt;li&gt;a safe, read-only SQL snippet (for structured lookups).

&lt;ol&gt;
&lt;li&gt;Add lightweight logging so you can see fusion behavior and adjust weights.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(I built a couple of n8n workflows to easily interact with the Context Mesh; workflows for ingestion calling the ingest edge function, and a workflow chat UI that interacts with the search edge function.)&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is this overkill for simple Q&amp;amp;A?&lt;/strong&gt;&lt;br&gt;
If your queries never need rollups, joins, or cross-entity context, plain hybrid RAG is fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a giant knowledge graph?&lt;/strong&gt;&lt;br&gt;
No. Start small: Customers, Orders, Tickets—then add edges as you see repeated questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about multilingual content?&lt;/strong&gt;&lt;br&gt;
Set FTS configuration per language and keep embeddings in a multilingual model; the pattern stays the same.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;After upserting the same documents into Context Mesh-enabled Supabase as well as a traditional vector store, I connected both to the chat agent. Context Mesh consistently outperforms regular RAG. &lt;/p&gt;

&lt;p&gt;That's because it has more access to structured data, temporal reasoning, relationship context, etc. All because of the additional context provided by nodes and edges from a knowledge graph. Hopefully this helps you down the path of superior retrieval as well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be well and build good systems.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>supabase</category>
      <category>database</category>
    </item>
  </channel>
</rss>
