Catch and fix hallucinations in any LangGraph workflow.
perf-langgraph drops Perf into your LangGraph in three lines: add a node, add an edge, get auto-corrected LLM output before downstream nodes see the mistake.
pip install perf-langgraph
export PERF_API_KEY=pk_...from langgraph.graph import END, StateGraph
from perf_langgraph import PerfRepair
graph = StateGraph(MyState)
graph.add_node("llm", call_llm)
graph.add_node("verify", PerfRepair()) # <-- new
graph.add_edge("llm", "verify") # <-- new
graph.add_edge("verify", END) # <-- new (was llm -> END)That's it. The LLM output flows through Perf's correction engine. Hallucinations are detected and rewritten before downstream nodes see the broken text. The corrected version is left in state["messages"] (or state["output"], or wherever the source text lives).
- Verifies LLM output against trusted sources via Perf's CER — web search, citation databases, NLI, internal consistency checks.
- Repairs errors automatically — temporal mistakes, fabricated citations, numerical errors, schema violations. Returns the corrected text plus a list of changes.
- Routes the result through standard LangGraph conditional edges.
pass/corrected/rejected/error.
Both nodes wrap the same engine. They differ in what they do with the result:
PerfVerify— runs verification and writes claim-level evidence tostate["perf"]. Does not modify the source text. Use when you want a routing decision without auto-correction.PerfRepair— runs verification and applies corrections. Writes the corrected text back to the source path in state by default. Use when you want the fix applied. Most users want this.
perf_router reads state["perf"]["status"] and returns one of "pass", "corrected", "rejected", or "error". Drop it into add_conditional_edges:
from perf_langgraph import perf_router
graph.add_conditional_edges(
"verify",
perf_router,
{
"pass": "respond",
"corrected": "respond", # often the same as pass
"rejected": "regenerate", # or route to human review
"error": "fallback_handler",
},
)For stricter routing where any modification should follow the recovery path, use strict_perf_router (it folds corrected → rejected).
After a Perf node runs, state looks like:
state["perf"] = {
"node": "verify" | "repair",
"status": "approved" | "corrected" | "rejected" | "error",
"verified_text": str, # original (verify) or corrected (repair)
"original_text": str,
"was_corrected": bool,
"confidence": float,
"evidence": [...], # claim-level (verify) or change-level (repair)
"error": None | str,
"latency_ms": int,
"mode": "fast" | "standard" | "thorough",
# repair-only:
"error_types_detected": list[str] | None,
}The node reads input from state["messages"][-1].content by default, falling back to state["output"]. Override with text_path="my_field" on the node constructor.
The router (PerfRepair only, with mutate_source=True, default): writes corrected_text back to whichever path the input came from. For messages, it appends a new AIMessage with the corrected content (preserving the original in the message history). For string fields, it replaces in place.
Both nodes share most options:
| Parameter | Type | Default | Description |
|---|---|---|---|
api_key |
str | None |
PERF_API_KEY env |
Perf API key. |
base_url |
str | None |
PERF_API_BASE env or prod |
API base URL. |
text_path |
str | None |
auto | State path to read input from. |
output_key |
str |
"perf" |
Where to write the result. |
on_failure |
"raise" | "return" | "fallback" |
"fallback" |
What to do on Perf API errors. |
timeout |
float |
30.0 |
Per-call timeout in seconds. |
max_retries |
int |
2 |
Retry count for retryable errors. |
PerfVerify-specific:
| mode | "fast" \| "standard" \| "thorough" | "standard" | CER mode. |
PerfRepair-specific:
| correction_budget | "fast" \| "thorough" | "fast" | Correction depth. (Note: no "standard" — /v1/correct doesn't accept it.) |
| original_prompt_path | str \| None | None | State path with the prompt that produced the content. Improves CER context. |
| target_schema | dict \| None | None | JSON Schema for schema-based correction. |
| mutate_source | bool | True | If True, write corrected text back to the source path. |
"raise"— re-raise the SDK'sPerfError. Crashes the graph. Good for dev."return"— setstatus="error"and let the router decide.verified_textisNone."fallback"(default) — setstatus="error"AND populateverified_textwith the original (un-verified) text. Production-safe — graph keeps running even if Perf is unreachable.
Both nodes are LangChain Runnables, so await graph.ainvoke(state) works:
result = await graph.ainvoke({"messages": [...]})How is this different from LangChain output parsers? Output parsers validate format (does this match a schema?). Perf validates truth (is this claim actually correct?) and corrects errors against trusted sources.
Does Perf replace LangGraph? No. LangGraph orchestrates where, when, how LLM calls happen. Perf operates on the output of any individual call. Different layer entirely. Use them together.
What modalities are supported? This package handles text. Perf itself supports text, images, audio, and video — the broader SDK provides multi-modal endpoints.
What about cost? Per-call pricing on the Perf side. See withperf.pro for current rates.
MIT