Haystack#
pip install "wardowl[haystack]"
WardowlRetriever is a Haystack component that returns only the documents a subject may read,
from any wardowl backend and authorizer. The extra requires haystack-ai 2.12 or later; CI tests
the current Haystack 3 release.
from haystack import Pipeline
from wardowl import PermissionedRetriever
from wardowl.haystack import WardowlRetriever
pipeline = Pipeline()
pipeline.add_component("retriever", WardowlRetriever(PermissionedRetriever(backend, authorizer), top_k=5))
# ... a prompt builder and a generator, connected to "retriever.documents"
result = pipeline.run({"retriever": {"query": question, "subject": subject}})
The subject is a run input#
One pipeline serves every user: the application passes the authenticated user as subject on
each run, as a wardowl Subject or a function that returns one, and the component refuses to run
with anything else. Build it from your session, the user id and groups your identity provider
vouches for, never from the request body or from a model’s output
(security model).
The subject input accepts any type, so that a value a model sends for it stays plain JSON (see
below). A component output typed Subject connects to it. Name the sockets when you connect to the
component, as in pipeline.connect("embedder.embedding", "retriever.query_embedding"): a
connection by component name alone is ambiguous.
In an agent#
An agent’s model fills in the inputs of the tools it calls, so it must never be able to fill in
subject. ComponentTool leaves WardowlRetriever’s subject out of the tool’s parameters,
because the input’s type includes a function. Haystack still passes on any argument a model sends,
even one the tool does not offer, and in an agent a model’s value wins over the state’s. Such a
value reaches the component as the JSON it is, never as a Subject, so the call fails with nothing
retrieved.
Give the tool the subject from the agent’s state:
from haystack.components.agents import Agent
from haystack.tools import ComponentTool
from wardowl import Subject
from wardowl.haystack import WardowlRetriever
search = ComponentTool(
component=WardowlRetriever(PermissionedRetriever(backend, authorizer)),
name="search_documents",
description="Search the company documents the current user may read.",
inputs_from_state={"subject": "subject"},
)
agent = Agent(chat_generator=chat_generator, tools=[search], state_schema={"subject": {"type": Subject}})
result = agent.run(messages=messages, subject=subject) # the authenticated user, from your session
inputs_from_state maps a key of the agent’s state to a tool input, the state key first: here the
state’s subject fills the component’s subject. The key must be declared in state_schema, and
agent.run(..., subject=...) sets it for that run.
The model fills in the tool’s other inputs: query, top_k and query_embedding. None of them
changes whose permissions apply, and a query_embedding the model makes up is searched like any
vector, under the same filter and checks. top_k, though, is bounded only by the retriever’s
max_candidates (1,000 by default; a larger value raises ValueError), so a model can pull far
more documents into its context than you planned. To cap it, give the tool its own
PermissionedRetriever with a smaller max_candidates, which also bounds how far post-filtering
searches.
To get the documents the agent found back with its answer, for citations, send the tool’s
documents output to the state with outputs_to_state:
from haystack import Document
search = ComponentTool(
component=WardowlRetriever(PermissionedRetriever(backend, authorizer)),
name="search_documents",
description="Search the company documents the current user may read.",
inputs_from_state={"subject": "subject"},
outputs_to_state={"documents": {"source": "documents"}},
)
agent = Agent(
chat_generator=chat_generator,
tools=[search],
state_schema={"subject": {"type": Subject}, "documents": {"type": list[Document]}},
)
result = agent.run(messages=messages, subject=subject)
documents = result["documents"] # what the tool returned, as Haystack Documents
BoundWardowlRetriever#
BoundWardowlRetriever takes the subject from a function instead and has no subject input at
all, for when your web layer sets the user in a contextvar for each request:
import contextvars
from haystack.tools import ComponentTool
from wardowl.haystack import BoundWardowlRetriever
current_subject = contextvars.ContextVar("current_subject") # your web layer sets it per request
search = ComponentTool(
component=BoundWardowlRetriever(PermissionedRetriever(backend, authorizer), current_subject.get),
name="search_documents",
description="Search the company documents the current user may read.",
)
The function is called on every run, in the run’s thread (or its task, for run_async), so a
contextvar the web layer set for the request is what it reads. A function that raises, as
current_subject.get does when no request set it, or that returns anything but a Subject, stops
the run with nothing returned.
Inputs and outputs#
Name |
Meaning |
|
|---|---|---|
Input |
|
The question to search for |
Input |
|
The authenticated user: a |
Input |
|
Optional; overrides the component’s |
Input |
|
Optional; a vector to search with instead of embedding |
Output |
|
Haystack |
Output |
|
False when wardowl knows results may be missing (see concepts) |
run_async has the same inputs and outputs, for Pipeline.run_async. Any failure of the
permission service raises AuthorizationError, so the pipeline stops rather than generating from
unfiltered documents. As a tool, the failure reaches the model: an Agent shows a tool’s error to
it. The error’s message is therefore fixed (“authorizer failed”, for example), and the text of the
exception behind it, which can name hosts or credentials, stays in its __cause__, for your logs.
Metadata#
Documents carry no metadata unless you name it, as in
WardowlRetriever(retriever, metadata_fields=["title", "source"]). Permission fields such as
allowed_users, allowed_groups and tenant say who may read what, and documents reach the model,
the prompt and any trace of them, so leave those out.
The components hold live connections to your vector store and permission service, so a pipeline that contains one cannot be serialized to YAML; build the pipeline in code.