TowerAI
The assistant core behind The Tower Run Tracker's in-app assistant, published as its own MIT package. It answers from a knowledge base you curate rather than from whatever a general model absorbed.
npm install towerai Get A Model Key
TowerAI does the retrieval and prompting; the sentences come from a model you point it at. Groq has a free tier that is plenty for a personal tool, and its API speaks the OpenAI chat format, so the setup below is the same shape for any provider that does.
- Sign up at console.groq.com — a Google or GitHub account is enough, and the free tier needs no card.
- Open API Keys in the left sidebar and choose Create API Key. Name it after the tool you are building.
- Copy the key when it is shown. It is displayed once; create a new one if you lose it.
- Put it in your environment rather than in your code, so it never reaches a browser bundle or a commit.
# .env — and add .env to .gitignore
GROQ_API_KEY=gsk_your_key_here
GROQ_ENDPOINT=https://api.groq.com/openai/v1/chat/completions
GROQ_MODEL=openai/gpt-oss-120bconsole.groq.com lists the models available to you and the requests per minute your
tier allows. Any of them work here; openai/gpt-oss-120b is a good default, and openai/gpt-oss-20b is faster.
Call The Model
Retrieve from your knowledge base, put what you find in a system message, and send the question. That grounding step is what makes the answer about your subject rather than about the model's general impression of it.
async function ask(question, knowledgeBase) {
const context = retrieve(knowledgeBase, question) // your top matches, as text
const response = await fetch(process.env.GROQ_ENDPOINT, {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${process.env.GROQ_API_KEY}`
},
body: JSON.stringify({
model: process.env.GROQ_MODEL,
messages: [
{
role: 'system',
content: `Answer using only the reference below.\n\nReference:\n${context}`
},
{ role: 'user', content: question }
]
})
})
const payload = await response.json()
return payload.choices[0].message.content
}Keep The Key Off The Client
Bundlers inline anything they can see. In Vite, a variable named VITE_* becomes a string
literal in the shipped JavaScript, which puts your key in every visitor's browser. Call the model from
a server route, a serverless function, or your own small proxy, and let the browser talk to that. The
demo on this site does exactly that — the page sends a question and receives an answer, and holds no
credential.
How The Knowledge Base Works
A knowledge base is an array of chunks. Each chunk carries a topic, tags, a disambiguation line saying what it is not about, and its content. Retrieval scores a question against those chunks and answers from the best match.
The split that matters: curated prose supplies meaning, and the SDK catalogs supply numbers. A
chunk that says "Attack Speed has 99 levels" in prose goes stale the next time the game
rebalances; one that reads the count from LAB_CATALOG does not.
Building One
import {
buildTrackerAiCanonicalKbChunks,
validateCanonicalKbArray,
buildCanonicalKbVersion,
} from 'towerai/kb'
import { LAB_CATALOG } from 'thetowersdk/data'
// The shipped chunks, then your own on top.
const base = buildTrackerAiCanonicalKbChunks()
const mine = LAB_CATALOG.map((lab) => ({
chunk_id: `lab_cost_${lab.slug}`,
source: 'My Notes',
section: 'Labs',
topic: `${lab.name} cost`,
title: `${lab.name} cost`,
disambiguation: 'Cost to max this lab, not research order.',
mechanics: [lab.name],
tags: ['labs', lab.name.toLowerCase()],
// Numbers come from the catalog, so prose cannot go stale.
content: `${lab.name} has ${lab.levels.length} levels.`,
}))
const knowledgeBase = [...base, ...mine]
// It tells you what is malformed instead of failing at query time.
validateCanonicalKbArray(knowledgeBase)
console.log(buildCanonicalKbVersion(knowledgeBase), knowledgeBase.length, 'chunks')Exports
buildTrackerAiCanonicalKbChunks— the shipped chunk set, to extend or replacevalidateCanonicalKbArray— reports malformed chunks up front, not at query timeformatKbValidationError— turns a validation failure into a readable messagebuildCanonicalKbVersion— a content-derived version string for cache keysloadCanonicalKbFromJson/loadCanonicalKbFromFile— load a prebuilt basetoCanonicalRuntimeKnowledgeRecord— chunk to the runtime record shape
Artifacts Are Fetched, Not Bundled
Embedding indexes are served from a manifest URL rather than shipped in the tarball, which keeps the package small. Point it at your own manifest to serve a knowledge base you host.
A Note On Accuracy
AI assistants are known to make mistakes. A curated base narrows what an assistant can say, and declining to answer is a valid outcome worth designing for — but it is not a guarantee of correctness. Say so wherever you surface answers to players.