Metadata-Version: 2.4
Name: relapse
Version: 0.1.0
Summary: Self-correcting LLM call wrapper: compress input, call the model, ground-check the answer, and on hallucination restructure the prompt and retry. The runtime guard rails for RAG.
Author: Asmit Dash
License: MIT
Project-URL: Homepage, https://github.com/asmitdash/relapse
Project-URL: Issues, https://github.com/asmitdash/relapse/issues
Keywords: rag,llm,hallucination,retry,self-correction,guardrails
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: ground
Requires-Dist: corroborate>=0.1; extra == "ground"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# relapse

**Self-correcting LLM call wrapper for RAG.** Wraps your LLM call in a feedback loop: call → ground-check the answer → restructure the prompt with explicit findings → retry. The first call uses your prompt verbatim; only on hallucination does relapse rewrite and re-ask. The user sees the final grounded answer, plus a transcript of what was rejected and why.

```bash
pip install relapse                  # core (Python stdlib only)
pip install relapse[ground]          # optional: pulls corroborate for richer grounding
```

```python
import relapse

def my_llm(prompt: str) -> str:
    # your wrapper around Anthropic / OpenAI / Bedrock / local model
    return client.messages.create(...).content[0].text

result = relapse.run(
    query="When did WW2 end?",
    retrieved_chunks=["World War II ended in 1945 with the surrender of Japan in September."],
    call_llm=my_llm,
    max_iterations=3,
)

print(result.answer)        # the final grounded answer
print(result.iterations)    # how many tries it took
print(result.grounded)      # True if the final answer passed the check
print(result.transcript)    # list of (prompt, answer, findings) per iteration
```

That's the whole API. `call_llm` is **provider-agnostic** — pass a function that takes a prompt and returns text. relapse doesn't bake in Anthropic, OpenAI, or anything else.

---

## What relapse is for

LLM hallucination at the answer layer. You can't fix it with better retrieval — the model has the right context but invents a fact anyway. The standard fix is "retry with feedback," but most teams hand-roll it badly: they retry on a vague "are you sure?" prompt, which often produces the same hallucination with more confidence.

relapse runs a **deterministic** check after each call (numbers, years, quoted spans must appear in the retrieved chunks), and feeds the *specific failed spans* back into the next prompt. That's what makes the retry productive.

---

## How it works

```
user query + retrieved chunks
       │
       ▼
┌─────────────────────────┐
│  iteration 1            │
│  build prompt with ctx  │
│  call_llm(prompt)       │
└─────────────────────────┘
       │
       ▼
┌─────────────────────────┐
│  grounding_check        │   <-- deterministic, no extra LLM call
│  - numbers in chunks?   │
│  - years in chunks?     │
│  - quotes in chunks?    │
└─────────────────────────┘
       │
       ▼
   grounded? ──yes──► return answer
       │
       no
       ▼
┌─────────────────────────┐
│  iteration N+1          │
│  rewrite prompt:        │
│   - quote prev answer   │
│   - cite each failed    │
│     span by code        │
│   - tell model to use   │
│     ONLY the context    │
│  call_llm(prompt)       │
└─────────────────────────┘
       │
       └─► loop until grounded OR max_iterations
```

Iteration 1 = the user's original prompt. Iterations 2+ explicitly cite what was wrong. relapse never silently rewrites a fine answer.

---

## Pluggable grounding check

The default check is a vendored copy of [corroborate](https://github.com/asmitdash/corroborate)'s FG001/002/003 (numbers / years / quoted spans). It's deterministic, sub-millisecond, and never produces a false negative on those classes.

For richer checks, plug your own:

```python
import corroborate

def my_check(query, chunks, answer):
    rep = corroborate.check_answer(query, chunks, answer)
    return relapse.GroundingResult(
        ok=rep.ok(),
        findings=tuple(
            relapse.GroundingFinding(code=f.code, message=f.message, spans=f.spans)
            for f in rep.findings
        ),
    )

result = relapse.run(query, chunks, call_llm=my_llm, grounding_check=my_check)
```

Or write a custom check that uses any signal you want — entity matching, NLI scoring, an LLM-as-judge if you really want one. relapse just expects `(query, chunks, answer) -> GroundingResult(ok, findings)`.

---

## Optional compression

```python
result = relapse.run(query, chunks, call_llm=my_llm, compress=True)
```

`compress=True` runs a light deterministic pass before the first LLM call: dedup, whitespace-normalize, cap total characters to 8000. Real semantic compression should use an LLM — that's not what relapse is for. Compression is **opt-in** because it changes token cost and the first-call behavior, so you have to ask for it.

---

## API reference

```python
relapse.run(
    query,                              # str
    retrieved_chunks,                   # list[str] or list[{"text": str, ...}]
    *,
    call_llm,                           # callable(prompt: str) -> str
    grounding_check=None,               # callable(query, chunks, answer) -> GroundingResult
    compress=False,                     # opt-in light compression
    max_iterations=3,                   # cap LLM calls
) -> RelapseResult
```

`RelapseResult`:
- `answer` — the final answer string.
- `grounded` — True iff the final answer passed the grounding check.
- `iterations` — number of LLM calls made (1 on success, ≤ max_iterations on failure).
- `transcript` — list of `IterationRecord(iteration, prompt, answer, grounded, findings)`.

`GroundingFinding(code, message, spans)`. `GroundingResult(ok, findings)`.

---

## Cost considerations

relapse calls your LLM up to `max_iterations` times. The default of 3 means **worst case 3× the token cost** of a naive single call. In practice:

- Most non-hallucinated answers terminate at iteration 1 (no extra cost).
- The retry prompt is longer (it includes the previous answer + findings), so a retry is ~1.3-1.5× a fresh call.
- Most hallucinations terminate at iteration 2 — the explicit "you said X but X isn't in the chunks" rephrasing is highly corrective.

If cost matters more than catch rate, lower `max_iterations` to 2. If you need maximum safety, raise it to 5.

---

## What relapse is NOT

- Not a runtime moderation layer — for that, see [LLM Guard](https://github.com/protectai/llm-guard) or [NeMo Guardrails](https://github.com/NVIDIA/NeMo-Guardrails).
- Not a retrieval improver — broken retrieval is upstream of relapse. See [chaffer](https://github.com/asmitdash/chaffer) for that.
- Not an answer reranker — relapse retries the same model with a tighter prompt, it doesn't pick between candidates.
- Not a free lunch on cost — see "Cost considerations" above.

---

## See also

- **[corroborate](https://github.com/asmitdash/corroborate)** — the standalone deterministic answer-grounding check. relapse vendors its core; for rich grounding, install corroborate and pass `corroborate.check_answer` as `grounding_check`.
- **[chaffer](https://github.com/asmitdash/chaffer)** — corpus linter (retrieval-quality bugs).
- **[redoubt](https://github.com/asmitdash/redoubt)** — corpus linter (prompt-injection scanner).
- **[staleness](https://github.com/asmitdash/staleness)** — corpus linter (freshness / drift).
- **[dash-mlguard](https://github.com/asmitdash/dash-mlguard)** — same author, same form factor, but for ML training pipelines.

---

## License

MIT — see [LICENSE](LICENSE).
