DEV Community

Cover image for Building an Ontology That Says No to a Fortune Teller
Yuuki Yamashita
Yuuki Yamashita

Posted on

Building an Ontology That Says No to a Fortune Teller

AWS quietly shipped something called Context Ontology Accelerator (COA) at the end of July. The pitch: point it at your company's databases and documents, and it drafts a formal ontology (classes, properties, a graph) so your AI agents stop guessing and start citing. A human reviews the draft before anything goes live. Once it's approved, any agent that queries it gets governed, explainable answers instead of whatever the model felt like saying.

I wanted to see what that actually feels like to build with. Not for a business, though. For something that has no business being governed at all: Shichuu Suimei, the Four Pillars of Destiny, a fortune-telling system built on centuries of pronouncements nobody can falsify. If an ontology forces a domain to show its receipts, what happens when the domain's receipts don't exist?

What an ontology actually is

Before the fortune-telling part, the word itself is worth pinning down, because it gets used loosely. An ontology, in this engineering sense, isn't a philosophy term. It's a formal, machine-readable model of a domain. You define a set of classes, the concepts in the domain ("Five Elements," "Heavenly Stem," "Day Master"), a set of properties, the typed relationships between them ("generates," "is classified under," "corresponds to"), and a graph connecting it all together.

Once a domain is expressed that way, a question against it stops being a free-form LLM guess. It becomes a retrieval against specific, inspectable nodes, and the answer can point back to exactly which nodes it used. That's the whole value proposition. Not smarter answers. Traceable ones.

The $930/month problem

COA's reference architecture is real enterprise infrastructure: 16 CDK stacks, Amazon Neptune for the graph, Amazon OpenSearch Serverless for vectors. I went looking for the numbers before spinning anything up, and the project's own docs say it plainly: idle cost is roughly $930 a month. That's a completely reasonable price for a company running this in production. For a weekend project running a few hours, it's absurd.

So before writing any code, I had to answer a smaller question. Could I use COA's actual induction pipeline, the Python package that drafts ontologies from documents, without paying for the infrastructure underneath it?

The answer turned out to be no, for a reason that had nothing to do with cost. coa-ontology, the package that does the drafting, depends on two sibling packages inside AWS's monorepo through uv workspace references, and those references only resolve inside the monorepo. There's no way to pip install just the induction logic. You either bring the whole repository along or you don't use it at all. Forking the whole thing just to vendor in one module felt like exactly the kind of dependency I didn't want in something meant to stay small and inspectable.

What I could take cleanly was much smaller: two Protocol definitions, GraphStore and VectorStore, sitting in a single file with no dependencies of its own. That file defines the contract (store a class, store a property, search embeddings, fetch a vertex's neighbors) without caring what's underneath it. So that's what I took, verbatim, with attribution, and built everything else from scratch against it.

Swapping the expensive half

The vector store became a from-scratch implementation on Amazon S3 Vectors, which only became generally available in December 2025 and cuts vector storage and query costs by up to 90% compared to the usual managed options. No idle floor, pay per request. Building it meant working around some real API limits along the way: 500 vectors per write call, no metadata filter at all on the list operation (only the query operation gets one, so listing by ontology means paginating and filtering client-side), and a distance field on similarity search that turned out to mean 1 - cosine_similarity, not the raw similarity score the Protocol expects. Small thing, but it's exactly the kind of detail that fails silently if you don't check it against a real response.

The graph store became rdflib writing to a single TriG file on disk. No managed graph database, checked straight into git. It only implements the handful of Protocol methods my own pipeline actually calls, not the full interface upstream needs for its multi-tenant catalog UI. That's a real, documented scope cut, not an oversight.

End result: the only AWS costs left are Bedrock tokens and S3 Vectors usage. No standing infrastructure to remember to tear down.

Building the ontology, then asking it something unfair

I wrote five short reference documents on Shichuu Suimei: the Five Elements and their generating and overcoming cycles, the ten Heavenly Stems and twelve Earthly Branches, the Four Pillars and Day Master, the Ten Gods, and Tenchūsatsu (the "void period" tied to a day-pillar's position in the sixty-term cycle). Fed through Bedrock (Claude Sonnet 4.6), that induced 59 classes and 16 properties as a JSON proposal. Nothing committed yet, just a draft sitting in a file for review.

I read through it by hand before approving anything. It held up. The class hierarchy matched the source documents, the relationships (generates, is-classified-under, corresponds-to) were the ones I'd actually written about, nothing invented. Approved, it went through Titan Text Embeddings V2 and landed as 75 vectors in S3 Vectors.

Then I asked it the unfair question: what's supposed to happen during Tenchūsatsu? Traditionally this is where a fortune-telling text gets its most confident. Void periods are where effort supposedly goes nowhere, where you're told to stop pushing and wait. My source document said as much. But that specific claim, the folk belief about what happens, as opposed to the formal definition of what the period is, never made it into any class's comment field during induction. It just didn't get extracted.

The agent's answer:

Tenchūsatsu is defined as a period tied to the two Earthly Branches that fall outside a day-pillar's ten-term group in the sixty-term cycle. However, the provided context doesn't describe what specifically happens during this period, so I can't answer that based on the available evidence.

It knew the definition. It didn't invent the folklore. That refusal is the actual result of this whole exercise: a domain built entirely on confident, unverifiable pronouncements, routed through a governed ontology, visibly declining to make one.

What broke along the way

Two things bit me that are worth writing down in case they save someone else an hour. First, calling a Bedrock Claude model by its bare model ID no longer works for on-demand throughput. You need an inference profile ID instead (us.anthropic.claude-sonnet-4-6, not anthropic.claude-sonnet-4-6), and the error message that tells you this is clear once you hit it, not before. Second, boto3's default read timeout is 60 seconds, which is comfortably too short for a single induction call asking for an 8,192-token structured JSON response. The first run just timed out silently until I set it to 300.

Neither is exotic. Both are the kind of thing you only find by actually running the thing against a real account, which is the point of building something small enough to actually run.

Code's on GitHub: yama3133/fortune-ontology. Apache 2.0, same as upstream COA.

Top comments (0)