agent-pack-haystack 0.0.2__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/CHANGELOG.md +15 -0
  2. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/PKG-INFO +1 -1
  3. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/agent.py +6 -28
  4. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/agent.py +49 -40
  5. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/tools.py +5 -5
  6. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/tests/advanced_rag/test_agent.py +1 -45
  7. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/tests/deep_research/test_agent.py +47 -18
  8. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/tests/deep_research/test_tools.py +1 -1
  9. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/.gitignore +0 -0
  10. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/LICENSE.txt +0 -0
  11. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/README.md +0 -0
  12. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/examples/advanced_rag_eval.py +0 -0
  13. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/pydoc/config_docusaurus.yml +0 -0
  14. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/pyproject.toml +0 -0
  15. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/__init__.py +0 -0
  16. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/__init__.py +0 -0
  17. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/hooks.py +0 -0
  18. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/prompts.py +0 -0
  19. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/tools.py +0 -0
  20. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/__init__.py +0 -0
  21. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/hooks.py +0 -0
  22. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/prompts.py +0 -0
  23. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/py.typed +0 -0
  24. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/tests/__init__.py +0 -0
  25. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/tests/advanced_rag/__init__.py +0 -0
  26. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/tests/advanced_rag/test_hooks.py +0 -0
  27. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/tests/advanced_rag/test_tools.py +0 -0
  28. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/tests/deep_research/__init__.py +0 -0
  29. {agent_pack_haystack-0.0.2 → agent_pack_haystack-0.1.0}/tests/deep_research/test_hooks.py +0 -0
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/agent_pack-v0.0.2] - 2026-08-26
4
+
5
+ ### 🚀 Features
6
+
7
+ - Use `AgentTool` in Deep Research Agent (#3850)
8
+
9
+ ### 🐛 Bug Fixes
10
+
11
+ - Fix new issues raised by ruff 0.16.0 (#3670)
12
+
13
+ ### 🧹 Chores
14
+
15
+ - Re-standardize READMEs (#3768)
16
+
17
+
3
18
  ## [integrations/agent_pack-v0.0.1] - 2026-07-20
4
19
 
5
20
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-pack-haystack
3
- Version: 0.0.2
3
+ Version: 0.1.0
4
4
  Summary: Pre-configured Haystack Agents
5
5
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/agent_pack#readme
6
6
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
@@ -2,17 +2,14 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from typing import Any
6
-
7
5
  from haystack import Document, Pipeline
8
6
  from haystack.components.agents import Agent
9
7
  from haystack.components.generators.chat import OpenAIResponsesChatGenerator
10
8
  from haystack.components.generators.chat.types import ChatGenerator
11
9
  from haystack.components.retrievers.types import TextRetriever
12
10
  from haystack.document_stores.types import DocumentStore
13
- from haystack.hooks import Hook, HookPoint
14
11
  from haystack.lazy_imports import LazyImport
15
- from haystack.tools import Tool, Toolset, ToolsType
12
+ from haystack.tools import Tool, Toolset
16
13
 
17
14
  from haystack_integrations.agent_pack.advanced_rag import prompts
18
15
  from haystack_integrations.agent_pack.advanced_rag.hooks import BackupAnswerHook
@@ -61,11 +58,6 @@ def create_advanced_rag_agent(
61
58
  system_prompt: str | None = None,
62
59
  max_agent_steps: int = 20,
63
60
  max_fetched_docs: int = 10,
64
- extra_tools: ToolsType | None = None,
65
- state_schema: dict[str, Any] | None = None,
66
- hooks: dict[HookPoint, list[Hook]] | None = None,
67
- raise_on_tool_invocation_failure: bool = False,
68
- tool_concurrency_limit: int = 4,
69
61
  ) -> Agent:
70
62
  """
71
63
  Create the advanced RAG agent.
@@ -79,6 +71,9 @@ def create_advanced_rag_agent(
79
71
  metadata inspection tools and must implement the metadata introspection methods (`get_metadata_fields_info`,
80
72
  `get_metadata_field_unique_values`, `get_metadata_field_min_max`).
81
73
 
74
+ To customize the returned agent further (e.g. add tools or hooks), use `Agent.clone`:
75
+ `agent = agent.clone(tools=[*agent.tools, my_tool])`.
76
+
82
77
  :param document_store: The document store the metadata inspection tools and the `fetch_documents_by_filter` tool
83
78
  run against.
84
79
  :param retriever: What retrieves for the `search_documents` tool (required). Either a standalone retriever
@@ -104,15 +99,6 @@ def create_advanced_rag_agent(
104
99
  :param max_fetched_docs: Maximum number of documents `fetch_documents_by_filter` shows per fetch. A filter fetch is
105
100
  not bounded by a retriever's `top_k`, so this caps the tool result instead; the scored `search_documents` tool
106
101
  is bounded by the `top_k` configured on your retrieval components.
107
- :param extra_tools: Additional tools (or toolsets) for the agent, appended after the built-in document-store
108
- toolset and the retrieval tool.
109
- :param state_schema: Additional entries merged into the agent's state schema. The built-in `documents` entry
110
- (the accumulated retrieved documents) always takes precedence.
111
- :param hooks: Additional hooks per hook point, merged with the built-in hooks. For `after_run`, the built-in
112
- backup-answer hook runs first, so custom hooks see the final answer.
113
- :param raise_on_tool_invocation_failure: If True, a failing tool call raises instead of being returned to the LLM
114
- as an error message it can recover from (the default).
115
- :param tool_concurrency_limit: Maximum number of tool calls executed in parallel within one agent step.
116
102
  :returns: The advanced RAG `Agent`. Call it with the question as a user message,
117
103
  `agent.run(messages=[ChatMessage.from_user(question)])`; the answer is in `last_message` (a `ChatMessage`) and
118
104
  `documents` carries every document the agent retrieved during the run (deduplicated by id, in first-retrieved
@@ -150,12 +136,6 @@ def create_advanced_rag_agent(
150
136
  DocumentStoreToolset(document_store, max_fetched_docs=max_fetched_docs),
151
137
  retrieval_tool,
152
138
  ]
153
- if extra_tools is not None:
154
- tools.extend([extra_tools] if isinstance(extra_tools, Toolset) else list(extra_tools))
155
-
156
- merged_hooks: dict[HookPoint, list[Hook]] = {"after_run": [BackupAnswerHook(chat_generator=backup_answer_llm)]}
157
- for hook_point, point_hooks in (hooks or {}).items():
158
- merged_hooks.setdefault(hook_point, []).extend(point_hooks)
159
139
 
160
140
  return Agent(
161
141
  chat_generator=llm,
@@ -163,8 +143,6 @@ def create_advanced_rag_agent(
163
143
  tools=tools,
164
144
  exit_conditions=["text"],
165
145
  max_agent_steps=max_agent_steps,
166
- state_schema={**(state_schema or {}), "documents": {"type": list[Document]}},
167
- hooks=merged_hooks,
168
- raise_on_tool_invocation_failure=raise_on_tool_invocation_failure,
169
- tool_concurrency_limit=tool_concurrency_limit,
146
+ state_schema={"documents": {"type": list[Document]}},
147
+ hooks={"after_run": [BackupAnswerHook(chat_generator=backup_answer_llm)]},
170
148
  )
@@ -18,7 +18,7 @@ from haystack.components.generators.chat import OpenAIResponsesChatGenerator
18
18
  from haystack.components.generators.chat.types import ChatGenerator
19
19
  from haystack.dataclasses import ChatMessage
20
20
  from haystack.lazy_imports import LazyImport
21
- from haystack.tools import AgentTool
21
+ from haystack.tools import AgentTool, Tool
22
22
 
23
23
  from haystack_integrations.agent_pack.deep_research import prompts
24
24
  from haystack_integrations.agent_pack.deep_research.hooks import ScopeHook, WriteHook
@@ -54,27 +54,29 @@ def _collect_note(current: list[str] | None, message: ChatMessage) -> list[str]:
54
54
  def _make_researcher_agent(
55
55
  *,
56
56
  researcher_llm: ChatGenerator,
57
- summarizer_llm: ChatGenerator,
57
+ page_summary_llm: ChatGenerator,
58
+ search_tool: Tool | None,
58
59
  max_researcher_steps: int,
59
- max_search_results: int,
60
- max_content_length: int,
60
+ max_page_chars: int,
61
61
  ) -> Agent:
62
62
  """
63
63
  The reusable sub-researcher.
64
64
 
65
65
  :param researcher_llm: LLM that drives the search/read/think loop and writes the summary.
66
- :param summarizer_llm: LLM used inside the `read_url` tool to summarize a page toward the question.
66
+ :param page_summary_llm: LLM used inside the `read_url` tool to summarize a page toward the question.
67
+ :param search_tool: Web search tool. Defaults to `TavilyWebSearchTool(top_k=10)`.
67
68
  :param max_researcher_steps: Cap on the researcher's agent loop (search/read/think + final summary).
68
- :param max_search_results: Results returned per `web_search` call.
69
- :param max_content_length: Raw page chars fed to the summarizer (pre-summarization cap).
69
+ :param max_page_chars: Raw page chars fed to the summarizer (pre-summarization cap).
70
70
  """
71
- tavily_import.check()
71
+ if search_tool is None:
72
+ tavily_import.check()
73
+ search_tool = TavilyWebSearchTool(top_k=10)
72
74
  return Agent(
73
75
  chat_generator=researcher_llm,
74
76
  system_prompt=prompts.RESEARCHER_TEMPLATE,
75
77
  tools=[
76
- TavilyWebSearchTool(top_k=max_search_results),
77
- make_read_url_tool(summarizer_llm=summarizer_llm, max_content_length=max_content_length),
78
+ search_tool,
79
+ make_read_url_tool(page_summary_llm=page_summary_llm, max_page_chars=max_page_chars),
78
80
  think_tool,
79
81
  ],
80
82
  exit_conditions=["text"], # exits when it writes its summary
@@ -84,55 +86,62 @@ def _make_researcher_agent(
84
86
 
85
87
  def create_deep_research_agent(
86
88
  *,
87
- scope_llm: ChatGenerator | None = None,
88
- orchestrator_llm: ChatGenerator | None = None,
89
+ llm: ChatGenerator | None = None,
90
+ system_prompt: str | None = None,
91
+ max_agent_steps: int = 8,
92
+ brief_llm: ChatGenerator | None = None,
89
93
  researcher_llm: ChatGenerator | None = None,
90
- summarizer_llm: ChatGenerator | None = None,
91
- writer_llm: ChatGenerator | None = None,
92
- max_subtopics: int = 5,
93
- max_concurrent_researchers: int = 5,
94
- max_orchestrator_steps: int = 8,
94
+ search_tool: Tool | None = None,
95
+ page_summary_llm: ChatGenerator | None = None,
96
+ report_llm: ChatGenerator | None = None,
95
97
  max_researcher_steps: int = 20,
96
- max_search_results: int = 10,
97
- max_content_length: int = 50_000,
98
+ max_concurrent_researchers: int = 5,
99
+ max_subtopics: int = 5,
100
+ max_page_chars: int = 50_000,
98
101
  ) -> Agent:
99
102
  """
100
103
  Create the deep research agent.
101
104
 
102
- :param scope_llm: LLM that rewrites the user query into a focused research brief.
105
+ To customize the returned agent further (e.g. add tools or hooks), use `Agent.clone`:
106
+ `agent = agent.clone(tools=[*agent.tools, my_tool])`.
107
+
108
+ :param llm: LLM that plans the investigation and delegates the sub-questions.
103
109
  Defaults to `OpenAIResponsesChatGenerator("gpt-5.4")`.
104
- :param orchestrator_llm: LLM that plans the investigation and delegates the sub-questions.
110
+ :param system_prompt: Overrides the pre-made system prompt. `{{ max_subtopics }}` is replaced with the value of
111
+ `max_subtopics`.
112
+ :param max_agent_steps: Maximum steps for the agent loop (reflect -> delegate rounds).
113
+ :param brief_llm: LLM that rewrites the user query into a focused research brief.
105
114
  Defaults to `OpenAIResponsesChatGenerator("gpt-5.4")`.
106
115
  :param researcher_llm: LLM that drives each sub-researcher's search/read/think loop.
107
116
  Defaults to `OpenAIResponsesChatGenerator("gpt-5.4-mini")`.
108
- :param summarizer_llm: LLM used inside the `read_url` tool to summarize a fetched page toward
117
+ :param search_tool: Web search tool used by each sub-researcher. Defaults to `TavilyWebSearchTool(top_k=10)`,
118
+ which requires `tavily-haystack`. The pre-made researcher prompt refers to the tool as `web_search`.
119
+ :param page_summary_llm: LLM used inside the `read_url` tool to summarize a fetched page toward
109
120
  the question. Defaults to `OpenAIResponsesChatGenerator("gpt-5.4-mini")`.
110
- :param writer_llm: LLM that turns the brief plus collected notes into the final report.
121
+ :param report_llm: LLM that turns the brief plus collected notes into the final report.
111
122
  Defaults to `OpenAIResponsesChatGenerator("gpt-5.4")`.
112
- :param max_subtopics: Maximum number of sub-questions the orchestrator may delegate (breadth).
113
- :param max_concurrent_researchers: Maximum number of sub-researchers that run at the same time.
114
- :param max_orchestrator_steps: Maximum steps for the orchestrator's agent loop (reflect -> delegate rounds).
115
123
  :param max_researcher_steps: Maximum steps for each sub-researcher's agent loop.
116
- :param max_search_results: Number of results returned per `web_search` call.
117
- :param max_content_length: Maximum raw page characters fed to the summarizer, before summarization.
124
+ :param max_concurrent_researchers: Maximum number of sub-researchers that run at the same time.
125
+ :param max_subtopics: Maximum number of sub-questions the agent may delegate (breadth).
126
+ :param max_page_chars: Maximum raw page characters fed to the summarizer, before summarization.
118
127
  :returns: The deep research `Agent`. Call it with the question as a user message,
119
128
  `agent.run(messages=[ChatMessage.from_user(question)])`; it returns a dict whose main output is
120
129
  `report` (the final markdown report, a `str`). The dict also carries the intermediate `brief`
121
130
  (`str`) and `notes` (`list[str]`), plus the standard Agent outputs `messages`, `last_message`,
122
131
  `step_count`, `token_usage` and `tool_call_counts`.
123
132
  """
124
- scope_llm = scope_llm or _default_llm("gpt-5.4")
125
- orchestrator_llm = orchestrator_llm or _default_llm("gpt-5.4")
133
+ llm = llm or _default_llm("gpt-5.4")
134
+ brief_llm = brief_llm or _default_llm("gpt-5.4")
126
135
  researcher_llm = researcher_llm or _default_llm("gpt-5.4-mini")
127
- summarizer_llm = summarizer_llm or _default_llm("gpt-5.4-mini")
128
- writer_llm = writer_llm or _default_llm("gpt-5.4")
136
+ page_summary_llm = page_summary_llm or _default_llm("gpt-5.4-mini")
137
+ report_llm = report_llm or _default_llm("gpt-5.4")
129
138
 
130
139
  researcher = _make_researcher_agent(
131
140
  researcher_llm=researcher_llm,
132
- summarizer_llm=summarizer_llm,
141
+ page_summary_llm=page_summary_llm,
142
+ search_tool=search_tool,
133
143
  max_researcher_steps=max_researcher_steps,
134
- max_search_results=max_search_results,
135
- max_content_length=max_content_length,
144
+ max_page_chars=max_page_chars,
136
145
  )
137
146
 
138
147
  research_subtopic = AgentTool(
@@ -147,22 +156,22 @@ def create_deep_research_agent(
147
156
  )
148
157
 
149
158
  scope = ScopeHook(
150
- generator=scope_llm,
159
+ generator=brief_llm,
151
160
  prompt_builder=ChatPromptBuilder(template=prompts.SCOPE_TEMPLATE, required_variables=["query"]),
152
161
  )
153
162
  write = WriteHook(
154
- generator=writer_llm,
163
+ generator=report_llm,
155
164
  prompt_builder=ChatPromptBuilder(template=prompts.WRITER_TEMPLATE, required_variables=["replies", "notes"]),
156
165
  )
157
166
 
158
- system_prompt = prompts.ORCHESTRATOR_TEMPLATE.replace("{{ max_subtopics }}", str(max_subtopics))
167
+ system_prompt = (system_prompt or prompts.ORCHESTRATOR_TEMPLATE).replace("{{ max_subtopics }}", str(max_subtopics))
159
168
 
160
169
  return Agent(
161
- chat_generator=orchestrator_llm,
170
+ chat_generator=llm,
162
171
  system_prompt=system_prompt,
163
172
  tools=[research_subtopic, think_tool],
164
173
  exit_conditions=["text"],
165
- max_agent_steps=max_orchestrator_steps,
174
+ max_agent_steps=max_agent_steps,
166
175
  tool_concurrency_limit=max_concurrent_researchers,
167
176
  state_schema={"notes": {"type": list}, "brief": {"type": str}, "report": {"type": str}},
168
177
  hooks={"before_run": [scope], "after_run": [write]},
@@ -65,14 +65,14 @@ def _read_url_result(result: dict) -> str:
65
65
  return "Page not read: the page returned no readable text."
66
66
 
67
67
 
68
- def make_read_url_tool(*, summarizer_llm: ChatGenerator, max_content_length: int) -> PipelineTool:
68
+ def make_read_url_tool(*, page_summary_llm: ChatGenerator, max_page_chars: int) -> PipelineTool:
69
69
  """
70
70
  `read_url` = PipelineTool over a fetch -> convert to text -> summarize-based-on-question pipeline.
71
71
 
72
- :param summarizer_llm: LLM that summarizes the fetched page toward the agent's question.
73
- :param max_content_length: Raw page chars fed to the summarizer (pre-summarization cap).
72
+ :param page_summary_llm: LLM that summarizes the fetched page toward the agent's question.
73
+ :param max_page_chars: Raw page chars fed to the summarizer (pre-summarization cap).
74
74
  """
75
- template = prompts.SUMMARIZE_TEMPLATE.replace("__MAXLEN__", str(max_content_length))
75
+ template = prompts.SUMMARIZE_TEMPLATE.replace("__MAXLEN__", str(max_page_chars))
76
76
 
77
77
  pipe = Pipeline()
78
78
  pipe.add_component("fetcher", LinkContentFetcher(raise_on_failure=True, retry_attempts=2, timeout=10))
@@ -82,7 +82,7 @@ def make_read_url_tool(*, summarizer_llm: ChatGenerator, max_content_length: int
82
82
  pipe.add_component("joiner", DocumentJoiner())
83
83
  pipe.add_component("content_gate", ContentGate())
84
84
  pipe.add_component("builder", ChatPromptBuilder(template=template, required_variables=["question", "documents"]))
85
- pipe.add_component("summarizer", summarizer_llm)
85
+ pipe.add_component("summarizer", page_summary_llm)
86
86
  pipe.connect("fetcher.streams", "router.sources")
87
87
  pipe.connect("router.text/html", "html.sources")
88
88
  pipe.connect("router.application/pdf", "pdf.sources")
@@ -4,13 +4,11 @@ import re
4
4
  import pytest
5
5
  from haystack import Document, Pipeline
6
6
  from haystack.components.agents import Agent
7
- from haystack.components.agents.state import State
8
7
  from haystack.components.generators.chat import MockChatGenerator, OpenAIResponsesChatGenerator
9
8
  from haystack.components.retrievers.in_memory import InMemoryBM25Retriever
10
9
  from haystack.dataclasses import ChatMessage, ToolCall
11
10
  from haystack.document_stores.in_memory import InMemoryDocumentStore
12
- from haystack.hooks import hook
13
- from haystack.tools import Toolset, tool
11
+ from haystack.tools import Toolset
14
12
 
15
13
  from haystack_integrations.agent_pack.advanced_rag import agent as agent_module
16
14
  from haystack_integrations.agent_pack.advanced_rag.agent import _default_llm, create_advanced_rag_agent
@@ -122,39 +120,6 @@ class TestCreateAdvancedRagAgent:
122
120
  )
123
121
  assert _flat_tool_names(agent.tools) == EXPECTED_TOOL_NAMES
124
122
 
125
- def test_supports_extra_tools_state_schema_and_hooks(self, store):
126
- @tool
127
- def extra_tool(text: str) -> str:
128
- """Do something extra."""
129
- return text
130
-
131
- @hook
132
- def custom_after_run(state: State) -> None: # noqa: ARG001
133
- return None
134
-
135
- @hook
136
- def custom_before_llm(state: State) -> None: # noqa: ARG001
137
- return None
138
-
139
- agent = create_advanced_rag_agent(
140
- document_store=store,
141
- retriever=InMemoryBM25Retriever(document_store=store),
142
- llm=MockChatGenerator(),
143
- extra_tools=[extra_tool],
144
- state_schema={"notes": {"type": list}},
145
- hooks={"after_run": [custom_after_run], "before_llm": [custom_before_llm]},
146
- raise_on_tool_invocation_failure=True,
147
- tool_concurrency_limit=2,
148
- )
149
-
150
- assert _flat_tool_names(agent.tools) == [*EXPECTED_TOOL_NAMES, "extra_tool"]
151
- assert set(agent.state_schema) >= {"notes", "documents"}
152
- # The built-in backup-answer hook runs first, custom after_run hooks after it.
153
- assert [type(h) for h in agent.hooks["after_run"]] == [BackupAnswerHook, type(custom_after_run)]
154
- assert agent.hooks["before_llm"] == [custom_before_llm]
155
- assert agent.raise_on_tool_invocation_failure is True
156
- assert agent.tool_concurrency_limit == 2
157
-
158
123
  def test_backup_answer_llm_is_never_the_agent_llm(self, store):
159
124
  llm = MockChatGenerator()
160
125
  agent = create_advanced_rag_agent(
@@ -174,15 +139,6 @@ class TestCreateAdvancedRagAgent:
174
139
  )
175
140
  assert agent.hooks["after_run"][0].chat_generator is backup_llm
176
141
 
177
- def test_built_in_documents_state_entry_wins_over_custom(self, store):
178
- agent = create_advanced_rag_agent(
179
- document_store=store,
180
- retriever=InMemoryBM25Retriever(document_store=store),
181
- llm=MockChatGenerator(),
182
- state_schema={"documents": {"type": str}},
183
- )
184
- assert agent.state_schema["documents"]["type"] == list[Document]
185
-
186
142
  def test_accepts_a_custom_system_prompt(self, store):
187
143
  agent = create_advanced_rag_agent(
188
144
  document_store=store,
@@ -5,7 +5,7 @@ from haystack.components.agents import Agent
5
5
  from haystack.components.generators.chat import MockChatGenerator, OpenAIResponsesChatGenerator
6
6
  from haystack.core.serialization import allow_deserialization_module
7
7
  from haystack.dataclasses import ChatMessage, ToolCall
8
- from haystack.tools import AgentTool
8
+ from haystack.tools import AgentTool, tool
9
9
 
10
10
  from haystack_integrations.agent_pack.deep_research.agent import (
11
11
  _collect_note,
@@ -47,10 +47,10 @@ def test_collect_note_skips_empty_message():
47
47
  def test_make_researcher_agent_wires_the_three_tools():
48
48
  researcher = _make_researcher_agent(
49
49
  researcher_llm=MockChatGenerator(),
50
- summarizer_llm=MockChatGenerator(),
50
+ page_summary_llm=MockChatGenerator(),
51
+ search_tool=None,
51
52
  max_researcher_steps=12,
52
- max_search_results=4,
53
- max_content_length=1000,
53
+ max_page_chars=1000,
54
54
  )
55
55
  assert isinstance(researcher, Agent)
56
56
  assert [t.name for t in researcher.tools] == ["web_search", "read_url", "think_tool"]
@@ -58,16 +58,32 @@ def test_make_researcher_agent_wires_the_three_tools():
58
58
  assert researcher.max_agent_steps == 12
59
59
 
60
60
 
61
+ def test_make_researcher_agent_with_custom_search_tool():
62
+ @tool
63
+ def my_search(query: str) -> str:
64
+ """Search."""
65
+ return query
66
+
67
+ researcher = _make_researcher_agent(
68
+ researcher_llm=MockChatGenerator(),
69
+ page_summary_llm=MockChatGenerator(),
70
+ search_tool=my_search,
71
+ max_researcher_steps=12,
72
+ max_page_chars=1000,
73
+ )
74
+ assert [t.name for t in researcher.tools] == ["my_search", "read_url", "think_tool"]
75
+
76
+
61
77
  def test_create_deep_research_agent():
62
78
  agent = create_deep_research_agent(
63
- scope_llm=MockChatGenerator(),
64
- orchestrator_llm=MockChatGenerator(),
79
+ llm=MockChatGenerator(),
80
+ brief_llm=MockChatGenerator(),
65
81
  researcher_llm=MockChatGenerator(),
66
- summarizer_llm=MockChatGenerator(),
67
- writer_llm=MockChatGenerator(),
82
+ page_summary_llm=MockChatGenerator(),
83
+ report_llm=MockChatGenerator(),
68
84
  max_subtopics=3,
69
85
  max_concurrent_researchers=2,
70
- max_orchestrator_steps=6,
86
+ max_agent_steps=6,
71
87
  )
72
88
  assert [t.name for t in agent.tools] == ["research_subtopic", "think_tool"]
73
89
  assert isinstance(agent.tools[0], AgentTool)
@@ -86,15 +102,28 @@ def test_create_deep_research_agent():
86
102
  assert default_agent.chat_generator.model == "gpt-5.4"
87
103
 
88
104
 
105
+ def test_create_deep_research_agent_with_custom_system_prompt():
106
+ agent = create_deep_research_agent(
107
+ llm=MockChatGenerator(),
108
+ brief_llm=MockChatGenerator(),
109
+ researcher_llm=MockChatGenerator(),
110
+ page_summary_llm=MockChatGenerator(),
111
+ report_llm=MockChatGenerator(),
112
+ system_prompt="Delegate at most {{ max_subtopics }} sub-questions.",
113
+ max_subtopics=2,
114
+ )
115
+ assert agent.system_prompt == "Delegate at most 2 sub-questions."
116
+
117
+
89
118
  def test_deep_research_run(monkeypatch):
90
119
  # agent.run() warms up the TavilyWebSearch tool component, which resolves TAVILY_API_KEY
91
120
  monkeypatch.setenv("TAVILY_API_KEY", "test-key")
92
121
  agent = create_deep_research_agent(
93
- scope_llm=MockChatGenerator("Brief: investigate the causes of X."),
94
- orchestrator_llm=MockChatGenerator(responses=_delegate_then_stop("What causes X?")),
122
+ llm=MockChatGenerator(responses=_delegate_then_stop("What causes X?")),
123
+ brief_llm=MockChatGenerator("Brief: investigate the causes of X."),
95
124
  researcher_llm=MockChatGenerator("X is caused by Y (https://example.com/x).\nSources:\nhttps://example.com/x"),
96
- summarizer_llm=MockChatGenerator(),
97
- writer_llm=MockChatGenerator("# Report\nX is caused by Y (https://example.com/x)."),
125
+ page_summary_llm=MockChatGenerator(),
126
+ report_llm=MockChatGenerator("# Report\nX is caused by Y (https://example.com/x)."),
98
127
  max_subtopics=1,
99
128
  )
100
129
  result = agent.run(messages=[ChatMessage.from_user("Tell me about X")])
@@ -108,11 +137,11 @@ def test_deep_research_run(monkeypatch):
108
137
  def test_create_deep_research_agent_serialization_roundtrip():
109
138
  allow_deserialization_module("haystack_integrations.agent_pack.*")
110
139
  agent = create_deep_research_agent(
111
- scope_llm=MockChatGenerator(),
112
- orchestrator_llm=MockChatGenerator(),
140
+ llm=MockChatGenerator(),
141
+ brief_llm=MockChatGenerator(),
113
142
  researcher_llm=MockChatGenerator(),
114
- summarizer_llm=MockChatGenerator(),
115
- writer_llm=MockChatGenerator(),
143
+ page_summary_llm=MockChatGenerator(),
144
+ report_llm=MockChatGenerator(),
116
145
  )
117
146
 
118
147
  restored = Agent.from_dict(agent.to_dict())
@@ -131,7 +160,7 @@ def test_create_deep_research_agent_serialization_roundtrip():
131
160
  reason="OPENAI_API_KEY and TAVILY_API_KEY required",
132
161
  )
133
162
  def test_deep_research_live_run():
134
- agent = create_deep_research_agent(max_subtopics=2, max_orchestrator_steps=4, max_researcher_steps=6)
163
+ agent = create_deep_research_agent(max_subtopics=2, max_agent_steps=4, max_researcher_steps=6)
135
164
  result = agent.run(messages=[ChatMessage.from_user("What are the main causes and effects of ocean acidification?")])
136
165
 
137
166
  assert result["brief"].strip()
@@ -38,7 +38,7 @@ def test_read_url_result_reports_no_readable_text():
38
38
 
39
39
 
40
40
  def test_make_read_url_tool_builds_pipeline_tool():
41
- tool = make_read_url_tool(summarizer_llm=MockChatGenerator(), max_content_length=1234)
41
+ tool = make_read_url_tool(page_summary_llm=MockChatGenerator(), max_page_chars=1234)
42
42
  assert isinstance(tool, PipelineTool)
43
43
  assert tool.name == "read_url"
44
44
  assert tool._input_mapping == {"urls": ["fetcher.urls"], "question": ["builder.question"]}