Trace ingestion client for Torina — captures LLM agent traces and forwards them over HTTP.
Project description
torina
Captures LLM agent traces (currently: LangChain) and forwards them to a
Torina HTTP ingestion endpoint via a single POST per trace, authenticated
with an x-api-key header. Not built on OpenTelemetry — it patches
LangChain's own callback system directly, so the only dependency is
wrapt.
Install
uv add torina
# or
pip install torina
Usage
import os
import torina
torina.auto_instrument()
torina.init_logger(
api_key=os.environ["TORINA_API_KEY"],
project="My Project (Python)",
endpoint="https://ingest.torina.example/v1/traces", # or set TORINA_ENDPOINT
)
auto_instrument() patches LangChain automatically if it's installed, so
every agent.invoke()/.stream() call anywhere in your app gets captured
with no per-call-site changes. A trace is uploaded whole — one HTTP request
per completed trace (every llm/tool span it contains, plus any chain
span that carries real data — empty structural wrapper chains, like
LangGraph's internal per-step nodes, are pruned), not one request per span.
Every trace carries payload["source"] ("langchain" today — the only
integration so far) so traces from different sources stay distinguishable as
more get added later.
A few things get deduplicated rather than taken at face value. Tool schemas
and the system prompt are both usually identical across every LLM call in a
trace — they're a property of the agent's definition, not the individual
call, but get resent on every call because that's how chat APIs work. Both
get hoisted to payload["tool_definitions"] / payload["system_prompt"]
when they match across every llm span — left per-span only if a trace
genuinely varied them mid-conversation.
Spans don't carry parent_span_id — there's no span tree. Relationships that
matter are expressed directly instead: a tool span's tool_call_id matches
the id in the requesting llm span's output_message.tool_calls, which is
more precise than a parent/child edge would be anyway (LangChain's own run
tree only reflects LangGraph's internal step wrappers, not which llm call
actually caused which tool call).
Conversation grouping, the agent's name, and arbitrary metadata all come from
LangChain's own config/create_agent(name=...), no Torina-specific code
needed:
agent = create_agent(..., name="weather-agent") # -> payload["agent_name"]
agent.invoke(
{"messages": [...]},
config={
"configurable": {"thread_id": thread_id}, # -> payload["conversation_id"]
"metadata": {"user_id": "u_123", "plan": "enterprise"}, # -> payload["metadata"]
},
)
For manual control over a specific call site instead of the global patch,
TorinaCallbackHandler is available directly (requires langchain-core):
from torina.langchain import TorinaCallbackHandler
agent.invoke({"messages": [...]}, config={"callbacks": [TorinaCallbackHandler()]})
dry_run
torina.init_logger(dry_run=True) # no api_key or endpoint needed
Traces are logged instead of sent — useful for local testing before you have
a real endpoint. Same code path either way; dry_run only swaps the network
call for a log line.
Development
This project uses uv.
uv sync
uv run pytest
uv run ruff check .
Releasing
Publishing to PyPI happens via .github/workflows/publish.yml,
triggered by a GitHub release. It uses trusted publishing
(OIDC), so no PyPI API token is stored in the repo.
- Bump
versioninpyproject.toml. - Commit and push to
main. - Tag and create a release:
git tag vX.Y.Z git push origin vX.Y.Z gh release create vX.Y.Z --title vX.Y.Z --generate-notes
- Publishing the release triggers the workflow, which runs
uv buildanduv publish. Check the Actions tab for the run, then confirm at pypi.org/project/torina.
Project details
Release history Release notifications | RSS feed
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 torina-0.1.1.tar.gz.
File metadata
- Download URL: torina-0.1.1.tar.gz
- Upload date:
- Size: 8.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
939ad20ee187a5b1f87ac67361f4121573636980d507c233718d874fedec7a5d
|
|
| MD5 |
4c1e9c36a08725353bffccf3d14f3270
|
|
| BLAKE2b-256 |
a9c41e09d3f940b43290aafadf39bf5ad7223d1e2e544fcd6a135749ba8eb47e
|
File details
Details for the file torina-0.1.1-py3-none-any.whl.
File metadata
- Download URL: torina-0.1.1-py3-none-any.whl
- Upload date:
- Size: 10.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
edb0afb8b4a77d6968e811c835bf4857d560bd4ac2376bbc1823a9c3d3df1bb0
|
|
| MD5 |
0a455346b604fe44219b585039cc8eae
|
|
| BLAKE2b-256 |
c4be216647628c198d94fc89ce25ccd47bca5b0942b7f12bc3ec2e75d5167d2c
|