Open source library

Polar Llama

A Python library for parallel LLM inference across providers, built on Polars DataFrames.

v0.7.2Released July 14, 2026
View on GitHubPyPI package
Version0.9.0latest0.8.30.8.20.8.10.8.00.7.30.7.20.7.10.7.00.6.30.6.20.6.10.6.00.5.30.5.20.5.10.5.00.3.00.2.20.2.10.2.00.1.7
You are viewing documentation for version 0.7.2. A newer version (0.9.0) is available.

Overview

Polar Llama is a Python library that enables parallel inference calls to multiple Large Language Model providers through Polars dataframes. It streamlines batch processing of AI queries without serial request delays, making it ideal for data-intensive AI applications. 0.7.2 adds survey data-quality flags: graded straightlining, gibberish, duplicate, length, and speeder scores, with an opt-in LLM tier.

Concurrent Processing

Send multiple inference requests in parallel without waiting for individual completions

🔌Six Providers

OpenAI, Anthropic, Gemini, Groq, AWS Bedrock, and on-device MLX (Apple Silicon)

🧰Tool Use / MCP

Dataframe-native tool calling: emission, batch-parallel execution, and synthesis are all ordinary columns

🔬Research Workflows

Qualitative and survey research tooling: codebook induction, inter-rater reliability, survey quality flags

Installation

Using pip

bash
pip install polar-llama==0.7.2

On-device inference (Apple Silicon)

Pulls in mlx and mlx-lm; requires an Apple Silicon Mac and Python ≥ 3.10

bash
pip install "polar-llama[local]"

Development Installation

bash
maturin develop

Quick Start

Get started with a simple example

python
import polars as pl
from polar_llama import Provider
import dotenv

dotenv.load_dotenv()

# Example questions
questions = [
    'What is the capital of France?',
    'What is the difference between polars and pandas?'
]

df = pl.DataFrame({'Questions': questions})

# Using the fluent .llama namespace (recommended)
df = df.with_columns(
    answer=pl.col('Questions').llama.inference_async(
        provider=Provider.OPENAI,
        model='gpt-4o-mini'
    )
)

What’s New in 0.7.2

New in 0.7.2

Survey Data-Quality Flags

quality_report(df, QualityConfig(...)) (#80)

Score every respondent in a survey DataFrame for common data-quality problems and get a per-flag summary back, without ever dropping a row. Every score is a graded Float64 in [0, 1] (higher = more suspicious). Booleans come from thresholding, and a null score (not enough signal) always resolves to flag = False.

python
from polar_llama import QualityConfig, quality_report

config = QualityConfig(
    id_column="respondent_id",
    grid_columns=["g1", "g2", "g3", "g4", "g5"],   # Likert grid
    text_columns=["oe1", "oe2"],                    # open-ends
    duration_column="duration_s",                   # seconds
)
result = quality_report(survey_df, config)

result.df.height == survey_df.height   # True: quality_report never drops a row
result.df.select("respondent_id", "quality").unnest("quality")
result.summary   # one row per flag: flag, n_scored, n_flagged, rate, threshold, mean_score
ScoreTierDefault thresholdSignal
straightlining_scoreHeuristic (Rust)0.85Mode share vs. normalized variance across a Likert grid
gibberish_scoreHeuristic (Rust)0.65Consonant runs, vowel-ratio deviation, and bigram entropy; English-only, so non-Latin text scores null
duplicate_answer_scoreHeuristic (Rust)0.8The same answer repeated across a respondent's open-ends
response_length_scoreHeuristic (Polars)|robust z| ≥ 3Robust z-score of answer length
speeder_scoreHeuristic (Polars)5th percentileCompletion time, by percentile or median fraction
near_duplicateEmbedding/LLM (opt-in)0.97Cross-respondent near-duplicates via embedding_async + knn_hnsw + cosine_similarity
likely_aiEmbedding/LLM (opt-in)0.75Stylistic heuristic via inference_messages(..., response_model=...); also standalone as ai_likelihood

QualityConfig(llm_tier=True) turns on the embedding/LLM tier. It is off by default, so there are zero network calls unless you ask for them. result.df gains a quality struct column (sub-structs for unconfigured inputs are omitted, not null-filled), and result.summary has one row per active flag plus a final any_flag row. The heuristic scores also work standalone and on the namespace, for example pl.col("g1").llama.straightlining_score([...]).

Flag, Not Verdict

Documented limitations and tunable thresholds

likely_ai carries a mandatory limitation: AI-text detectors have documented false-positive risk, including bias against non-native English speakers, and the flag must never be the sole grounds for excluding a respondent or sanctioning a panelist. Every default threshold is a documented, subjective convention rather than a validated cutoff, so tune them to your own panel (see docs/QUALITY_FLAGS.md).

Everything Since 0.2.2

0.7.2 is cumulative: every feature shipped in 0.2.2 is still here, plus everything added across 0.3.0, 0.5.0, 0.5.1, 0.5.2, 0.5.3, 0.6.0, 0.6.1, 0.6.2, 0.6.3, 0.7.0, 0.7.1.

0.3.0 – 0.5.2: Tool Use, Prompt Optimization, Caching, Local MLX

Released 2026-06-10 to 2026-07-12

🧰Tool Use / MCP

tools_to_response_model, mcp_tools, execute_tool_calls, tool_results_to_message: batch-parallel tool calling over an MCP server or a Python executor (0.3.0)

🗄️Provider-Native Prompt Caching

cache=True / CacheConfig shares a cached system prefix across rows via Anthropic cache_control, with 5-minute and 1-hour TTLs (0.3.0)

🎛️Prompt Optimization Engine

Signature, Predict, evaluate, BootstrapFewShot, InstructionOptimizer: DSPy-style instruction and few-shot tuning (0.3.0)

🍎Local MLX Backend

inference_local with server and in_process engines, collapsed prefix prefill, batched quantized KV cache (0.5.0), and an on-device prompt-tuning bridge (0.5.1)

Along the way: updated default models for every provider, OPENAI_BASE_URL / ANTHROPIC_BASE_URL overrides and POLAR_LLAMA_MAX_CONCURRENCY (0.3.0), and fixes for in-process Gemma 3n inference (0.5.2). See the 0.5.2 docs for the full details.

0.5.3: Strict-Mode Taxonomy Fix

Released 2026-07-13

tag_taxonomy() works on OpenAI strict mode again. The per-field thinking reasoning is now List[{value, reasoning}] instead of a dict keyed by value name, and $ref sibling keywords are stripped from generated schemas (#51). A new warning flags Dict-typed fields in user-supplied response models.

0.6.x: Production Runs

Streaming, checkpointing, usage accounting, and response caching. Released 2026-07-13 to 2026-07-14

ReleaseFeatureWhat shipped
0.6.0Streaminginference_stream() with an on_token(row_index, delta) callback and a Struct{text, finished} column; native SSE for OpenAI, Groq, Anthropic; GROQ_BASE_URL override
0.6.1Checkpointingcheckpoint="path" / Checkpoint(...) on inference_async and inference_messages: resumable runs over a crash-durable Parquet store keyed by content + config hash
0.6.2Usage & costusage=True returns Struct{response, usage{input_tokens, output_tokens, cached_tokens, latency_ms, cost_usd}} from an overridable price table (price_table=, register_model_price)
0.6.3Dedupe & response cachededupe=True collapses duplicate rows in-run; response_cache= / ResponseCache(path, ttl=...) reuses results across jobs; DedupeStats reports hits and calls

0.7.0 – 0.7.1: Research Workflows

Released 2026-07-14

ReleaseFeatureWhat shipped
0.7.0Codebook inductioncluster_embeddings (hand-rolled spherical k-means in Rust), induce_codebook, apply_codebook, codebook_to_taxonomy
0.7.1Inter-rater reliabilitycohens_kappa and krippendorffs_alpha as aggregation expressions, bit-for-bit with sklearn / krippendorff, with bootstrap CIs

Examples & Cookbooks

Tool Use: Emit, Execute, Synthesize

The agent loop unrolled into ordinary dataframe columns

python
from polar_llama import (
    mcp_tools, tools_to_response_model, execute_tool_calls,
    tool_results_to_message, combine_messages, inference_messages, Provider,
)

tools = mcp_tools("http://localhost:8811/mcp")     # tools/list introspection
ToolCalls = tools_to_response_model(tools)          # emission schema

df = (
    df
    # 1. Emit: the LLM parameterizes N calls per row (structured output)
    .with_columns(calls=pl.col("meal").llama.inference_async(
        provider=Provider.OPENAI, model="gpt-4o-mini", response_model=ToolCalls))
    # 2. Execute: all calls across all rows, in parallel, errors as data
    .with_columns(results=execute_tool_calls(
        pl.col("calls"), transport="http://localhost:8811/mcp", tools=tools))
    # 3. Synthesize: fold results back through a second inference pass
    .with_columns(answer=inference_messages(
        combine_messages(
            pl.col("meal").llama.to_message(role="user"),
            tool_results_to_message(pl.col("results")),
        ),
        provider=Provider.OPENAI, model="gpt-4o-mini"))
)

Prompt Optimization (DSPy-style)

Declare a task, then let an optimizer tune instructions or mine few-shot demos

python
import polars as pl
from polar_llama import Predict, Signature, BootstrapFewShot, InstructionOptimizer, evaluate

# 1. Declare the task
module = Predict(
    Signature("question -> answer", instructions="Answer concisely."),
    provider="openai",
    model="gpt-4o-mini",
)

# 2. Labeled training data
trainset = pl.DataFrame({
    "question": ["What is 2+2?", "Capital of France?", "Largest planet?"],
    "answer": ["4", "Paris", "Jupiter"],
})

# 3. A metric: (gold row, prediction) -> bool | float
def exact_match(example, prediction):
    return example["answer"].strip().lower() == (prediction["answer"] or "").strip().lower()

# 4a. Bootstrap few-shot demos from rows the model already gets right
compiled = BootstrapFewShot(metric=exact_match, max_demos=4).compile(module, trainset)

# 4b. Or search for better instructions (COPRO-style)
optimizer = InstructionOptimizer(metric=exact_match, n_candidates=4)
compiled = optimizer.compile(module, trainset)
print(optimizer.history)  # [(instructions, score), ...]

# 5. Run the optimized module on new data; outputs land in pred_* columns
result = compiled(pl.DataFrame({"question": ["What is 3+3?"]}))
print(result["pred_answer"])

# Score any module against a labeled set
print(evaluate(compiled, trainset, exact_match).score)

Production Runs

Features from earlier releases for long, expensive batch jobs

python
import polars as pl
from polar_llama import (
    inference_async, Provider, inference_stream, ResponseCache, DedupeStats,
)

# Streaming (0.6.0+): token deltas via a callback, Struct{text, finished} per row
df = df.with_columns(
    streamed=inference_stream(pl.col("prompt"), provider=Provider.OPENAI,
                              model="gpt-4o-mini", on_token=lambda i, d: print(d, end=""))
)

# Checkpointing (0.6.1+): re-run after a crash and only unfinished rows hit the API
df = df.with_columns(
    answer=inference_async(pl.col("prompt"), provider=Provider.OPENAI,
                           model="gpt-4o-mini", checkpoint="run1.ckpt")
)

# Usage & cost (0.6.2+): Struct{response, usage{input_tokens, ..., cost_usd}}
df = df.with_columns(
    r=inference_async(pl.col("prompt"), provider=Provider.OPENAI,
                      model="gpt-4o-mini", usage=True)
)
spend = df.select(pl.col("r").struct.field("usage").struct.field("cost_usd").sum())

# Dedupe + persistent response cache (0.6.3+): pay once per unique request
stats = DedupeStats()
df = df.with_columns(
    cached=inference_async(pl.col("prompt"), model="gpt-4o-mini",
                           response_cache=ResponseCache("responses.cache", ttl="24h"),
                           dedupe_stats=stats)
)
print(stats.hit_rate, stats.calls_made)

Research Workflows

Qualitative-coding and survey tooling from the 0.7.x releases

python
import polars as pl
from polar_llama import (
    Provider, induce_codebook, apply_codebook, cohens_kappa, krippendorffs_alpha,
)

# Codebook induction (0.7.0+): discover themes, then apply them as multi-label codes
result = induce_codebook(responses, "text", provider=Provider.OPENAI, model="gpt-4o-mini")
coded = result.df.with_columns(
    labels=apply_codebook(pl.col("text"), result.codebook, provider=Provider.OPENAI)
)

# Agreement (0.7.1+): LLM vs. human and across a rater panel, per group
labels.group_by("batch").agg(
    kappa=cohens_kappa("llm_label", "human_label"),
    alpha=krippendorffs_alpha(["rater_1", "rater_2", "rater_3"]),
)

On-Device Inference (Apple Silicon, in-process engine)

Batched generation via mlx-lm, no API keys, no network

python
import polars as pl
import polar_llama  # registers the .llama namespace

df = pl.DataFrame({"ticket": [
    "My laptop won't turn on even when it's plugged in.",
    "I forgot my password and I'm locked out of my account.",
]})

tagged = df.with_columns(
    tag=pl.col("ticket").llama.inference_local(
        model="mlx-community/gemma-3n-E4B-it-lm-4bit",
        system="Classify the ticket as exactly one of: Hardware, Account, Billing.",
        engine="in_process",   # on-device mlx-lm, batched over the column
        max_tokens=16,
        temperature=0.0,
    )
)

Data Analysis Pipeline

Process customer feedback at scale

python
import polars as pl
from polar_llama import string_to_message, inference_async, Provider

# Load customer feedback data
feedback_df = pl.DataFrame({
    'customer_id': [101, 102, 103, 104, 105],
    'feedback': [
        'The product is amazing but shipping was slow',
        'Great quality, highly recommend!',
        'Disappointed with customer service',
        'Perfect for my needs, will buy again',
        'Product arrived damaged, requesting refund'
    ]
})

# Create sentiment analysis prompts
sentiment_prompt = """Analyze the sentiment of this customer feedback
and classify it as Positive, Negative, or Neutral.
Also provide a brief reason.

Feedback: {feedback}"""

df = feedback_df.with_columns(
    prompt=pl.format(sentiment_prompt, pl.col('feedback'))
)

# Convert to messages and run inference
df = df.with_columns(
    message=string_to_message("prompt", message_type='user')
)

df = df.with_columns(
    sentiment_analysis=inference_async('message',
                                      provider=Provider.OPENAI,
                                      model='gpt-4o-mini')
)

# Extract key insights
print(df.select(['customer_id', 'feedback', 'sentiment_analysis']))

Vector Similarity Search

Generate embeddings, then find nearest neighbors with HNSW

python
from polar_llama import knn_hnsw, embedding_async, Provider

# Create corpus of documents
corpus = pl.DataFrame({
    "doc": ["AI research", "cooking tips", "machine learning", "recipes"]
}).with_columns(
    embedding=embedding_async(pl.col("doc"), provider=Provider.OPENAI)
)

# Create query
query = pl.DataFrame({
    "query": ["artificial intelligence"]
}).with_columns(
    query_emb=embedding_async(pl.col("query"), provider=Provider.OPENAI),
    corpus_emb=pl.lit([corpus["embedding"].to_list()])
).with_columns(
    neighbors=knn_hnsw(
        pl.col("query_emb"),
        pl.col("corpus_emb").list.first(),
        k=2  # Find 2 nearest neighbors
    )
)

# Get nearest neighbor documents
indices = query["neighbors"][0]
print(corpus[indices]["doc"])  # ['AI research', 'machine learning']

Taxonomy-Based Tagging

Classify documents with reasoning, reflection, and confidence scores

python
import polars as pl
from polar_llama import tag_taxonomy, Provider

# Define your taxonomy
taxonomy = {
    "sentiment": {
        "description": "The emotional tone of the text",
        "values": {
            "positive": "Text expresses positive emotions or favorable opinions",
            "negative": "Text expresses negative emotions or unfavorable opinions",
            "neutral": "Text is factual and objective without clear emotional content"
        }
    },
    "urgency": {
        "description": "How urgent the content is",
        "values": {
            "high": "Requires immediate attention",
            "medium": "Should be addressed soon",
            "low": "Can be addressed at any time"
        }
    }
}

df = pl.DataFrame({
    "id": [1, 2],
    "message": [
        "URGENT: Server is down!",
        "Thanks for your help yesterday."
    ]
})

result = df.with_columns(
    tags=tag_taxonomy(
        pl.col("message"),
        taxonomy,
        provider=Provider.GROQ,
        model="llama-3.3-70b-versatile"
    )
)

# Extract specific values
result.select([
    "message",
    pl.col("tags").struct.field("sentiment").struct.field("value").alias("sentiment"),
    pl.col("tags").struct.field("sentiment").struct.field("confidence").alias("confidence"),
    pl.col("tags").struct.field("urgency").struct.field("value").alias("urgency")
])

# Per-candidate reasoning: thinking is a list of {value, reasoning} (0.5.3+)
result.select(
    "id",
    pl.col("tags").struct.field("sentiment").struct.field("thinking").alias("thinking"),
).explode("thinking").unnest("thinking")

Provider Support

Six inference targets: five hosted providers plus on-device MLX

OpenAI

Default model: gpt-4o-mini

python
df = df.with_columns(
    answer=inference_async('prompt',
                          provider=Provider.OPENAI,
                          model='gpt-4o-mini')
)

Anthropic (Claude)

Default model: claude-opus-4-8; supports cache=True for prompt caching

python
df = df.with_columns(
    answer=inference_async('prompt',
                          provider=Provider.ANTHROPIC,
                          model='claude-opus-4-8',
                          system_prompt='You are a helpful assistant.',
                          cache=True)
)

AWS Bedrock

Default model: us.anthropic.claude-haiku-4-5-20251001-v1:0; region resolved from AWS_REGION / AWS_DEFAULT_REGION

python
# Requires AWS credentials configured
df = df.with_columns(
    answer=inference_async('prompt',
                          provider='bedrock',
                          model='us.anthropic.claude-haiku-4-5-20251001-v1:0')
)

Google Gemini

Default model: gemini-2.5-flash; native system_instruction and JSON-schema structured outputs

python
df = df.with_columns(
    answer=inference_async('prompt',
                          provider=Provider.GEMINI,
                          model='gemini-2.5-flash')
)

Groq

Default model: llama-3.3-70b-versatile; GROQ_BASE_URL overrides the endpoint (0.6.0+)

python
df = df.with_columns(
    answer=inference_async('prompt',
                          provider=Provider.GROQ,
                          model='llama-3.3-70b-versatile')
)

Local (Apple Silicon / MLX)

No API key, no network: the server engine points at a local OpenAI-compatible endpoint, and in_process drives mlx-lm directly

python
# engine="server" (default): point at a local server you started yourself
df = df.with_columns(
    answer=pl.col('prompt').llama.inference_local(
        model='mlx-community/gemma-4-e2b-it-4bit',
        engine='server',
        base_url='http://localhost:8080',
    )
)

# engine="in_process": batched generation directly via mlx-lm
df = df.with_columns(
    answer=pl.col('prompt').llama.inference_local(
        model='mlx-community/gemma-4-e2b-it-4bit',
        engine='in_process',
        max_tokens=256,
    )
)

API Surface

Core expressions exported from polar_llama

FunctionPurpose
inference_async(expr, *, provider, model, response_model, cache, system_prompt, checkpoint, usage, price_table, dedupe, response_cache, dedupe_stats)Parallel async inference; plus prompt caching, checkpointing, usage/cost, dedupe and a response cache
inference(expr, *, provider, model, response_model)Synchronous inference (deprecated in favor of inference_async)
inference_messages(expr, *, provider, model, response_model, cache, checkpoint, usage, price_table, dedupe, response_cache, dedupe_stats)Multi-turn conversation inference over JSON or List(Struct) message arrays
inference_stream(expr, *, provider, model, on_token, messages)Token-by-token streaming; returns Struct{text, finished} per row
string_to_message(expr, *, message_type)Convert text to a {role, content} message
combine_messages(*exprs)Merge message columns/arrays into one ordered conversation
tag_taxonomy(expr, taxonomy, *, provider, model)Classify text against a taxonomy with reasoning, reflection, and confidence
embedding_async(expr, *, provider, model)Parallel embedding generation (OpenAI, Gemini, Bedrock)
cosine_similarity / dot_product / euclidean_distance(vec1, vec2)Rust-powered vector similarity metrics
knn_hnsw(query_expr, reference_expr, *, k)Stateless approximate nearest-neighbor search via HNSW
mcp_tools(transport, *, timeout_s)Fetch tool definitions from an MCP server (tools/list)
tools_to_response_model(tools, *, model_name)Build a Pydantic emission schema so the LLM emits structured tool calls
execute_tool_calls(expr, *, transport, executor, tools, concurrency, timeout_s)Run every emitted call of every row in parallel; failures are data
tool_results_to_message(expr, *, role)Render tool results as a message for the synthesis inference pass
Signature / Predict / evaluate / BootstrapFewShot / InstructionOptimizerDSPy-style prompt optimization engine (polar_llama.optimize)
Checkpoint(path, *, flush_every, retry_failed, on_mismatch)Checkpoint store configuration for checkpoint=
register_model_price(provider, model, *, input_per_1m, output_per_1m, cached_input_per_1m) / set_price_table(table)Add or override prices used for cost_usd
ResponseCache(path, ttl, on_mismatch) / DedupeStats()Persistent cross-job response cache and dedupe counters
cluster_embeddings(expr, *, k, k_min, k_max, max_iter, n_init, seed, silhouette_sample)Whole-column spherical k-means with automatic k selection
induce_codebook(df, column, *, provider, model, embedding_column, k, n_exemplars, ...)Embed, cluster, and LLM-name a codebook; returns .df and .codebook
apply_codebook(expr, codebook, *, provider, model) / codebook_to_taxonomy(codebook)Multi-label coding against a codebook / bridge to tag_taxonomy
cohens_kappa(a, b, *, weights, n_bootstrap, ci, seed) / krippendorffs_alpha(cols, *, level, n_bootstrap, ci, seed)Inter-rater reliability as aggregation expressions
quality_report(df, config, *, output_column) / QualityConfig(...)Per-respondent survey quality flags plus a summary table
straightlining_score / gibberish_score / duplicate_answer_score / response_length_score / speeder_score / ai_likelihoodThe individual quality scores as standalone expressions
col(...).llama.inference_local(*, model, system, engine, base_url, max_tokens, temperature, top_p, stop, usage, price_table)On-device inference on Apple Silicon via mlx-lm
polar_llama.local.make_local_inference_fn(model, *, engine, collapse, max_tokens, ...)Build an inference_fn that backs the optimizer with on-device Gemma 3n

Every expression is also available on the fluent .llama namespace (pl.col("text").llama.inference_async(...), .llama.to_message(...), .llama.embedding(...), .llama.inference_stream(...), .llama.cohens_kappa(...), and so on). DataFrame-level orchestration functions (induce_codebook, quality_report) are plain functions, not namespace methods.

Advanced Features

Environment Configuration

Set up your API keys and overrides in a .env file:

.envbash
OPENAI_API_KEY=your_openai_key
ANTHROPIC_API_KEY=your_anthropic_key
GEMINI_API_KEY=your_gemini_key
GROQ_API_KEY=your_groq_key
AWS_ACCESS_KEY_ID=your_aws_key
AWS_SECRET_ACCESS_KEY=your_aws_secret
AWS_REGION=us-east-1

# Optional endpoint overrides for proxies and gateways (0.3.0+)
OPENAI_BASE_URL=https://your-proxy.example.com
ANTHROPIC_BASE_URL=https://your-proxy.example.com
GROQ_BASE_URL=https://your-proxy.example.com         # 0.6.0+

# Bound concurrent in-flight requests per batch (default: 64)
POLAR_LLAMA_MAX_CONCURRENCY=64

# Local MLX backend (Apple Silicon only)
POLAR_LLAMA_LOCAL_COLLAPSE=1        # collapsed prefix prefill for inference_local (0.5.1+)
POLAR_LLAMA_LOCAL_KV_BITS=4         # quantized batched KV cache, 0.5.0+ (mutually exclusive with COLLAPSE)

Prompt Caching

Share a cached system prefix across rows (Anthropic cache_control):

python
from polar_llama import CacheConfig, CacheStrategy

# Simple: enable automatic caching
df = df.with_columns(
    response=inference_async(
        pl.col("prompt"),
        provider=Provider.ANTHROPIC,
        model="claude-opus-4-8",
        system_prompt="You are a careful, concise research assistant.",
        cache=True,   # AUTO strategy, min_tokens=1024, ttl="5m"
    )
)

# Advanced: configure caching behavior explicitly
config = CacheConfig(strategy=CacheStrategy.SYSTEM_PROMPT, ttl="1h")
df = df.with_columns(
    response=inference_async(pl.col("prompt"), cache=config)
)

Testing

Run tests with configured providers:

bash
pip install -r tests/requirements.txt
pytest tests/ -v
cargo test --test model_client_tests -- --nocapture

# Local MLX backend: CPU-safe logic tests only (no GPU, no mlx import)
pytest tests/ -m "not local_gpu"

Changelog Highlights (0.5.2 → 0.7.2)

  • 0.7.2: quality_report / QualityConfig: straightlining, gibberish, duplicate-answer, length-outlier and speeder scores, plus an opt-in near-duplicate / likely-AI tier
  • 0.7.1: cohens_kappa and krippendorffs_alpha as aggregation expressions, bit-for-bit with sklearn / krippendorff, with bootstrap CIs
  • 0.7.0: cluster_embeddings (hand-rolled spherical k-means in Rust), induce_codebook, apply_codebook, codebook_to_taxonomy
  • 0.6.3: dedupe=True collapses duplicate rows in-run; response_cache= / ResponseCache(path, ttl=...) reuses results across jobs; DedupeStats reports hits and calls
  • 0.6.2: usage=True returns Struct{response, usage{input_tokens, output_tokens, cached_tokens, latency_ms, cost_usd}} from an overridable price table (price_table=, register_model_price)
  • 0.6.1: checkpoint="path" / Checkpoint(...) on inference_async and inference_messages: resumable runs over a crash-durable Parquet store keyed by content + config hash
  • 0.6.0: inference_stream() with an on_token(row_index, delta) callback and a Struct{text, finished} column; native SSE for OpenAI, Groq, Anthropic; GROQ_BASE_URL override
  • 0.5.3: tag_taxonomy() works on OpenAI strict mode again: thinking is now List[{value, reasoning}] and $ref siblings are stripped (#51); new warning for Dict-typed response models
  • 0.5.2: fixed in-process local inference on Gemma 3n (mlx-lm #1384 patch applied at load time; a masked ZeroDivisionError in BatchGenerator teardown no longer hides the real error)
  • 0.3.0 – 0.5.1: tool use / MCP, provider-native prompt caching, DSPy-style prompt optimizer, local MLX backend, and the on-device prompt-tuning bridge

Common Use Cases

📊Data Analysis

Process large datasets with AI insights: sentiment analysis, classification, entity extraction with validated structured outputs

🛡️Long-Running Batch Jobs

Resume after crashes, track spend per row, and never pay twice for the same request

🔬Qualitative and Survey Research

Induce codebooks, measure LLM/human agreement, flag low-quality respondents, and fold reviewer corrections back into the prompt

🧰Tool-Augmented Pipelines

Let the LLM call databases, internal APIs, or MCP servers at scale, with every call, result, and retry as an ordinary dataframe column

🔒Private, Local Inference

Run classification, extraction, or tuning on local models with no API keys and no data leaving the machine

🎛️Prompt Tuning at Scale

Bootstrap few-shot demos or search for better instructions against a labeled dataset, entirely offline or on-device

Resources
GitHub RepositoryPyPI PackagePolars Documentation

Licensed under MIT.

Questions or issues? Open one on GitHub.