helpers/ — CLI Chrome & LLM Wrappers

Three files that keep the lessons readable

The infrastructure boundary between every example.js and the outside world (terminal, OpenAI API, llama.cpp). Centralises chalk-coloured output, ora spinners, and minimal LLM clients so lessons can focus on the concept they teach.

3 files 152 LOC chalk + ora Used by every lesson

3
files
152
LOC
15
lessons depend on it
2
LLM backends

📚 What is this & why does it exist?

helpers/ is the infrastructure boundary between every lesson and the outside world (terminal, OpenAI, llama.cpp). Without it, every example.js would re-implement chalk-coloured headers, ora spinners, OpenAI API boilerplate, and llama.cpp model-loading. By centralising these, the lessons can focus on the concept being taught without drowning in setup boilerplate.

💡 Real-world analogy

Picture a cooking show kitchen. The aprons, the stage lights, the camera angles, the chyron at the bottom of the screen — none of it is the recipe. But without it, the chef would be cooking in a dark, unlabelled kitchen and you couldn't follow along. helpers/ is that production crew. Critically, you don't ship the production crew with the recipe book (src/) — the crew exists for the demos, not the dish.

🎯 Why This Module Exists

helpers/ exists because every lesson needs the same chrome and the same minimal LLM client, but neither belongs in src/. The src/ library is what a downstream consumer imports; helpers are what the demos rely on for chrome and CLI ergonomics. They're stylistic, not architectural — and that distinction is enforced by directory placement.

Without this module: every example.js would carry its own try/catch wrapping for timing, its own chalk-coloured headers, its own ora spinner setup, its own OpenAI client construction. We'd see 50 lines of identical boilerplate at the top of each lesson. With helpers/, the lessons are 50 lines shorter and visually consistent.

🏗️ Design Decisions

DecisionChoice MadeAlternativesWhy
OutputHelper as static classAll methods static, no stateModule of free functions; instance-based loggerStatic keeps call-sites short (OutputHelper.log.info(…)); namespace-pattern common in JS demos.
Two LLM wrappers (LlamaPrompter + src/llms/LlamaCpp)CoexistingPick oneLlamaPrompter is simpler — 39 lines, no BaseLLM inheritance, no grammar plumbing. LlamaCpp is what a library exposes.
OpenAI via responses.createNew "Responses API"Chat completions APIMatches OpenAI's most recent recommended interface.
dotenv/config import side-effectAt top of openai-prompter.jsManual process.env checksReads .env automatically — every lesson "just works" if the user copied .env_example.
Spinners via orawithSpinner(message, fn) higher-orderInline console.logLong ops show progress; failure auto-reports spinner.fail(msg).
Higher-order runExample(title, fn)Wraps every lesson bodyInline try/catch + timing per lessonVisual consistency across 15 lessons; automatic timing; error capture.

Architecture

classDiagram
    class OutputHelper {
      +divider
      +createHeader(title, subtitle)
      +formatStats(stats)
      +formatChunkPreview(chunk, i)
      +formatStrategyComparison(name, ...)
      +log: {success, info, warn, error}
      +withSpinner(msg, fn)
      +analyzeChunks(chunks)
      +runExample(title, fn, subtitle)
    }
    class OpenAIClient {
      +client: OpenAI
      +model: string
      +constructor(apiKey, model)
      +send(input)
    }
    class LlamaPrompter {
      +modelPath: string
      +llama / model / context / session
      +init()
      +prompt(message)
      +dispose()
    }
    class chalk
    class ora
    class openai
    class node_llama_cpp
    OutputHelper ..> chalk
    OutputHelper ..> ora
    OpenAIClient ..> openai
    LlamaPrompter ..> node_llama_cpp

A Lesson's Output Pipeline

sequenceDiagram
    participant E as example.js
    participant OH as OutputHelper
    participant OR as ora
    participant CH as chalk
    participant T as Terminal
    E->>OH: runExample("Title", async () => {...})
    OH->>OH: createHeader(title)
    OH->>CH: chalk.bold(title) + dim subtitle
    CH-->>T: header lines
    OH->>E: invoke fn()
    E->>OH: withSpinner("loading model", loadFn)
    OH->>OR: ora("loading model").start()
    OR-->>T: [spin] loading model (animated)
    OH->>E: await loadFn()
    E-->>OH: model
    OH->>OR: spinner.succeed()
    OR-->>T: [done] loading model
    E->>OH: formatStats({size: 1024, ...})
    OH->>CH: chalk.cyan(value)
    CH-->>T: stats table
    OH-->>E: completed
    OH->>CH: chalk.green("Completed in Xs")
    CH-->>T: success line

Files

FileLOCRole & Why It Exists
helpers/output-helper.js85OutputHelper static class. Wraps chalk + ora. runExample() is the demo shell. analyzeChunks() reports avg/min/max/median chunk size.
helpers/openai-prompter.js28OpenAIClient thin wrapper around openai.responses.create(). Reads OPENAI_API_KEY from .env. Used optionally in 06/05.
helpers/llama-prompter.js39LlamaPrompter simpler local-LLM wrapper. Used directly by 06/05 query rewriting. Coexists with src/llms/LlamaCpp.

📝 Annotated Code Walkthrough — OutputHelper.runExample

The single most-reused function in the project. It's the demo shell that wraps every example.js body.

Render the header

static async runExample(title, fn, subtitle = '') {
  this.createHeader(title, subtitle);

Why first? Because if fn() throws immediately, the reader still sees what lesson they were attempting. Without this, a missing GGUF would just print a stack trace with no context.

Capture start time

  const t0 = Date.now();

Wall-clock timing. performance.now() would be more precise but the lessons are second-grain — Date.now() is plenty.

Run the lesson body

  try {
    await fn();

The lesson body is async-by-construction (every interesting RAG operation is async). The try/catch is what turns a stack trace into a friendly red line.

Success path: report timing

    console.log(chalk.green(`\nCompleted in ${(Date.now() - t0) / 1000}s\n`));

Green on success matches every other tool the reader will encounter (npm, pytest, etc.). The trailing newline gives the next lesson room to breathe.

Failure path: red message

  } catch (e) {
    console.error(chalk.red(`Failed: ${e.message}`));
  }
}

Note e.message only — the full stack would dominate the terminal. The reader gets the gist; process.env.DEBUG would let them re-run with full trace if they wanted (not implemented today).

Subtle bug to be aware of: analyzeChunks(chunks) calls sizes.sort((a,b) => a-b) in-place. If the caller reuses sizes after, it's been mutated. The lessons throw the array away immediately, so this never bites — but it's a code smell to remember if the helper is generalised.

⚠ Edge Cases & Boundaries

ScenarioWhat Happens
OPENAI_API_KEY not in .envOpenAIClient constructor throws "Missing OPENAI_API_KEY environment variable." — user follows .env_example.
LlamaPrompter.prompt() called before initAuto-inits on first call (if (!this.session) await this.init()) — lazy by design.
Spinner fails because terminal is non-interactiveora detects TTY; degrades to plain text. No special handling needed in helpers.
runExample body throws non-Errore.message is undefined — minor cosmetic issue. Acceptable for demos.

Directory Map

helpers/ (flat, 3 files)
helpers/
├── output-helper.js     (85 LOC)   ← OutputHelper static class; chalk + ora wrappers
├── openai-prompter.js   (28 LOC)   ← OpenAIClient thin wrapper around responses API
└── llama-prompter.js    (39 LOC)   ← LlamaPrompter simpler local LLM (used by 06/05)

Configuration

KnobDefaultDescription
OutputHelper.divider'─' × 80Header separator. Static; not configurable per call.
OpenAIClient(apiKey, model)process.env.OPENAI_API_KEY, "gpt-4o"API key from env; default model overridable.
LlamaPrompter(modelRelativePath)"../models/hf_Qwen_Qwen3-1.7B.Q8_0.gguf"Path resolved relative to the helper file.
chalk coloursgreen=ok, blue=info, yellow=warn, red=err, cyan=value, dim=subtitleHard-coded for visual consistency across lessons.

Code Examples

Wrap a lesson body

import { OutputHelper } from '../helpers/output-helper.js';

await OutputHelper.runExample('Lesson 03 — Text Splitting', async () => {
  // ... lesson body ...
  const chunks = await splitter.splitDocuments(docs);
  OutputHelper.formatStats(OutputHelper.analyzeChunks(chunks));
}, 'Compare three splitting strategies on the same PDF');

Long-running operation with spinner

const model = await OutputHelper.withSpinner(
  'Loading bge-small-en-v1.5...',
  async () => await llama.loadModel({ modelPath: MODEL_PATH })
);

Quick local LLM prompt (used in 06/05)

import { LlamaPrompter } from '../../../helpers/llama-prompter.js';

const lp = new LlamaPrompter();
const rewritten = await lp.prompt('Rewrite this query as 3 paraphrases: "cheap red shoes"');
console.log(rewritten);
await lp.dispose();