TunaRAG API Specification¶
Status¶
This is the living V0.3 Python API contract. The companion
01_API_Specification.docx contains the full design narrative. Implemented and planned APIs
are separated below so design commitments are not mistaken for runtime behavior.
Implemented surface¶
tunarag exports Candidate, MetricValue, UsageRecord, Secret, ValueStatus, objective
types, built-in stop conditions, RetryPolicy, SQLiteStore, study/trial records, and the
cache types CacheKey, CacheEntry, CacheLookup, CacheLookupStatus, CachePrivacy, and
SQLiteCache, search-space types, RandomSearch, LLMSearch, LLMSuggestionRequest, and
LLMSearchProvider. Extension protocols also live in tunarag.contracts.
The evaluator surface includes MetricEvaluator, MetricScorer, RagasEvaluator, and
RagasSampleMapper.
LangChain and LangGraph adapters¶
V0.3 exports LangChainAdapter, LangGraphAdapter, and GoogleADKAdapter from tunarag and
tunarag.integrations. LangChain requires a real Runnable; LangGraph requires a real
CompiledStateGraph; Google ADK requires a real 2.x Runner. Factories are validated on every
sample. Optimizer rejects custom run adapters before creating a store. Framework imports are
lazy and missing extras raise MissingOptionalDependencyError.
LangChainAdapter(
runnable=None,
*,
factory=None,
input_key="question",
input_mapper=None,
config_mapper=None,
output_mapper=None,
usage_mapper=None,
capture_message_usage=True,
identity=None,
)
LangGraphAdapter(
graph=None,
*,
factory=None,
input_key="question",
input_mapper=None,
config_mapper=None,
output_mapper=None,
usage_mapper=None,
capture_message_usage=True,
identity=None,
)
Exactly one fixed runnable/graph or factory(candidate) is required. Canonical
EvaluationExample values become {input_key: example.query}; arbitrary examples pass through
unchanged. Candidate parameters become {"configurable": dict(candidate.parameters)}. Custom
input and config mappers receive the current Candidate, allowing parameters to enter graph
state or rebuild retrievers, prompts, splitters, and models. Output mappers receive raw framework
output. Usage mappers return UsageRecord values.
The adapters always report exact invocation latency. With automatic message usage enabled, they
recursively collect unique mapping, sequence, or message objects exposing standard
usage_metadata.input_tokens and usage_metadata.output_tokens. Provider costs remain unknown
unless an explicit usage mapper supplies them. Framework exceptions propagate to the optimizer's
normal timeout, retry, redaction, and trial-isolation boundaries.
Adapter fingerprints include framework, schema version, runnable/factory type identity, mapper
identities, usage behavior, and caller-supplied semantic identity. They never introspect a
framework object's internal configuration because it may contain credentials. Install supported
framework releases with tunarag-python[langchain], [langgraph], or [frameworks].
Evaluation datasets¶
EvaluationExample is the canonical immutable sample: ID, query, optional reference answer,
reference contexts, relevant document IDs, tags, JSON metadata, DatasetSplit, and a synthetic
flag. Text is normalized to Unicode NFC and LF line endings, with BOM/outer whitespace removed
and unsafe control characters rejected. IDs, queries, list entries, tags, and document IDs must
remain nonempty; tags and document IDs are unique. Metadata is recursively validated as finite
JSON data and deep-frozen.
EvaluationDataset.from_records, .from_json, .from_jsonl, and .from_csv normalize data via
DatasetFieldMap. Missing IDs are deterministic hashes of normalized semantic content. JSON and
JSONL require objects; CSV collection and metadata cells contain JSON arrays/objects. Duplicate
IDs, malformed values, empty datasets, and I/O failures raise redacted DatasetError values with
source location and exception type but no record contents. UTF-8 with an optional BOM is accepted.
The dataset version is a domain-separated SHA-256 hash over schema version, ordered normalized
examples, and split membership; paths and ingestion times are excluded. with_splits(seed=0,
ratios=SplitRatios(), group_metadata_key=None) deterministically assigns 80/10/10 optimize,
validation, and test splits by default. A group key keeps all examples sharing a string/integer
metadata value together. select(split) returns a nonempty split-specific dataset. The existing
InMemoryDataset remains for arbitrary user-defined examples and explicit external versions.
Synthetic QA generation¶
SyntheticDatasetBuilder(provider, samples_per_document=3, concurrency=4, seed=0,
generator_version="unspecified") generates an EvaluationDataset from SourceDocument values.
Providers implement async SyntheticQAProvider.generate(SyntheticGenerationRequest) and return a
SyntheticGenerationResponse containing exactly the requested number of typed SyntheticQA
items plus UsageRecord values. TunaRAG owns bounded document concurrency and validates response
type, count, usage, confidence, and half-open SourceSpan bounds. Provider exception messages are
discarded; DatasetError retains only document ID and exception type.
Each generated example is marked synthetic and stores source document ID/metadata, spans,
provider type and optional fingerprint, generator version, deterministic per-document seed,
confidence, and unreviewed status. Supporting span text becomes reference context; the whole
document is used when no spans are supplied. IDs and dataset versions are deterministic for
identical validated outputs. Provider credentials, prompts, caching, and retry policy remain
provider-owned in V0.1; exact/estimated generation usage is returned separately.
Candidate requires a nonempty parameter mapping. Metrics and usage retain exact,
estimated, or unavailable provenance. Objective.evaluate combines configured terms.
SQLiteStore initializes or migrates experiment schema version 2. It creates and transitions
studies; atomically reserves unique ordered trials; starts, finishes, and recovers leased
attempts; atomically commits terminal metrics, usage, attempt state, trial state, and event;
lists replayable trials, attempt history, metrics, usage, recoverable work, and ordered events;
and closes its connection. Only running studies reserve trials. Terminal states are immutable. Unknown IDs
raise KeyError; invalid transitions raise ValueError.
CacheKey.from_parts(namespace, *parts, format_version=1) creates secret-safe canonical
identity. SQLiteCache.put atomically stores bytes with privacy and optional positive TTL.
SQLiteCache.get returns explicit hit, miss, expired, corrupt, or quarantined status; only a
verified hit contains payload bytes. delete removes one key, while prune removes expired
entries and optionally quarantined entries. Cache metadata and payloads survive reopen.
Settings.resolve applies defaults, an explicitly selected [tunarag] TOML table, known
TUNARAG_ variables, and explicit overrides in ascending precedence. It never loads .env,
reads provider credentials, or creates directories. Settings.as_dict is safe to persist.
RetryPolicy(max_retries=2, sample_timeout_seconds=60.0,
trial_timeout_seconds=300.0, base_delay_seconds=0.5,
max_delay_seconds=30.0) validates bounded attempts and deterministic capped exponential
backoff. None disables an individual timeout when constructing the policy directly. Known
retryable failures are structured TunaRAGError values marked retryable, TimeoutError, and
ConnectionError; arbitrary exceptions are nonretryable.
StopContext carries committed totals, elapsed time, success/failure counts, and objective score
history in durable trial order. Built-in conditions are MaxTrials(limit), CostBudget(limit,
count_estimated=True), TimeBudget(seconds), TargetScore(target), MaxFailures(limit,
consecutive=False), and NoImprovement(patience, min_delta=0, warmup_trials=1). Objective scores
are benefit-oriented, so targets and improvements always use larger-is-better comparisons.
Failures count as non-improving trials; consecutive failures are represented by trailing missing
objective scores. Every built-in condition fingerprints its parameters for resume compatibility.
Public package errors derive from TunaRAGError and expose stable ErrorCode, message, stage,
retryability, and recursively redacted structured details. Implemented subclasses cover
configuration, search space, dataset, adapter, evaluation, store, resume, budget, optional dependency,
and sync-in-async failures. Low-level domain values still raise ValueError or TypeError;
unknown trial IDs raise KeyError; database constraints raise sqlite3.IntegrityError until
their package-boundary translations are implemented.
Search strategies¶
SearchSpace.validate(parameters) requires exact keys and validates integer, float, and
categorical bounds. SearchSpace.describe() produces structured provider input rather than a
package-owned natural-language prompt. RandomSearch remains seeded and isolated.
LLMSearch(space, provider, max_suggestion_attempts=3, fallback_seed=0, fallback=True) calls an
injected async LLMSearchProvider.suggest(LLMSuggestionRequest). Each provider result is treated
as untrusted data and validated against the search space. Provider exceptions and invalid
outputs consume a bounded attempt without exposing exception messages. Exhaustion either samples
a deterministic fallback keyed by suggestion number or raises redacted SearchSpaceError when
fallback is disabled. Requests include provider-safe prior observations; sensitive keys and
Secret values are redacted recursively. Replay advances suggestion numbering and restores
observation context. The strategy fingerprint includes search space, provider type and optional
provider fingerprint, limits, fallback seed, and fallback policy.
Provider-call token/cost accounting is not yet part of the SearchStrategy.suggest return
contract; providers should retain their own usage until the planned strategy-usage extension is
implemented. This limitation is explicit rather than reporting unknown search cost as zero.
Evaluators¶
MetricEvaluator(name, scorer, status=ValueStatus.EXACT) adapts one synchronous or asynchronous
numeric callable to the evaluator protocol. A finite numeric result becomes one metric. None,
NaN, and infinity become an unavailable metric with zero coverage. Booleans and nonnumeric
values raise EvaluationError. The reserved tunarag.objective metric name is rejected.
RagasEvaluator(metrics, sample_mapper, metric_prefix="ragas.", llm=None, embeddings=None,
raise_exceptions=False) is available without importing RAGAS at package import time. The caller
must explicitly map (output, example) to keyword fields accepted by RAGAS SingleTurnSample;
this mapping is the privacy boundary controlling which application data reaches evaluator
providers. Each call builds a one-row EvaluationDataset, invokes async ragas.aevaluate with
progress disabled, and converts the one-row score mapping into MetricValue observations.
Missing/nonfinite scores are unavailable with zero coverage. Provider exception messages are not
included in public errors. Install with pip install 'tunarag-python[ragas]'. The supported V0.1
integration range is RAGAS >=0.4,<0.5; TunaRAG does not configure provider credentials.
Both built-ins implement semantic fingerprints. Callable identity uses module and qualified name;
users should avoid anonymous lambdas for persisted studies that must remain compatible across
code moves. RAGAS metric, LLM, and embedding objects can implement Fingerprintable to include
behavioral configuration without exposing secrets.
Optional MLflow mirror¶
tunarag.integrations.MLflowCallback(store, experiment_id="0", tracking_uri=None, tags=None,
client=None) implements EventCallback. Install with pip install 'tunarag-python[mlflow]';
V0.1 supports MLflow >=3,<4. Supplying a client enables testing or custom authentication without
importing MLflow. A callback instance creates one MLflow run per study encountered in the current
process. On terminal trial events it reads already committed candidates and metrics from SQLite,
logs redacted candidate parameters under sequence-qualified names, logs metrics at the durable
trial sequence step, and records trial status. Study completion/failure/cancellation terminates
the mirror run as FINISHED, FAILED, or KILLED.
SQLite remains authoritative. Local committed state is read before MLflow network calls, and the
blocking low-level client runs in a worker thread without sharing the SQLite connection there.
MLflow failures follow normal callback isolation: they append callback.failed diagnostics and
never alter trial or study outcomes. Resume callback delivery does not replay historical events,
so a newly attached callback creates a new run for resumed events rather than reconstructing the
old mirror. Sensitive keys and Secret values are redacted; MLflow keys are deterministically
normalized to its supported character and length limits.
Optimizer entry point¶
optimizer = Optimizer(
adapter=adapter,
strategy=RandomSearch(search_space, seed=42),
evaluators=evaluators,
objective=objective,
store=store,
cache=cache,
settings=settings,
retry_policy=RetryPolicy(max_retries=2),
)
result = await optimizer.aoptimize(dataset, study_name="support-rag")
callbacks=[...] accepts objects implementing async
on_event(event: StudyEvent) -> None. Events are delivered only after their database
transaction commits and in durable sequence order. New studies deliver their full live event
stream; resume starts after the existing event tail and does not replay historical callback
notifications. Retry scheduling is delivered before backoff. A callback exception never changes
the trial/study result: TunaRAG appends a redacted callback.failed event with callback identity,
source sequence, and exception type, then continues other callbacks. Diagnostic callback events
are not recursively dispatched.
aoptimize is authoritative. The implemented slice runs bounded trial batches, aggregates each
metric by arithmetic mean across samples, preserves coverage and confidence, computes a
benefit-oriented objective, isolates adapter/evaluator/objective failures, persists partial
usage on failure, and observes committed successful trials. Each retry creates a new durable
attempt and appends usage from failed attempts before sleeping. Adapter and evaluator calls use
the sample timeout; the complete candidate execution uses the trial timeout. Duplicate
candidates are rejected; a bounded run of duplicates stops as search exhaustion. optimize
owns an event loop only when none is active and otherwise raises
SyncInAsyncContextError.
The optimizer schedules up to Settings.concurrency trials per batch and executes up to
Settings.sample_concurrency samples within each trial. Trial outcomes and strategy
observations are applied in durable sequence order after a batch, regardless of completion
timing. MaxTrials is a hard reservation cap; snapshot-based cost or time stop conditions can
overshoot by work already in the active batch. Score, stagnation, and failure rules have the same
bounded batch overshoot; use concurrency=1 when exact immediate stopping is required.
Evaluation examples are currently materialized
once per trial before bounded sample tasks start, so callers should keep V0.1 evaluation splits
finite and memory-appropriate.
When a cache is supplied, CachePolicy controls reads and writes. Evaluation cache keys include
the payload format, dataset version, canonical candidate, and adapter/evaluator identities.
Components implementing Fingerprintable.fingerprint() contribute semantic configuration;
other custom components are identified by type only.
Hits restore aggregate metrics, recompute the current objective, incur no current-run usage,
and set TrialSummary.cached=True. Only successful uncached metrics are written, after the
trial commit. Cache I/O or semantic-decoding failures produce durable diagnostic events and
fall back to execution; they do not fail the trial. Custom components should implement a stable,
secret-safe fingerprint when behavior can change without changing the Python type.
await optimizer.aresume(dataset, study_id=...) is the authoritative resume entry point;
optimizer.resume(...) is its sync wrapper and rejects an active event loop. Resume requires a
matching configuration hash, abandons expired attempts only in the selected study, rejects live
leases with retryable ResumeError, reconstructs all terminal summaries and usage, replays
successful trials into the strategy, and continues pending/recoverable work. Compatible
completed studies load without adapter/evaluator execution. Failed and cancelled studies are
terminal. The optional ReplayableSearchStrategy.replay contract lets a strategy distinguish
state restoration from live observation. Replay-aware strategies receive every durably reserved
candidate, including failed or unfinished trials; ordinary strategies receive only successful
observations. RandomSearch uses replay to verify candidates and advance its seeded generator.
OptimizationResult exposes study and stop status, all terminal trial summaries, best trial
and candidate, successful/failed subsets, aggregate usage with exact-versus-estimated cost,
and a valid no-success outcome. ranked_trials orders successful available objectives from best
to worst with trial sequence as the stable tie-breaker. metric_names returns a lexical union;
metric_leaders(name, direction=...) independently ranks available values for one metric.
TrialSummary.metric(name) returns one observation or None.
as_dict() returns report schema version 1 with summary counts, aggregate usage, redacted
candidates, per-trial metrics, raw usage records, objective, attempts, cache state, and bounded
error text. to_json() is deterministic UTF-8-capable JSON; write_json(path) adds a final
newline. write_csv(path) emits one trial per row, a redacted canonical candidate JSON cell,
trial/objective/attempt/cache fields, per-trial usage totals, and three columns per metric for
value, status, and coverage. Writers replace existing files but do not create parent directories.
Secret values and sensitive mapping keys are recursively redacted in both formats.
Each TrialSummary reports attempt count and whether metrics came from cache. InMemoryDataset
provides a repeatable versioned dataset. Formal Pareto/frontier reporting remains post-V0.1;
V0.1 ranks the configured scalar objective and individual metrics. Study forking, streaming
sample windows, and mandatory custom-component fingerprints are post-V0.1 work.
Versioning¶
V0.3 removes the generic public RAG adapter protocol and restricts Optimizer(adapter=...) to
LangChainAdapter | LangGraphAdapter | GoogleADKAdapter. A wrong adapter type raises TypeError
before allocating a store. A wrong factory result fails its trial through normal retry policy.
GoogleADKAdapter(runner=None, *, factory=None, state_mapper=None, output_mapper=None, identity=None)
requires exactly one fixed runner or synchronous candidate factory. state_mapper(candidate)
returns initial session state; without it, the candidate parameters are used. output_mapper(event)
receives the final ADK event; by default, the output is an answer mapping containing text parts
without reasoning parts. A new session is created for every sample/retry and deleted after
execution, including failure or cancellation. The application retains runner ownership.
Missing final responses or error events raise AdapterError; missing SDKs raise
MissingOptionalDependencyError. Reported input/output tokens and runner latency are returned as
UsageRecord values; provider cost is unavailable by default.
The distribution is tunarag-python; the import package is tunarag. Public APIs follow
semantic versioning. SQLite schemas and cache formats have independent versions and explicit
migration or invalidation rules.
Version 0.1.0 is the first feature-complete V0.1 contract. During the 0.x series, incompatible
public API changes require a minor-version increase and are recorded in CHANGELOG.md.
Version 0.2.0 adds optional framework adapters without changing the V0.1 optimizer contracts.