Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
wheel-test/bin/python -c
"import asyncio, importlib.metadata;
from trimwise import ContextSource, ContextSourceResult, ContextTrimResult, Trimmer;
assert importlib.metadata.version('trimwise') == '0.6.0';
assert importlib.metadata.version('trimwise') == '0.7.0';
sources = [ContextSource('a', '<s>', '</s>')];
sync_result = Trimmer().trim_context(sources, 8, unit='characters');
async_result = asyncio.run(Trimmer().atrim_context(sources, 8, unit='characters'));
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ Unlike token-pruning compressors, it keeps readable source pieces; see the
[research comparison](https://trimwise.readthedocs.io/en/latest/research-foundations/#how-trimwise-compares-with-model-based-compression)
for the tradeoff.

## Observe trimming in your application

Trimwise creates OpenTelemetry spans for its public operations when your application configures
an OpenTelemetry SDK. Without an SDK provider, the API stays a no-op and sends nothing. Your
application keeps control of exporters, collector endpoints, credentials, resources, sampling,
batching, and shutdown; none of those settings are added to `TrimConfig`.

See [Observability](https://trimwise.readthedocs.io/en/latest/observability/) for OTLP/gRPC and
OTLP/HTTP setup, span names and attributes, trace parenting, and privacy guarantees.

A tight limit can leave out evidence needed to answer a question. The limit applies to
Trimwise's returned text, not your entire prompt, so leave room for instructions and the
model's answer. Read the [guarantees and limitations](https://trimwise.readthedocs.io/en/latest/guarantees-and-limitations/)
Expand Down
5 changes: 5 additions & 0 deletions docs/configuration-and-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,10 @@ Most applications should keep the defaults. Configuration changes affect every t
through that `Trimmer`; per-call choices such as strategy, query, limit, unit, and custom token
counter remain method arguments.

OpenTelemetry exporters, endpoints, credentials, resources, and sampling are application runtime
settings, so they are intentionally absent from `TrimConfig`. See [Observability](observability.md)
for optional tracing setup and the emitted span contract.

### Configuration validation

| Setting | Accepted boundary |
Expand Down Expand Up @@ -667,3 +671,4 @@ style, MMR balance, and the managed semantic backend.
- Follow the [Getting Started guide](getting-started.md).
- Compare ranking behavior in [Choosing a Strategy](strategies.md).
- Configure embeddings with [Semantic Models and Async Usage](semantic-and-async.md).
- Add optional tracing with [Observability](observability.md).
1 change: 1 addition & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,4 +436,5 @@ not factual truth.
- Trim several sources with [one shared limit](multi-source-context.md).
- Review the current [strategy guide](https://github.com/tenwritehq/trimwise#which-strategy-should-i-use).
- Learn about [embedding callbacks and FastEmbed](https://github.com/tenwritehq/trimwise#semantic-models).
- Add Trimwise to application traces with [OpenTelemetry](observability.md).
- See the [public package on PyPI](https://pypi.org/project/trimwise/).
9 changes: 9 additions & 0 deletions docs/guarantees-and-limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ callback is awaited on the calling event loop.
The underlying synchronous counter, callback, or FastEmbed inference may continue after the
awaiting task is cancelled.

### Built-in tracing excludes source content

Optional OpenTelemetry spans contain bounded operation metadata such as strategy, unit, counts,
and source or batch size. Trimwise does not attach source text, output text, queries, context
wrappers, exception messages, or stack traces. The application still owns any attributes added to
parent spans or spans created inside callbacks. See [Observability](observability.md) for the exact
attribute contract.

## Best-effort goals

### Structural mode aims for document-wide coverage
Expand Down Expand Up @@ -439,4 +447,5 @@ starting point; representative downstream evaluation decides whether they work f
- Inspect the pipeline in [How Trimwise Works](how-it-works.md).
- Configure backends in [Semantic Models and Async Usage](semantic-and-async.md).
- Review the public contract in [Configuration and API Reference](configuration-and-api.md).
- Add optional tracing with [Observability](observability.md).
- Track deferred work in the [roadmap](https://github.com/tenwritehq/trimwise/blob/main/ROADMAP.md).
208 changes: 208 additions & 0 deletions docs/observability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
title: Observe Trimwise with OpenTelemetry
description: Add Trimwise spans to your application's existing traces without giving the library control of exporters, endpoints, or credentials.
---

# Observe Trimwise with OpenTelemetry

Trimwise can add its work to your application's distributed traces. Tracing is optional: if your
application does not configure an OpenTelemetry SDK, the installed OpenTelemetry API is a no-op
and Trimwise sends nothing over the network.

Trimwise creates spans only. Your application chooses whether to collect them and owns the SDK,
exporter, collector endpoint, authentication headers, credentials, TLS, resources, sampling,
batching, flushing, and shutdown. These operational settings do not belong in `TrimConfig`, which
remains limited to trimming behavior. This follows the
[OpenTelemetry guidance for instrumented libraries](https://opentelemetry.io/docs/specs/otel/library-guidelines/):
libraries use the API, while the final application configures the SDK and exporters.

## What you get

Each public operation creates one span:

| Call | Span name |
| --- | --- |
| `trim()` and `atrim()` | `trimwise.trim` |
| `trim_context()` and `atrim_context()` | `trimwise.trim_context` |
| `atrim_many()` | `trimwise.trim_many` |

Sync and async forms use the same names so a dashboard does not need separate queries. A batch
creates one span for the whole call rather than one span per input.

When your application already has a current span, the Trimwise span becomes its child. This works
through the worker threads used by `atrim()` and `atrim_context()`, and through an awaited async
embedding callback.

Successful single-source and context spans use these attributes:

| Attribute | Meaning |
| --- | --- |
| `trimwise.strategy.requested` | Valid strategy passed by the caller, including `auto` |
| `trimwise.strategy.resolved` | Strategy actually used after resolving `auto` |
| `trimwise.unit` | `tokens`, `words`, or `characters` |
| `trimwise.limit` | Requested output ceiling |
| `trimwise.input.count` | Measured input size in `trimwise.unit` |
| `trimwise.output.count` | Measured output size in `trimwise.unit` |
| `trimwise.trimmed` | Whether any source text was removed |
| `trimwise.source.count` | Number of input sources for a context call |

A successful `trimwise.trim_many` span records `trimwise.batch.size` and whether any item was
trimmed. Batch inputs may use different units, so Trimwise does not add misleading aggregate input
or output counts.

Failed and cancelled calls set the span status to error and add `error.type`, such as
`builtins.ValueError`. Trimwise does not attach the exception message or stack trace.

## Send traces with OTLP/gRPC

Install the SDK and the gRPC exporter in your **application**:

```bash
python -m pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
```

Configure OpenTelemetry once when your application starts:

```python
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor


provider = TracerProvider(
resource=Resource.create(
{
"service.name": "answer-api",
"deployment.environment.name": "production",
}
)
)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)
```

The exporter reads `OTEL_EXPORTER_OTLP_ENDPOINT`. Use your collector's TLS and authentication
settings in production. Call `provider.shutdown()` from your application's shutdown hook so the
batch processor can flush pending spans.

## Send traces with OTLP/HTTP

Use the HTTP exporter when your collector accepts OTLP over HTTP/protobuf:

```bash
python -m pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
```

The provider setup is the same except for the exporter import:

```python
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor


provider = TracerProvider(resource=Resource.create({"service.name": "answer-api"}))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)
```

With the base endpoint above, the HTTP exporter sends traces to `/v1/traces`. Call
`provider.shutdown()` when the application stops.

## See Trimwise inside an application trace

Once the provider is configured, use Trimwise normally:

```python
from opentelemetry import trace
from trimwise import Trimmer


app_tracer = trace.get_tracer("answer-api")
trimmer = Trimmer()

with app_tracer.start_as_current_span("answer.generate") as span:
span.set_attribute("app.plan", "standard")
result = trimmer.trim(
document,
limit=500,
strategy="lexical",
query="What caused the outage?",
)
```

The trace has this shape:

```text
answer.generate
└── trimwise.trim
```

Put application-specific fields on your `Resource` or parent span as shown above. That keeps
deployment and tenant metadata under application control. Trimwise does not accept an unrestricted
custom-attribute dictionary.

## Privacy and cardinality

Trimwise records operation metadata, not content. Its spans never include:

- Input or output text.
- Queries.
- Context prefixes, suffixes, or separators.
- Embedding passages or vectors.
- Exception messages or stack traces.
- Collector endpoints, headers, credentials, or other configuration.

Do not put source text, queries, user IDs, request IDs, or other high-cardinality or sensitive
values on parent spans unless your own telemetry policy permits them. OpenTelemetry context flows
into embedding callbacks, so spans created by your callback can become descendants of the
Trimwise span; the callback remains responsible for its own attributes and privacy controls.

## What Trimwise does not configure

Trimwise depends only on `opentelemetry-api`. It does not install or configure:

- An OpenTelemetry SDK.
- OTLP/gRPC or OTLP/HTTP exporters.
- A collector or observability backend.
- Sampling, queues, retries, or batch limits.
- Metrics or log export.
- Global propagators or resource attributes.

Your collector can derive request counts, error rates, and duration histograms from spans if its
span-metrics connector is enabled. That is a collector choice rather than a Trimwise runtime
feature.

## Turn tracing off

Do not configure an OpenTelemetry SDK, or configure your application's sampler to drop these
spans. No Trimwise flag is required. The OpenTelemetry API safely returns no-op spans when no SDK
provider is installed.

## Troubleshooting

**No Trimwise spans appear:** verify that the SDK is configured before the first trim call, the
exporter package matches your collector protocol, and the exporter endpoint uses port `4317` for
gRPC or `4318` for HTTP by convention.

**Spans appear under the wrong service:** set `service.name` on the application's `Resource`.
Trimwise deliberately does not choose a service name.

**The process exits before spans arrive:** call `provider.shutdown()` during application shutdown.
Do not call it after every trim.

**You expected metrics or logs:** Trimwise emits trace spans only. Configure application logging
and collector-derived span metrics separately.

## Continue exploring

- Follow the [Getting Started guide](getting-started.md).
- Review every call and result in [Configuration and API Reference](configuration-and-api.md).
- Understand worker threads and callbacks in [Semantic Models and Async Usage](semantic-and-async.md).
- Read the [guarantees and limitations](guarantees-and-limitations.md).
6 changes: 6 additions & 0 deletions docs/semantic-and-async.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,11 @@ For CPU-only structural or lexical work, async calls can overlap at the worker-t
FastEmbed, calls sharing one `Trimmer` still wait on that instance's model lock. `atrim_many()`
does not add cross-call background batching or make parallel CPU inference requests.

When the application configures OpenTelemetry, the public operation span remains current across
these worker-thread and async-callback boundaries. Spans created inside an embedding callback can
therefore appear below the Trimwise span. See [Observability](observability.md) for setup, span
names, and the privacy contract.

## Cancellation

Cancellation behavior depends on what `atrim()`, `atrim_context()`, or `atrim_many()` is awaiting:
Expand Down Expand Up @@ -469,3 +474,4 @@ compression ratios.
- Follow the [Getting Started guide](getting-started.md).
- Compare all selection modes in [Choosing a Strategy](strategies.md).
- Review planned semantic quality work in the [roadmap](https://github.com/tenwritehq/trimwise/blob/main/ROADMAP.md).
- Add optional tracing with [Observability](observability.md).
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ nav:
- How it works: how-it-works.md
- Strategies: strategies.md
- Semantic models and async: semantic-and-async.md
- Observability: observability.md
- Configuration and API: configuration-and-api.md
- Guarantees and limitations: guarantees-and-limitations.md
- Research foundations: research-foundations.md
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"

[project]
name = "trimwise"
version = "0.6.0"
version = "0.7.0"
description = "High-signal text trimming for better LLM prompts."
readme = "README.md"
requires-python = ">=3.10"
Expand Down Expand Up @@ -32,6 +32,7 @@ classifiers = [
dependencies = [
"markdown-it-py>=4.2,<5",
"numpy>=1.26,<3",
"opentelemetry-api>=1.44,<2",
"tiktoken>=0.13,<1",
]

Expand All @@ -41,6 +42,7 @@ semantic-gpu = ["fastembed-gpu>=0.8,<1"]
dev = [
"build>=1.5,<2",
"mypy>=2.3,<3",
"opentelemetry-sdk>=1.44,<2",
"pytest>=9.1,<10",
"pytest-asyncio>=1.4,<2",
"pytest-cov>=7.1,<8",
Expand Down
Loading
Loading