Metadata-Version: 2.4
Name: llama-index-postprocessor-needlepath
Version: 0.1.0
Summary: llama-index postprocessor needlepath context selection integration
Project-URL: Homepage, https://nextmoca.com
Author: Next Moca Global, Inc.
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: <4.0,>=3.10
Requires-Dist: llama-index-core<0.15,>=0.13.0
Requires-Dist: needlepath<1.0.0,>=0.1.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# `llama-index-postprocessor-needlepath`

Needlepath context selection as a LlamaIndex `BaseNodePostprocessor`: given the
retrieved nodes and the query, keep the nodes that carry the answer and replace
each one's text with the part of it that does.

```bash
pip install llama-index-postprocessor-needlepath
```

```python
from llama_index.postprocessor.needlepath import NeedlepathPostprocessor

query_engine = index.as_query_engine(
    node_postprocessors=[NeedlepathPostprocessor(operating_point="np-2026-07-r2")],
)
```

The key comes from `NEEDLEPATH_API_KEY`. If the service is slow, down, or stands
down, the query engine gets the nodes it was given, unchanged.

## Scope, stated honestly

This is a RAG-retrieval seam. It never sees tool outputs, so it exercises the
weakest of Needlepath's use cases — retrieved chunks are already short and
already ranked. It exists because LlamaIndex is where a large number of
developers already have a postprocessor slot, and an integration that does not
exist cannot be evaluated. If you are choosing where to try Needlepath first and
you have an agent with tool calls, start there instead.

## Configuration

| Parameter | Default | What it does |
|---|---|---|
| `operating_point` | — | **Required.** Immutable label. Also `NEEDLEPATH_OPERATING_POINT`. |
| `max_context_tokens` | `4000` | Budget for the selected context, and the default trigger. |
| `trigger_tokens` | `0` | Skip the call below this. `0` means "use the budget". |
| `excerpt` | `True` | Replace kept nodes' text with their excerpt. `False` filters only. |
| `update_scores` | `False` | Replace `NodeWithScore.score` with the selection score. |
| `preserve_input_order` | `False` | Return kept nodes in retrieval order rather than ranked order. |
| `shadow` | `False` | Measure and report; change nothing. |
| `enabled` | `True` | Kill switch. Needs no credentials when `False`. |

Any other keyword is forwarded to the core client (`base_url`, `api_key`,
`timeout`, …).

## Two LlamaIndex-specific decisions

**Scores are not overwritten by default.** A reranker replaces
`NodeWithScore.score` with its own. Ours is not calibrated against your
retriever's similarity, and silently substituting one for the other corrupts any
downstream threshold or `similarity_cutoff`. The selection score is available as
node metadata; set `update_scores=True` if you want reranker-like behaviour and
know what your thresholds mean.

**Provenance metadata is hidden from the model.** Node metadata is interpolated
into the prompt under `MetadataMode.LLM` and into embeddings under
`MetadataMode.EMBED`. Every `needlepath_*` key this package writes is added to
`excluded_llm_metadata_keys` and `excluded_embed_metadata_keys` on the copy, so
debug data never reaches the model or an index.

## Node identity survives

A rewritten node is a `model_copy(deep=True)` with `set_content()` applied, so
`id_`, `metadata`, `relationships` and therefore `ref_doc_id` are carried over
verbatim. Building a fresh `TextNode` would mint a new random `id_` and drop
`relationships`, severing `ref_doc_id`, dedup and `PrevNextNodePostprocessor`.

The nodes you pass in are never mutated.

## Fail open

| What happens | What the query engine gets |
|---|---|
| Timeout, 5xx, throttling, contract violation | the original nodes, unchanged |
| The gate stands down, or selects nothing | the original nodes, unchanged |
| No `QueryBundle` | the original nodes, unchanged |
| Every returned id is unrecognized | the original nodes, unchanged |

The no-query case is worth calling out: every shipped reranker raises
`ValueError("Missing query bundle in extra info.")` there. This does not. A
postprocessor that raises turns a missing query into a failed query for the whole
engine, and the fail-open rule is binding at a framework boundary.

`postprocessor.stats.as_dict()` aggregates the counters.

## Async

`_apostprocess_nodes` is overridden rather than inherited. The base class's
default is `asyncio.to_thread(self._postprocess_nodes, …)`, which is correct but
burns a thread per query on what is a network call — and nine call sites in
`llama-index-core` reach the async path, including `RetrieverQueryEngine` and
every chat engine.

## Packaging

Named `llama-index-postprocessor-needlepath` rather than `needlepath-llamaindex`.
That is the one naming exception to Needlepath's vendor-first convention: a
package outside the host convention is not discoverable on LlamaHub at all.

PEP 420 namespace layout — `llama_index/` and `llama_index/postprocessor/` carry
no `__init__.py`, so this distribution coexists with `llama-index-core` and every
other integration under the same directories.

## Tested against

`llama-index-core==0.14.23`. Range `>=0.13.0,<0.15`, matching every shipped
`llama-index-postprocessor-*` package. CI runs against the newest minor.
