npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

grounded-llm

v0.2.3

Published

TypeScript library to reduce hallucination in LLM-generated responses and decisions by grounding them in retrieved context.

Readme


English

TypeScript library to reduce hallucination in LLM-generated responses, by forcing literal fact extraction and an explicit sufficiency check before generating a final answer.

Multi-Provider Support: Supports OpenAI (default), Anthropic (Claude), and Google (Gemini) out of the box with 100% backward compatibility for existing OpenAI configurations. You can select providers dynamically or add custom provider adapters by implementing LLMProviderContract.

Provider Selection & Configuration

Providers are resolved using deterministic precedence:

  1. Explicit parameter (provider: 'anthropic' or provider: 'google')
  2. Environment variable (GROUNDED_LLM_PROVIDER or LLM_PROVIDER)
  3. Default ('openai')
import { GroundedGenerator, providerRegistry } from 'grounded-llm';

// Example: Using Anthropic provider
const generator = new GroundedGenerator({
  provider: 'anthropic',
  apiKey: process.env.ANTHROPIC_API_KEY,
  fallbackValue: "I don't have enough information to answer that.",
});

// Example: Using Google Gemini provider
const geminiGenerator = new GroundedGenerator({
  provider: 'google',
  apiKey: process.env.GEMINI_API_KEY,
  fallbackValue: "I don't have enough information to answer that.",
});

Adding a Custom Provider

Extend grounded-llm with custom providers by implementing LLMProviderContract and registering it in providerRegistry:

import {
  providerRegistry,
  type LLMProviderContract,
  type ProviderRequest,
  type ProviderResponse,
} from 'grounded-llm';

class CustomProviderAdapter implements LLMProviderContract {
  readonly providerId = 'my-custom-provider';
  readonly capabilities = { structuredOutput: true };

  async completeStructured<T>(request: ProviderRequest): Promise<ProviderResponse<T>> {
    // Implement model call logic and return normalized ProviderResponse
    return {
      data: {} as T,
      finishStatus: 'stop',
    };
  }
}

providerRegistry.registerProvider(new CustomProviderAdapter());

How it fights hallucination: chain-of-thought grounding

Every component in this library forces the model through the same explicit chain-of-thought sequence instead of asking for a final answer directly:

  1. Extract — the model pulls the literal, verbatim excerpts from the context that are relevant to the request. No paraphrasing is allowed at this step.
  2. Judge sufficiency — using only those excerpts, the model explicitly decides whether the context is enough to respond safely (contradictions or partial matches count as insufficient).
  3. Answer or fall back — only if the context was judged sufficient does the model write a final answer, and it may use only what was extracted in step 1. Otherwise, the call returns the developer-configured fallback instead of letting the model invent something plausible.
  4. Explain — the model must always return reasoning, an explanation that ties the extracted excerpts to the sufficiency decision and (when applicable) to the final answer.

This forced extract → judge → answer → explain pipeline, enforced through structured output (schema-validated, not just a prompting convention), is what makes hallucination structurally harder: the model cannot skip straight to a confident-sounding answer without first grounding it in literal text it can point to. Every result exposes this reasoning chain via result.extractedFacts and result.reasoning, so you can inspect why the model answered — or refused to — instead of trusting a black-box output.

Generators

The library offers four components. Three are anchored in retrieved context, for different context-grounded LLM call scenarios; the fourth, GroundedComposer, is anchored in per-call instructions instead — for scenarios where the message content is already fully determined by rules, not by information to look up.

| Component | Use case | Method | | ----------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------- | | GroundedGenerator | Generate the final answer to the user from retrieved context | .generate({ context, question }) | | GroundedEnricher | Enrich an existing base text with retrieved context | .generate({ baseContent, context }) | | GroundedExtractor | Extract a structured object (fields you define) from the user message | .extract({ message }) | | GroundedComposer | Compose a message anchored in per-call instructions, with context as optional support | .compose({ instructions, context }) |

The first three share the same principles: optional fallback at construction (see below), structured output via schema, temperature zero by default, and operational errors (ModelUnavailableError, ContextTooLargeError, InvalidModelOutputError) always distinct from a fallback result. GroundedComposer shares the structured output, temperature, and operational-error principles, but has no fallback concept at all — see its own section below.

fallbackValue is optional. When configured, it's the canned value returned in place of the model's output whenever the component judges its own result unsafe to return (insufficient context, nothing extractable). When omitted:

  • GroundedGenerator and GroundedEnricher always let the model produce a real answer — GroundedGenerator falls back to a best-effort answer (general knowledge, or a clarifying question) instead of an empty result; GroundedEnricher's behavior is unchanged either way, since it already returns baseContent unchanged on insufficient context rather than a configured fallback.
  • GroundedExtractor always returns the model's raw extraction (null for fields it couldn't find), ignoring strict, instead of substituting a fallback object.

All three also accept, at construction, three optional parameters to customize the model's behavior for that call:

  • identity — the model's role/objective for this call (e.g. "You are the support assistant for Acme Corp.").
  • rules — additional rules constraining the call (e.g. style, domain-specific constraints).
  • tone — the desired tone/personality for the response (e.g. "be empathetic, kind, and natural" — useful for chatbot scenarios).

All three are appended as extra sections in the same system prompt, in the order identityrulestone, always after the component's built-in grounding/anti-hallucination instructions — they complement persona and style, but never override the grounding rules.

Using a LangChain model (LangSmith tracing)

By default, every component talks to the OpenAI API directly (standalone mode) — no LangChain dependency required. If your application already runs on LangChain and you want these calls to show up in your LangSmith traces alongside the rest of your pipeline, pass an already-configured LangChain chat model via langchainModel instead of client/apiKey/model/temperature:

import { ChatOpenAI } from '@langchain/openai';
import { GroundedGenerator } from 'grounded-llm';

const langchainModel = new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 });

const generator = new GroundedGenerator({
  langchainModel,
  fallbackValue: "Sorry, I don't have enough information to answer that.",
});
  • langchainModel is mutually exclusive with client, apiKey, model, and temperature — the chat model already carries its own credentials, model id, and temperature, so combining it with any of those throws a configuration error at construction.
  • maxContextTokens still applies; when omitted in this mode, a conservative default of 128 000 tokens is used (there's no OpenAI model id to derive a known limit from).
  • identity/rules/tone/fallbackValue, the result shape, and the operational error types (ModelUnavailableError/ContextTooLargeError/InvalidModelOutputError) all behave identically whether you use client/apiKey or langchainModel.
  • @langchain/core is an optional peer dependency — install it (and whichever LangChain chat model integration you use, e.g. @langchain/openai) only if you use langchainModel. Standalone consumers never need it.

GroundedGenerator

Generates a final answer strictly grounded in retrieved context, or falls back to a developer-configured value when the context is insufficient — instead of inventing an answer.

import { GroundedGenerator } from 'grounded-llm';

const generator = new GroundedGenerator({
  fallbackValue: "Sorry, I don't have enough information to answer that.",
  // Optional: fallbackValue (see "Generators" above for what happens when it's
  // omitted), model (default "gpt-4o-mini"), apiKey (default OPENAI_API_KEY),
  // temperature (default 0), maxContextTokens, or an already-configured `client`
  // instance from the `openai` package (or `langchainModel` instead — see above).
  // Also accepts identity/rules/tone.
});

const result = await generator.generate({
  context: 'Paris is the capital of France.',
  question: 'What is the capital of France?',
});

console.log(result.usedFallback); // false
console.log(result.finalAnswer); // "Paris is the capital of France."
console.log(result.extractedFacts); // ["Paris is the capital of France."]
console.log(result.reasoning); // explanation connecting facts to the answer

GroundedGenerator is standalone by default — it depends only on the official openai client, so it can be plugged into any pipeline (LangGraph, a manual chain, or a direct call) without requiring any third-party types. See "Using a LangChain model" above if you'd rather route calls through an existing LangChain chat model.

Error handling

generate() throws one of three distinct operational errors (none of which are retried automatically — retry policy is the caller's responsibility):

  • ModelUnavailableError — technical failure calling the model (network, timeout).
  • ContextTooLargeError — the context exceeds the model's processable limit.
  • InvalidModelOutputError — the model's response failed schema validation or was refused.

These are distinct from a normal result with usedFallback: true, which is a valid outcome (insufficient context), not an error.

GroundedEnricher

Enriches an existing base text with retrieved context (e.g., via RAG) — useful when you already have a template/draft response and want to add dynamic information to it, instead of generating a response from scratch.

import { GroundedEnricher } from 'grounded-llm';

const enricher = new GroundedEnricher({
  fallbackValue: 'N/A', // required for API consistency; never actually returned in normal use (see note below)
  // Also accepts identity/rules/tone and langchainModel, plus the same config options as GroundedGenerator.
});

const result = await enricher.generate({
  baseContent: 'Thanks for your order!',
  context: 'Orders ship within 3 business days.',
});

console.log(result.usedFallback); // false
console.log(result.finalAnswer); // "Thanks for your order! Orders ship within 3 business days."
console.log(result.extractedFacts); // ["Orders ship within 3 business days."]
console.log(result.reasoning); // explanation connecting facts to the enrichment

Fallback semantics differ from GroundedGenerator: when the context is insufficient to enrich safely, GroundedEnricher returns baseContent unchanged (with usedFallback: true) — never fallbackValue. fallbackValue is required at construction only for consistency with the other components in the family; it's never returned by any success path. An empty/blank baseContent is treated as invalid usage and throws immediately, without calling the model.

GroundedExtractor

Extracts a structured object with fields you define from a user message — useful for the "JSON mode" scenarios common in chatbots (name, email, intent, etc.), without requiring a closed set of actions or logprob-based confidence (that's the future GroundedDecider's job).

import { GroundedExtractor } from 'grounded-llm';
import { z } from 'zod';

const extractor = new GroundedExtractor({
  fields: { name: z.string(), email: z.string() },
  fallbackValue: { name: null, email: null }, // whole object, same shape as `fields`
  // Optional: strict (default false) — see below. Also accepts identity/rules/tone and langchainModel.
});

const result = await extractor.extract({
  message: "Hi, I'm Ada Lovelace, [email protected]",
});

console.log(result.usedFallback); // false
console.log(result.data); // { name: "Ada Lovelace", email: "[email protected]" }
console.log(result.reasoning);

Partial extraction and strict mode: if the message only supports part of the fields, the default behavior (strict: false) returns the extracted fields with null for the rest, without triggering fallbackValue. With strict: true, any missing field triggers fallbackValue (whole object) instead of a partial result. If no field can be safely extracted (or the message is empty), fallbackValue is returned regardless of strict.

GroundedComposer

Composes a final message anchored primarily in instructions provided for that call — not in retrieved context. Useful for rule-driven flows where another part of your system has already decided exactly what needs to be said (e.g. the next question in a step-by-step data-collection flow); GroundedComposer just drafts that message following the instructions to the letter. context (e.g. a conversation summary plus data already collected) is optional and only ever used as support — to detect a conflict with the instructions, acknowledge progress, or reference data already mentioned — never as a sufficiency gate.

import { GroundedComposer } from 'grounded-llm';

const composer = new GroundedComposer({
  // Also accepts identity/rules/tone and langchainModel, plus the same config
  // options as GroundedGenerator. `fallbackValue`, if passed, is accepted but
  // ignored — this component never falls back (see below).
});

const result = await composer.compose({
  instructions:
    'Ask for the customer\'s service protocol, offering these options: 1159293, 1159292, or "start a new service".',
  context: 'Customer already provided their name earlier in this conversation.',
});

console.log(result.usedFallback); // always false
console.log(result.finalAnswer); // the composed question, following the instructions
console.log(result.extractedFacts); // literal excerpts from `instructions` (+ `context`, when used)
console.log(result.reasoning); // explanation connecting instructions (and context, if used) to the message

This component never abstains or falls back: unlike the other three generators, there is no concept of "insufficient input" here — instructions alone always fully determines the message, so finalAnswer is always produced and usedFallback is always false. fallbackValue, if configured, is silently ignored — it exists only because it's part of the shared GroundedCallConfig shape, not because GroundedComposer has any code path that reads it. An empty/blank instructions is treated as invalid usage and throws immediately, without calling the model; an empty/blank/absent context is not an error — the message is simply composed from instructions alone.

Structured logging hooks

All four generators (GroundedGenerator, GroundedEnricher, GroundedExtractor, GroundedComposer) accept three optional lifecycle callbacks at construction — onCall, onResult, onError — so you can observe every call in production without wrapping each .generate()/.extract()/.compose() call site by hand. They work identically in standalone mode and with langchainModel.

  • onCall fires once, right before the model is reached.
  • onResult fires once, on success, with durationMs and usedFallback.
  • onError fires once, on failure, with durationMs and an errorType classifying the failure as 'model-unavailable', 'invalid-output', 'context-too-large', 'provider-error', or 'unknown'.
  • Exactly one of onResult/onError fires per call. Every event carries a callId shared across a call's onCall/onResult/onError, so you can correlate them even under concurrent calls.
  • Callbacks are synchronous/fire-and-forget: they are never awaited, and an exception thrown inside one is caught and discarded — it can never block, delay, or change the call's own result.
  • Payloads carry metadata only (callId, operation, timing, fallback/error info) — never the raw context/question/instructions/answer text.

Basic console logging:

import { GroundedGenerator } from 'grounded-llm';

const generator = new GroundedGenerator({
  fallbackValue: "I don't know.",
  onCall: ({ callId, operation }) => console.log(`[${callId}] ${operation} started`),
  onResult: ({ callId, durationMs, usedFallback }) =>
    console.log(`[${callId}] ok in ${durationMs}ms (usedFallback=${usedFallback})`),
  onError: ({ callId, durationMs, errorType }) =>
    console.error(`[${callId}] failed in ${durationMs}ms (${errorType})`),
});

Prometheus-style metrics:

import { Counter, Histogram } from 'prom-client';
import { GroundedGenerator } from 'grounded-llm';

const callDuration = new Histogram({
  name: 'grounded_llm_call_duration_ms',
  help: 'Duration of grounded-llm calls',
  labelNames: ['operation', 'outcome'],
});
const callErrors = new Counter({
  name: 'grounded_llm_call_errors_total',
  help: 'Failed grounded-llm calls by type',
  labelNames: ['operation', 'error_type'],
});

const generator = new GroundedGenerator({
  fallbackValue: "I don't know.",
  onResult: ({ operation, durationMs }) =>
    callDuration.labels(operation, 'success').observe(durationMs),
  onError: ({ operation, durationMs, errorType }) => {
    callDuration.labels(operation, 'error').observe(durationMs);
    callErrors.labels(operation, errorType).inc();
  },
});

Token usage & cost metadata

Every generator's result (GroundedGenerator, GroundedEnricher, GroundedComposer, and GroundedExtractor's GroundedExtractionResult) carries an optional usage field with token counts reported by the underlying provider:

interface ProviderUsage {
  promptTokens?: number;
  completionTokens?: number;
  totalTokens?: number;
}
  • In standalone mode (OpenAI, Anthropic, Google), usage is populated from the provider's own reported token counts whenever the provider includes them in its response.
  • In langchainModel mode, usage is always undefined — the wrapped LangChain chat model's raw usage metadata isn't extracted today, so don't rely on this field being present when using that mode.
  • usage is only ever absent or fully reported — it is never a fabricated/zeroed object, so you can safely treat its absence as "unknown," not "zero tokens used."

Logging and aggregating usage across calls:

import { GroundedGenerator } from 'grounded-llm';

const generator = new GroundedGenerator({ fallbackValue: "I don't know." });

const totals = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };

for (const request of requests) {
  const result = await generator.generate(request);
  console.log(`usage for this call:`, result.usage);

  totals.promptTokens += result.usage?.promptTokens ?? 0;
  totals.completionTokens += result.usage?.completionTokens ?? 0;
  totals.totalTokens += result.usage?.totalTokens ?? 0;
}

console.log('total usage across all calls:', totals);

Result cache

All four generators accept an optional cache option at construction — a minimal { get(key), set(key, value) } contract you implement against whatever store you choose (in-memory Map, Redis, or anything else). When configured, an identical repeated call is served from the cache without running the pipeline or contacting the model provider at all.

  • Opt-in only: omitting cache leaves behavior exactly as before — every call runs the full pipeline.
  • Storage-agnostic: the library ships no default cache implementation and requires nothing from your store beyond get/set. Both may be synchronous or return a Promise — either works with no adapter code.
  • Deterministic key: the cache key is derived internally from the request's content fields plus any output-affecting per-instance configuration (identity, rules, tone, model, temperature, and, for GroundedExtractor, its fields/strict). Two calls only ever share a cache entry when all of these match.
  • No invalidation policy: the library never expires or evicts entries — that is entirely your cache implementation's responsibility (e.g. a TTL on a Redis key, or clearing a Map yourself).
  • Fails open: if your cache's get or set throws or rejects, the request is still served normally (falling back to running the pipeline) — a broken cache backend never fails a call.
  • onCall/onResult still fire for a cache hit, reporting the real (cached) outcome.
import { GroundedGenerator } from 'grounded-llm';

const store = new Map<string, unknown>();

const generator = new GroundedGenerator({
  fallbackValue: "I don't know.",
  cache: {
    get: (key) => store.get(key),
    set: (key, value) => {
      store.set(key, value);
    },
  },
});

const first = await generator.generate({ context, question }); // runs the pipeline
const second = await generator.generate({ context, question }); // served from cache

Releasing

CI (.github/workflows/ci.yml) runs type-check, tests, a coverage summary (published to the workflow run's summary page), and build on every push/PR to main. Publishing to npm (.github/workflows/release.yml) is triggered by pushing a v*.*.* tag:

npm version patch   # or minor / major — bumps package.json and creates a git tag
git push --follow-tags

The release workflow verifies the tag matches package.json's version, then runs the same build/test steps before publishing with npm provenance. Requires an NPM_TOKEN secret (an npm Automation token) configured in the repository settings.

Contributing

See CONTRIBUTING.md for local environment setup and collaboration standards (branching, commit messages, PR checklist).


Português

Biblioteca TypeScript para reduzir alucinação em respostas geradas por LLM, forçando extração literal de fatos e uma checagem explícita de suficiência de contexto antes de gerar a resposta final.

Nesta versão, o alvo é a API da OpenAI. Por padrão, cada componente usa o client oficial openai internamente (injetado por você ou criado a partir de uma apiKey). Opcionalmente, você pode em vez disso passar um chat model LangChain já configurado via langchainModel — veja "Usando um modelo LangChain" abaixo.

Como o combate à alucinação funciona: chain-of-thought ancorado em contexto

Todos os componentes da biblioteca forçam o modelo a passar pela mesma sequência explícita de chain-of-thought (cadeia de raciocínio), em vez de pedir a resposta final diretamente:

  1. Extrair — o modelo retira do contexto os trechos literais e relevantes para a solicitação, verbatim. Paráfrase não é permitida nesta etapa.
  2. Julgar suficiência — usando apenas esses trechos extraídos, o modelo decide explicitamente se o contexto é suficiente para responder com segurança (contradições ou correspondências parciais contam como insuficientes).
  3. Responder ou usar fallback — só se o contexto for julgado suficiente o modelo escreve uma resposta final, e ela só pode usar o que foi extraído no passo 1. Caso contrário, a chamada retorna o fallback configurado pelo desenvolvedor, em vez de deixar o modelo inventar algo plausível.
  4. Explicar — o modelo sempre deve retornar reasoning, uma explicação que conecta os trechos extraídos à decisão de suficiência e (quando aplicável) à resposta final.

Esse pipeline forçado de extrair → julgar → responder → explicar, garantido via saída estruturada (validada por schema, não apenas uma convenção de prompt), é o que torna a alucinação estruturalmente mais difícil: o modelo não consegue pular direto para uma resposta com aparência confiante sem antes ancorá-la em texto literal que ele pode apontar. Todo resultado expõe essa cadeia de raciocínio via result.extractedFacts e result.reasoning, permitindo inspecionar por que o modelo respondeu — ou se recusou a responder — em vez de confiar em uma saída caixa-preta.

Generators

A lib oferece quatro componentes. Três são ancorados em context recuperado, para cenários diferentes de chamada LLM ancorada em contexto; o quarto, o GroundedComposer, é ancorado em instructions por chamada — para cenários em que o conteúdo da mensagem já é totalmente determinado por regras, não por informação a ser buscada.

| Componente | Uso | Método | | ------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------- | | GroundedGenerator | Gerar a resposta final ao usuário a partir de contexto recuperado | .generate({ context, question }) | | GroundedEnricher | Enriquecer um texto-base existente com contexto recuperado | .generate({ baseContent, context }) | | GroundedExtractor | Extrair um objeto estruturado (campos definidos por você) da mensagem do usuário | .extract({ message }) | | GroundedComposer | Compor uma mensagem ancorada em instruções por chamada, com contexto como apoio opcional | .compose({ instructions, context }) |

Os três primeiros compartilham os mesmos princípios: fallback opcional na construção (veja abaixo), saída estruturada via schema, temperature zero por padrão, e erros operacionais (ModelUnavailableError, ContextTooLargeError, InvalidModelOutputError) sempre distintos de um resultado com fallback. O GroundedComposer compartilha os princípios de saída estruturada, temperature e erros operacionais, mas não tem nenhum conceito de fallback — veja sua própria seção abaixo.

fallbackValue é opcional. Quando configurado, é o valor fixo retornado no lugar da saída do modelo sempre que o componente julga seu próprio resultado inseguro para retornar (contexto insuficiente, nada extraível). Quando omitido:

  • GroundedGenerator e GroundedEnricher sempre deixam o modelo produzir uma resposta real — o GroundedGenerator recorre a uma resposta best-effort (conhecimento geral, ou uma pergunta de esclarecimento) em vez de um resultado vazio; o comportamento do GroundedEnricher não muda de qualquer forma, já que ele já retorna o baseContent inalterado quando o contexto é insuficiente, em vez de um fallback configurado.
  • GroundedExtractor sempre retorna a extração bruta do modelo (null nos campos não encontrados), ignorando strict, em vez de substituir por um objeto de fallback.

Os três também aceitam, na construção, três parâmetros opcionais para customizar o comportamento do modelo naquela chamada:

  • identity — o papel/objetivo do modelo naquela chamada (ex: "Você é o assistente de suporte da Acme Corp.").
  • rules — regras adicionais para restringir a chamada (ex: estilo, restrições específicas do domínio).
  • tone — o tom/personalidade desejado para a resposta (ex: "seja empático, gentil e natural" — útil em cenários de chatbot).

Os três são anexados como seções extras no mesmo system prompt, na ordem identityrulestone, sempre depois das instruções internas de ancoragem/anti-alucinação — eles complementam persona e estilo, mas nunca sobrescrevem as regras de grounding.

Usando um modelo LangChain (tracing do LangSmith)

Por padrão, todos os componentes falam diretamente com a API da OpenAI (modo standalone) — sem exigir nenhuma dependência do LangChain. Se sua aplicação já roda sobre LangChain e você quer que essas chamadas apareçam nos seus traces do LangSmith junto com o restante do seu pipeline, passe um chat model LangChain já configurado via langchainModel, em vez de client/apiKey/model/temperature:

import { ChatOpenAI } from '@langchain/openai';
import { GroundedGenerator } from 'grounded-llm';

const langchainModel = new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 });

const generator = new GroundedGenerator({
  langchainModel,
  fallbackValue: 'Desculpe, não tenho informação suficiente para responder isso.',
});
  • langchainModel é mutuamente exclusivo com client, apiKey, model e temperature — o chat model já traz suas próprias credenciais, model id e temperatura, então combiná-lo com qualquer um desses campos lança um erro de configuração na construção.
  • maxContextTokens continua valendo; quando omitido nesse modo, um limite conservador padrão de 128.000 tokens é usado (não há um model id da OpenAI do qual derivar um limite conhecido).
  • identity/rules/tone/fallbackValue, o formato do resultado, e os tipos de erro operacionais (ModelUnavailableError/ContextTooLargeError/ InvalidModelOutputError) se comportam de forma idêntica, seja usando client/apiKey ou langchainModel.
  • @langchain/core é uma peerDependency opcional — instale-a (e a integração de chat model LangChain que você usar, ex: @langchain/openai) apenas se for usar langchainModel. Consumidores do modo standalone nunca precisam dela.

GroundedGenerator

Gera uma resposta final estritamente ancorada no contexto recuperado, ou recorre a um valor de fallback configurado pelo desenvolvedor quando o contexto é insuficiente — em vez de inventar uma resposta.

import { GroundedGenerator } from 'grounded-llm';

const generator = new GroundedGenerator({
  fallbackValue: 'Desculpe, não tenho informação suficiente para responder isso.',
  // Opcional: fallbackValue (ver "Generators" acima para o que acontece quando
  // omitido), model (default "gpt-4o-mini"), apiKey (default OPENAI_API_KEY),
  // temperature (default 0), maxContextTokens, ou uma instância `client` já
  // configurada do pacote `openai` (ou `langchainModel` em vez disso — veja acima).
  // Também aceita identity/rules/tone.
});

const result = await generator.generate({
  context: 'Paris é a capital da França.',
  question: 'Qual é a capital da França?',
});

console.log(result.usedFallback); // false
console.log(result.finalAnswer); // "Paris é a capital da França."
console.log(result.extractedFacts); // ["Paris é a capital da França."]
console.log(result.reasoning); // explicação conectando os fatos à resposta

O GroundedGenerator é standalone — depende apenas do client oficial openai, então pode ser plugado em qualquer pipeline (LangGraph, uma chain manual, ou uma chamada direta) sem exigir nenhum tipo de terceiros.

Tratamento de erros

generate() lança um de três erros operacionais distintos (nenhum deles é reexecutado automaticamente — a política de retry é responsabilidade de quem consome a lib):

  • ModelUnavailableError — falha técnica na chamada ao modelo (rede, timeout).
  • ContextTooLargeError — o contexto excede o limite processável do modelo.
  • InvalidModelOutputError — a resposta do modelo falhou na validação do schema ou foi recusada.

Esses erros são distintos de um resultado normal com usedFallback: true, que é um desfecho válido (contexto insuficiente), não um erro.

GroundedEnricher

Enriquece um texto-base existente com contexto recuperado (por exemplo, via RAG) — útil quando você já tem uma resposta-template e quer adicionar informação dinâmica a ela, em vez de gerar uma resposta do zero.

import { GroundedEnricher } from 'grounded-llm';

const enricher = new GroundedEnricher({
  fallbackValue: 'N/A', // exigido por consistência de API; nunca é retornado em uso normal (ver nota abaixo)
  // Também aceita identity/rules/tone e langchainModel, além das mesmas opções de configuração do GroundedGenerator.
});

const result = await enricher.generate({
  baseContent: 'Obrigado pelo seu pedido!',
  context: 'Pedidos são entregues em até 3 dias úteis.',
});

console.log(result.usedFallback); // false
console.log(result.finalAnswer); // "Obrigado pelo seu pedido! Pedidos são entregues em até 3 dias úteis."
console.log(result.extractedFacts); // ["Pedidos são entregues em até 3 dias úteis."]
console.log(result.reasoning); // explicação conectando os fatos ao enriquecimento

Semântica de fallback diferente do GroundedGenerator: quando o contexto é insuficiente para enriquecer com segurança, o GroundedEnricher retorna o baseContent inalterado (com usedFallback: true) — nunca o fallbackValue. O fallbackValue é exigido na construção apenas por consistência com os demais componentes da família; ele nunca é retornado em nenhum fluxo de sucesso. Um baseContent vazio/em branco é tratado como uso inválido e lança uma exceção imediatamente, sem chamar o modelo.

GroundedExtractor

Extrai um objeto estruturado com campos definidos por você a partir de uma mensagem do usuário — útil para os cenários de "JSON mode" de chatbots (nome, e-mail, intenção, etc.), sem exigir um conjunto fechado de ações nem cálculo de confiança via logprob (isso é responsabilidade do futuro GroundedDecider).

import { GroundedExtractor } from 'grounded-llm';
import { z } from 'zod';

const extractor = new GroundedExtractor({
  fields: { name: z.string(), email: z.string() },
  fallbackValue: { name: null, email: null }, // objeto completo, mesmo formato de `fields`
  // Opcional: strict (default false) — veja abaixo. Também aceita identity/rules/tone e langchainModel.
});

const result = await extractor.extract({
  message: 'Oi, sou a Ada Lovelace, [email protected]',
});

console.log(result.usedFallback); // false
console.log(result.data); // { name: "Ada Lovelace", email: "[email protected]" }
console.log(result.reasoning);

Extração parcial e modo strict: se a mensagem preencher só parte dos campos, o comportamento padrão (strict: false) retorna os campos extraídos e null nos demais, sem acionar o fallbackValue. Com strict: true, qualquer campo ausente aciona o fallbackValue (objeto completo) em vez de um resultado parcial. Se nenhum campo puder ser extraído com segurança (ou a mensagem estiver vazia), o fallbackValue é retornado independentemente do modo strict.

GroundedComposer

Compõe uma mensagem final ancorada primariamente em instructions fornecidas naquela chamada — não em context recuperado. Útil para fluxos orientados por regras onde outra parte do seu sistema já decidiu exatamente o que precisa ser dito (ex: a próxima pergunta de um fluxo de coleta de dados campo-a-campo); o GroundedComposer só redige essa mensagem seguindo as instruções ao pé da letra. O context (ex: um resumo da conversa mais os dados já coletados) é opcional e serve só como apoio — para detectar um conflito com as instruções, reconhecer progresso, ou referenciar um dado já mencionado — nunca como critério de suficiência.

import { GroundedComposer } from 'grounded-llm';

const composer = new GroundedComposer({
  // Também aceita identity/rules/tone e langchainModel, além das mesmas opções de
  // configuração do GroundedGenerator. `fallbackValue`, se passado, é aceito mas
  // ignorado — este componente nunca recorre a fallback (veja abaixo).
});

const result = await composer.compose({
  instructions:
    'Pergunte o protocolo de atendimento do cliente, apresentando estas opções: 1159293, 1159292, ou "novo atendimento".',
  context: 'O cliente já informou o nome anteriormente nesta conversa.',
});

console.log(result.usedFallback); // sempre false
console.log(result.finalAnswer); // a pergunta composta, seguindo as instruções
console.log(result.extractedFacts); // trechos literais de `instructions` (+ `context`, quando usado)
console.log(result.reasoning); // explicação conectando instructions (e context, se usado) à mensagem

Este componente nunca se abstém nem recorre a fallback: diferente dos outros três generators, não existe aqui o conceito de "entrada insuficiente" — instructions sozinha já determina totalmente a mensagem, então finalAnswer é sempre produzida e usedFallback é sempre false. O fallbackValue, se configurado, é silenciosamente ignorado — ele existe apenas porque faz parte do formato compartilhado de GroundedCallConfig, não porque o GroundedComposer tenha algum caminho de código que o leia. Um instructions vazio/em branco é tratado como uso inválido e lança uma exceção imediatamente, sem chamar o modelo; um context vazio/em branco/ausente não é erro — a mensagem é simplesmente composta a partir de instructions sozinha.

Hooks de logging estruturado

Os quatro generators (GroundedGenerator, GroundedEnricher, GroundedExtractor, GroundedComposer) aceitam três callbacks opcionais de ciclo de vida na construção — onCall, onResult, onError — para observar cada chamada em produção sem precisar envolver manualmente cada chamada de .generate()/.extract()/.compose(). Funcionam de forma idêntica em modo standalone e com langchainModel.

  • onCall dispara uma vez, imediatamente antes de a chamada alcançar o modelo.
  • onResult dispara uma vez, em caso de sucesso, com durationMs e usedFallback.
  • onError dispara uma vez, em caso de falha, com durationMs e um errorType classificando a falha como 'model-unavailable', 'invalid-output', 'context-too-large', 'provider-error', ou 'unknown'.
  • Exatamente um entre onResult/onError dispara por chamada. Todo evento carrega um callId compartilhado entre onCall/onResult/onError daquela chamada, permitindo correlacioná-los mesmo sob chamadas concorrentes.
  • Os callbacks são síncronos/fire-and-forget: nunca são aguardados (await), e uma exceção lançada dentro de um deles é capturada e descartada — nunca pode bloquear, atrasar ou alterar o resultado da própria chamada.
  • Os payloads carregam apenas metadados (callId, operation, tempos, informação de fallback/erro) — nunca o texto bruto de context/question/instructions/resposta.

Logging básico via console:

import { GroundedGenerator } from 'grounded-llm';

const generator = new GroundedGenerator({
  fallbackValue: 'Não sei.',
  onCall: ({ callId, operation }) => console.log(`[${callId}] ${operation} iniciada`),
  onResult: ({ callId, durationMs, usedFallback }) =>
    console.log(`[${callId}] ok em ${durationMs}ms (usedFallback=${usedFallback})`),
  onError: ({ callId, durationMs, errorType }) =>
    console.error(`[${callId}] falhou em ${durationMs}ms (${errorType})`),
});

Métricas estilo Prometheus:

import { Counter, Histogram } from 'prom-client';
import { GroundedGenerator } from 'grounded-llm';

const callDuration = new Histogram({
  name: 'grounded_llm_call_duration_ms',
  help: 'Duração das chamadas do grounded-llm',
  labelNames: ['operation', 'outcome'],
});
const callErrors = new Counter({
  name: 'grounded_llm_call_errors_total',
  help: 'Chamadas do grounded-llm que falharam, por tipo',
  labelNames: ['operation', 'error_type'],
});

const generator = new GroundedGenerator({
  fallbackValue: 'Não sei.',
  onResult: ({ operation, durationMs }) =>
    callDuration.labels(operation, 'success').observe(durationMs),
  onError: ({ operation, durationMs, errorType }) => {
    callDuration.labels(operation, 'error').observe(durationMs);
    callErrors.labels(operation, errorType).inc();
  },
});

Uso de tokens e metadados de custo

O resultado de cada generator (GroundedGenerator, GroundedEnricher, GroundedComposer, e o GroundedExtractionResult do GroundedExtractor) carrega um campo opcional usage com as contagens de tokens reportadas pelo provider subjacente:

interface ProviderUsage {
  promptTokens?: number;
  completionTokens?: number;
  totalTokens?: number;
}
  • No modo standalone (OpenAI, Anthropic, Google), usage é preenchido a partir das contagens de tokens que o próprio provider reporta, sempre que essa informação vem na resposta.
  • No modo langchainModel, usage é sempre undefined — os metadados brutos de uso do chat model do LangChain não são extraídos hoje, então não conte com esse campo estando presente nesse modo.
  • usage só existe como ausente ou totalmente preenchido — nunca é um objeto zerado artificialmente, então sua ausência pode ser tratada com segurança como "desconhecido", não "zero tokens usados".

Logging e agregação de uso entre chamadas:

import { GroundedGenerator } from 'grounded-llm';

const generator = new GroundedGenerator({ fallbackValue: 'Não sei.' });

const totais = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };

for (const request of requests) {
  const result = await generator.generate(request);
  console.log(`uso desta chamada:`, result.usage);

  totais.promptTokens += result.usage?.promptTokens ?? 0;
  totais.completionTokens += result.usage?.completionTokens ?? 0;
  totais.totalTokens += result.usage?.totalTokens ?? 0;
}

console.log('uso total entre todas as chamadas:', totais);

Cache de resultado

Os quatro generators aceitam uma opção cache opcional na construção — um contrato mínimo { get(key), set(key, value) } que você implementa contra o armazenamento que escolher (Map em memória, Redis, ou qualquer outro). Quando configurado, uma chamada idêntica repetida é atendida pelo cache sem rodar o pipeline nem contatar o provider.

  • Opt-in: omitir cache mantém o comportamento exatamente como antes — toda chamada roda o pipeline completo.
  • Agnóstico de armazenamento: a biblioteca não traz implementação padrão nem exige nada além de get/set. Ambos podem ser síncronos ou retornar uma Promise — os dois funcionam sem código adaptador.
  • Chave determinística: a chave de cache é derivada internamente a partir dos campos de conteúdo da requisição mais qualquer configuração por instância que afete a saída (identity, rules, tone, model, temperature e, no GroundedExtractor, seus fields/strict). Duas chamadas só compartilham uma entrada de cache quando tudo isso coincide.
  • Sem política de invalidação: a biblioteca nunca expira ou remove entradas — isso é responsabilidade total da sua implementação de cache (TTL numa chave Redis, ou limpar um Map manualmente).
  • Falha de forma segura: se get ou set do seu cache lançar exceção ou rejeitar, a chamada ainda é atendida normalmente (caindo para rodar o pipeline) — um backend de cache quebrado nunca falha uma chamada.
  • onCall/onResult continuam disparando num cache hit, reportando o resultado real (vindo do cache).
import { GroundedGenerator } from 'grounded-llm';

const store = new Map<string, unknown>();

const generator = new GroundedGenerator({
  fallbackValue: 'Não sei.',
  cache: {
    get: (key) => store.get(key),
    set: (key, value) => {
      store.set(key, value);
    },
  },
});

const primeira = await generator.generate({ context, question }); // roda o pipeline
const segunda = await generator.generate({ context, question }); // vem do cache

Releases

O CI (.github/workflows/ci.yml) roda type-check, testes, um resumo de cobertura (publicado na página de resumo da execução do workflow) e build em todo push/PR para main. A publicação no npm (.github/workflows/release.yml) é disparada ao subir uma tag v*.*.*:

npm version patch   # ou minor / major — atualiza o package.json e cria a tag git
git push --follow-tags

O workflow de release confere se a tag bate com a versão do package.json, roda o mesmo build/test novamente, e então publica com npm provenance. Requer um secret NPM_TOKEN (token de automação do npm) configurado nas configurações do repositório.

Contribuindo

Veja CONTRIBUTING.md (em inglês) para configuração do ambiente local e padrões de colaboração (branches, mensagens de commit, checklist de PR).