The first sentence of NVIDIA’s Auto-Ontology README is a promise that indicts the project:
Agents can retrieve documents and query databases, but reliability gates agentic analytics: a revenue metric answered faster is worth nothing if the number is wrong.
I agree with that sentence. It is the correct diagnosis of why text-to-SQL demos do not survive contact with a finance team. The tool then goes on to build a semantic layer whose entire purpose is to make the number right, and ships three database connectors that — as I confirmed on this machine — return silently wrong numbers whenever a join produces two columns with the same name.
Here is the actual output of the repository’s own SQLite connector, run against a two-table join with four selected columns:
columns: ['id', 'id', 'name', 'name']
values : [[1, 1, 'Ada', 'Ada']]
expected: [[1, 100, 'Ada', 'first order']]
The customer’s id 1 overwrote the order’s id 100, and the customer’s name Ada overwrote the order’s name. error is None. There is no warning, no retry, no exception. If an agent runs that query and reports “two rows, customer Ada”, nothing in the pipeline knows it is lying.
That is the headline finding of this audit. The rest of the article is about the machinery around it: what NVIDIA actually shipped, what the star count does not tell you, what the maintainers say about the reliability boundary themselves, and what a team should verify before wiring this into anything that produces numbers a human will act on.
What Auto-Ontology actually is
Strip the marketing and the architecture is conventional and sensible. Ingestion reads your relational sources, profiles columns, samples values, and asks a model to propose a first-draft ontology: terms (business concepts mapped to columns), SQL attributes (reviewed expressions like margin or rolling total), and relationships (how tables join, including bridge tables). Those land in Postgres with embeddings in pgvector. If you have built a graph-backed RAG pipeline before — the shape LLM Graph Builder takes — the ingestion half will look familiar; the difference is that Auto-Ontology keeps the graph in Postgres and treats certification by humans as a required stage rather than an optional one.
At question time an agent classifies intent, extracts entities, retrieves candidate terms and attributes by embedding search, writes SQL from those candidates, checks the filter values against the live data, repairs SQL when a check fails, and returns the answer with its SQL and the steps it took.
| Component | Technology | Role | Port |
|---|---|---|---|
| Frontend / gateway | Next.js 16, Better Auth, Prisma | Web UI, auth, public API | 3000 |
| Backend | FastAPI, LangChain | NL-to-SQL agent, catalog | 3001 |
| Ingestion worker | FastAPI | Ingest, embed, semantic compile | 3002 |
| MCP server | FastMCP | Read-only agent tools | 3003 |
| Postgres + pgvector | PostgreSQL 17 | Catalog, ontology, vectors | 5432 |
Ten source engines are supported: PostgreSQL, MySQL, Databricks, Snowflake, ClickHouse, Trino, Kyuubi, HeavyDB, DuckDB, SQLite. Inference goes through NVIDIA NIM by default (nvidia/nemotron-3.5-lightning-30b-a3b for both reasoning and non-reasoning roles), and every model role is configured by an <PREFIX>_ENDPOINT / _API_KEY / _MODEL triplet that falls back to a shared DEFAULT_MODELS default — so self-hosted NIM or any OpenAI-compatible endpoint is a matter of three variables.
The part I think is genuinely well designed is the part that gets the least attention: the MCP surface. The team took a 119-operation OpenAPI spec and published 9 curated tools instead of exposing the API wholesale. Their own reasoning, in a source comment:
The spec describes 87 operations. Exposing all of them would be actively harmful: agent tool-selection degrades well before that count, and most of the surface — SSO providers, token management, custom prompts, compilation control — has no business being agent-callable.
I measured the consequence by building the server from the repo’s mcp/ distribution and asking it for its tool list: 9 tools, 6,286 bytes of name + description + JSON schema, roughly 1,571 tokens. The full spec is 437,217 bytes. Exposing the spec naively would have been about 70 times more context for a worse agent. The tool descriptions are also written for a caller rather than for an API doc reader, which is rarer than it should be.
That comment has a stale number in it, though, and it matters more than it looks — see the drift section below.
The numbers the README does not print
GitHub’s own API, read on 2026-11-07:
| Metric | Value |
|---|---|
| Stars | 9,026 |
Watchers (subscribers_count) | 46 (0.51% of stars) |
| Forks | 725 (8.03% of stars) |
| Created | 2026-04-27 |
| Commits | 1,367 |
| Contributors | 13 |
| Open issues | 34 (plus 28 open pull requests) |
| GitHub releases | 0 |
| Git tags | 1 |
| Remote branches | 101 |
Two of these deserve a paragraph each.
46 watchers. Watchers are the people who asked to be notified; stars are bookmarks. Across the audits in this series that ratio has run from 0.22% to 0.94%. At 0.51%, Auto-Ontology sits in the middle — better than several projects with ten times its commits-and-abandonment profile, worse than a project with a real user base. The fork ratio is the more unusual number: 8.03% of stargazers forked it, which is high for a project that accepts no external pull requests. People are not just bookmarking it; they are forking it, presumably because forking is the only path open to them.
1,367 commits in eight months, and 54.5% of them from two people. The contributor API returns 13 logins, with DinaLaptii at 503 commits and yuvalshkolar at 242. git shortlog overcounts, because three maintainers commit under two author names each (dlaptii/DinaLaptii, Liav Nave/liav nave, Lio Fleishman/liofnv); the API has already merged those identities, and 503 + 242 = 745 of 1,367 commits is 54.5% from the top two accounts. Bus factor: two.
Monthly commit volume tells the story the star curve hides:
| Month | Commits |
|---|---|
| 2026-04 | 144 |
| 2026-05 | 125 |
| 2026-06 | 295 |
| 2026-07 | 257 |
| 2026-08 | 217 |
| 2026-09 | 165 |
| 2026-10 | 146 |
| 2026-11 (6 days) | 18 |
Peak June, steady decline since: October is 50.5% of the June peak. This is a project that was built fast inside NVIDIA, opened at the end of April, and is now in maintenance-with-features mode while the star count keeps climbing.
Zero releases, one tag, and where the artifacts actually live
The single tag is named pre-main-merge-backup. It is not a version. There is no v1.2.0 tag and no GitHub release, in a repository whose pyproject.toml, mcp/pyproject.toml, and Helm Chart.yaml all say 1.2.0.
Before writing “no releases” as a finding, I checked whether the release process simply lives elsewhere. DEPLOYMENT.md says it does, and it is right: the artifacts are on NGC, not GitHub.
Each release tags its images with the chart version, and the chart pins its own images to that tag.
I fetched the NGC Helm index directly (helm.ngc.nvidia.com/nvidia/index.yaml, 131,716 bytes) and found exactly two versions of the chart:
| Chart version | Published |
|---|---|
| 1.1.0 | 2026-10-13T17:03Z |
| 1.2.0 | 2026-11-06T18:15Z |
So the release discipline is real, the lag is one day (version bump in git on 11-05, chart on 11-06), and my initial suspicion was wrong in an instructive way: an enterprise project can have zero GitHub releases and still ship. That is worth stating plainly, because the reflex “0 releases = unmaintained” would have been a false accusation here.
The honest version of the finding is narrower and still useful: you cannot pin Auto-Ontology from GitHub. There is no tag to check out, no release notes to read, no changelog to diff. The documented MCP install command is uvx --from "git+https://github.com/NVIDIA/auto-ontology.git#subdirectory=mcp" — no ref, no version, always whatever main is at that moment. auto-ontology-server and auto-ontology-mcp are not on PyPI (I checked all four plausible names; all 404). So the reproducibility story runs through NGC image digests and a chart version, and nowhere else.
A smaller artifact of the same history: the project used to be called something else. The first README in the repo is titled GSF — generative semantic fabric, the frontend package.json is still named gsf, and the 0.1.1 “security patch release” commit from 2026-05-11 changed exactly three files — frontend/package.json, pyproject.toml, and uv.lock — and no code at all. A version string is not a release.
I ran the connector, and it returned the wrong row
The bug is tracked as issue #408, opened 2026-11-04 by hyeonsang010716, and it is the most important thing on the tracker. I did not take the reporter’s word for it. I extracted auto_ontology/connectors/sqlite.py, base.py, and their two dependencies into a minimal package, installed the pinned dependency range (pandas>=2.3.3,<3, resolved to 2.3.3), and ran the reporter’s own reproduction.
The mechanism is one line, and it is the same mistake in three places:
# auto_ontology/connectors/sqlite.py, _execute()
rows = cur.fetchall()
return pd.DataFrame(
[dict(row) for row in rows], columns=[col[0] for col in cur.description]
)
conn.row_factory is sqlite3.Row. dict(row) keys by column name, so c.id and o.id both become the key id, and the second one wins. The columns= argument then asks pandas for four labels against a dict that holds two keys, and pandas repeats the surviving value across both columns. Postgres has the same shape via psycopg.rows.dict_row; MySQL the same via cursor(dictionary=True).
The reporter’s measured output per engine, which my SQLite run reproduced exactly:
| Connector | Columns returned | Row returned | Expected |
|---|---|---|---|
| Expected | id, id, name, name | 1, 100, Ada, first order | — |
| Postgres 17.7 | id, name | 100, first order | both customer values gone |
| MySQL 9.4 | id, id, name, name | 100, 100, first order, first order | order values duplicated over customer |
| SQLite | id, id, name, name | 1, 1, Ada, Ada | customer values duplicated over order |
| DuckDB | id, id_1, name, name_1 | 1, 100, Ada, first order | correct |
Aliasing avoids it. Nothing forces an agent to alias. And the failure reaches the text-to-SQL path: on Postgres, _run_sql returns [{"id":100,"name":"first order"}] with error=None, so the repair loop — the mechanism the whole architecture is built around — has nothing to repair.
The detail that makes this more than a bug report: the project already has a defense against duplicate result columns, and the defense cannot see the bug. _deduplicate_result_columns was added in PR #397 to handle exactly this class of problem, but it operates on the DataFrame after the connector has built it. By then the collision has already happened. Its unit test passes because it constructs the frame by hand with dialect="postgres" — a frame the Postgres connector is structurally incapable of producing.
There is a fix: PR #422 by an outside contributor, open since 2026-11-05, which switches the three connectors to tuple rows and takes names from cursor.description the way the Trino and Snowflake connectors already do.
The quickstart question does not finish against the default endpoint
Issue #440, opened 2026-11-06, is the second one that should worry anybody planning a pilot. The reporter ran the README’s quickstart — Docker Compose, hosted NVIDIA API, nvidia/nemotron-3.5-lightning-30b-a3b — and the example question about sushi-roll flash drives never gets a complete answer.
The mechanism is a version skew in the structured-output path. langchain-nvidia-ai-endpoints==1.4.3 (which the backend image pins) sends a JSON schema two ways: OpenAI-style response_format, and a nvext: {guided_json: ...} envelope. Against the live endpoint:
| Request shape | Result |
|---|---|
response_format: {type: json_schema} | 200, but empty content — parser yields None |
nvext: {guided_json: schema} | 400 — unknown field 'guided_json' |
top-level guided_json: schema | 200 with correct JSON |
Because _FallbackRunnable re-raises the last exception, the surfaced error is the 400; the silent empty-content path is masked. Each non-reasoning call fails three times, falls back to the reasoning model, and each pipeline step goes from seconds to about two minutes. The reporter measured a full answer at 10–15 minutes instead of about one, ending in unconstructable_sql_response and a hedged reply.
I could not reproduce this one: it needs an nvapi- key, which I do not have in this environment. It is one report, zero comments, four days old, and it may well be fixed by a pin bump next week. But it is the kind of failure that costs a team a two-day evaluation before anyone thinks to read the tracker — so if you are evaluating, read auto_ontology/utils/llm_invoke.py first and check whether guided_json is bound at the top level.
The quickstart publishes the two ports the README says must stay private
This one I verified statically, from the repo, and it is structural rather than a bug.
README, line 271:
Keep ports 3001 and 3002 on a private network. FastAPI trusts the user identity the frontend forwards in the
x-auto-ontology-user-idheader, and the ingestion service’s endpoints (ingest, delete, reset, compile) have no authentication of their own.
The code agrees, and is blunt about it. auto_ontology/server/identity.py, line 24:
FastAPI must not be publicly reachable because the header is not independently authenticated here.
And resolve_internal_user() does exactly what the docstring admits: it reads the header, strips it, and returns whatever string is there. No signature, no shared secret, no mTLS.
Now docker-compose.yml, the file the quickstart runs, line 47 and line 76:
auto-ontology:
ports:
- "3001:3001"
ingestion-service:
ports:
- "3002:3002"
Bare HOST:CONTAINER port mappings bind 0.0.0.0. So the documented local bring-up publishes, on every interface, the identity-trusting backend and the unauthenticated ingestion worker — the two services the same document tells you to keep private. It is a local-development stack and the README does warn you to replace the default passwords, so this is not a remote vulnerability claim; it is a mismatch between the advice and the default. But “it is only localhost” stops being true the moment someone runs this compose file on a shared build box or a cloud VM with the firewall group set to default-open, which is how evaluation environments usually happen.
The drift I could measure
Four inconsistencies, all verifiable in the current tree:
The reranker has two different defaults in the same README. Line 63 says nvidia/llama-nemotron-rerank-vl-1b-v2. Line 415 says nvidia/nemotron-3.5-rerank-8b. .env.example line 55 agrees with 415, and adds a comment that the catalog serves reranking from ai.api.nvidia.com rather than integrate.api.nvidia.com — a distinction that will break your setup if you get it from line 63.
The curated tool count is documented as 87 operations; the spec has 119. That is +36.8%. The docstring’s argument (“exposing all of them would be actively harmful”) is only strengthened by the drift — the new operations are mostly exactly the kind of thing it says not to expose — but the number a reader takes away is wrong.
The components table lists “Relational Database” as the Technology for Postgres. The HN thread noticed before I did, and it is a fitting kind of error for a project about eliminating semantic drift.
docs/mcp.md still describes the repo as NVIDIA-internal and needing GitHub credentials (issue #403, open). The repo went public in April.
One thing that is not drifting: the copy of the OpenAPI spec shipped inside the Python package and the one in docs/ are byte-identical — same MD5, 96625768bb275b472293ec2d6046af6a, 437,217 bytes. The CI gate that enforces this genuinely works, which is why the stale number above is a single stale comment rather than a systemic problem.
I also checked the skill-signing pipeline that AGENTS.md describes, since signed agent skills are a claim worth testing. Issue #432 says the signatures no longer match. I ran the reporter’s verifier — standard library only, DSSE payloads decoded, SHA-256 compared — and got the same answer: 6 of 21 signed resources mismatch, and all six are BENCHMARK.md + skill-card.md in the three skills. Removing the 144-byte SPDX header that PR #363 added on 2026-10-12 restores the signed digest in every case. So it is a missed re-sign, not tampering — exactly as reported.
“Does not accept external pull requests” versus 18 pending outside PRs
CONTRIBUTING.md line 9 is unambiguous:
This project does not accept external pull requests. Code changes are made by the Auto-Ontology maintainer team.
The README repeats it at line 28. And the brief lives up to that: three skills, AGENTS.md mapping each user action to an owning skill, CLAUDE.md telling maintainers which CI gates are the easy ones to miss — this is a repo organized for an internal team, published as source.
Meanwhile, at the moment of writing, there are 28 open pull requests, and 18 of them come from accounts that are not in the contributor list:
| PR | Opened | Author | What it fixes |
|---|---|---|---|
| #395 | 2026-10-30 | JGalego | DuckDB declared primary/foreign keys |
| #411 | 2026-11-04 | piersroberts | OpenAI-style embeddings vs 2048-wide vector store |
| #412 | 2026-11-04 | joemcelroy | Elasticsearch vector store |
| #414 | 2026-11-04 | cmscmadd | Postgres FKs referencing another schema |
| #417 | 2026-11-05 | blueamethyst | database_name for list_schemas |
| #419 | 2026-11-05 | sandeepvijayarao09 | Withhold PII-tagged sample values from prompts |
| #420 | 2026-11-05 | blueamethyst | Prevent SQL hallucination |
| #421 | 2026-11-05 | Aswinesag | BigQuery connector |
| #422 | 2026-11-05 | Husseinadq | The duplicate-column fix from issue #408 |
| #423 | 2026-11-05 | klh | Isolate per-query PostgresVDB.retrieval failures |
| #424 | 2026-11-05 | anush008 | Qdrant vector store |
| #429 | 2026-11-05 | HoneyTyagii | Databricks nullability |
| #437 | 2026-11-06 | anupamme | better-auth 1.7.4 → 1.7.7 |
None has a review. The oldest has been open seven days. The pattern is what you would predict from a documented no-PR policy combined with a 290-point Hacker News launch: the community files fixes as pull requests anyway, and the project treats them as suggestions it did not ask for. That is a legitimate governance choice. It does mean the fix for silent data corruption exists in a fork-and-open branch that nobody is obliged to read — and that bug reporters, having read CONTRIBUTING.md, increasingly put patches inside issue bodies instead, which makes them invisible to tooling and to git log.
One of those PRs is worth naming carefully: #437 bumps better-auth from 1.7.4 to 1.7.7 for CVE-2026-114607, which NVD scores 9.1 and describes as affecting better-auth from 1.4.0-beta.18 until 1.7.7. The repo pins 1.7.4 across six packages. Two honest caveats: the CVE’s trigger is the Magic Link provider with storeToken set to plain, and frontend/auth/auth.ts registers admin, sso, apiKey and OAuth rather than magic link, so the vulnerable path looks unreachable in this configuration; and the PR is an automated scanner output that says of itself “I have not verified that your code reaches the affected function.” The recommendation is still to bump — a patch release costs nothing — but the 9.1 does not mean this repository is exploited in the wild.
What the Hacker News thread argued, and what the README never answers
The launch story (2026-11-03) reached 290 points and 139 comments. Three objections recur, and none is addressed anywhere in the repository’s documentation:
It is a semantic layer that does not solve the semantic gap. The strongest version: the database’s column names are often wrong, and the knowledge about why they are wrong lives with people, not in information_schema. One commenter worries about the executive who trusts an answer they cannot audit; another argues an LLM ontology should read application code to recover column context, and this one does not.
Why not RDF/OWL/SPARQL? Multiple commenters ask what this adds over thirty years of semantic web standards — the same question a graph database like TerminusDB has to answer, and answers in the opposite direction. The README’s answer is a different axis entirely — Apache Ossie model import/export — which is about exchanging semantic models, not about formal ontology reasoning. Whether that is enough is a real question, and the project does not argue the case.
The README reads as generated. That is not a style complaint when the subject is an ontology engine. Commenters describe bouncing off the prose, and one says they had to run it through a model to make it comprehensible. There is an irony gap between a product whose job is to make meaning legible to machines and a front page that humans report being unable to parse.
The README does answer the API-key question, and the thread shows it being answered by other commenters rather than by the authors: nvapi- is only needed for NVIDIA-hosted endpoints, and any OpenAI-compatible or self-hosted NIM works.
What to verify yourself before trusting a number
Whatever else this audit says, the checks that matter are cheap and specific. Run all of them on your own data, not on the sample.
- Duplicate-column integrity, on your engine. The repro is nine lines and needs no server on SQLite. Run the join from issue #408 against
SQLiteDatabase, thenPostgresDatabase, then whatever you actually use, and comparedf.values.tolist()against the same query with aliases. If the unaliased run differs, the semantic layer above it is decoration. - Check whether your pin has the fix.
grep -n "dict_row\|dictionary=True\|dict(row)" auto_ontology/connectors/{postgres,mysql,sqlite}.py. Tuple rows pluscursor.descriptionis the correct shape; dict rows is the bug. - Probe structured output before you trust the pipeline. Send one trivial schema to your endpoint three ways —
response_format,nvext.guided_json, top-levelguided_json— and check not just the status code but the parsed content. A200with empty content is the failure mode that costs a day. - Read the trust boundary as code, not as prose.
auto_ontology/server/identity.pyis 42 lines. Read it, then read the port mappings indocker-compose.ymlor the Helm values you intend to deploy. - Do not trust the ontology’s term count as a quality signal. The README’s example compilation of Wide World Importers produced 31 terms. That is a number about one database and one default model, not a benchmark.
- Ask the tracker, not the README. 34 open issues, and the two most consequential (#408 data corruption, #440 quickstart breakage) are four days old and have zero comments. Silence on a tracker is not a fix.
And for anything involving predictions: setting KUMO_RFM_API_KEY routes predictive questions to a hosted service that uploads the connected tables to it, capped at KUMO_MAX_TABLES (20 by default) with KUMO_MAX_ROWS_PER_TABLE unlimited unless set. The README says to review what that sends before enabling it. It is right to say so.
The verdict
Auto-Ontology is not a demo, and it is not slop. The architecture is defensible, the MCP curation is better than most of what ships in this space, the OpenAPI gate actually runs, and the release process exists even though GitHub makes it look like it does not. A team with one big Postgres warehouse, a willingness to certify terms by hand, and a two-person platform team could get real value from the current 1.2.0 chart.
What it is not, yet, is a tool you install and point at a warehouse without reading it. The failure mode it was built to prevent — a confident number that is wrong — is present in its own connector layer, on the default path, in three of the ten supported engines, with error=None, and the fix has been sitting in an unreviewed community pull request since 2026-11-05. The README’s opening sentence is the correct standard. Hold the project to it: run the join, alias nothing, and look at the values.
FAQ
Does it work without an NVIDIA API key?
Yes, if you point the model triplets at your own endpoint. DEFAULT_MODELS_ENDPOINT, _API_KEY, and _MODEL control everything, and <ROLE>_ENDPOINT/_API_KEY/_MODEL override per role. The nvapi- key is only required for NVIDIA-hosted endpoints.
Is it on PyPI?
No. I checked auto-ontology-server, auto-ontology-mcp, auto-ontology, and auto_ontology_mcp — all four return 404. Install from the NGC Helm chart, the NGC OCI images, or a git checkout of main.
Why are there no GitHub releases or version tags?
Releases are published to NGC, and DEPLOYMENT.md documents that. Only 1.1.0 (2026-10-13) and 1.2.0 (2026-11-06) exist in the Helm index. The single git tag, pre-main-merge-backup, is a backup marker, not a version.
Can I contribute a fix?
Officially no — CONTRIBUTING.md says external pull requests are not accepted, and issue reports are the sanctioned channel. In practice 18 of 28 open PRs are from non-contributors, including the fix for the data-corruption bug.
Is the MCP server read-only?
Effectively yes. All 9 tools map to reads except ask_question (streaming) and check_answerable (a POST that grades coverage without mutating anything). Writes go through the Next.js /api/... surface, and MCP has no credential of its own — each call runs as the signed-in user.
How big is it?
434 Python files and 78,836 lines of source in auto_ontology, plus 43,655 lines of tests (159 files) — a test-to-source ratio of 0.55. Add 1.5 MB of TypeScript frontend and 1.2 MB of PLpgSQL. The clone is 29 MB.