Skip to main content

Chain-Receipt SDK — emit, verify, replay, and chain Receipts (v1.0.0) from LangChain, LlamaIndex, LangGraph, Pydantic AI, DSPy, or any Python callable.

Project description

chain-receipt-sdk

Emit, verify, replay, and chain Chain-Receipts (v1.0.0) from any Python LLM-agent loop. Drop-in callback for LangChain, plus a framework-agnostic ReceiptBuilder for raw API loops.

Verifiable record of every LLM call: emit a signed Receipt, replay it against any vendor, and check whether the chain holds.

pip install chain-receipt-sdk
chain-receipt verify sha256:<hash>
chain-receipt replay sha256:<hash> --n 10

60-second demo

from chain_receipt_sdk import ReceiptBuilder, ClientInfo, Interaction
from chain_receipt_core import compute_text_hash, compute_tool_calls_hash, generate_keypair

sk, pub = generate_keypair()
client = ClientInfo(
    name="my-agent",
    version="0.1.0",
    platform="python-3.12",
    emitter_pubkey=f"ed25519:{pub}",
)
b = ReceiptBuilder(client=client, private_key=sk, chain_seed=pub.encode())

inter = Interaction(
    vendor="anthropic",
    model="claude-sonnet-4-5",
    temperature=0.0,
    system_prompt_hash=compute_text_hash("you are helpful"),
    prompt_hash=compute_text_hash("hello"),
    response_hash=compute_text_hash("hi"),
    tool_calls_hash=compute_tool_calls_hash([]),
    n_tool_calls=0,
    latency_ms=120,
)
r = b.build(interaction=inter)
print(r.receipt_id, r.chain.sequence_number)

LangChain callback

The ReceiptCallback is a BaseCallbackHandler — pass it via config={"callbacks": [cb]} to any LangChain LLM, chat model, or LCEL chain. One Receipt is emitted per on_llm_end / on_chat_model_end event; sequential calls chain automatically.

Minimal working example (zero-cost; uses LangChain's built-in fake LLM):

from langchain_core.language_models.fake import FakeListLLM
from chain_receipt_sdk.callback import ReceiptCallback

cb = ReceiptCallback(
    client_name="my-langchain-agent",
    vendor="anthropic",
    model="claude-sonnet-4-5",
)
llm = FakeListLLM(responses=["the answer is 42"])
llm.invoke("what is 6 times 7?", config={"callbacks": [cb]})

print(f"emitted {len(cb.receipts)} Receipts")
print(cb.receipts[0].receipt_id, cb.receipts[0].chain.sequence_number)

Production example (real LLM; swap the fake for ChatAnthropic / ChatOpenAI / any chat model):

from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from chain_receipt_sdk.callback import ReceiptCallback

cb = ReceiptCallback(
    client_name="my-langchain-agent",
    vendor="anthropic",
    model="claude-sonnet-4-5",
    capture_payload=True,  # only set this if you control the data sensitivity
)
llm = ChatAnthropic(model="claude-sonnet-4-5", temperature=0)
prompt = ChatPromptTemplate.from_messages(
    [("system", "be terse"), ("human", "{q}")]
)
chain = prompt | llm | StrOutputParser()
chain.invoke({"q": "what is 6 times 7?"}, config={"callbacks": [cb]})

Note on RunnableLambda: wrapping a plain Python function via RunnableLambda does NOT fire on_llm_start / on_llm_end — those events only fire when an actual language-model class is invoked. If you need to attribute receipts to non-LLM steps, use ReceiptCallback.emit(...) directly.

LlamaIndex callback

Same shape, different framework. chain_receipt_sdk.llamaindex.ReceiptCallback is a BaseCallbackHandler for LlamaIndex's CallbackManager. One Receipt per CBEventType.LLM event; FUNCTION_CALL events between LLM events accumulate into the next emitted Receipt's tool_calls_hash.

from llama_index.core import Settings
from llama_index.core.callbacks import CallbackManager
from chain_receipt_sdk.llamaindex import ReceiptCallback

cb = ReceiptCallback(
    client_name="my-llamaindex-agent",
    vendor="anthropic",
    model="claude-sonnet-4-5",
    capture_payload=True,  # only if you control the data sensitivity
)
Settings.callback_manager = CallbackManager([cb])

# ... your normal LlamaIndex query / chat / agent pipeline ...
# Each LLM call emits one Receipt; multi-step pipelines emit one per step.

for r in cb.receipts:
    print(r.receipt_id, r.chain.sequence_number, r.interaction.tool_calls_hash)

Install: pip install chain-receipt-sdk[llamaindex] (or pin llama-index-core>=0.13 yourself).

LangGraph adapter

LangGraph is built on LangChain's callback infrastructure, so the LangChain ReceiptCallback works automatically when attached to a compiled graph via compiled.invoke(config={"callbacks": [cb]}). The chain_receipt_sdk.langgraph module adds a thin convenience helper attach_receipts() that pre-attaches the callback so you don't have to thread config={"callbacks": [...]} through every call site:

from langgraph.graph import StateGraph, START, END
from chain_receipt_sdk.langgraph import ReceiptCallback, attach_receipts

# ... build your graph as usual ...
compiled = graph.compile()

cb = ReceiptCallback(
    client_name="my-langgraph-agent",
    vendor="anthropic",
    model="claude-sonnet-4-5",
)
wrapped = attach_receipts(compiled, cb)

result = wrapped.invoke({"question": "hi"})
for r in wrapped.callback.receipts:
    print(r.receipt_id, r.chain.sequence_number)

attach_receipts() returns a wrapper that exposes the same invoke / ainvoke / stream / astream surface as the compiled graph and merges your callback into any user-supplied callbacks. Conditional routing emits Receipts only for the chosen branch (verified in tests).

Install: pip install chain-receipt-sdk[langgraph].

Pydantic AI adapter

Pydantic AI uses native OpenTelemetry instrumentation. Once Cruxia CV v0.1.0 ships with always-on gen_ai.* span emission, Pydantic AI's own OTel spans will be visible alongside Cruxia CV's chain-receipt spans in any OTel backend (Datadog / Honeycomb / Grafana / New Relic). That's the recommended production integration.

For direct Receipt emission without an OTel collector, chain_receipt_sdk.pydantic_ai ships a callback + Agent wrapper:

from pydantic_ai import Agent
from chain_receipt_sdk.pydantic_ai import ReceiptCallback, attach_receipts

agent = Agent("anthropic:claude-sonnet-4-5", system_prompt="be terse")

cb = ReceiptCallback(
    client_name="my-pydantic-ai-agent",
    vendor="anthropic",
    model="claude-sonnet-4-5",
)
wrapped = attach_receipts(agent, cb)

result = wrapped.run_sync("what is 6*7?")
for r in wrapped.callback.receipts:
    print(r.receipt_id, r.chain.sequence_number)

attach_receipts() wraps the Agent so every .run_sync() / .run() auto-emits a Receipt at completion. Alternatively, call cb.emit_from_result(result) manually after each agent.run_sync(...).

Note on instructions= vs system_prompt=: Pydantic AI's instructions= parameter is passed to the model but NOT included in the message history, so the adapter can't see it. Use system_prompt= if you want the system context captured in the Receipt's system_prompt_hash.

Install: pip install chain-receipt-sdk[pydantic-ai].

DSPy adapter

DSPy (≥3.0) exposes BaseCallback at dspy.utils.callback. Register via dspy.settings.configure(callbacks=[cb]) (or scoped dspy.context(callbacks=[cb])); DSPy fires on_lm_start / on_lm_end / on_tool_start / on_tool_end around every LM call across your Predict / ChainOfThought / ReAct / BootstrapFewShot pipeline.

import dspy
from chain_receipt_sdk.dspy import ReceiptCallback

cb = ReceiptCallback(
    client_name="my-dspy-program",
    vendor="anthropic",
    model="claude-sonnet-4-5",
)

# Scoped via context (preferred — doesn't mutate global settings)
with dspy.context(lm=dspy.LM("anthropic/claude-sonnet-4-5"), callbacks=[cb]):
    program = dspy.Predict("question -> answer")
    result = program(question="what is 6*7?")

for r in cb.receipts:
    print(r.receipt_id, r.chain.sequence_number)

Each LM call emits one Receipt; tool calls (via dspy.ReAct etc.) accumulate between LM events and fold into the next Receipt's tool_calls_hash.

Install: pip install chain-receipt-sdk[dspy].

CLI

chain-receipt status                       # local chain head + count
chain-receipt verify sha256:<hash>         # validate signature + chain link
chain-receipt replay sha256:<hash> --n 10  # re-run captured prompt N times
chain-receipt chain --since 2026-04-25     # list Receipts since timestamp
chain-receipt publish sha256:<hash>        # push to chain-determinism.org

Local chain storage

Receipts are written to ~/.chain-receipt/chain.jsonl (one JSON per line) when emitted via ReceiptCallback or ReceiptBuilder.build(persist=True). The chain_receipt_sdk.chain.LocalChain reader/writer is the public API for that file.

Replay determinism

chain_receipt_sdk.replay re-runs the captured prompt against the same vendor + model + temperature N times and reports final_answer_consistent, tool_seq_identical, tool_args_identical, and a Wilson 95% CI for the divergence rate. Integration test gate in tests/test_replay.py.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

chain_receipt_sdk-0.2.0.tar.gz (40.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

chain_receipt_sdk-0.2.0-py3-none-any.whl (33.4 kB view details)

Uploaded Python 3

File details

Details for the file chain_receipt_sdk-0.2.0.tar.gz.

File metadata

  • Download URL: chain_receipt_sdk-0.2.0.tar.gz
  • Upload date:
  • Size: 40.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.15

File hashes

Hashes for chain_receipt_sdk-0.2.0.tar.gz
Algorithm Hash digest
SHA256 2a05c75149559fde063b0cbc7f7a0c228269d96a2b37babc892a325424ab8c16
MD5 292a7f32fa0f167d7213971e5a1381d4
BLAKE2b-256 8ce2eb48f9794a4cb6d7d0f08ed9170b1b9e4e38158505a6fa4916b28973e728

See more details on using hashes here.

File details

Details for the file chain_receipt_sdk-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for chain_receipt_sdk-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e39ce24c8772bd572cf09c5125b2bc4d7019b802cf5e44044585d3047f48343e
MD5 1693a04c223a630208daa8c12de28fa1
BLAKE2b-256 a10ba508982611e699381d0cb404502bea305b746576af7a3adec7634d337f1f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page