Metadata-Version: 2.4
Name: pntx
Version: 0.1.0
Summary: Generate and classify text from user-defined (positive, negative) example pairs
Keywords: nlp,llm,llama.cpp,text-generation,text-classification,few-shot
Author: pillyshi
Author-email: pillyshi <pillyshi21@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
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: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Linguistic
Classifier: Typing :: Typed
Requires-Dist: anthropic ; extra == 'anthropic'
Requires-Dist: sentence-transformers ; extra == 'embeddings'
Requires-Dist: llama-cpp-python ; extra == 'llama'
Requires-Python: >=3.10
Project-URL: Homepage, https://github.com/pillyshi/pntx
Project-URL: Repository, https://github.com/pillyshi/pntx
Project-URL: Issues, https://github.com/pillyshi/pntx/issues
Provides-Extra: anthropic
Provides-Extra: embeddings
Provides-Extra: llama
Description-Content-Type: text/markdown

# pntx

`pntx` is a Python library that turns a handful of user-supplied `(positive, negative)`
text pairs into:

1. **Generation** — synthesize new text on either side of the pair.
2. **Classification** — label arbitrary text as `positive` or `negative`.

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 pairs; it only uses them as
few-shot and scoring material.

```python
from pntx import PNTX

model = PNTX(backend="llama", model_path="model.gguf")

pairs = [
    ("The movie was fantastic", "The movie was boring"),
    ("Support was quick and helpful", "Support was slow and unhelpful"),
]
model.fit(pairs)

# Generation
texts = model.generate(
    n=20,
    side="positive",
    temperature=1.0,
    dedup=True,          # filter near-duplicates (of each other and of the seed pairs)
    verify=True,         # self-classify and reject anything that doesn't match `side`
    min_confidence=0.8,  # confidence threshold used by verify
)

# Classification
result = model.classify("The staff were incredibly friendly")
result.label        # "positive" | "negative"
result.confidence   # float in [0.0, 1.0]
result == "positive"  # True

results = model.classify_batch(texts)  # batched, not a naive per-item loop
```

## Installation

`pntx` uses [uv](https://docs.astral.sh/uv/) for package management.

```bash
uv add pntx              # core (zero dependencies)
uv add "pntx[llama]"     # + llama.cpp in-process backend
uv add "pntx[anthropic]" # + Anthropic API backend
uv add "pntx[embeddings]" # + semantic similarity for selectors
```

The core package has no runtime dependencies. Each backend/feature 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:

- **`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.

```python
model = PNTX(backend="llama", model_path="model.gguf")
model = PNTX(backend="anthropic", model="claude-...")

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

## Selecting exemplars

When there are more fitted pairs than comfortably fit in a prompt, a `Selector`
decides which ones to use:

- **`RandomSelector`** (default) — a uniform random subset.
- **`NearestSelector`** — picks pairs whose text is most similar to the text being
  classified; dynamic, per-query selection.
- **`DiversitySelector`** — greedily picks a maximally diverse subset.

Both `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:

```python
from pntx import PNTX
from pntx.selection import NearestSelector

model = PNTX(backend="llama", model_path="model.gguf", selector=NearestSelector())
```

## Development

```bash
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:

```bash
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
```
