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.

  1. Sign up at console.groq.com — a Google or GitHub account is enough, and the free tier needs no card.
  2. Open API Keys in the left sidebar and choose Create API Key. Name it after the tool you are building.
  3. Copy the key when it is shown. It is displayed once; create a new one if you lose it.
  4. 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-120b

console.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 replace
  • validateCanonicalKbArray — reports malformed chunks up front, not at query time
  • formatKbValidationError — turns a validation failure into a readable message
  • buildCanonicalKbVersion — a content-derived version string for cache keys
  • loadCanonicalKbFromJson / loadCanonicalKbFromFile — load a prebuilt base
  • toCanonicalRuntimeKnowledgeRecord — 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.

MCP & Oracles → · Knowledge Graph →