Troubleshooting¶
Start here:
It reports which extras are installed, which credentials are configured, what the defaults resolve to, and runs a complete offline pipeline as a smoke test. Most problems are visible in that output.
Installation¶
MissingDependencyError¶
MissingDependencyError: The Pinecone vector store is not installed.
Hint: Run:
pip install "windlass[pinecone]"
Working as designed — run the command it printed. Check what you have with windlass list; uninstalled components are marked.
A raw ImportError from an optional package¶
That is a bug. Windlass translates optional-dependency import failures at a single choke point (windlass.core.lazy). Please report it with the traceback.
windlass: command not found¶
The console script is installed into the environment's Scripts/bin. Either activate the environment, or run python -m windlass.cli.
import windlass is slow¶
Measure it:
Windlass's own overhead is a few milliseconds; the floor is pydantic-settings, which costs more on its own than the whole framework. If you see torch or chromadb in that output, something is importing eagerly — that is a bug worth reporting.
Configuration¶
ComponentNotFoundError¶
ComponentNotFoundError: No chunker named 'sematic' is registered.
Hint: Available chunkers: code, markdown, parent_child, recursive, semantic, token
A typo, or a plugin that did not load. Check with Windlass.list("chunker") and Windlass.plugins(strict=True).
AuthenticationError despite the key being set¶
Check what Windlass actually sees:
Common causes: the variable is set in a different shell, a .env file in the working directory is overriding it, or the key belongs to an account without access to that model.
configure() seems not to take effect¶
configure() merges onto current settings and returns the new object. Read them back with settings(), and remember explicit builder arguments always win:
Settings leak between tests¶
They are a process-wide singleton. Reset them:
Retrieval quality¶
The answer is wrong or made up¶
Look at what was retrieved first — this is the answer to most quality complaints:
answer = rag.ask("...")
for hit in answer.contexts:
print(f"{hit.score:.3f} {hit.retriever:<12} {hit.chunk.content[:80]}")
If the right chunk is not there, it is a retrieval problem and no prompt change will fix it.
Then, in order:
rag.strict()— refuse rather than invent when nothing is retrieved.rag.retriever("hybrid")— especially if the query contains an identifier or product name.- Raise
top_k. - Change the chunker (
markdownfor docs,parent_childfor mixed content). - Upgrade the embedding model and re-ingest.
Retrieval returns nothing¶
If zero, ingestion failed silently — see below. If not zero, check that a filter is not excluding everything:
rag.search("query", k=10) # unfiltered
rag.search("query", k=10, filters={"tenant": "x"}) # filtered
Retrieval got worse after changing the embedding model¶
Expected. Vectors from two different models are not comparable. Clear the store and re-ingest:
The same passage appears five times in the results¶
Your corpus has duplicates, or your chunks overlap heavily.
rag.preprocessor("dedup", threshold=0.9) # at ingestion
rag.retriever("vector", diversity=0.3) # MMR at query time
Exact terms are not found¶
Dense retrieval misses identifiers, codes and SKUs. That is what BM25 is for:
Ingestion¶
IngestionError: No documents were produced¶
The path is wrong, the directory is empty, or no loader claims those extensions.
from windlass.rag.loading import extension_map
print(sorted(extension_map())) # what this install can read
Most formats need pip install "windlass[loaders]".
A PDF produced no text¶
pypdf extracts embedded text. A scanned PDF contains images, not text.
PDF page rendering additionally needs pdf2image and Poppler.
Everything was dropped during preprocessing¶
A filter is too strict — usually min_length on the cleaner, or a language filter:
Ingestion is slow¶
See Performance.
One corrupt file aborts the run¶
It should not — the default is on_error="skip". If you set raise, one bad file will stop everything:
Agents¶
MaxIterationsExceeded¶
The agent looped without finishing. Raising the ceiling treats the symptom; the cause is usually one of:
- A tool description the model misreads. It keeps calling the wrong tool. Rewrite the description as a prompt.
- A tool returning something unusable. A forty-field object teaches the model to guess. Return three fields.
- A task that needs decomposition. Split into specialists with a supervisor.
Look at the trace:
The agent never calls a tool¶
- Does the model support tool calling? Windlass raises at build time if not.
- Is the description clear enough for the model to know when to use it?
- Is the tool bound?
agent.build().tools.names()
ToolNotFoundError, or the model invents tool names¶
It hallucinated a name. Windlass returns the error to the model with the list of real tools, so it usually recovers on the next turn. If it happens repeatedly, your tool names are too similar or too generic.
A tool times out¶
The default is 60 seconds. A hung tool fails the call, not the run.
AgentInterrupt when you did not expect one¶
A tool declares requires_approval=True, or .human_in_the_loop() is on. Inspect and resume:
resume() says there is no checkpoint¶
Resuming requires a checkpointer, and memory does not survive a process restart:
Streaming¶
Streaming yields one chunk then stops¶
If this happens with a custom sync bridge, the cause is asyncio.run per item: it calls loop.shutdown_asyncgens() on exit, which finalises the suspended generator. Use windlass.core.concurrency.iter_sync, which pins the whole iteration to one loop.
Inside Windlass this is already handled — if you see it from a Windlass method, please report it.
RuntimeError: asyncio.run() cannot be called from a running event loop¶
Not from Windlass — run_sync detects a running loop and dispatches to a background thread. If you see it, something else in your stack is calling asyncio.run. In async code, prefer the a* methods directly.
Guardrails¶
Legitimate content is being blocked¶
Then narrow the policy:
PII redaction is too aggressive¶
The phone pattern is the usual over-matcher — it is deliberately broad, since a missed phone number is worse than a false positive.
PII is not detected¶
Regex detection is precise for structured identifiers and blind to unstructured ones — it will not find "Ada Lovelace" as a name.
Providers¶
Rate limits¶
Windlass honours a provider's advertised Retry-After header over its own backoff. Do not add another retry loop on top; you will multiply the two.
ProviderTimeoutError¶
Local models loading for the first time routinely need more than 60 seconds. keep_alive keeps Ollama models resident between requests.
Cannot reach the Ollama daemon¶
A model rejects the tool schema¶
Some models are strict about JSON Schema. Simplify the annotation — deeply nested Pydantic models are the usual culprit — and check what is actually being sent:
Getting help¶
Include this in a bug report:
windlass doctor
windlass config # secrets are masked
python -c "import windlass, sys; print(windlass.__version__, sys.version)"
Plus the full traceback. Windlass errors carry structured context worth including: