# Haystack ```bash 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. ```python 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](../security.md#the-subject-must-come-from-authentication)). 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: ```python 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`: ```python 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: ```python 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 | `query` | The question to search for | | Input | `subject` | The authenticated user: a `wardowl.Subject`, or a function returning one; `WardowlRetriever` only | | Input | `top_k` | Optional; overrides the component's `top_k` for this run | | Input | `query_embedding` | Optional; a vector to search with instead of embedding `query` (connect a text embedder's `embedding`). `query` may then be empty. See [Your own vectors](../vectors.md) | | Output | `documents` | Haystack `Document`s: the chunk id, text and score, best first, with the metadata keys named in `metadata_fields` | | Output | `complete` | False when wardowl knows results may be missing (see [concepts](../concepts.md)) | `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.