Traces
If you haven't already, we recommend reading Spans first.
None of the tooling below requires LLMs. It is, however, very handy when working with them.
As mentioned in the section on nested spans, traces are really just collections of spans.
In the context of observability, a "trace" often refers to a record of events in a system's execution. Generally this means capturing the inputs, outputs, and additional metadata for each function or module in your system's execution flow.
Lilypad makes this simple and easy with the trace
decorator:
import lilypad
lilypad.configure()
@lilypad.trace()
def child(text: str) -> str:
return "Child Finished!"
@lilypad.trace()
def parent(text: str) -> str:
output = child("I'm the child!")
print(output)
return "Parent Finished!"
output = parent("I'm' the parent!")
print(output)
# > Child Finished!
# > Parent Finished!
Custom Name
By default, the trace
decorator will use the decorated function's name for the trace. Sometimes it makes more sense to use a custom (more readable) name:
import lilypad
lilypad.configure()
@lilypad.trace(name="Answer Question")
def answer_question(question: str) -> str:
return "The capital of France is Paris."
answer = answer_question("What is the capital of France?")
print(answer)
# > The capital of France is Paris.
Updating Trace Metadata
The trace
decorator captures information such as inputs/outputs by default, but often you'll want to log additional information or metadata as part of that function's span (and not a sub-span).
We've made this possible in a type-safe way with a special trace_ctx
reserved argument name.
import lilypad
lilypad.configure()
@lilypad.trace(name="Answer Question")
def answer_question(trace_ctx: lilypad.Span, question: str) -> str:
trace_ctx.log("I'm the span for Answer Question.")
return "The capital of France is Paris."
answer = answer_question("What is the capital of France?")
print(answer)
# > The capital of France is Paris.
If a trace
-decorated functions has trace_ctx: lilypad.Span
as it's first argument, the decorator will inject the span into the argument so that you can access it directly inside the function.
The resulting decorated function's call signature will then be updated such that trace_ctx
is excluded (since trace_ctx
will be supplied by the decorator, not the user).
Above, answer_question
only expects question
as an input — and your editor knows this.
Tracing LLM Calls
Since LLMs are non-deterministic, we recommend versioning any functions that use them.
Since Lilypad can create spans for LLM API calls automatically, simple calling the API inside of a trace
-decorated function will nest that span inside of the parent function:
from google.genai import Client
import lilypad
lilypad.configure(auto_llm=True)
client = Client()
@lilypad.trace(name="Answer Question")
def answer_question(question: str) -> str | None:
response = client.models.generate_content(
model="gemini-2.0-flash-001",
contents=f"Answer this question: {question}",
)
return response.text
response = answer_question("What is the capital of France?")
print(response)