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:
- Explicit parameter (
provider: 'anthropic'orprovider: 'google') - Environment variable (
GROUNDED_LLM_PROVIDERorLLM_PROVIDER) - 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:
- Extract — the model pulls the literal, verbatim excerpts from the context that are relevant to the request. No paraphrasing is allowed at this step.
- 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).
- 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.
- 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:
GroundedGeneratorandGroundedEnricheralways let the model produce a real answer —GroundedGeneratorfalls 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 returnsbaseContentunchanged on insufficient context rather than a configured fallback.GroundedExtractoralways returns the model's raw extraction (nullfor fields it couldn't find), ignoringstrict, 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
identity → rules → tone, 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.",
});langchainModelis mutually exclusive withclient,apiKey,model, andtemperature— 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.maxContextTokensstill applies; when omitted in this mode, a conservative default of 128 000 tokens is used (there's no OpenAImodelid 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 useclient/apiKeyorlangchainModel.@langchain/coreis an optional peer dependency — install it (and whichever LangChain chat model integration you use, e.g.@langchain/openai) only if you uselangchainModel. 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 answerGroundedGenerator 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 enrichmentFallback 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 messageThis 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.
onCallfires once, right before the model is reached.onResultfires once, on success, withdurationMsandusedFallback.onErrorfires once, on failure, withdurationMsand anerrorTypeclassifying the failure as'model-unavailable','invalid-output','context-too-large','provider-error', or'unknown'.- Exactly one of
onResult/onErrorfires per call. Every event carries acallIdshared across a call'sonCall/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 rawcontext/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),
usageis populated from the provider's own reported token counts whenever the provider includes them in its response. - In
langchainModelmode,usageis alwaysundefined— 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. usageis 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
cacheleaves 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 aPromise— 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, forGroundedExtractor, itsfields/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
Mapyourself). - Fails open: if your cache's
getorsetthrows or rejects, the request is still served normally (falling back to running the pipeline) — a broken cache backend never fails a call. onCall/onResultstill 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 cacheReleasing
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-tagsThe 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
openaiinternamente (injetado por você ou criado a partir de umaapiKey). Opcionalmente, você pode em vez disso passar um chat model LangChain já configurado vialangchainModel— 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:
- Extrair — o modelo retira do contexto os trechos literais e relevantes para a solicitação, verbatim. Paráfrase não é permitida nesta etapa.
- 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).
- 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.
- 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:
GroundedGeneratoreGroundedEnrichersempre deixam o modelo produzir uma resposta real — oGroundedGeneratorrecorre a uma resposta best-effort (conhecimento geral, ou uma pergunta de esclarecimento) em vez de um resultado vazio; o comportamento doGroundedEnrichernão muda de qualquer forma, já que ele já retorna obaseContentinalterado quando o contexto é insuficiente, em vez de um fallback configurado.GroundedExtractorsempre retorna a extração bruta do modelo (nullnos campos não encontrados), ignorandostrict, 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 identity →
rules → tone, 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 comclient,apiKey,modeletemperature— 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.maxContextTokenscontinua valendo; quando omitido nesse modo, um limite conservador padrão de 128.000 tokens é usado (não há ummodelid 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 usandoclient/apiKeyoulangchainModel.@langchain/coreé uma peerDependency opcional — instale-a (e a integração de chat model LangChain que você usar, ex:@langchain/openai) apenas se for usarlangchainModel. 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 à respostaO 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 enriquecimentoSemâ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) à mensagemEste 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.
onCalldispara uma vez, imediatamente antes de a chamada alcançar o modelo.onResultdispara uma vez, em caso de sucesso, comdurationMseusedFallback.onErrordispara uma vez, em caso de falha, comdurationMse umerrorTypeclassificando a falha como'model-unavailable','invalid-output','context-too-large','provider-error', ou'unknown'.- Exatamente um entre
onResult/onErrordispara por chamada. Todo evento carrega umcallIdcompartilhado entreonCall/onResult/onErrordaquela 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 decontext/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é sempreundefined— 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. usagesó 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
cachemanté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 umaPromise— 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,temperaturee, noGroundedExtractor, seusfields/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
Mapmanualmente). - Falha de forma segura: se
getousetdo 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/onResultcontinuam 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 cacheReleases
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-tagsO 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).
