Tutorial: Using Pre-Built Agents from Agent Pack
Last Updated: July 22, 2026
- Level: Advanced
- Time to complete: 25 minutes
- Components/Packages Used:
agent-pack-haystack(create_advanced_rag_agent,create_deep_research_agent),Agent,InMemoryDocumentStore - Prerequisites: Haystack 3.0 or later, an OpenAI API key, and a Tavily API key (free tier available)
- Goal: After completing this tutorial, you’ll understand what Agent Pack is and why it exists, and you’ll have run and customized two ready-made agents: the Advanced RAG Agent and the Deep Research Agent.
Overview
A language model and a set of tools are the core building blocks of an agent. But a robust agent usually needs more than that: an architecture that splits work across sub-agents, techniques for keeping the context window small but focused, and hooks to shape the agent loop before and after it runs. Assembling those pieces correctly is where most of the effort goes.
Agent Pack is a collection of complex, pre-configured Haystack agents that package these techniques into working agents. Each one is a complete architecture built from Haystack primitives (
Agent,
Tools,
hooks,
State, and Pipelines), exposed behind a single create_* entry point.
There are three ways to use an agent from the pack:
- Run it as is. Call the
create_*function, pass a question, and get an answer. The defaults are chosen to work out of the box. - Customize it. Each entry point exposes keyword arguments for swapping models, adding tools, and tuning behavior.
- Copy it. Read the implementation and adapt it as a blueprint for your own architecture.
Why these two agents?
The pack ships agents that solve genuinely hard, recurring problems, so you don’t have to rebuild the architecture each time:
- The Advanced RAG Agent tackles a common RAG failure mode: an agent that guesses which metadata fields and values exist and builds broken filters. Instead, this agent inspects the document store (fields, values, ranges) and constructs valid Haystack filters to narrow retrieval. Reach for it when your corpus has rich metadata (categories, dates, ratings, languages…) and answering questions well depends on filtering by it.
- The Deep Research Agent is a canonical multi-agent pattern: it researches a question on the web with an orchestrator that delegates focused sub-questions to isolated sub-researchers, then writes a cited Markdown report. Its key idea is context management through isolation and compression. Reach for it when a question needs broad, multi-source web research rather than a single lookup.
In this tutorial you’ll run both agents, look at what they return, and customize them.
โ ๏ธ Agent Pack is experimental. Its APIs and agent architectures can change in any release, without following the usual Haystack deprecation policy. It is distributed separately from
haystack-ai(inhaystack-core-integrations) precisely so these patterns can evolve quickly. Expect things to change and occasionally break, and pin a version if you depend on it in production.
Preparing the Environment
First, install agent-pack-haystack along with the optional runtime dependencies the two agents need:
arrowrenders today’s date into the agents’ system prompts (both the Advanced RAG and Deep Research agents use it), so they can reason about relative dates like “the last 5 years”.tavily-haystack,trafilatura, andpypdfpower the Deep Research Agent’s web search and its HTML/PDF page reading.
%%bash
pip install -q agent-pack-haystack arrow tavily-haystack trafilatura pypdf
Enter API Keys
Both agents use OpenAI models by default. The Deep Research Agent additionally uses Tavily for web search. Enter the keys below:
from getpass import getpass
import os
if not os.environ.get("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = getpass("Enter your OpenAI API key:")
if not os.environ.get("TAVILY_API_KEY"):
os.environ["TAVILY_API_KEY"] = getpass("Enter your Tavily API key:")
Part 1: The Advanced RAG Agent
The Advanced RAG Agent answers questions from documents it retrieves out of a document store. What makes it advanced is that it doesn’t guess your metadata: it can inspect the store, discover which fields and values exist, and build a metadata filter to narrow retrieval when that helps. Plain, unfiltered retrieval stays available when it doesn’t.
Indexing a corpus with metadata
Let’s index a small set of documents with varied metadata (category, year, rating), then ask a question that can only be answered well by filtering on that metadata.
from haystack import Document
from haystack.document_stores.in_memory import InMemoryDocumentStore
document_store = InMemoryDocumentStore()
document_store.write_documents(
[
Document(
content="CRISPR gene editing corrected a hereditary blindness mutation in a clinical trial.",
meta={"category": "science", "year": 2021, "rating": 4.6},
),
Document(
content="A quantum computer demonstrated error-corrected logical qubits.",
meta={"category": "science", "year": 2023, "rating": 4.8},
),
Document(
content="Dolly the sheep became the first mammal cloned from an adult somatic cell.",
meta={"category": "science", "year": 1996, "rating": 4.2},
),
Document(
content="The Berlin Wall fell, a decisive moment in the end of the Cold War.",
meta={"category": "history", "year": 1989, "rating": 4.7},
),
Document(
content="Argentina won the FIFA World Cup final against France on penalties.",
meta={"category": "sports", "year": 2022, "rating": 4.9},
),
]
)
Creating the agent
create_advanced_rag_agent needs two things: the document_store (which feeds the metadata-inspection tools and a direct fetch-by-filter tool) and a retriever (which becomes the agent’s relevance-scoring search_documents tool). Here we use a keyword
InMemoryBM25Retriever, but it can also be an embedding retriever or a full retrieval Pipeline.
The retriever you pass should be relevance-scoring (BM25, embedding, or hybrid). Direct, unscored fetching by metadata is already handled by the agent’s built-in fetch_documents_by_filter tool.
from haystack.components.retrievers.in_memory import InMemoryBM25Retriever
from haystack_integrations.agent_pack import create_advanced_rag_agent
rag_agent = create_advanced_rag_agent(
document_store=document_store, retriever=InMemoryBM25Retriever(document_store=document_store, top_k=5)
)
Running the agent
Call the agent with your question as a user message. We pass
print_streaming_chunk as the streaming_callback so you can watch the agent work: it first lists the metadata fields, checks the category values and the year range, then retrieves with a filter it builds itself.
from haystack.dataclasses import ChatMessage
from haystack.components.generators.utils import print_streaming_chunk
result = rag_agent.run(
messages=[ChatMessage.from_user("What science advances happened after 2015?")],
streaming_callback=print_streaming_chunk,
)
The agent’s answer is in last_message, and every document it retrieved during the run is accumulated (deduplicated by id) under documents. The answer cites documents by the first 8 characters of their id, for example [doc a1b2c3d4].
print("ANSWER:\n", result["last_message"].text)
print("\nRETRIEVED DOCUMENTS:")
for doc in result["documents"]:
print(f"[doc {doc.id[:8]}] {doc.meta} :: {doc.content[:60]}")
How it works under the hood
The agent is a single Haystack
Agent that works through three logical stages using five tools:
- Inspect metadata with
list_metadata_fields,get_metadata_field_values, andget_metadata_field_range(the system prompt tells it to list the fields first). - Retrieve documents with
search_documents(your relevance-scoring retriever, optionally narrowed by a filter) orfetch_documents_by_filter(a direct metadata lookup when scoring isn’t needed). - Answer using only the retrieved documents, citing each as
[doc <short-id>].
To keep filters valid, the Haystack filter grammar is embedded directly in the retrieval tools’ parameter descriptions, so the model learns it at the point of use rather than from a long system prompt. Every retrieved document is accumulated in the agent’s
State under documents (deduplicated by id), which is why the run returns them alongside the answer. And if the loop is cut off by max_agent_steps before an answer is written, a built-in BackupAnswerHook (an after_run hook) makes one extra call to produce a best-effort answer, so last_message always carries text.
๐ก The metadata tools rely on document store methods (
get_metadata_fields_info,get_metadata_field_unique_values,get_metadata_field_min_max) that aren’t part of the baseDocumentStoreprotocol.InMemoryDocumentStoreand most integrations implement them (OpenSearch, Elasticsearch, Weaviate, Chroma, pgvector, Qdrant, Pinecone, and more).
Customizing the agent
Everything is configured through keyword arguments. Only document_store and retriever are required. A few useful ones:
llm: the chat generator that drives the agent loop. Defaults toOpenAIResponsesChatGenerator("gpt-5.4")with low reasoning effort. Swap in any tool-calling generator, from OpenAI or another provider.max_agent_steps: caps the loop (default20). If the loop is cut off before an answer is written, a built-inBackupAnswerHookmakes one extra call to produce a best-effort answer, solast_messagealways carries text.max_fetched_docs: how many documentsfetch_documents_by_filtershows per call (default10).extra_tools,state_schema,hooks: extend the agent with your own tools, state, and hooks.
Here we swap the LLM for a smaller, widely available model and tighten the step budget:
from haystack.components.generators.chat import OpenAIChatGenerator
custom_rag_agent = create_advanced_rag_agent(
document_store=document_store,
retriever=InMemoryBM25Retriever(document_store=document_store, top_k=5),
llm=OpenAIChatGenerator(model="gpt-4o-mini"),
max_agent_steps=10,
)
result = custom_rag_agent.run(messages=[ChatMessage.from_user("Which documents describe events before 1990?")])
print(result["last_message"].text)
Using a retrieval pipeline. To use a multi-component retrieval flow (for example hybrid retrieval), pass a Pipeline as retriever and supply retrieval_pipeline_input_mapping (mapping the tool’s query and filters to your pipeline’s input sockets) and, optionally, retrieval_pipeline_output_mapping. See the
Advanced RAG Agent docs for a full hybrid-retrieval example.
Using the tools on their own. The four document-store-backed tools are exported individually and bundled as DocumentStoreToolset, so you can drop them into your own Agent with your own prompt, treating the pack as a toolbox rather than a finished agent:
from haystack.components.agents import Agent
from haystack_integrations.agent_pack.advanced_rag import DocumentStoreToolset
agent = Agent(
chat_generator=...,
tools=[DocumentStoreToolset(document_store), my_retrieval_tool],
)
Part 2: The Deep Research Agent
The Deep Research Agent takes a question, researches it on the web, and produces a structured, cited Markdown report. Under the hood it’s a small multi-agent system (we’ll look at how it works right after running it), but the entry point is a single call.
Running the agent
create_deep_research_agent() works with zero arguments. But the defaults research broadly (up to 5 sub-questions, 5 concurrent sub-researchers, 20 steps each), which can take several minutes and cost real tokens. For this tutorial we’ll deliberately scope it down, which also previews the customization knobs. Even so, expect this cell to take a couple of minutes.
from haystack_integrations.agent_pack import create_deep_research_agent
research_agent = create_deep_research_agent(
max_subtopics=2, # delegate at most 2 sub-questions (breadth)
max_concurrent_researchers=2, # run at most 2 sub-researchers at once
max_researcher_steps=6, # cap each sub-researcher's search/read/think loop
max_search_results=5, # results per web_search call
)
from haystack.dataclasses import ChatMessage
result = research_agent.run(
messages=[ChatMessage.from_user("What are the main techniques for managing the context window in LLM agents?")]
)
print(result["report"])
The main output is report: the final Markdown report with inline [text](url) citations. The run also returns the intermediate brief and notes, plus the standard Agent outputs (messages, last_message, step_count, token_usage, tool_call_counts). Let’s look at the brief the Scope phase produced and how many research notes were collected:
print("RESEARCH BRIEF:\n", result["brief"])
print(f"\nCollected {len(result['notes'])} research note(s).")
print("\nFIRST NOTE (a sub-researcher summary):\n", result["notes"][0][:800])
How it works under the hood
The agent is built around a single top-level
Agent acting as the orchestrator, with two
hooks that wrap its loop to create three phases:
- Scope (a
before_runhook): a plain LLM call rewrites your question into a focused researchbrief, stored on the agent’sState. - Research (the orchestrator loop): the orchestrator splits the brief into a few non-overlapping sub-questions and delegates each to an isolated sub-researcher agent, exposed to it as a tool. It can fire several off at once (bounded by
max_concurrent_researchers) and collects their summaries into a sharednoteslist. - Write (an
after_runhook): a final LLM call turns the brief plusnotesinto thereport.
Each sub-researcher is its own Agent that searches the web (web_search), optionally reads promising pages (read_url, which fetches and summarizes a page or PDF toward the question), reflects (think_tool), and finishes by writing one short, cited summary.
The key idea is context management. Raw web content (search results, full pages, PDFs) is large and noisy; if it all piled into one context window, output quality would degrade. So each sub-researcher runs in its own private context, and only its short summary leaves it, controlled by two settings on the delegation tool: outputs_to_string (what the orchestrator sees) and outputs_to_state (what’s appended to notes for the writer). The bulky raw research never propagates to the orchestrator or the writer.
Customizing the agent
Each phase takes its own ChatGenerator, so you can mix models by cost and capability, or swap in a different provider entirely:
scope_llm,orchestrator_llm,writer_llm: default toOpenAIResponsesChatGenerator("gpt-5.4")(the heavier reasoning steps).researcher_llm,summarizer_llm: default toOpenAIResponsesChatGenerator("gpt-5.4-mini")(run many times, so a cheaper model keeps cost down).
And the breadth/depth of the investigation is fully tunable: max_subtopics, max_concurrent_researchers, max_orchestrator_steps, max_researcher_steps, max_search_results, and max_content_length.
For example, to make the whole run cheaper you might point every phase at a smaller model:
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack_integrations.agent_pack import create_deep_research_agent
cheap_agent = create_deep_research_agent(
scope_llm=OpenAIChatGenerator(model="gpt-4o-mini"),
orchestrator_llm=OpenAIChatGenerator(model="gpt-4o-mini"),
researcher_llm=OpenAIChatGenerator(model="gpt-4o-mini"),
summarizer_llm=OpenAIChatGenerator(model="gpt-4o-mini"),
writer_llm=OpenAIChatGenerator(model="gpt-4o-mini"),
max_subtopics=3,
)
Agent Pack as a blueprint
Beyond running and customizing, the third way to use the pack is to copy it. Both agents are built entirely from public Haystack primitives, so their source doubles as a reference architecture:
- The Deep Research Agent shows how to nest agents (a sub-agent exposed to the orchestrator as a
ComponentTool) and howoutputs_to_stringandoutputs_to_statecontrol what a sub-agent returns to the caller versus what it saves for later, the core of its context isolation. - The Advanced RAG Agent shows how to build tools over a document store, embed a grammar in a tool’s parameter description, accumulate results in
State, and use anafter_runhook as a safety net.
Browse the source in
haystack-core-integrations/integrations/agent_pack and adapt whichever parts fit your use case.
What’s next
๐ Congratulations! You’ve run and customized both agents in Agent Pack, seen how they work under the hood, and how they can serve as blueprints for your own architectures.
Realistically, what to expect from Agent Pack going forward: it’s an early, experimental project, so treat these agents as a fast-moving starting point rather than a stable API. Expect the create_* signatures and internals to change as the patterns settle (the customization interface in particular is still being refined), so pin a version if you build on it. The pack lives outside haystack-ai on purpose so it can move quickly, and it doubles as a place to prototype capabilities that may later graduate into Haystack itself. More agents are likely to land here over time.
The most useful thing you can do is give feedback: if you hit a bug, or have an idea for a complex agent that belongs in the pack, open an issue. Keep an eye on the documentation and the repository for updates.
To go deeper on building agents from scratch, check out these tutorials:
- Build a Tool-Calling Agent
- Creating a Multi-Agent System with Haystack
- Human-in-the-Loop with Haystack Agents
To stay up to date on the latest Haystack developments, you can sign up for our newsletter or join Haystack discord community.
