Metadata-Version: 2.4
Name: traceAI-exa
Version: 0.1.0
Summary: OpenTelemetry instrumentation for Exa
License: Apache-2.0
Keywords: traceai,futureagi,opentelemetry,tracing,observability,llm,exa
Author: Future AGI
Author-email: no-reply@futureagi.com
Requires-Python: >=3.10,<3.14
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: OpenTelemetry
Classifier: Framework :: OpenTelemetry :: Instrumentations
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Monitoring
Requires-Dist: exa-py (>=2.25.0,<3)
Requires-Dist: fi-instrumentation-otel (>=1.1.0)
Requires-Dist: wrapt (>=1.0.0)
Project-URL: Changelog, https://github.com/future-agi/traceAI/blob/dev/python/frameworks/exa/CHANGELOG.md
Project-URL: Documentation, https://docs.futureagi.com/docs/tracing
Project-URL: Homepage, https://futureagi.com
Project-URL: Issues, https://github.com/future-agi/traceAI/issues
Project-URL: Repository, https://github.com/future-agi/traceAI/tree/dev/python/frameworks/exa
Description-Content-Type: text/markdown

# traceAI-exa

OpenTelemetry instrumentation for the [`exa-py`](https://github.com/exa-labs/exa-py) client.

This package wraps Exa client calls; it is not an Exa exporter. Exa does not publish an exporter.

## Installation

```bash
pip install traceAI-exa
```

It accepts `exa-py>=2.25.0,<3` and is tested with `exa-py` 2.25.0 on Python
3.10, 3.11, 3.12 and 3.13. The test suite also passes on `exa-py` 2.26.0.

Live vendor verification (2026-10-17): a real run against `api.exa.ai` with
`exa-py` 2.25.0 (`search`, `get_contents`, `answer`, both streams, `AsyncExa.search`
and an invalid-key 401 recorded as ERROR) exported the expected spans to Future
AGI, and `FI_HIDE_INPUTS=true` kept the query out of every stored column.

## Usage

```python
from exa_py import Exa
from fi_instrumentation import register
from fi_instrumentation.fi_types import ProjectType
from traceai_exa import ExaInstrumentor

tracer_provider = register(project_name="exa-search", project_type=ProjectType.OBSERVE)
ExaInstrumentor().instrument(tracer_provider=tracer_provider)

client = Exa(api_key="your-exa-api-key")
client.search("recent retrieval research", num_results=5)
```

## Instrumented calls

`Exa` and `AsyncExa` calls to `search`, `get_contents`, `answer`, and the
deprecated `search_and_contents` alias (traced as `exa.search`) each produce
one span. `stream_search` and `stream_answer` produce one span that stays open
until the stream is fully read, closed, or garbage-collected; the returned
object is still an instance of the vendor's stream class. A stream still open
when the interpreter exits is ended as cancelled before `register()`'s exit
hook shuts the exporter down (when `instrument()` runs after `register()`).

`gen_ai.span.kind` is `RETRIEVER` for `search`, `answer` and the two streams,
and `TOOL` for `get_contents`, which fetches the pages you name rather than
searching. Each span is a child of the active span
when there is one, and a root span otherwise. The span is current while
exa-py sends the request, so a span from an HTTP client instrumentation nests
under it.

## Span attributes

| Attribute | Calls | Value |
|---|---|---|
| `gen_ai.retrieval.query`, `input.value` | `search`, `answer`, streams | The query, with the client's Exa API key replaced by `[redacted]`, cut to 1 KB of UTF-8 on a character boundary. |
| `exa.url_count` | `get_contents` | Number of requested URLs (a single URL, a list of URLs, or a list of results). |
| `exa.urls` | `get_contents` with `capture_urls=True` | See [get_contents URLs](#get_contents-urls). |
| `exa.result_count` | successful calls | `results` for `search` and `get_contents`, `citations` for `answer`, and the citations summed over every chunk of a fully read stream. Absent when the count is unknown (an error or a cancellation), never 0 for unknown. |
| `exa.cancelled` | cancelled calls and streams | `true`; see [Errors and cancellation](#errors-and-cancellation). |

The spans never carry response content: no result titles, text, highlights or
summaries, no answer or stream text, no cost, token count or model name. There
is no setting that adds them. The Exa API key is never recorded; it is
replaced by `[redacted]` wherever it appears verbatim in a recorded query, URL
or error text.

### get_contents URLs

`get_contents` spans record the number of requested URLs, not the URLs,
because URLs can carry tokens or personal data. To record the URLs as well,
opt in when instrumenting:

```python
ExaInstrumentor().instrument(tracer_provider=tracer_provider, capture_urls=True)
```

With `capture_urls=True`, `exa.urls` holds at most the first 20
requested URLs, each with the Exa API key replaced by `[redacted]` and cut to
1 KB. Query strings are otherwise kept as given. Hidden inputs (below) turn
URL capture off.

## Trace config and context

Spans are created through `fi_instrumentation`'s `FITracer`, as in the other
traceAI instrumentors. Pass a `TraceConfig` to `instrument()`, or set the
matching `FI_*` environment variables; without `config`, the environment is
read when `instrument()` runs.

```python
from fi_instrumentation import TraceConfig

ExaInstrumentor().instrument(
    tracer_provider=tracer_provider, config=TraceConfig(hide_inputs=True)
)
```

With `hide_inputs=True` (or `FI_HIDE_INPUTS=true`), `input.value` is
`__REDACTED__`, `gen_ai.retrieval.query` and `exa.urls` are not recorded,
and the query or the requested URLs are replaced by `__REDACTED__` in error
text. The URL and result counts stay. `pii_redaction` applies to the recorded
attributes and to the error text described below, and
`using_session`, `using_user`, `using_metadata` and `using_tags` attributes are
added to the Exa spans.

## Errors and cancellation

An error raised by exa-py (for example `ValueError` for an HTTP 4xx/5xx
response) sets the span status to ERROR and records an exception event. The
exception is re-raised unchanged.

exa-py puts the server's response body in its error message, and a body can
repeat the request. On the span, the status description
(`<ExceptionType>: <message>`) and the event's `exception.message` therefore
have the Exa key replaced by `[redacted]` (and, with hidden inputs, the query
or the requested URLs by `__REDACTED__`; with `pii_redaction`, PII by tokens such
as `<EMAIL_ADDRESS>`) and the message cut to 1 KB of UTF-8.
If the exception's own `str()` fails, the span records only its type. The event carries
`exception.type` and `exception.message` only: no `exception.stacktrace` is
exported, because frames hold local file paths, user names and host names.
Chained exceptions' messages are not copied. Only verbatim
copies are found: an escaped, normalised or truncated echo of the key, query or
URLs (for example a JSON-escaped `\/` or a percent-encoded URL) is not.

Closing a stream before its last chunk (`close()` or `aclose()`), dropping it
mid-iteration, or cancelling an `AsyncExa` call or stream ends the span with
status ERROR, description `cancelled`, and `exa.cancelled` = `true`.

A traced `AsyncExa` stream always has `aclose()`, which ends the span and closes
the HTTP response. exa-py's own async streams have `aclose()` from 2.26.0 only;
2.25.0's have none. On both versions their `close()` raises `RuntimeError`
(traced or not; the span is ended first). Code that must also run on exa-py
2.25.0 without this instrumentor can call `aclose()` only when it exists, for
example `aclose = getattr(stream, "aclose", None)`.

A missing API key raises `ValueError` in `Exa()` or `AsyncExa()` itself,
before any traced method runs, so no span is recorded for it.

`find_similar` and `find_similar_and_contents` are not traced.

Do not use this package together with another instrumentor that wraps the same
Exa client methods, or duplicate spans may result.

