Agentic Filter
Overview
The agentic filter op keeps or drops each unit of a The Corpus based on a
natural-language criterion. It is an instantiation of Agentic Map-Reduce’s
map: it runs the same tool-using agent over the corpus, but reads each unit’s result
as a keep/drop verdict and returns the surviving units.
Reach for an agentic filter (over sem_filter, which is one model call per row) when
the keep/drop decision can’t be made by reading alone — it needs to run code, parse
content, compute a value, or otherwise investigate. If a single LLM call over the row would
do, use sem_filter instead.
Use it standalone to narrow a corpus, or as a stage of a pipeline that then maps and/or reduces the survivors.
Example: keep only the buggy functions
Deciding whether a function has a bug is not reliable by inspection — you have to run it. This is exactly where an agentic filter earns its keep: each agent executes its function in a sandboxed REPL and keeps only the ones that misbehave.
import lotus
from lotus.models import LM
from lotus.tools import PythonREPLTool
lotus.settings.configure(lm=LM(model="gpt-5", reasoning_effort="low"))
snippets = [
"def average(nums): return sum(nums) / (len(nums) - 1)", # bug: off-by-one denominator
"def reverse(s): return s[::-1]", # correct
"def percent(part, whole): return part / whole", # bug: missing * 100
"def area(r): return 3.14159 * r * r", # correct
]
corpus = lotus.Corpus.from_documents(snippets, ids=["average", "reverse", "percent", "area"])
result = corpus.agent(
task="Keep only the functions that contain a bug (verify by running them).",
ops=["filter"],
tools=[PythonREPLTool()],
)
print([u.id for u in result.corpus.units]) # -> ['average', 'percent']
A standalone filter returns a Result whose corpus holds the surviving units;
output is None (nothing was reduced to a single answer).
Another tool-driven criterion — keep trips whose total exceeds a threshold, where the agent uses the REPL for exact arithmetic:
expenses = [
"Trip A: flights 420.50, hotel 610.00, meals 133.25.",
"Trip B: taxi 38.00, meals 52.40.",
"Trip C: flights 980.00, hotel 1200.00, car 340.00.",
]
corpus = lotus.Corpus.from_documents(expenses, ids=["A", "B", "C"])
result = corpus.agent(
task="Keep only trips whose total cost exceeds $1000.",
ops=["filter"],
tools=[PythonREPLTool()],
)
print([u.id for u in result.corpus.units]) # -> ['A', 'C']
Composing filter with other ops
filter is Corpus → Corpus, so it chains naturally in front of map and reduce.
The survivors flow into the next op:
result = corpus.agent(
task="Keep only functions with a bug, then write one summary of the bugs found.",
ops=["filter", "reduce"],
tools=[PythonREPLTool()],
)
print(result.output) # a summary over only the units that survived the filter
Strategies
Like map, a filter runs under an execution strategy (see
Agentic Operators), chosen by the planner or pinned with strategies=:
per_unit(default) — one unit per agent; best for self-contained decisions like the buggy-function check above.batched— several units per agent, which see each other as context, with a keep/drop verdict returned per unit. Best for comparative criteria (“keep the strongest”, drop near-duplicates) or many tiny units (cheaper).shared_context— one unit per agent plus injected background (viacontexts=), e.g. a reference definition every unit is judged against.
corpus.agent(task="Drop near-duplicate complaints.", ops=["filter"],
strategies={"filter": "batched"})
How it works
Same core as map. A filter shards and runs agents exactly like
map; the only difference is that each unit’s result is read as a verdict and used to select the returned corpus.Verdict. In
per_unit/shared_context, each agent ends with a lineVERDICT: KEEPorVERDICT: DROP; inbatchedit returns a per-unit JSON array of keep/drop. If a verdict can’t be parsed, the unit is kept (LOTUS never silently drops data) and a warning is logged.Reliability. Keep/drop on ambiguous criteria benefits from a stronger model; a reasoning model such as
gpt-5gives the most consistent verdicts.
Note
result.corpus is a full Corpus, so you can keep operating on it — run
another agent pipeline, inspect .units, or hand it to a downstream step.