Skip to main content

Cut LLM cost without losing quality: capture traffic, build golden datasets, and optimize prompts.

Project description

runapprentice

Capture your LLM traffic, build a golden dataset, and optimize your prompt against it. Training a small model to take over from the frontier model is planned for a later release.

This README documents the implemented surface (v0.1). Anything not shown here does not exist yet.

Install

uv add runapprentice[langchain]    # [langchain] extra enables zero-code capture

Quickstart (the whole trial journey)

from runapprentice import Apprentice

client = Apprentice(
    api_key="ap_live_...",   # create once in the console, or env APPRENTICE_API_KEY
)                            # talks to the hosted API; set base_url only to self-host

# 1. Create a task (a "task" = one repeatable LLM job you want to make cheaper)
client.tasks.create("ticket-triage")

# 2. Point us at the data you already have (CSV with input/output columns)
client.datasets.upload(
    "ticket-triage",
    path="golden.csv",
    input_col="input",
    output_col="output",
    prompt="...your current system prompt...",   # the baseline to beat
)

# 3. Optimize the prompt against your data (GEPA, held-out eval)
job = client.optimize("ticket-triage")
report = job.wait().report()
print(report.baseline_score, "->", report.optimized_score)

# 4. Pull the optimized prompt back into your code (versioned)
best = client.prompts.get("ticket-triage")
print(best.version, best.text)

Live capture: LangChain (one line, zero code changes)

# init_chat_model comes from your LangChain model package (for example
# langchain[openai]); runapprentice[langchain] only adds langchain-core for
# the callback.
from langchain.chat_models import init_chat_model
from runapprentice.langchain import ApprenticeCallback

model = init_chat_model(
    "gpt-5.5",   # whatever YOU already use. We observe, we don't choose
    callbacks=[ApprenticeCallback("ticket-triage", client)],
)
# Every call now logs input/output/model/latency/tokens to the task's dataset (raw tier).

Fail-open guarantee: the capture path can NEVER break your app. If the Apprentice backend is down, unreachable, or slow, your LLM calls proceed untouched. The only loss is the trace. First drop logs a WARNING, repeats log at DEBUG.

PII redaction runs in your process, before anything is transmitted:

ApprenticeCallback("ticket-triage", client, redact=lambda s: my_scrubber(s))

RAG quickstart (grounding + refusal-aware)

For retrieval-augmented tasks, each row carries the exact context the model saw (the retrieved passages), not just the question. Pick the rag_composite metric so optimization rewards grounding and correct refusals, not just answer overlap.

from runapprentice import Apprentice
client = Apprentice(api_key="ap_live_...")

# rag_composite scores answer correctness + faithfulness to context + refusal
# correctness (it should say "not enough information" when the context lacks it).
client.tasks.create("support-rag", metric="rag_composite")

client.datasets.upload("support-rag", rows=[
    {"inputs": {"question": q, "context": retrieved_passages}, "output": gold_answer}
    for q, retrieved_passages, gold_answer in your_golden_set
])

client.prompts.register("support-rag", {
    "format": "f-string",
    "messages": [
        {"role": "system", "template":
            "Answer using only the context. If the context does not contain the "
            "answer, say you do not have enough information.\n{context}"},
        {"role": "human", "template": "{question}"},
    ],
    "input_variables": ["context", "question"],
})

report = client.optimize("support-rag").wait().report()
print(report.baseline_score, "->", report.optimized_score)

RAG capture for simple chains. For a standard LangChain RAG chain, the ApprenticeCallback records the retrieved context from on_retriever_end, capturing the {question, context} shape. For custom formatting, multiple retrievers, or non-standard chains, call client.capture(..., inputs={"question": ..., "context": ...}) explicitly.

Register a LangChain prompt directly

prompts.register also accepts a LangChain ChatPromptTemplate, no raw dict needed:

from langchain_core.prompts import ChatPromptTemplate
client.prompts.register("support-rag", ChatPromptTemplate.from_messages([
    ("system", "Answer using only the context. If it lacks the answer, refuse.\n{context}"),
    ("human", "{question}"),
]))
# round-trip the optimized prompt back into LangChain:
optimized = client.prompts.to_langchain("support-rag")

Metric menu

metric= Use for Scored by
auto (default) let the backend infer from your rows inferred
json_f1 JSON / structured extraction deterministic
text_f1 short free-text answers deterministic
semantic_f1 free-text / RAG answers (default for RAG) LLM judge
rag_faithfulness is every claim supported by the context LLM judge
rag_composite RAG grounding and refusal correctness LLM judge

RAG rows auto-route to semantic_f1; pass metric="rag_composite" explicitly when you want grounding + refusal optimized together.

Feedback (your end-users' signal)

client.feedback(trace_id, good=True)                  # thumbs
client.feedback(trace_id, score=0.5, note="partial")  # graded

Debugging

import runapprentice
runapprentice.enable_debug_logging()   # or env APPRENTICE_DEBUG=1

Shows every API call with status + latency, every capture attempt, every job poll. Control-plane errors are designed to tell you what to do (e.g. unreachable backend includes the URL it tried; optimize with too few rows tells you the count and the minimum).

API reference (implemented surface)

Call Returns Raises
Apprentice(api_key=, base_url=, timeout=) client ApprenticeError on bad config
client.tasks.create(name, metric="auto") dict (created bool) ApprenticeError on HTTP failure
client.datasets.upload(task, path= or rows=, input_col=, output_col=, prompt=) DatasetStatus same; also if both/neither of path/rows
client.datasets.status(task) DatasetStatus(gold, silver, raw, ready_for_optimization) same
client.prompts.register(task, template) dict ImportError if a LangChain template is passed without the [langchain] extra
client.prompts.to_langchain(task= or artifact=) LangChain prompt ImportError without the [langchain] extra
client.optimize(task) Job 400 if fewer than the backend's configured verified-row minimum (20 by default)
client.job(job_id) / job.refresh() / job.wait(poll_seconds=, timeout_seconds=) Job ApprenticeError on timeout
job.report() OptimizationReport(baseline_score, optimized_score, optimized_prompt, ...) if job has no report
client.prompts.get(task, version=None) PromptVersion(version, text, score) 404 if never optimized
client.prompts.history(task) list[PromptVersion] ApprenticeError on API failure
client.feedback(trace_id, good=, score=, note=) None 404 unknown trace
client.post_trace_failopen(record) trace_id or None never raises
ApprenticeCallback(task, client, redact=None) LangChain callback never raises into your chain

Data tiers (how your rows are treated)

  • raw: captured from live traffic, unverified
  • silver: uploaded by you (curated) or passed deterministic checks
  • gold: human-verified
  • Optimization uses gold + silver; eval gates will use gold only.

For AI coding tools

Module docstrings and this README are the source of truth. Key invariants an agent must preserve when editing this package:

  1. post_trace_failopen and everything in runapprentice/langchain.py must never raise into the caller. Capture is fail-open by contract (see tests/test_failopen.py).
  2. Control-plane methods must raise ApprenticeError with actionable messages.
  3. Dependencies stay minimal: httpx + pydantic; LangChain only via the [langchain] extra; never import from apprentice-api.
  4. Required tests for any new method: add a row to ../needed_test.md.

Project details


Download files

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

Source Distribution

runapprentice-0.1.0.tar.gz (21.3 kB view details)

Uploaded Source

Built Distribution

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

runapprentice-0.1.0-py3-none-any.whl (19.4 kB view details)

Uploaded Python 3

File details

Details for the file runapprentice-0.1.0.tar.gz.

File metadata

  • Download URL: runapprentice-0.1.0.tar.gz
  • Upload date:
  • Size: 21.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for runapprentice-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6534203df90fd4e9f73da38de0f623719ac51ba48982ab64f18b2b30821e7985
MD5 abbf867d0a5a64b11dbc2705e9f6e09f
BLAKE2b-256 98e150fe3b1be182f1a61b47942ce37993b680dbcdc4c72bc06b5a01b8bbe31e

See more details on using hashes here.

Provenance

The following attestation bundles were made for runapprentice-0.1.0.tar.gz:

Publisher: publish-sdk.yml on singh-abhishekk/Apprentice

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file runapprentice-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: runapprentice-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 19.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for runapprentice-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a46dbfe4f7721965630cf6787feeaa8b62eead02749eac6d1728b8a47403d627
MD5 03e4119acc8809bf35ba7d1eb7c67c2a
BLAKE2b-256 0a5c41e776cec718435887db5e2ca8fa4cc37ff0448036dea75bc9863ac61a97

See more details on using hashes here.

Provenance

The following attestation bundles were made for runapprentice-0.1.0-py3-none-any.whl:

Publisher: publish-sdk.yml on singh-abhishekk/Apprentice

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page