Open source library

Polar Llama

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

v0.9.0Released September 19, 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

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.9.0 adds the TypeSafe System One inference layer: typed, calibrated yes/no, choice, and score answers per row, plus Pydantic contracts evaluated per line of a document in a single request.

Concurrent Processing

Send multiple inference requests in parallel without waiting for individual completions

🔌Hosted and Local Models

OpenAI, Anthropic, Gemini, Groq, AWS Bedrock, on-device MLX, and any OpenAI-compatible local server such as llama.cpp

🎯Typed, Calibrated Decisions

TypeSafe System One answers yes/no, choice, and score questions as probabilities and confidences you can threshold

🧾Production and Audit

Streaming, checkpointing, per-row cost, response caching, and deterministic run manifests

Installation

Using pip

bash
pip install polar-llama==0.9.0

On-device inference (Apple Silicon)

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

bash
pip install "polar-llama[local]"

Excel review exports

Only needed for export_review_sample(..., format="xlsx"); CSV needs nothing extra

bash
pip install "polar-llama[excel]"

Not on Apple Silicon? Local inference also works against any OpenAI-compatible local server, such as llama.cpp’s llama-server on Linux, Windows, or macOS. It needs no extra; see Provider Support below.

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.9.0

New in 0.9.0

TypeSafe System One Inference Layer

typesafe_eval with noul / choice / score questions: typed, calibrated answers as ordinary columns

0.9.0 adds a native Rust client for the TypeSafe System One API, exposed as a Polars expression and defaulting to the jev-latest model. TypeSafe is deliberately not a chat-completions provider: there is no prompt and no free-text completion, so it has its own expression rather than a Provider entry. One request carries a single state plus a map of typed questions, and returns one typed answer each:

QuestionAsksAnswer
noulA yes/no questionA probability in [0, 1]
choicePick one of a closed setThe pick, a probability per option, and a confidence
scoreRate against ordered levelsA probability-weighted value (it can land between levels), a probability per level, and a confidence

Because the answers are typed and calibrated rather than parsed out of prose, they land as ordinary Float64 / String columns you can filter, sort, threshold, and join on. Every question for a row rides in one request, following TypeSafe’s own "speculative fan-out" guidance, so you can’t accidentally pay for the state once per question.

python
import polars as pl
from polar_llama import typesafe_eval, noul, choice, score

df = pl.DataFrame({"message": [
    "Help! My payouts have been failing for 3 days.",
    "Hi, just wondering what your enterprise pricing looks like.",
]})

out = df.with_columns(
    ts=typesafe_eval(
        pl.col("message"),
        questions={
            "is_urgent": noul(
                "Does this convey urgency?",
                true="Explicitly time-sensitive",
                false="No urgency expressed",
            ),
            "department": choice(
                "Which team should handle this?",
                {
                    "billing": "Payments, invoicing, refunds",
                    "technical": "Bugs, outages, integrations",
                    "sales": "Pricing, upgrades, new accounts",
                },
            ),
            "frustration": score(
                "How frustrated is the customer?",
                ["Calm", "Frustrated", "Very angry"],
            ),
        },
    )
).unnest("ts")

# message              is_urgent  department  department_confidence  frustration  _error
# "Help! My payouts…"  0.95       billing     0.79                   1.04         null
# "Hi, just wonderi…"  0.06       sales       1.0                    0.0          null

# Confidence is the lever: act automatically, or route to a human.
auto   = out.filter(pl.col("department_confidence") > 0.9)
review = out.filter(pl.col("department_confidence").is_between(0.5, 0.9))
human  = out.filter(pl.col("department_confidence") < 0.5)

The fluent form works too: pl.col("message").llama.typesafe_eval(questions={...}). Scale the confidence threshold with the stakes of the action: a read-only lookup and an irreversible refund do not deserve the same bar.

Output Schema

Resolved from the questions before any request, so .collect_schema() costs nothing

Question type / optionColumns
noul<id>: Float64
choice<id>: String, <id>_confidence: Float64
score<id>: Float64, <id>_confidence: Float64
(always)_error: String, null on success
probabilities=True<id>_p_<option> (choice) and <id>_p_<level index> (score): Float64
usage=True_model (the resolved version that ran, e.g. jev-1.13.0, not the jev-latest alias), _input_tokens, _output_tokens, _latency_ms (total wall clock including retries)

Columns come out in the order the questions were declared. The dtype is derived from the questions before any request is made, so .collect_schema() on a LazyFrame resolves the full shape without spending a token. A noul gets no _confidence column because TypeSafe returns none: a single probability already is the distribution. Colliding field names (a question a next to a_confidence) are rejected at schema time.

Structured State

Several columns become one JSON object; constants broadcast

A single expression is sent as that bare value. Several are sent as a JSON object keyed by column name, with numbers and booleans keeping their JSON type. List columns become JSON arrays and Struct columns JSON objects, recursively. A length-1 input broadcasts across the frame, so a shared policy or reference document can ride along with per-row state. state_json=True treats string inputs as pre-encoded JSON documents.

python
typesafe_eval(
    pl.col("message"), pl.col("order_id"), pl.col("charge_count"),
    questions={"duplicate": noul("Do the records show a duplicate charge?")},
)
# state = {"message": "...", "order_id": "A-104", "charge_count": 2}

typesafe_eval(
    pl.col("message"),
    pl.lit(refund_policy).alias("policy"),   # broadcast to every row
    questions={"refund_ok": noul("Does the policy support a refund here?")},
)

instructions, choice option descriptions, score level descriptions, and noul true / false descriptions accept string | object | array | null. Raw TypeSafe question JSON is accepted alongside the builders, so an existing payload works unchanged.

Contracts: a Pydantic Model as the Feature Set

contract= on typesafe_eval and typesafe_eval_each; contract_questions, choice_field, score_field

A contract is a Pydantic model naming the features to extract. Each field’s Python type picks the question type, so the struct you want out is the specification of the work. Field(description=...) becomes the question’s instructions, so write it like a question.

Field typeQuestionAnswer
boolNoulA probability in [0, 1]
Literal[...] / EnumChoiceThe pick + _confidence
Numeric + score_field(description, levels)ScoreA weighted value + _confidence
python
from typing import Literal
from pydantic import BaseModel, Field
from polar_llama import typesafe_eval, score_field

class ClauseFeatures(BaseModel):
    is_payment: bool = Field(description="Does this clause create a payment obligation?")
    category: Literal["fees", "term", "liability", "other"] = Field(
        description="What kind of clause is this?")
    severity: float = score_field(
        "How onerous is this clause for the Customer?",
        ["Benign", "Notable", "Onerous"])

df.with_columns(ts=typesafe_eval(pl.col("clause"), contract=ClauseFeatures)).unnest("ts")

A bool field returns a probability, not True / False, so you threshold it where the stakes say you should. A plain str field is rejected with an explanation: TypeSafe answers are typed over a closed set and there is no free-text primitive. contract= and questions= are mutually exclusive; call contract_questions(Model) to see the questions a model produces. choice_field(description, criteria) adds per-option rubrics to a choice field.

Per-Line Evaluation

typesafe_eval_each: one contract answered for every line, clause, or chunk, in one request per document

typesafe_eval fans out over questions for one state. typesafe_eval_each fans out over segments: a List column of lines, clauses, passages, or chunks goes in, and every segment comes back with the same contract answered. All of a row’s segments are evaluated together, so the model sees each line’s neighbours as context. A clause like "renews automatically unless either party gives 60 days notice" is unreadable on its own.

python
from polar_llama import typesafe_eval_each

clauses = (
    df.with_columns(clause=pl.col("contract_text").str.split("\n"))
      .with_columns(f=typesafe_eval_each(pl.col("clause"), contract=ClauseFeatures))
      .explode("f")
      .unnest("f")
)

# doc  line_id  line                                is_payment  category  severity
# msa  0        "Definitions. 'Services' means…"    0.03        other     0.00
# msa  1        "Fees. Customer shall pay all…"     0.99        fees      0.12
# msa  4        "Limitation of Liability. In no…"   0.04        liability 1.52

clauses.filter(pl.col("is_payment") > 0.8)

# Extra positional args are shared context folded into every chunk's state
typesafe_eval_each(pl.col("clause"), pl.col("doc_title"), contract=ClauseFeatures)
Strategy (8-clause contract, live API)Input tokensRound tripsDocument context
One request per line2,3678Lost
One request, 8 per-line questions7431Kept

That is 3.2× fewer input tokens and 8× fewer round trips. The return is List[Struct{line_id, line, <answers>, _error}]. An expression can’t change row count, so .explode() gives one row per segment, and include_segment=False drops line. Passing a String column instead of a List fails at schema resolution, naming str.split as the fix, before a request is billed.

Chunking, Failures, and Retries

Token-aware chunking; errors stay on the row that caused them

The request ceiling is token-based, not count-based: 640 questions (about 40k input tokens) succeeded against the live API, while 1,200 returned 400 max_tokens_exceeded. max_questions (default 200) caps questions per request. Each segment costs one question per contract field, so the chunk size is max_questions / len(contract). Chunks fire concurrently under POLAR_LLAMA_MAX_CONCURRENCY, and line_id stays global, so a chunk boundary never renumbers a line.

  • Failure is per row, never per frame. _error is always present and null on success. A failing chunk marks only its own segments, and the rest of the document still resolves.
  • A missing input is not a failure. A row whose state is entirely null (or a null segments list) is never sent, and comes back null with a null _error.
  • Declined questions stay null. A question TypeSafe declines to answer leaves its columns null rather than erroring.
  • Local mistakes fail before billing. No questions, a one-option choice, an unknown question type, or colliding output names raise in Python or at schema time.
  • Only helpful retries. 429, 529, and transient 5xx retry with exponential backoff and jitter, honouring Retry-After, under a real 30s cap. 401 and 422 are never retried, since a bad key or a malformed question won’t fix itself.

Configuration

Environment variables and the model catalogue

VariableMeaning
TYPESAFE_API_KEYBearer token (required)
TYPESAFE_BASE_URLAPI root override; default https://api.typesafe.ai
POLAR_LLAMA_MAX_CONCURRENCYShared in-flight request cap (default 64)
POLAR_LLAMA_TYPESAFE_MAX_RETRIESRetry budget for 429/529/5xx (default 3)
python
from polar_llama import typesafe_models

[m["name"] for m in typesafe_models()]   # e.g. ['jev-latest', 'jev-preview']

Batches reuse the shared pooled HTTP client. There are zero new Python or Rust dependencies. tests/test_typesafe.py drives the real Rust expression against a local stdlib mock of POST /v1/systemone, so it needs no key and no network; a gated live test runs only when TYPESAFE_API_KEY is set. Full guide: docs/TYPESAFE.md.

Changed: cargo test Runs the Crate's Unit Tests

pyo3's extension-module is now a default Cargo feature

pyo3’s extension-module feature leaves the CPython symbols undefined. The importable wheel needs that, but it made the crate impossible to link into a test binary, so cargo test couldn’t run any #[cfg(test)] module. It is now a default Cargo feature (default = ["extension-module"]), and cargo test --lib --no-default-features links against libpython and runs them. cargo build, maturin develop, and the wheel build behave exactly as before.

Everything Since 0.2.2

0.9.0 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.7.2, 0.7.3, 0.8.0, 0.8.1, 0.8.2, 0.8.3.

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.x: Research Workflows

Codebook induction, agreement metrics, survey quality flags, and human review. 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
0.7.2Survey quality flagsquality_report / QualityConfig: straightlining, gibberish, duplicate-answer, length-outlier and speeder scores, plus an opt-in near-duplicate / likely-AI tier
0.7.3Human-in-the-loop reviewexport_review_sample, import_corrections (with kappa), corrections_to_trainset, retune_from_corrections; optional [excel] extra

0.8.x: Vector Search, Local Backends, and Reproducibility

Persistent ANN index, offline embeddings, llama.cpp, and run manifests. Released 2026-07-14

ReleaseFeatureWhat shipped
0.8.0Persistent HNSW indexHnswIndex: build / add / remove / query / knn / compact / save / load over a staging buffer + tombstones on instant-distance
0.8.1Local embeddingsembedding_local via mlx_embeddings (List[Float64], drop-in for embedding_async); FakeEmbeddingEngine for CI
0.8.2llama.cpp local serverDocumented and CI-tested engine="server" against llama.cpp's llama-server on Linux, Windows, and macOS, with a local-backend feature matrix; no library changes
0.8.3Run manifestsRunManifest / build_manifest / with_manifest_id / load_manifest / replay: deterministic, integrity-checked audit records with verified replay

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, build_manifest, with_manifest_id,
)

# 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)

# Run manifests (0.8.3+): a deterministic, integrity-checked record of the run
manifest = build_manifest(df, symbol="inference_async", provider="openai",
                          model="gpt-4o-mini")
manifest.save("runs/nightly.manifest.json")
df = with_manifest_id(df, manifest)

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, QualityConfig, quality_report, export_review_sample, import_corrections,
)

# 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"]),
)

# Survey quality flags (0.7.2+): graded scores, never drops a row
report = quality_report(survey_df, QualityConfig(
    id_column="respondent_id", grid_columns=["g1", "g2", "g3"],
    text_columns=["oe1"], duration_column="duration_s",
))
report.summary

# Human review loop (0.7.3+): sample for review, then score the corrections
export_review_sample(coded_df, n=100, strata="code", path="review_batch.csv")
review = import_corrections(coded_df, reviewed_df, code_column="code")
print(review.kappa, review.n_changed)

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

Embed a corpus, build a persistent HnswIndex, and query it

python
import polars as pl
from polar_llama import HnswIndex, embedding_async, Provider

corpus = pl.DataFrame({
    "doc_id": ["doc-1", "doc-2", "doc-3", "doc-4"],
    "text": ["AI research", "cooking tips", "machine learning", "recipes"],
}).with_columns(
    embedding=embedding_async(pl.col("text"), provider=Provider.OPENAI)
)
# Or fully offline on Apple Silicon (0.8.1+), same List[Float64] output:
#   embedding=embedding_local(pl.col("text"))
# Build a persistent index once, then query it as often as you like
index = HnswIndex.build(corpus, id_col="doc_id", embedding_col="embedding")

queries = pl.DataFrame({"q": ["artificial intelligence"]}).with_columns(
    embedding=embedding_async(pl.col("q"), provider=Provider.OPENAI)
)
print(index.query(queries, embedding_col="embedding", k=2))
# query_id | neighbor_id | distance | rank  ->  doc-1, doc-3

index.save("corpus.hnsw")        # reload later with HnswIndex.load(...)

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 local models) and the TypeSafe evaluation layer

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 Models (MLX or llama.cpp)

No API key, no network. engine="server" works with any OpenAI-compatible local server (mlx_lm.server, vllm-mlx, or llama.cpp's llama-server on Linux, Windows, and macOS); engine="in_process" drives mlx-lm directly on Apple Silicon

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,
    )
)

# Any platform: llama.cpp's llama-server (start it with: llama-server -m model.gguf --port 8080)
# Sampling is set with llama-server flags; max_tokens/temperature/top_p/stop
# are not forwarded on the server engine.
df = df.with_columns(
    answer=pl.col('prompt').llama.inference_local(
        model='model.gguf',
        engine='server',
        base_url='http://localhost:8080',
    )
)

TypeSafe System One (typed evaluation)

Not a chat provider: its own expression for typed, calibrated noul / choice / score answers. Default model: jev-latest; set TYPESAFE_API_KEY

python
from polar_llama import typesafe_eval, noul

df = df.with_columns(
    ts=typesafe_eval(pl.col('prompt'),
                     questions={'is_question': noul('Is this a question?')})
).unnest('ts')

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)
embedding_local(expr, *, model, engine, batch_size, normalize)Offline, in-process embeddings via mlx_embeddings; same List[Float64] output
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
HnswIndex.build(df, id_col, embedding_col, ...) / .add / .remove / .query / .query_one / .knn / .compact / .save / HnswIndex.loadPersistent, incrementally updatable HNSW index
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
export_review_sample / import_corrections / corrections_to_trainset / retune_from_correctionsHuman-in-the-loop review loop feeding BootstrapFewShot
build_manifest(df, *, symbol, provider, model, system_prompt, response_model, prompt_template, params, seed, usage_column, checkpoint, response_cache, dedupe_stats, store_texts)Build a deterministic RunManifest
with_manifest_id / save_manifest / load_manifest / replay(manifest, df, input_column, *, system_prompt, response_model, prompt_template, verify)Attach, persist, integrity-check, and verified-replay manifests
typesafe_eval(*state, questions, contract, model, probabilities, usage, state_json)TypeSafe System One: typed questions answered per row in one request
typesafe_eval_each(segments, *context, questions, contract, model, probabilities, usage, include_segment, max_questions)One contract answered for every segment of a List column, one request per document
noul(instructions, *, true, false) / choice(instructions, criteria) / score(instructions, criteria)Question builders
contract_questions(model) / choice_field(description, criteria) / score_field(description, levels) / typesafe_models()Pydantic contracts and the model catalogue
col(...).llama.inference_local(*, model, system, engine, base_url, max_tokens, temperature, top_p, stop, usage, price_table)Local inference: on-device via mlx-lm, or any OpenAI-compatible local server
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(...), .llama.embedding_local(...), .llama.typesafe_eval(...), and so on). DataFrame-level orchestration functions (induce_codebook, quality_report, the review-loop functions, manifests) 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
TYPESAFE_API_KEY=your_typesafe_key      # TypeSafe System One (0.9.0+)

# 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+
TYPESAFE_BASE_URL=https://api.typesafe.ai              # 0.9.0+

# Bound concurrent in-flight requests per batch (default: 64)
POLAR_LLAMA_MAX_CONCURRENCY=64
POLAR_LLAMA_TYPESAFE_MAX_RETRIES=3   # TypeSafe retry budget for 429/529/5xx (0.9.0+)

# 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)
POLAR_LLAMA_LOCAL_ENGINE=fake       # dependency-free fake engines for generation + embeddings (tests/CI)

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

# Crate unit tests (0.9.0+): drop pyo3's extension-module so the crate links
cargo test --lib --no-default-features

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

# llama.cpp live test (0.8.2+), against a llama-server you started
POLAR_LLAMA_LLAMACPP_URL=http://127.0.0.1:8080 pytest tests/test_llamacpp_server_live.py

# TypeSafe (0.9.0+): runs against a local mock, no key or network needed
python -m pytest tests/test_typesafe.py -q

Changelog Highlights (0.5.2 → 0.9.0)

  • 0.9.0: typesafe_eval (noul / choice / score), Pydantic contracts, and typesafe_eval_each for per-line evaluation in one request per document; cargo test now runs the crate’s unit tests
  • 0.8.3: RunManifest / build_manifest / with_manifest_id / load_manifest / replay: deterministic, integrity-checked audit records with verified replay
  • 0.8.2: Documented and CI-tested engine="server" against llama.cpp’s llama-server on Linux, Windows, and macOS, with a local-backend feature matrix; no library changes
  • 0.8.1: embedding_local via mlx_embeddings (List[Float64], drop-in for embedding_async); FakeEmbeddingEngine for CI
  • 0.8.0: HnswIndex: build / add / remove / query / knn / compact / save / load over a staging buffer + tombstones on instant-distance
  • 0.7.3: export_review_sample, import_corrections (with kappa), corrections_to_trainset, retune_from_corrections; optional [excel] extra
  • 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

🎯Calibrated Triage and Routing

Answer yes/no, choice, and score questions per row or per line with TypeSafe, then act automatically above a confidence threshold and route the rest to a human

🛡️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

🧭Semantic Search and Retrieval

Build a persistent HNSW index once, update it incrementally, and query it from lazy pipelines, with offline embeddings from 0.8.1

🧾Auditable Pipelines

Record a deterministic manifest for every run and replay it later with verified prompts and schemas

Resources
GitHub RepositoryPyPI PackagePolars Documentation

Licensed under MIT.

Questions or issues? Open one on GitHub.