Metadata-Version: 2.4
Name: pelennor
Version: 0.1.2
Summary: Send your AI agent's OpenTelemetry traces to Pelennor.
Project-URL: Homepage, https://pelennor.ai
Author: Pelennor
License-Expression: MIT
License-File: LICENSE
Keywords: agents,llm,observability,opentelemetry,otel,tracing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.29
Requires-Dist: opentelemetry-sdk>=1.29
Provides-Extra: anthropic
Requires-Dist: openinference-instrumentation-anthropic>=0.1; extra == 'anthropic'
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Provides-Extra: langchain
Requires-Dist: openinference-instrumentation-langchain>=0.1; extra == 'langchain'
Provides-Extra: llama-index
Requires-Dist: openinference-instrumentation-llama-index>=0.1; extra == 'llama-index'
Provides-Extra: openai
Requires-Dist: openinference-instrumentation-openai>=0.1; extra == 'openai'
Description-Content-Type: text/markdown

# pelennor

Send your AI agent's [OpenTelemetry](https://opentelemetry.io/) traces to
[Pelennor](https://pelennor.ai), where they become **conversations** you can
read, search, and turn into evals.

```bash
pip install "pelennor[langchain]"
```

```python
import pelennor

pelennor.init(api_key="plk_...")          # the key from your agent's Lens page

with pelennor.conversation(id=thread_id, user="cust_123"):
    agent.invoke({"messages": [("user", message)]})
```

That's the integration. Every model call, tool call and span your agent produces
inside the `conversation` block is exported and stitched into one conversation
keyed by `id`.

`init()` reads `PELENNOR_API_KEY` from the environment when you don't pass it, so
nothing but the key need live in code.

## Documentation

| | |
|---|---|
| **[LangChain & LangGraph](https://github.com/dotslashpwn/pelennor/blob/main/pelennor-sdk/docs/langchain.md)** | Full setup for the stack most people are on. Start here. |
| **[Conversations](https://github.com/dotslashpwn/pelennor/blob/main/pelennor-sdk/docs/conversations.md)** | How turns get stitched, and what to use as an id. |
| **[Privacy and content capture](https://github.com/dotslashpwn/pelennor/blob/main/pelennor-sdk/docs/privacy.md)** | Exactly what leaves your process, and how to send less. |
| **[Manual instrumentation](https://github.com/dotslashpwn/pelennor/blob/main/pelennor-sdk/docs/manual.md)** | For a framework nothing instruments. |
| **[Reference](https://github.com/dotslashpwn/pelennor/blob/main/pelennor-sdk/docs/reference.md)** | Every argument, every environment variable. |
| **[Troubleshooting](https://github.com/dotslashpwn/pelennor/blob/main/pelennor-sdk/docs/troubleshooting.md)** | "I see no traces." Start at the top. |

## How it works

**We don't write your spans.** Your framework already emits them through an
*instrumentor* — a library that hooks its callbacks. `init()` switches on
whichever ones you have installed and gets out of the way. An instrumentor sees
the model's real message list, real token counts and real exceptions;
hand-written spans can only approximate all three.

**We add the one thing OpenTelemetry lacks: a conversation.** OTel's largest
unit is a trace, which is one request — turn 2 of a chat arrives as a completely
unrelated trace. `pelennor.conversation(id=...)` stamps a shared
`gen_ai.conversation.id` on every span started inside it, so the turns join back
up. It's a `ContextVar`, so it follows your code across `await` points and into
worker threads, which is where agent frameworks do their work.

**We export a copy, we don't take over.** If your process already has a
`TracerProvider`, we attach to it — your exporters keep working and a copy comes
to us. We never replace it; OpenTelemetry's global provider is set-once, so
replacing it would silently do nothing.

## Auto-instrumentation

```bash
pip install "pelennor[langchain]"     # LangChain and LangGraph
pip install "pelennor[openai]"        # OpenAI
pip install "pelennor[anthropic]"     # Anthropic
pip install "pelennor[llama-index]"   # LlamaIndex
```

The core install is deliberately thin — the OTel SDK and an OTLP/HTTP exporter —
so it isn't tied to one instrumentation vendor. `init()` activates whichever
instrumentors are present, whether they came from an extra or from your own
dependencies.

## Options

```python
pelennor.init(
    api_key="plk_...",
    endpoint="https://self-hosted",  # or PELENNOR_ENDPOINT; defaults to the cloud
    service_name="support-bot",      # shows up on your traces
    capture_content=False,           # send metadata only, never message text
    instrument=True,                 # auto-activate installed instrumentors
)
```

`capture_content=False` sets every content switch the installed instrumentors
offer, before any of them start, so message text is never put on a span in the
first place — nothing is stripped later in the pipeline. Model names, token
counts, tool identity, timings and errors still come through. Two things it does
**not** cover: retrieved documents (no instrumentor offers a switch for RAG
passage text) and spans you write yourself. The
[privacy guide](https://github.com/dotslashpwn/pelennor/blob/main/pelennor-sdk/docs/privacy.md)
has the full list.

Every variable is applied with `setdefault`, so anything already set in your
environment wins over ours.

## You don't need this SDK

Pelennor's ingest speaks OTLP/HTTP and reads eight tracing dialects —
OpenTelemetry GenAI (current, event-era and legacy), OpenInference,
OpenLLMetry, Traceloop, Langfuse and the Vercel AI SDK. Point an exporter you
already run at `https://api.pelennor.ai/ingest/v1/traces` with an
`Authorization: Bearer` header and set a conversation id yourself.

The SDK exists to make the common case one line, not to be a toll gate.

## It will never break your agent

`init()` never raises. A missing key, an unreachable endpoint or a broken
instrumentor each degrade to "no telemetry" and a log line, never an exception in
your request path. Telemetry is not allowed to take down the thing it observes.

## License

MIT
