Skip to main content

langchain-scrubkit

Clean tool outputs in LangChain, before your agent ever reads them. Fixes broken encoding, leftover HTML and invisible characters — and tells the agent when a tool came back with a bot wall instead of content.

PyPI Python License

Your scraping tool returns Café <b>Noir</b>&nbsp;. Your model reads it, and reasons about the damage as though it were content. There is no later stage that undoes that.

pip install langchain-scrubkit
from langchain.agents import create_agent
from langchain_scrubkit import ScrubkitMiddleware

agent = create_agent(model, tools=[scrape], middleware=[ScrubkitMiddleware()])

That is the whole setup. Every tool result is now repaired at the moment it is produced — the conversation never contains the broken version.


What your agent stops seeing

Tool returns Model reads
Café Café
<p>Warm light.</p><p>Fits any room.</p> Warm light. Fits any room.
Solid oak &mdash; seats&nbsp;two. Solid oak — seats two.
Tokyo​ Ltd­ Tokyo Ltd

Deterministic, no LLM, no network call. The same input always produces the same output, so a cached run stays a cached run.

The part nobody else does

A scraper that hits a bot wall returns HTTP 200 and a page saying Access denied. Every agent framework hands that to the model as a normal tool result. The model has no way to know the extraction failed, so it treats the error page as data and answers confidently from nothing.

Without the middleware, this is what reaches the model:

Please wait, verifying you are human

With it, the same result carries its diagnosis:

Please wait, verifying you are human

[scrubkit] This tool result looks like what a bot wall or error page returns
(matched 'please wait'), not the page's content. The extraction probably
failed: re-run the tool or try another source rather than treating the text
above as data. If the text is genuinely about that phrase, ignore this note.

The agent can now retry, switch source, or tell the user it failed — instead of inventing an answer. The original text is kept, never replaced.

Three modes:

ScrubkitMiddleware(on_flags="note")    # default: annotate, let the agent decide
ScrubkitMiddleware(on_flags="ignore")  # repair silently
ScrubkitMiddleware(on_flags="raise")   # stop the run (ExtractionFailedError)

What this saves you writing

Without it, cleaning tool output means writing the middleware yourself:

@wrap_tool_call
def clean_tool_output(request, handler):
    result = handler(request)
    result.content = ftfy.fix_text(result.content)   # and then?
    return result

That one line is the easy part. What is left:

  • HTML — ftfy does not strip tags. <p>A</p><p>B</p> must not become AB.
  • content is not always a string. It is typed str | list[str | dict]; the list form carries content blocks, and a text repairer must not touch an image payload.
  • JSON tool results. Cleaning a whole JSON document as one opaque string works badly — and parsing it means you now own re-serialisation, and the decision not to reformat a document that was already fine.
  • CSV tool results must not come back as JSON.
  • Knowing what not to touch. "None" is a surname, "NA" is Namibia, ‌ is a required letter-shaping character in Persian, and the joiner inside 👨‍👩‍👧 holds a family emoji together.
  • Never breaking the agent. A repair that throws must not end someone's run.
  • The bot-wall case above, which no text repairer addresses at all.

This package is those decisions, tested. If your only problem is broken Unicode, ftfy is excellent and you may not need anything else.

What it does not do

By default it runs only scrubkit's AUTO tier: seven rules that provably cannot change a value's meaning, its type, or the shape of your data. It cannot drop a row, retype a field, or rename a key unless you ask by name.

It does not touch user input or model output. Those did not come from a scraper; repairing them would be overreach.

And it never destroys a legitimate value:

Input Kept as-is because
"None" it is a common surname
"NA" it is Namibia's ISO country code
می‌تواند the zero-width non-joiner is required Persian orthography
👨‍👩‍👧 removing the joiner splits one family into three people
AT&T, 5 < 7 a real HTML entity ends in ; — these are not markup
3 m², ½ cm Unicode NFKC would rewrite these to 3 m2 and 1/2

A known limitation, stated rather than hidden. Bot-wall detection matches phrases, with no notion of context. A product genuinely called CAPTCHA Solver Pro, or an article titled A guide to CAPTCHA design, trips the same rule as a real captcha page. That is why the note quotes what triggered it and hedges rather than asserts: the model sees both the text and the reason, and can disagree with us. Nothing is deleted either way.

Usage

Clean only the tools that reach the open web. A calculator or a SQL tool has nothing to repair:

ScrubkitMiddleware(tools=["scrape_products", "fetch_page"])

Opt into the riskier repairs, each off by default because each changes something you may depend on:

ScrubkitMiddleware(
    placeholder_policy="null_high_confidence",  # "N/A" -> None; "None" stays
    drop_exact_duplicates=True,                 # changes your row count
    coerce_numeric_text=True,                   # changes a value's type
    repair_keys=True,                           # changes your schema
)

For chains rather than agents, the same cleaning as a Runnable:

from langchain_scrubkit import scrub

chain = scraper | scrub() | prompt | model

scrub() preserves shape — a string stays a string, a row stays a row, a list keeps every element. It defaults to on_flags="ignore", since a chain has no conversation to annotate; on_flags="raise" is the useful one there, to stop rather than write a captcha page into your vector store.

Verify it yourself

pip install "langchain-scrubkit[test]"
python -m pytest --pyargs langchain_scrubkit

101 tests ship inside the package. They cover shape safety, the control set of legitimate data above, both content forms, sync and async, and — through a real create_agent — that a broken tool result reaches the model repaired.

The engine

The cleaning itself is scrubkit: a standalone, dependency-free Python library under Apache-2.0. Use it directly if you are cleaning scraped data outside LangChain.

The same engine also runs as a hosted service at aidatatools.dev, which adds dataset-level quality scoring that neither library includes.

Requirements

Python 3.10+, langchain>=1.0. The middleware hooks wrap_tool_call, checked against langchain 1.0.0 and 1.3.x.

License

Apache-2.0. See LICENSE.

Metadata

Release files for langchain-scrubkit 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for langchain-scrubkit 0.1.0
File Size Uploaded
langchain_scrubkit-0.1.0.tar.gz 30.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for langchain-scrubkit 0.1.0
File Interpreter ABI Platform
langchain_scrubkit-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 64.5 kB

Release files / langchain_scrubkit-0.1.0.tar.gz

Download URL langchain_scrubkit-0.1.0.tar.gz
Size 30.7 kB
Tags Source
SHA-256 checksum
How to use checksums
59df44abaec3068721e8f0d16adec17da4a5f5ab0f1a4e762294f629d1f57aa0
BLAKE2b-256 checksum
How to use checksums
05caecb98e181769344a7931fcfa51d8284c3a7cb51b005d00b3ed6cde2b98a7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.3

Release files / langchain_scrubkit-0.1.0-py3-none-any.whl

Download URL langchain_scrubkit-0.1.0-py3-none-any.whl
Size 33.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5594b119e0f822f2f55b1dda0865b4071866b6205041f33c226c8383609e9256
BLAKE2b-256 checksum
How to use checksums
88e19111d7b8c24b905d833492a30be4a45273422cea38812ebb0548ead6a5d3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.3

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page