Metadata-Version: 2.4
Name: chain-receipt-sdk
Version: 0.2.0
Summary: Chain-Receipt SDK — emit, verify, replay, and chain Receipts (v1.0.0) from LangChain, LlamaIndex, LangGraph, Pydantic AI, DSPy, or any Python callable.
Author-email: Mars Ausili <mars@cruxia.ai>
License-Expression: MIT
Project-URL: Homepage, https://github.com/Cruxia-AI/chain-receipt-sdk
Project-URL: Specification, https://github.com/Cruxia-AI/chain-receipt-core/blob/main/SCHEMA.md
Keywords: audit,replay-attestation,receipt,langchain,llamaindex,ed25519,agent-loop,llm-agent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: chain-receipt-core>=0.1.3
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == "langchain"
Provides-Extra: llamaindex
Requires-Dist: llama-index-core>=0.13; extra == "llamaindex"
Provides-Extra: langgraph
Requires-Dist: langgraph>=0.2; extra == "langgraph"
Requires-Dist: langchain-core>=0.3; extra == "langgraph"
Provides-Extra: pydantic-ai
Requires-Dist: pydantic-ai>=0.1; extra == "pydantic-ai"
Provides-Extra: dspy
Requires-Dist: dspy>=3; extra == "dspy"
Provides-Extra: http
Requires-Dist: httpx>=0.25; extra == "http"
Provides-Extra: all
Requires-Dist: langchain-core>=0.3; extra == "all"
Requires-Dist: llama-index-core>=0.13; extra == "all"
Requires-Dist: langgraph>=0.2; extra == "all"
Requires-Dist: pydantic-ai>=0.1; extra == "all"
Requires-Dist: dspy>=3; extra == "all"
Requires-Dist: httpx>=0.25; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"

# 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.

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

## 60-second demo

```python
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):

```python
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):

```python
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`.

```python
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:

```python
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:

```python
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.

```python
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

```bash
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`.
