rag-jev
Filter and rerank retrieved passages before they reach your answering model.
your retriever → rag-jev / Jev → your answering model
Keep your existing ingestion, vector store and answer model. Inspect each selection, replay different cutoffs, preserve connected evidence, and measure the tradeoff on your data. Python, HTTP, TypeScript, LangChain and Dify integrations are included.
MIT-licensed integration. Live scoring uses the external TypeSafe Jev API and requires its key. Query and passage text leave your application during live scoring; arbitrary metadata and evaluation labels stay local. Optional answer comparison uses your configured model endpoint. See data handling.
Try it without an API key
git clone https://github.com/EmreKaplaner/rag-jev.git
cd rag-jev
uv sync --locked
uv run rag-jev serve --replay-only
Open http://127.0.0.1:8000. Explore a recorded improvement and a recorded regression, inspect dropped passages, and change selection settings without scoring again. Recorded answers are clearly labeled. Live inference is disabled in replay mode, even with keys in the environment. The separate hand-authored fixtures are labeled as fixtures.
Python 3.11+ and uv are required. Node 20+ is needed only for TypeScript development. Release wheels and npm tarballs are available from GitHub Releases.
Use live scoring
Copy .env.example to .env, set TYPESAFE_API_KEY from TypeSafe,
and restart without --replay-only. An answer-model key is optional.
uv run rag-jev serve
uv run rag-jev select examples/request.json
For your application:
from rag_jev import ContextSelector, Document, Jev
async with Jev() as provider:
selector = ContextSelector(provider)
result = await selector.select(
query=question,
documents=[Document(id=c.id, text=c.text) for c in retrieved_chunks],
scoring_strategy="contextual",
mode="filter_and_rerank",
min_relevance=0.20, # Illustrative; calibrate on your data.
max_context_tokens=3000, # Passage-text estimate; leave room for prompts/output.
shadow=True, # Observe proposed selection while returning original context.
)
Real RAG integration guide · Calibrate on your data · Independent human review · Contribute
What the measurements show
| Evaluation | Observed result | Scope |
|---|---|---|
| SciFact retrieval | NDCG@10: 75.13 Jev / 72.11 Ettin / 66.47 BM25 | All 300 test queries; same BM25 top20 candidates |
| HotpotQA answers | F1 76.70 → 77.20; estimated API cost −19.7% | 200 isolated questions; three answers per arm/question |
| MuSiQue answers | F1 70.31 → 72.80; estimated API cost −22.8% | 200 isolated questions; three answers per arm/question |
Both answer-F1 difference intervals include zero. The answer-accuracy superiority gate was not met. Costs use uncached normalization and include Jev; Ettin CPU costs are separate. These are scoped results, not universal improvements. Human adjudication is pending.
Controlled study and uncertainty · SciFact results · Methods and reproduction · Robustness diagnostics · Demo script
The historical research bundle preserves original code and raw records. Its fingerprints should be checked inside that bundle; the current product has evolved since the experiment.
Python
Install this checkout with pip install . or use uv sync. The distribution is
rag-jev, the import is rag_jev. PyPI/npm registry publication is pending; downloadable
packages are provided in the GitHub release.
import asyncio
from rag_jev import ContextSelector, Document, Jev
async def main():
async with Jev() as provider: # reads TYPESAFE_API_KEY from the environment
selector = ContextSelector(provider, timeout_ms=5000, max_concurrency=8)
result = await selector.select(
query="What is the refund window?",
documents=[
Document(id="shipping", text="Orders ship within 5 business days."),
Document(id="refund", text="Request a refund within 30 days.",
metadata={"source": "refunds.md", "page": 2}),
],
min_relevance=0.20,
shadow=True,
)
# Feed result.documents to your existing generator.
# In shadow mode these are the originals; selected_ids shows what would change.
print(result.model_dump_json(indent=2))
asyncio.run(main())
The library does not implicitly read .env; export the key or call
dotenv.load_dotenv() yourself. See examples/python_rag.py.
Reuse one provider and selector per application event loop, then close the provider
at shutdown. The concurrency bound is shared across all requests using that selector.
Any retriever can supply {id, text, metadata} objects. For frameworks, map their
documents into this shape and use returned IDs to recover the original objects.
Include relevant titles/headings in text: metadata is preserved but not sent
to Jev. The query, passage text, and explicit relevance guidance are sent to TypeSafe.
TypeScript
npm --prefix clients/typescript ci
npm --prefix clients/typescript run build
# With the service already running:
node examples/typescript-rag.mjs
The client README covers local packaging and usage in another project. The client uses generated OpenAPI types, validates responses, supports caller cancellation, and has no runtime dependencies. Use it server-side.
Selection contract
POST /v1/select accepts the same options as selector.select(...):
| Argument | Behavior |
|---|---|
query |
Required nonblank text, up to 8,000 characters |
documents |
Up to 256 documents with unique nonblank string IDs |
mode |
filter (default), rerank, or filter_and_rerank |
min_relevance |
Required for filtering, in [0,1]; rejected in rerank-only mode |
top_n |
Optional positive target after thresholding; pins and whole groups may exceed it |
max_context_tokens |
Optional passage-text token budget; preserves pins and whole groups, reports pinned overruns |
scoring_strategy |
independent (default) or contextual shared-candidate scoring |
retrieval_query |
Optional standalone query for conversational retrieval |
shadow |
Default false; when true, return unchanged context and proposed decisions |
relevance_guidance |
Optional domain-specific criteria, up to 2,000 characters |
Filtering retains scores greater than or equal to the threshold. Reranking sorts descending; ties preserve input order. Reranking uses probability of usefulness, not a graded relevance rubric. Empty inputs make no provider calls. If every chunk fails the threshold, a successful request returns an empty selection.
Independent scoring uses one Jev request per passage with a yes/no (Noul) question.
Contextual scoring judges the candidate passages together and reports shared usage. The rubric includes
partial evidence, useful context, and facts correcting a false premise. It does not
require one passage to independently answer the whole query. A Noul has no separate
confidence field; uncertainty is reflected in its probability.
The response includes:
documents: the actual context to send downstream.selected_ids: the proposed ordered selection, ornullwhen selection was bypassed.decisions: one row per original chunk, in original order, with probability, resolved model, observed tokens, selected/returned flags, and a policy reason.status:applied,shadow, orbypassed.error_code: a sanitized failure code on bypass.models,prompt_version, andpolicy_version: reproducibility information.usage,elapsed_ms, and original/selected/returned character counts.
Reasons are retained, below_threshold, beyond_top_n, beyond_token_budget, pinned, group_retained,
and bypassed. They are
policy explanations, not model-generated rationales. Shadow decisions distinguish
what would be selected from what was actually returned.
Failures and limits
The total deadline includes queueing, network calls, and retries. The SDK may retry
once within that budget. On an upstream error, timeout, or invalid response, the
default library/service policy returns the entire original context, even when
top_n was requested. status="bypassed" makes that explicit. Partial rankings
are never applied. Outstanding tasks are canceled and awaited.
Use ContextSelector(..., on_error="raise") to raise ProviderError instead.
The service's equivalent is RAG_JEV_ON_ERROR=raise; failures become HTTP 502
(provider) or 504 (deadline). The CLI always raises on upstream failure and exits
nonzero, so evaluations cannot accidentally count passthrough as successful scoring.
Caller cancellation propagates rather than becoming passthrough.
On bypass, observed token usage can undercount provider billing: canceled or failed
calls may already have consumed tokens. usage.complete=false records this.
Core selection reports character counts. The workbench adds labeled token estimates and,
after optional generation, actual provider prompt/output usage plus configured-rate cost estimates.
Input limits: 60,000 characters per passage, 1,000,000 passage characters in total, and 2 MB per HTTP body. These are local bounds, not promises about the provider's token budget. Oversized or malformed input is rejected, never silently truncated. Provider context-limit errors use normal failure handling.
TYPESAFE_API_KEY is required to start the live service. There is no silent demo
mode. /healthz checks service liveness, not provider availability.
| Environment variable | Default |
|---|---|
TYPESAFE_API_KEY |
Required for real calls |
RAG_JEV_MODEL |
jev-latest; pin a returned model version for repeatable evaluation |
RAG_JEV_TIMEOUT_MS |
5000 |
RAG_JEV_MAX_CONCURRENCY |
8 per selector / server process |
RAG_JEV_ON_ERROR |
passthrough for the service |
RAG_JEV_API_TOKEN |
Optional on localhost; required by CLI for external binds |
The service bearer token is separate from the provider key. When configured, send
Authorization: Bearer <service-token> to /v1/select. Keep the service behind
TLS/authenticated infrastructure if exposed beyond localhost. This MVP is a library
and single-service deployment, not a multitenant hosted SaaS. API access logs are
disabled by the CLI and input/response bodies are not logged by application code.
Evaluate before enabling
Evaluation cases contain id, query, documents, relevant_ids, and an optional
reference_answer. Use representative positive, ambiguous, contradictory, and
unanswerable queries. Labels never enter the Jev request.
Offline demonstration:
uv run rag-jev eval examples/eval-cases.json \
--replay examples/fixture-traces.json --min-relevance 0.20
Live scoring, then offline threshold adjustment:
mkdir -p artifacts
uv run rag-jev eval examples/eval-cases.json \
--min-relevance 0.20 --save-traces artifacts/live-traces.json \
--output artifacts/live-report.json
uv run rag-jev eval examples/eval-cases.json \
--replay artifacts/live-traces.json --min-relevance 0.35 \
--output artifacts/threshold-035.json
The report compares baseline retrieval, filter, rerank, and filter-and-rerank. Jev scoring happens once per passage; policy comparisons reuse those judgments. Saved traces include an input fingerprint and prompt version, and mismatches fail. Traces contain judgments and input hashes, not passage text.
Metrics include precision, evidence recall within retrieved candidates, binary nDCG,
relevant chunks dropped, total loss of useful evidence, empty-result correctness,
context characters, actual scoring token usage, and observed scoring p95 latency.
Summary values are query-level means. Recall/nDCG exclude no-relevant-document
queries; empty-result correctness measures those separately. Precision is zero for
empty outputs. A shared top_n cap applies to every policy, including baseline;
nDCG uses that fixed cutoff (or candidate count), padding shorter outputs with zeros.
These metrics do not establish answer correctness. The report explicitly says
answer_quality="not_evaluated" unless you pass an application callback to the Python
evaluate() function:
from rag_jev.evaluation import evaluate
async def generate_and_grade(case, documents):
answer = await your_generator(case.query, documents)
return await your_grader(case.query, answer, case.reference_answer) # bool
report = await evaluate(cases, traces, min_relevance=0.20,
answer_evaluator=generate_and_grade)
The callback runs once per case per policy. Its inference cost/latency is not part of the scoring metrics. Evaluate it separately with your existing generation stack. Filtering cannot recover missing evidence, guarantee correct judgments, or act as a prompt-injection security boundary.
Development and verification
make setup
make check
uv build
make check runs lint/format checks, strict Python type checking, TypeScript build
and client tests, then Python tests including real local socket connections:
TypeScript client → HTTP service → selector → official TypeSafe SDK → test HTTP endpoint
The controlled endpoint exercises provider responses, failures, and deadlines. It does not verify live Jev quality. Real calls are explicitly opt-in:
RAG_JEV_LIVE=1 uv run pytest tests/test_live.py -q
Generate updated HTTP/client contracts with make schema after changing models.
uv.lock and the TypeScript package-lock.json are checked in. Build distributions
with uv build; build and npm pack the TypeScript client for another application.
The core is in src/rag_jev, examples in examples, and
contract/failure/integration tests in tests. No account system, database,
semantic cache, or scorer-provider switching layer is required for this MVP. The workbench
is bundled with the service. Native LangChain and Dify integrations are documented in
the real-RAG guide.
Official references: TypeSafe SDK, Noul, RAG passage filtering.
Metadata
Release files for rag-jev 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rag_jev-0.2.0.tar.gz | 505.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rag_jev-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 645.0 kB
Release files / rag_jev-0.2.0.tar.gz
| Download URL | rag_jev-0.2.0.tar.gz |
|---|---|
| Size | 505.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a23363504aab8b92fc040bccc85df7a166df6e30ddc31a2d3adc83936fb7e874
|
|
BLAKE2b-256 checksum How to use checksums |
628b0da18490fb9d342130ea352ec5af06ad4209af803ad4a2460e819c1292bc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / rag_jev-0.2.0-py3-none-any.whl
| Download URL | rag_jev-0.2.0-py3-none-any.whl |
|---|---|
| Size | 139.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ee560b5718c7a6f0ce0c39d136b2b311de2816e0691b15dd6bed778de7fe31a7
|
|
BLAKE2b-256 checksum How to use checksums |
5e27dc79471b025e63e4db42bd9788bae0e5b14e6b8551d2c1ac3b98fa62e496
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|