lilac-sdk
Python SDK for Lilac — agent observability. Capture LLM exchanges with one line, no wrapping of your provider call, no added latency.
Install
pip install lilac-sdk
Quickstart
import lilac
lilac.init(api_key="sk_live_...", agent_id="agt_yourAgentId")
# Self-hosted: set LILAC_ENDPOINT (and LILAC_DEPLOYMENT_MODE=self_hosted) in
# your environment instead of passing endpoint= here. See "Endpoint
# resolution" below.
import lilac
from openai import OpenAI, APIError, RateLimitError
client = OpenAI()
lilac.init(api_key="sk_live_...", agent_id="agt_yourAgentId")
def handle_user_message(conversation_id: str, user_text: str) -> str:
call_input = [{"role": "user", "content": user_text}]
try:
response = client.responses.create(model="gpt-5.5", input=call_input)
except (APIError, RateLimitError) as e:
# The call still happened and still belongs to this conversation —
# record it as a failed exchange rather than letting it vanish.
lilac.capture_failure(input=call_input, conversation_id=conversation_id, error=str(e))
raise
lilac.capture(response, input=call_input, conversation_id=conversation_id)
return response.output_text
That's the whole integration for a single exchange. capture() reads
model/token usage off the real OpenAI/Anthropic response object
automatically — you never set those by hand.
Endpoint resolution
lilac.init() resolves where to send data by checking these in order:
- Explicit
endpoint=passed toinit() LILAC_ENDPOINTenvironment variable — the normal path, set once per deployment in your own application's environment- Default
https://api.trylilac.ai/v1— used automatically only for the Online/hosted product (or when deployment mode is unset)
Self-hosted deployments fail closed, not open. Set
LILAC_DEPLOYMENT_MODE=self_hosted alongside LILAC_ENDPOINT (or pass
deployment="self_hosted" to init()). With this set, if LILAC_ENDPOINT
is missing or empty for any reason, init() raises immediately at startup
instead of silently falling back to the public api.trylilac.ai endpoint.
This matters most for government/compliant-channel deployments, where
telemetry reaching the public endpoint even once is a real compliance
failure, not just a misconfiguration.
# Self-hosted
export LILAC_DEPLOYMENT_MODE=self_hosted
export LILAC_ENDPOINT=https://your-lilac-host/v1
# Online — omit both; the SDK defaults to the hosted endpoint
capture()
lilac.capture(
response,
input=call_input,
conversation_id=conversation_id,
user_id=current_user.id, # hashed at ingest, never stored in the clear
system_prompt=SYSTEM_PROMPT, # feeds judge-tier grounding when set
output_type="answer", # answer | handoff | action | refusal | clarification
correlation_ref=order_id, # machine-to-machine join key
tags={"plan": "enterprise", "region": "us-east"},
)
| Parameter | Required | Notes |
|---|---|---|
input |
required | The same input/messages value you passed to the provider call — this is the only source of the user's turn; the response object doesn't contain it. |
conversation_id |
required (or via context manager, below) | Groups this event with the rest of its conversation. |
user_id |
optional | Enables cross-session/return-rate analysis; hashed at ingest. |
system_prompt |
optional | Feeds judge-tier grounding when set. |
output_type |
optional | answer, handoff, action, refusal, or clarification. |
correlation_ref |
optional | Machine-to-machine join key for linking this session to another. |
tags |
optional | Any flat dict of string keys/values — segmentation (plan tier, region, experiment arm, etc). |
capture_failure()
The except-block counterpart to capture() — records a failed exchange so
provider-side outages/rate limits don't silently vanish from your session
data. Optional, but recommended for any agent running at meaningful volume.
try:
response = client.responses.create(model="gpt-5.5", input=call_input)
except (APIError, RateLimitError) as e:
lilac.capture_failure(input=call_input, conversation_id=conversation_id, error=str(e))
raise
Streaming
call_input = [{"role": "user", "content": user_text}]
stream = client.responses.create(model="gpt-5.5", input=call_input, stream=True)
for chunk in lilac.capture_stream(stream, input=call_input, conversation_id=conversation_id):
yield chunk # your consumption of the stream is unchanged
Multi-turn conversations — the context manager
If a single conversation spans several separate capture() calls (a
multi-turn chat, or a call several function calls deep inside a tool
dispatcher), with lilac.conversation(id): sets conversation_id once and
every nested capture() call underneath inherits it automatically — a real
contextvars value,
propagating through async/await and nested calls with no explicit
threading.
with lilac.conversation(conversation_id):
previous_id = None
for user_text in incoming_messages():
response = client.responses.create(
model="gpt-5.5", previous_response_id=previous_id, input=user_text,
)
lilac.capture(response, input=user_text) # conversation_id inherited
previous_id = response.id
lilac.set_conversation(id) is the bare-setter equivalent for places a
context manager doesn't fit cleanly (middleware, framework hooks, background
workers).
Precedence, never silent: explicit conversation_id= argument > context
variable > raise. If neither is set, Lilac never guesses — it raises
LilacUsageError so the gap is visible during development.
Drop-in wrapper
For bespoke/custom agent loops with call sites scattered across a large codebase — swap your provider import, every call through that client is captured automatically:
# Before
from openai import OpenAI
client = OpenAI()
# After
from lilac.openai import OpenAI
client = OpenAI(lilac_api_key="sk_live_...", agent_id="agt_yourAgentId")
# Every call below is captured automatically — no lilac.capture() needed
with lilac.conversation(conversation_id):
response = client.responses.create(model="gpt-5.5", input=[...])
Trade-off worth knowing: this wrapper owns the client instance, so it's the
right fit for a single-vendor integration but doesn't compose as cleanly if
another tool is also wrapping the same client. Use capture() instead if
you're layering Lilac alongside another observability tool on the same call
sites.
Callback function
from lilac.integrations.langchain import LilacCallbackHandler
agent_executor.invoke(
{"input": user_text},
config={"callbacks": [LilacCallbackHandler(conversation_id=conversation_id)]},
)
Requires the langchain extra: pip install lilac-sdk[langchain].
The same pattern applies to any framework exposing a callback/hook interface around its LLM calls (LlamaIndex, CrewAI, Semantic Kernel, and similar) — the handler implementation differs per framework's callback interface, but this SDK currently ships only the LangChain handler; others are on the roadmap.
Raw OTLP (non-Python stacks)
Send OpenTelemetry spans directly to the same endpoint every integration
method above uses under the hood: POST {endpoint}/ingest/otlp. See the
full API reference for the span attribute
contract.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lilac_sdk-0.1.0.tar.gz.
File metadata
- Download URL: lilac_sdk-0.1.0.tar.gz
- Upload date:
- Size: 18.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f8929b469561e55d917a6e2647a239bedc74193a35610787c66175bdc6e4aaab
|
|
| MD5 |
f59150216c5f8562853623221ce28d76
|
|
| BLAKE2b-256 |
2542225c82cd23e1f0584adfb98beca5b84315d1430cebb53894880748aadf81
|
File details
Details for the file lilac_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: lilac_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23bb2de6a7fd0a8eff369e8329f6ee4e0c6d2a1cdced2ccb2423abe7457006c8
|
|
| MD5 |
9db013ad1b125f2ba2294289e2c85602
|
|
| BLAKE2b-256 |
10158e0b58325c138d3963a424f2aec01ba3b0df923e70a8f7b31296306055aa
|