Skip to main content

pntx

pntx is a Python library that turns user-supplied positive/negative text pools into two independent components:

  1. pntx.t2pn.Classifier (text → positive/negative) — a scikit-learn Classifier: label arbitrary text as positive or negative.
  2. pntx.pn2t.OverSampler (positive/negative → text) — an imbalanced-learn-style oversampler: generate new "hard positive" text to balance an imbalanced dataset.

The meaning of "positive" and "negative" is entirely up to you. It doesn't have to be sentiment — it can be formal/casual, policy-compliant/violating, or any other contrast you define with examples. pntx never interprets the pools; it only uses them as few-shot and scoring material.

from pntx.t2pn import Classifier
from pntx.pn2t import OverSampler

# --- t2pn: classification (a scikit-learn Classifier) ---
clf = Classifier(backend="llama", backend_kwargs={"model_path": "model.gguf"})

X = ["The movie was fantastic", "Support was quick and helpful",
     "The movie was boring", "Support was slow and unhelpful"]
y = ["positive", "positive", "negative", "negative"]  # 0/1 works too

clf.fit(X, y)
clf.predict(["The staff were incredibly friendly"])        # array(['positive'], dtype='<U8')
clf.predict_proba(["The staff were incredibly friendly"])  # shape (1, 2), columns follow clf.classes_

# drops straight into the scikit-learn ecosystem
from sklearn.model_selection import cross_val_score
cross_val_score(clf, X, y, cv=5)

# --- pn2t: generation (an imbalanced-learn-style OverSampler) ---
sampler = OverSampler(backend="llama", backend_kwargs={"model_path": "model.gguf"})

X_aug, y_aug = sampler.fit_resample(X, [1, 1, 0, 0])  # binary labels only; positive class = 1
sampler.generation_result_.hard_positives  # generated texts + the LLM's rationale for each

OverSampler.fit_resample generates "hard positives" — texts an expert would label positive but that shallow classifiers or untrained humans might mislabel negative — by first asking the backend to analyze what distinguishes the two classes. It's a full port of semaxis's HardPositiveOverSampler, routed through pntx's own Backend abstraction so it can share a loaded model with Classifier instead of loading its own. v1 only generates the positive side and supports binary {0, 1} labels; imbalanced-learn itself isn't required (fit_resample is duck-typed, so imblearn.pipeline.Pipeline still works if it's installed separately).

Installation

pntx uses uv for package management.

uv add pntx               # core (scikit-learn + pydantic)
uv add "pntx[llama]"      # + llama.cpp in-process backend
uv add "pntx[anthropic]"  # + Anthropic API backend
uv add "pntx[embeddings]" # + semantic similarity for selectors

scikit-learn and pydantic are core dependencies (Classifier's scikit-learn contract and OverSampler's structured LLM output need them respectively). Each backend/feature otherwise lives behind its own extra, and using one without installing it raises a clear ImportError with the install command to run.

Backends

pntx runs models two ways, shared by both Classifier and OverSampler:

  • LlamaCppBackend (pntx[llama]) — runs a GGUF model in-process via llama-cpp-python. This is the primary, most-tuned backend: classification uses token log-probabilities directly (score_choices), and batched classification reuses the shared few-shot prefix's KV cache across every item instead of re-evaluating it per item.
  • AnthropicBackend (pntx[anthropic]) — calls the Anthropic Messages API. Since that API doesn't expose log-probabilities, classification asks the model to name the label and parses it out of the response instead (confidence is then a fixed convention value, not a calibrated probability). Batched classification runs requests concurrently (asyncio + a semaphore), not in a sequential loop.
clf = Classifier(backend="llama", backend_kwargs={"model_path": "model.gguf"})
clf = Classifier(backend="anthropic", backend_kwargs={"model": "claude-..."})

# or pass a backend instance directly, e.g. for dependency injection in tests
from pntx.backends.llama import LlamaCppBackend
clf = Classifier(backend=LlamaCppBackend(model_path="model.gguf"))

backend_kwargs is only used when backend is given as a string; it's a single dict (rather than **kwargs) so Classifier/OverSampler stay compatible with scikit-learn's get_params()/clone().

LlamaCppBackend accepts either a local model_path or a repo_id (optionally narrowed to one file with filename) to pull a GGUF model from the Hugging Face Hub via Llama.from_pretrained. Any other keyword — n_ctx, n_gpu_layers, flash_attn, verbose, ... — is forwarded as-is to llama_cpp.Llama:

clf = Classifier(
    backend="llama",
    backend_kwargs={
        "repo_id": "Qwen/Qwen2.5-1.5B-Instruct-GGUF",
        "filename": "*q4_k_m.gguf",
        "n_ctx": 4096,
        "n_gpu_layers": -1,  # offload all layers to GPU
        "flash_attn": True,
    },
)

To share one loaded model between Classifier and OverSampler (recommended for local inference — avoids loading the same GGUF twice), construct the backend once and pass the instance to both:

from pntx.backends.llama import LlamaCppBackend

backend = LlamaCppBackend(model_path="model.gguf")
clf = Classifier(backend=backend)
sampler = OverSampler(backend=backend)

Selecting exemplars

When there are more fitted texts (on either side) than comfortably fit in a prompt, a Selector decides which ones to use — Classifier calls it independently for the positive and negative pools:

  • RandomSelector (default) — a uniform random subset.
  • NearestSelector — picks texts most similar to the text being classified; dynamic, per-query selection.
  • DiversitySelector — greedily picks a maximally diverse subset.
  • BudgetSelector — picks as many texts as fit within a token budget (used internally by OverSampler for its exemplar sampling).

NearestSelector and DiversitySelector take a similarity_fn. It defaults to a dependency-free character n-gram similarity (pntx.dedup.similarity); pass pntx.embeddings.cosine_similarity_fn() (requires pntx[embeddings]) for semantic similarity instead:

from pntx.t2pn import Classifier
from pntx.selection import NearestSelector

clf = Classifier(backend="llama", backend_kwargs={"model_path": "model.gguf"}, selector=NearestSelector())

OverSampler doesn't take a Selector; instead its sample_method constructor argument picks a budget-based sampling strategy (a full port of semaxis's own sample_method/embedding_model):

  • "random" (default) — a uniform random subset, filled until the token budget runs out (BudgetSelector under the hood).
  • "kmeans" — embeds the pool via embedding_model (requires pntx[embeddings]) and picks one representative text per K-Means cluster.
  • "votek" — embeds the pool and runs the Vote-K algorithm (Su et al. 2022), balancing representativeness and diversity.
sampler = OverSampler(
    backend="llama",
    backend_kwargs={"model_path": "model.gguf"},
    sample_method="votek",
    embedding_model="paraphrase-albert-small-v2",  # sentence-transformers model name
)

Development

uv sync                          # install dev dependencies
uv run pytest                    # unit tests (integration tests are skipped by default)
uv run ruff check .
uv run mypy src tests

Integration tests that hit a real model or API are opt-in:

PNTX_LLAMA_MODEL_PATH=/path/to/model.gguf uv run pytest tests/integration
ANTHROPIC_API_KEY=... uv run pytest tests/integration/test_anthropic_backend.py

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pntx-0.6.1.tar.gz (25.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pntx-0.6.1-py3-none-any.whl (32.7 kB view details)

Uploaded Python 3

File details

Details for the file pntx-0.6.1.tar.gz.

File metadata

  • Download URL: pntx-0.6.1.tar.gz
  • Upload date:
  • Size: 25.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.20

File hashes

Hashes for pntx-0.6.1.tar.gz
Algorithm Hash digest
SHA256 6992db41f2d499894c23f7a3098116d2a8e4329f7e35e4b04da8bda42b1459c3
MD5 353b15b1fb9b38a91773367bd58b4028
BLAKE2b-256 7bad71e52a874792c7f98d50ddd68b0cd74785330b2e4f9957fa69c19134370f

See more details on using hashes here.

File details

Details for the file pntx-0.6.1-py3-none-any.whl.

File metadata

  • Download URL: pntx-0.6.1-py3-none-any.whl
  • Upload date:
  • Size: 32.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.20

File hashes

Hashes for pntx-0.6.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8cda4809d9aceeffc10b9216d77be4f0ca99937343a7539c935cb59ce5c14303
MD5 dcd2c3171fdd13df7d2d21eaa00aa595
BLAKE2b-256 3e9d4b309844742f195e25f8eb459ac3d820cbf0d7989dcffb612646fe032412

See more details on using hashes here.

Release history Release notifications | RSS feed

0.15.0

2 files

0.14.0

2 files

0.13.0

2 files

0.12.1

2 files

0.12.0

2 files

0.11.0

2 files

0.10.0

2 files

0.9.1

2 files

0.9.0

2 files

0.8.3

2 files

0.8.2

2 files

0.8.1

2 files

0.8.0

2 files

0.7.0

2 files

This release

0.6.1 This release

2 files

0.6.0

2 files

0.5.1

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page