Integration overview
Every integration surface does the same thing underneath: it assembles a trace — (query, context, response) — and sends it to Veralith, which grades it asynchronously. They differ only in how they pull those three values out of your code.
The SDK contract
Adding Veralith to a production pipeline is low-risk by design. Every surface makes the same three guarantees:
- Fail-safe — any error inside Veralith is caught and warned; your RAG keeps serving. Instrumentation can't take down the thing it instruments.
- Fire-and-forget — calls return the instant the trace is queued; grading runs asynchronously. Zero added latency in your request path.
- No-op without a key — with no
VERALITH_API_KEYset, the SDK warns once and does nothing — safe to ship to every environment.
The single deliberate exception to fail-safe is a hard budget cap, which raises BudgetExceeded — and even that is opt-out (enforce_budget=False). Evaluation is hosted-mode only, so sync=True is not supported.
1. veralith.log(...) — works with any code
The explicit one-liner. Works with any stack or code shape, and gives you the most control.
import veralith
veralith.log(
query="What is the Rule of 72?",
context=chunks, # list[str] | list[dict] | list[ContextChunk]
response=answer,
latency_ms=elapsed, # optional — shown on the trace
)Use it anywhere the three values are in scope: any framework, class methods, notebooks, web handlers, or streaming (call it after you've assembled the final response). It's the universal fallback.
2. @veralith.trace — for RAG functions
Decorate the function that receives the query and returns the answer. The wrapped function still returns just response to callers, so your call sites don't change.
import veralith
@veralith.trace
def rag(query):
chunks = retriever(query)
response = generator(query, chunks)
return response, chunks # (response, context) tuple
# If your function returns only a string, hand over context explicitly:
@veralith.trace(query_arg="user_question")
def rag(user_question):
...
return veralith.TraceReturn(response=answer, context=chunks)async def is supported. The query is taken from the first positional argument by default, or from the parameter you name with query_arg=. Latency is measured for you.
3. LangChain adapter — zero-code auto-tracing
import veralith.adapters.langchain as adapter
adapter.install() # once, at startup
# every RetrievalQA / RetrievalQAWithSourcesChain .invoke() now auto-logsIt patches those chains at the class level and extracts the query, answer, and source_documents automatically. If a version returns an unexpected shape it warns and passes the result through untouched — your chain never breaks.
Warning: the adapter only covers
RetrievalQAandRetrievalQAWithSourcesChain. LCEL / customRunnablechains, LlamaIndex, and agents are not auto-traced — uselog()at the point where the answer is assembled.
Which surface for which code shape
The decorator needs a single function where the query is an argument and (response, context) is the return. If your code doesn't have that chokepoint — the common "messy codebase" case — use log().
| Your code | Use |
|---|---|
Function returns (response, chunks) | @veralith.trace |
| Function returns only a string | @veralith.trace + TraceReturn |
| Class pipeline (retrieve + generate methods) | @trace on the entry method, or log() inside |
| Web handler returning a dict/Response | log() before returning |
| Streaming (yields tokens) | log() after the stream completes |
LangChain RetrievalQA | adapter.install() |
| LangChain LCEL / LlamaIndex / agents | log() at assembly |
The rule for any codebase: find the single point where
query, the retrievedcontext, and the finalresponseall exist together, and putlog()there. If they never coexist in one scope, thread one value to where the other two are.