Natural language querying across two databases — no ERD provided, relationships auto-inferred from instance data via SQL-pushed column profiling, semantic ontology built by LLM from column stats (null density, distinct ratio, value distributions), SQL generated by Claude Haiku.
You type a plain English question. The agent:
- Loads a semantic ontology inferred entirely from column statistics — no hardcoded schema knowledge
- Resolves join paths across two databases via structural FK inference
- Generates SQL with exact enum values and correct table prefixes
- Executes cross-DB SQL and returns results + chart config
Example queries that work today:
- "What is the breakdown of assets by equipment class?"
- "Which assets have the most critical outages?"
- "Which engineers own the most active campaigns?"
- "What product families are most involved in delayed maintenance events?"
- "Which asset equipment classes appear most in critical severity outages and have open service cases?" — 3-table cross-DB join
This repo contains two end-to-end NL-to-SQL pipelines built on the same architecture, applied to different Siemens domains.
operations.db + quality.db — machines, maintenance logs, parts inventory, production batches, defect reports.
Human-in-the-loop annotation step for semantic relationship verbs.
domain1.db + domain2.db — two domains: Operational & Power Grid + Execution & Energy Service Campaigns.
Fully LLM-driven semantic inference — no human annotation step, no hardcoded edges.
Column stats computed entirely via SQL (null density, distinct ratio, top value distributions, min/max/avg for numeric columns).
Raw data (CSV / SQLite)
↓ generate_energy.py
domain1.db (Power Grid) domain2.db (Campaigns)
↓ schema_profiler_v2.py ← SQL-pushed stats per column
column_profiles.json relationships.json
↓ semantic_inferrer_v2.py ← Haiku infers entity names + relationship verbs
from null density, distinct ratio, value distributions
ontology.json semantic_graph_v2.png
↓ query_agent_v2.py
NL → SQL → DataFrame + chart config
| Table | Rows | Key entities |
|---|---|---|
ASSET_VW |
500 | Physical energy assets — turbines, transformers, substations |
OUTAGE__C_VW |
100 | Unplanned grid failures and forced outages |
CASE_VW |
3,000 | Field service tickets and NCRs |
SERVICE_WINDOW_C_VW_TEST |
200 | Pre-approved plant maintenance windows |
| Table | Rows | Key entities |
|---|---|---|
USER_VW |
50 | Field engineers and project managers |
PRODUCT2_VW |
100 | Spare parts and service catalog items |
CAMPAIGN_VW |
30 | Strategic retrofit and service campaigns |
EVENT_VW |
500 | Field execution touchpoints |
Auto-inferred relationships (7 total, zero hardcoded):
OUTAGE__C_VW.ASSET_ID→ASSET_VW.ASSET_IDCASE_VW.ASSET_ID→ASSET_VW.ASSET_IDSERVICE_WINDOW_C_VW_TEST.ASSET_ID→ASSET_VW.ASSET_IDCASE_VW.OUTAGE_ID→OUTAGE__C_VW.OUTAGE_IDCAMPAIGN_VW.OWNER_ID→USER_VW.USER_ID(synonym: OWNER_ID ↔ USER_ID)EVENT_VW.PRODUCT_ID→PRODUCT2_VW.PRODUCT_IDEVENT_VW.CAMPAIGN_ID→CAMPAIGN_VW.CAMPAIGN_ID
| File | Input | Output | Purpose |
|---|---|---|---|
generate_energy.py |
— | domain1.db, domain2.db |
Generates Siemens Energy data with realistic value distributions, null density, and referential integrity |
schema_profiler_v2.py |
domain1.db, domain2.db |
column_profiles.json, relationships.json |
SQL-pushed column profiling: null density, distinct ratio, top value distributions, min/max/avg for numeric columns. Structural FK inference — no value sampling |
semantic_inferrer_v2.py |
column_profiles.json, relationships.json |
ontology.json, semantic_graph_v2.png |
LLM infers semantic entity names and relationship verbs from column stats alone. No hardcoded edges, no human annotation step |
query_agent_v2.py |
ontology.json, domain1.db, domain2.db |
SQL results + chart config | NL-to-SQL agent with semantic context injection, exact enum values, cross-DB join resolution |
| File | Input | Output | Purpose |
|---|---|---|---|
generate.py |
— | data/*.csv, operations.db, quality.db |
Generates Siemens manufacturing data — machines, maintenance, parts, batches, defects |
schema_profiler_local.py |
operations.db, quality.db |
relationships.json, schema_context.json |
Profiles both DBs, infers FK relationships |
schema_graph.py |
relationships.json, schema_context.json |
schema_graph.png |
Structural NetworkX graph with find_join_path() |
generate_ontology_draft.py |
siemens_schema.yaml |
ontology_draft.json |
Human-annotatable ontology draft with AUTO_PROPOSED edge markers |
semantic_inferrer.py |
ontology_draft.json |
semantic_graph.png |
Semantic graph after human annotation |
query_agent.py |
ontology_draft.json, operations.db, quality.db |
SQL results + chart config | NL-to-SQL agent — original pipeline |
Stats are computed directly in SQL — no Python-side row fetching. For varchar columns: null_count, null_percent, distinct_count, distinct_ratio, top 10 value distributions with frequency and percentage, 10 random samples. For numeric columns: same null/distinct stats plus MIN, MAX, AVG. This matches Snowflake ETL best practices and scales to 1M+ rows without memory pressure.
Relationships are inferred by identifying columns where exactly one side has distinct_ratio > 0.95 (true PK — nearly every row unique) and the other side references it (FK). A synonym map handles mismatched column names (OWNER_ID → USER_ID). No value overlap sampling needed — pure structural inference.
semantic_inferrer_v2.py makes two types of Haiku calls:
- Per table — feeds null density, distinct ratio, top value distributions, sample values → Haiku infers
entity_labelanddescription - Per relationship — feeds both tables' full column profiles + join key → Haiku infers the relationship verb (
AFFECTS,DOCUMENTS,REQUIRES_MAINTENANCE_ON)
No hardcoded entity names or relationship verbs anywhere in the codebase.
schemauto generalize-tsvs runs on all CSVs to produce siemens_schema.yaml — a formal LinkML schema with inferred types, enumerations, and identifiers from instance data alone. No metadata or ERD required.
- Python 3.10+
- Anthropic API key
pip install faker pandas numpy networkx matplotlib pyyaml anthropic python-dotenv schema-automatorCreate a .env file in the project root:
ANTHROPIC_API_KEY=your_key_hereRun in order — each step depends on the previous output.
# Step 1 — Generate data
python generate_energy.py
# → domain1.db, domain2.db
# Step 2 — Profile schemas via SQL
python schema_profiler_v2.py
# → column_profiles.json, relationships.json
# Step 3 — LLM semantic inference
python semantic_inferrer_v2.py
# → ontology.json, semantic_graph_v2.png
# Step 4 — Run the query agent
python query_agent_v2.py
# → 7 test queries across both DBs# Step 1 — Generate data
python generate.py
# Step 2 — Profile and build structural graph
python schema_profiler_local.py
python schema_graph.py
# Step 3 — schema-automator
schemauto generalize-tsvs data\machines.csv data\maintenance_logs.csv data\parts_inventory.csv data\production_batches.csv data\defect_reports.csv --column-separator "," -o siemens_schema.yaml
# Step 4 — Generate ontology draft + annotate
python generate_ontology_draft.py
# Open ontology_draft.json, search AUTO_PROPOSED, review 6 edges, set to REVIEWED
# Step 5 — Build semantic graph and run agent
python semantic_inferrer.py
python query_agent.pyNL query
↓
Semantic context injected into prompt:
- Entity names + descriptions
- Named relationships (LLM-inferred verbs)
- Exact enum values per column
- Pre-computed join paths
↓
Claude Haiku → JSON: { sql, chart, explanation }
↓
SQLite ATTACH (domain1.db attaches domain2.db as 'domain2')
↓
Cross-DB SQL executes natively
↓
DataFrame + chart config
SQL prefix rules (SQLite ATTACH):
domain1tables → no prefix:ASSET_VW,OUTAGE__C_VW,CASE_VW,SERVICE_WINDOW_C_VW_TESTdomain2tables →domain2.prefix:domain2.USER_VW,domain2.CAMPAIGN_VW, etc.
schema_graph.png — structural
Tables as nodes, FK column names as edge labels. Blue = same-DB. Red dashed = cross-DB. Built from structural inference only.
semantic_graph_v2.png — semantic ontology
Business entities as nodes: PowerAsset, GridOutage, ServiceCase, ServiceWindow, EnergySystemUser, Product, EnergyCampaign, MaintenanceEvent.
Named relationship edges: AFFECTS, REPORTS, REQUIRES_MAINTENANCE_ON, DOCUMENTS, OWNS, INVOLVES, BELONGS_TO.
Inferred by Haiku from column stats — no hardcoded labels.
When moving from local SQLite to Snowflake production data:
| What changes | How |
|---|---|
DB connections in query_agent_v2.py |
Swap sqlite3.connect + ATTACH for snowflake.connector.connect |
| Schema profiler | Same SQL stat queries work natively on Snowflake — change connection only |
| SQL prefix rules | Drop domain2. prefix — Snowflake handles cross-schema joins via schema.table notation |
| FK inference | Value overlap will work at scale — large ETL tables reuse ID pools so sampling catches them automatically |
| schema-automator | schemauto import-sql snowflake+snowflake://... |
Everything else — semantic_inferrer_v2.py, query_agent_v2.py, ontology.json — is identical.
| Component | Notes |
|---|---|
Frontend (app.py) |
Streamlit — chat input, results table, auto chart, graph viewer. ~1 session. |
| Annotation UI | generate_ontology_draft.py (Pipeline 1) produces AUTO_PROPOSED markers for domain expert review. Could become a simple web form. |
| ChromaDB embeddings | Semantic retrieval for large schemas (10+ tables). Not needed at current scale. |
| OAK ontology annotation | schema-automator's full annotation pipeline. Needs a manufacturing/energy-specific ontology source. Long-term production upgrade. |
knowledge-graph-nl-sql/
│
├── Pipeline 2 — Energy Domain (current)
│ ├── generate_energy.py
│ ├── schema_profiler_v2.py
│ ├── semantic_inferrer_v2.py
│ ├── query_agent_v2.py
│ ├── domain1.db
│ ├── domain2.db
│ ├── column_profiles.json
│ ├── relationships.json
│ ├── ontology.json
│ └── semantic_graph_v2.png
│
├── Pipeline 1 — Manufacturing Domain (original)
│ ├── generate.py
│ ├── schema_profiler_local.py
│ ├── schema_graph.py
│ ├── generate_ontology_draft.py
│ ├── semantic_inferrer.py
│ ├── query_agent.py
│ ├── operations.db
│ ├── quality.db
│ ├── siemens_schema.yaml
│ ├── ontology_draft.json
│ └── semantic_graph.png
│
├── .env
└── README.md
| Layer | Tool |
|---|---|
| Data generation | Python, Faker, NumPy, Pandas |
| Local databases | SQLite (ATTACH for cross-DB joins) |
| Column profiling | SQL-pushed stats (null density, distinct ratio, top distributions, min/max/avg) |
| Schema inference | schema-automator (LinkML ecosystem, Monarch Initiative) |
| Structural graph | NetworkX, Matplotlib |
| Semantic inference | Claude Haiku (claude-haiku-4-5) — entity names + relationship verbs from column stats |
| NL-to-SQL agent | Claude Haiku via Anthropic API |
| Production DB (future) | Snowflake |