rudra-js

createComponentGenerator

import { createComponentGenerator } from '@rudra-js/core';

Call createComponentGenerator once on your server to get a generator. Its generate method turns one page view into a block for RudraComponent to render.

createComponentGenerator(options?: ComponentGeneratorOptions) => ComponentGenerator

Reference

createComponentGenerator(options?)

Parameters

  • optionsoptional: Settings for the generator. Each one has a default, so you can leave this out.
    • provideroptional: The model to call, such as createAnthropicProvider or your own ComponentProvider. Defaults to null, which calls no model and bills nothing.
    • cacheoptional: Where the model's answers are kept between requests. Defaults to createMemorySpecCache(). Pass createNullSpecCache() to keep nothing.
    • modelTimeoutMsoptional: How long the model gets to answer, in milliseconds. Defaults to 1500. Past that, core cancels the call and gives you the block core builds without a model.
    • cacheTimeoutMsoptional: How long a cache read gets, in milliseconds. Defaults to 50. Past that, the request carries on as if the cache were empty.
    • generationoptional: Defaults to 'cohort'. Shoppers in the same cohort share the layout and copy the model wrote. Core fills in each shopper's own products. With 'per-shopper', the model writes a block for every shopper and picks the products too.
    • rankoptional: How core orders the products. 'signals', the default, orders them by the shopper's signals. 'given' keeps the order you sent. It applies in 'cohort' mode and to the block core builds without a model. In 'per-shopper' mode the model picks the order.
    • onEventoptional: Called once each time the generator builds a block, with a report on how it went. If your function throws, core ignores the error.
      • key: The cache key this request used. It is null when the generator has no provider, and for generateDeterministic. A store with delete can drop one bad entry by this key.
      • source: 'llm' when the model answered this request. 'cache' when an earlier answer came from the cache. 'fallback' when core built the block without the model.
      • elapsedMs: How long the request took, in milliseconds.
      • calledModel: true when this request started a model call. A request that joined a call already running for the same key gets false.
      • violationsoptional: What core took out of the model's block before serving it. Each one is a string, such as 'empty-block:grid'. They can name products the shopper bought, disliked or has in the basket, so treat logs of them as personal data.
      • usageoptional: The token counts the provider reported. Requests that joined a running call carry the same usage, so add it up only where calledModel is true.
      • degradedReasonoptional: Why core built the block without the model. It is set when source is 'fallback'.
        • 'no-provider': The generator has no provider.
        • 'provider-error': The provider threw.
        • 'timeout': The model took longer than modelTimeoutMs.
        • 'invalid-generation': The model's answer didn't fit the schema.
        • 'unusable-on-serve': Core's checks left no headline or no product to show.
        • 'requested': You called generateDeterministic.
      • erroroptional: What was thrown, when degradedReason is 'provider-error' or 'timeout'.
      • cacheoptional: How the cache read went: 'hit', 'miss', 'error' or 'timeout'. It is missing when no cache was read, as with no provider.

Returns

A ComponentGenerator object with the two methods below.

Caveats

  • Create one generator and reuse it for every request. Its default cache and its list of model calls in progress live inside it. A new generator starts with neither.
  • generate never rejects because of the model. When the model times out, fails or gives an answer core can't use, you get the block core builds without a model.
  • Input that fails parseTrackingInput makes generate reject and generateDeterministic throw. Neither reports an event.

generator.generate(input)

Call generate with one page view to get the block for it.

Parameters

  • input: The page view: the shopper, the page, the products you're willing to show and the shopper's signals. Pass what parseTrackingInput returned, or the raw object, which generate parses for you.

Returns

A promise of the ComponentSpec to pass to RudraComponent as spec.

Caveats

  • With a provider, a request that isn't in the cache waits for the model, for up to modelTimeoutMs.
  • Requests for the same cache key that arrive while a model call is running share that call.

generator.generateDeterministic(input)

Call generateDeterministic to get the block core builds without a model, even when the generator has a provider.

Parameters

  • input: The same page view generate takes.

Returns

The ComponentSpec itself. You don't need to await it.

Caveats

  • It never calls the model or reads the cache.
  • Its event has source: 'fallback' and degradedReason: 'requested'.
  • It still uses the shopper's signals to pick and order products. For a shopper who objects to profiling, see Running it in a regulated shop.

Usage

Creating one generator for your server

Create the generator at the top of a module. Call it from every place that renders a block, so they all share one cache.

import { createComponentGenerator, parseTrackingInput, type Product } from '@rudra-js/core';

const generator = createComponentGenerator();

export async function recommendationsFor(shopperId: string, sku: string, catalog: Product[]) {
  const input = parseTrackingInput({
    user: { id: shopperId },
    context: { surface: 'pdp', currentSku: sku },
    candidates: catalog,
  });
  const spec = await generator.generate(input);
  return { spec, products: input.candidates };
}

Render the result with RudraComponent.

Turning the model on

Pass a provider, and the model writes the layout, the headings and the copy. Core still checks the block against your catalog before it reaches the page.

import { createAnthropicProvider } from '@rudra-js/anthropic';
import { createComponentGenerator } from '@rudra-js/core';

const provider = createAnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! });

export const generator = createComponentGenerator({ provider });

The provider takes the API key as an option, so it can come from wherever you keep secrets.

Give a slower model more time with modelTimeoutMs.

Seeing whether the model answered

Read source in onEvent to see where each block came from.

import { createAnthropicProvider } from '@rudra-js/anthropic';
import { createComponentGenerator } from '@rudra-js/core';

export const generator = createComponentGenerator({
  provider: createAnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! }),
  onEvent(event) {
    switch (event.source) {
      case 'llm':
        console.log(`The model answered in ${event.elapsedMs}ms`);
        break;
      case 'cache':
        console.log('An earlier answer came from the cache');
        break;
      case 'fallback':
        console.warn('Built without the model:', event.degradedReason, event.error);
        break;
    }
  },
});

The rendered block carries the same source in its data-rudra-source attribute, so you can also check the HTML.

To track this over time, see Watching it in production.

Troubleshooting

Every block looks the same and source is 'fallback'

Log event.degradedReason. It names the cause.

'timeout' means the model took longer than modelTimeoutMs. Raise it, or pick a faster model.

'provider-error' means the provider threw. The error is in event.error. With Anthropic, look it up under source is always fallback. A 400 that asks for a workspace has its own entry.

'unusable-on-serve' means core's checks left the model's block with no headline or no product. Log event.violations to see what they took out.

'invalid-generation' means the provider returned a spec that doesn't fit the schema. A provider you wrote should throw instead, as ComponentProvider shows, so event.error says what was wrong.

'no-provider' means the generator has no provider. Pass one, as in Turning the model on.

Each request calls the model again

You're creating a new generator for each render. Unless you pass a cache, every generator makes its own, so a new one starts empty.

Create it once at the top of a module and reuse it.

import { createAnthropicProvider } from '@rudra-js/anthropic';
import { createComponentGenerator, type TrackingInputDraft } from '@rudra-js/core';

const provider = createAnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! });

// wrong
export async function slowRecommendations(input: TrackingInputDraft) {
  const generator = createComponentGenerator({ provider });
  return generator.generate(input);
}

// right
const generator = createComponentGenerator({ provider });

export async function recommendations(input: TrackingInputDraft) {
  return generator.generate(input);
}