Purrx

Rebuild the pipeline on LangChain

Assemble research, the zod outline, deck writing and the repair loop into one LangChain pipeline that takes a model as a parameter, replacing the hand-written generate.js.

13 min+40 XPHands-on

You have a model factory, a structured outline and web research. This lesson connects them with the deck writing and repair loop from module 7 into one function: generateDeck(topic, options).

With AI (Gemini)

Topic

“cobots in small factories”

Research

Google Search, optional

Outline

structured JSON

SlideML

<CardsSlide>…

Without AI (your engine)

Parse

text → tree

Components + theme

short tags → layout

Layout + text fitting

exact boxes

.pptx file

pptxgenjs

Everything you'll build. The model only writes words and structure; deterministic code does all the geometry.

The steps

  1. Research, only if options.research is on.
  2. Outline with generateOutline, passing the research notes.
  3. Write the deck with writeDeck (written for you): system prompt plus outline, extract the <deck>.
  4. Compile and repair: the loop from lesson 7.3, now passing the model to repairDeck.

The model is a parameter

Every step receives model instead of creating one. generateDeck uses createModel() when you don't pass one. In the fallback models lesson that lets the pipeline switch to a backup model, and in module 11 it lets tests pass a that needs no network at all.

Your project so far

18 files · 1 new or changed in this lesson

src/pipeline.js

// The whole generator, rebuilt on LangChain chat models:
//
//   (optional) research → outline → write the deck → compile → repair what's broken
//
// It replaces generate.js. The steps are the same; what changed is that each
// step receives a `model`, so we can swap in a different model, or a fake one in tests.

import { HumanMessage, SystemMessage } from "@langchain/core/messages";
import { contentText, createModel } from "./llm.js";
import { generateOutline } from "./outline.js";
import { researchTopic } from "./research.js";
import { buildSystemPrompt, extractTag } from "./prompt.js";
import { LIBRARY } from "./library.js";
import { compileDeck } from "./compile.js";

const MAX_REPAIR_ROUNDS = 2;

/** Outline → SlideML, following the component library. */
export async function writeDeck(model, outline) {
  const reply = await model.invoke([
    new SystemMessage(buildSystemPrompt(LIBRARY)),
    new HumanMessage(`Write the deck for this outline:\n${JSON.stringify(outline, null, 2)}`),
  ]);
  const xml = extractTag(contentText(reply), "deck");
  if (!xml) throw new Error("The model didn't reply with a <deck> element.");
  return xml;
}

/** Send the deck back with the compiler's problems and ask for a corrected deck. */
export async function repairDeck(model, xml, problems) {
  const list = problems.map((problem) => `- Slide ${problem.slide}: ${problem.message}`).join("\n");
  const reply = await model.invoke([
    new SystemMessage(buildSystemPrompt(LIBRARY)),
    new HumanMessage(`This deck has problems:\n${list}\n\nFix them and reply with the whole corrected <deck>.\n\n${xml}`),
  ]);
  return extractTag(contentText(reply), "deck");
}

/**
 * options:
 *   model       a LangChain chat model (default: createModel())
 *   research    true to search the web first
 *   slideCount  how many slides to plan (default 6)
 *   theme       "light" or "dark"
 *   compile     the compiler to use (tests pass a fake one)
 */
export async function generateDeck(topic, options = {}) {
  const { model = createModel(), research = false, slideCount = 6, theme, compile = compileDeck } = options;

  const findings = research ? await researchTopic(model, topic) : { notes: "", sources: [] };
  const outline = await generateOutline(model, topic, { slideCount, research: findings.notes });
  let xml = await writeDeck(model, outline);
  let result = compile(xml, theme);
  let repairs = 0;

  while (result.problems.length > 0 && repairs < MAX_REPAIR_ROUNDS) {
    repairs += 1;
    const fixed = await repairDeck(model, xml, result.problems);
    if (!fixed) break;
    const next = compile(fixed, theme);
    if (next.problems.length >= result.problems.length) break;
    xml = fixed;
    result = next;
  }

  return { outline, xml, slides: result.slides, problems: result.problems, repairs, sources: findings.sources };
}

Key takeaways

  • pipeline.js runs research, outline, deck and repair, with the model passed in.
  • Passing the model as a parameter makes it easy to swap models, or use a fake in tests.
  • The steps are the same as generate.js: the architecture didn't change, only the model API did.

Sign in to run the exercise

Reading is free. Writing code here needs an account so we have somewhere to keep your Gemini key and the +40 XP you are about to earn.