agent-pack-haystack 0.0.1__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.1.0/CHANGELOG.md +29 -0
  2. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/PKG-INFO +4 -3
  3. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/README.md +1 -0
  4. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/pyproject.toml +2 -1
  5. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/agent.py +6 -28
  6. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/agent.py +51 -65
  7. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/tools.py +5 -5
  8. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/advanced_rag/test_agent.py +1 -45
  9. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/deep_research/test_agent.py +51 -17
  10. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/deep_research/test_tools.py +1 -1
  11. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/.gitignore +0 -0
  12. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/LICENSE.txt +0 -0
  13. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/examples/advanced_rag_eval.py +0 -0
  14. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/pydoc/config_docusaurus.yml +0 -0
  15. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/__init__.py +0 -0
  16. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/__init__.py +0 -0
  17. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/hooks.py +0 -0
  18. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/prompts.py +0 -0
  19. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/tools.py +0 -0
  20. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/__init__.py +0 -0
  21. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/hooks.py +0 -0
  22. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/prompts.py +0 -0
  23. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/py.typed +0 -0
  24. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/__init__.py +0 -0
  25. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/advanced_rag/__init__.py +0 -0
  26. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/advanced_rag/test_hooks.py +0 -0
  27. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/advanced_rag/test_tools.py +0 -0
  28. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/deep_research/__init__.py +0 -0
  29. {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/deep_research/test_hooks.py +0 -0
@@ -0,0 +1,29 @@
1
+ # Changelog
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
+
18
+ ## [integrations/agent_pack-v0.0.1] - 2026-07-20
19
+
20
+ ### 🚀 Features
21
+
22
+ - Add advanced rag agent (#3600)
23
+
24
+ ### 🧹 Chores
25
+
26
+ - Move agent pack to core integrations (#3591)
27
+ - Prepare Agent pack for 3.0 release (#3611)
28
+
29
+ <!-- generated by git-cliff -->
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: agent-pack-haystack
3
- Version: 0.0.1
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
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.14
20
20
  Classifier: Programming Language :: Python :: Implementation :: CPython
21
21
  Classifier: Programming Language :: Python :: Implementation :: PyPy
22
22
  Requires-Python: >=3.10
23
- Requires-Dist: haystack-ai>=3.0.0
23
+ Requires-Dist: haystack-ai>=3.1.0
24
24
  Description-Content-Type: text/markdown
25
25
 
26
26
  # agent-pack-haystack
@@ -28,6 +28,7 @@ Description-Content-Type: text/markdown
28
28
  [![PyPI - Version](https://img.shields.io/pypi/v/agent-pack-haystack.svg)](https://pypi.org/project/agent-pack-haystack)
29
29
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/agent-pack-haystack.svg)](https://pypi.org/project/agent-pack-haystack)
30
30
 
31
+ - [Integration page](https://docs.haystack.deepset.ai/docs/agent-pack)
31
32
  - [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/agent_pack/CHANGELOG.md)
32
33
 
33
34
  ---
@@ -3,6 +3,7 @@
3
3
  [![PyPI - Version](https://img.shields.io/pypi/v/agent-pack-haystack.svg)](https://pypi.org/project/agent-pack-haystack)
4
4
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/agent-pack-haystack.svg)](https://pypi.org/project/agent-pack-haystack)
5
5
 
6
+ - [Integration page](https://docs.haystack.deepset.ai/docs/agent-pack)
6
7
  - [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/agent_pack/CHANGELOG.md)
7
8
 
8
9
  ---
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Programming Language :: Python :: Implementation :: CPython",
24
24
  "Programming Language :: Python :: Implementation :: PyPy",
25
25
  ]
26
- dependencies = ["haystack-ai>=3.0.0"]
26
+ dependencies = ["haystack-ai>=3.1.0"]
27
27
 
28
28
  [project.urls]
29
29
  Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/agent_pack#readme"
@@ -133,6 +133,7 @@ ignore = [
133
133
  "PLR0912",
134
134
  "PLR0913",
135
135
  "PLR0915",
136
+ "PLR0917",
136
137
  # Allow `Any` type - used legitimately for dynamic types and SDK boundaries
137
138
  "ANN401",
138
139
  ]
@@ -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 ComponentTool
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
@@ -82,110 +84,94 @@ def _make_researcher_agent(
82
84
  )
83
85
 
84
86
 
85
- # Minimal input schema for the research_subtopic tool
86
- _RESEARCH_SUBTOPIC_PARAMS = {
87
- "type": "object",
88
- "properties": {
89
- "messages": {
90
- "type": "array",
91
- "description": "Exactly one user message whose content is the sub-question to research.",
92
- "items": {
93
- "type": "object",
94
- "properties": {
95
- "role": {"type": "string", "enum": ["user"]},
96
- "content": {"type": "string", "description": "The sub-question to research."},
97
- },
98
- "required": ["role", "content"],
99
- },
100
- }
101
- },
102
- "required": ["messages"],
103
- }
104
-
105
-
106
87
  def create_deep_research_agent(
107
88
  *,
108
- scope_llm: ChatGenerator | None = None,
109
- 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,
110
93
  researcher_llm: ChatGenerator | None = None,
111
- summarizer_llm: ChatGenerator | None = None,
112
- writer_llm: ChatGenerator | None = None,
113
- max_subtopics: int = 5,
114
- max_concurrent_researchers: int = 5,
115
- max_orchestrator_steps: int = 8,
94
+ search_tool: Tool | None = None,
95
+ page_summary_llm: ChatGenerator | None = None,
96
+ report_llm: ChatGenerator | None = None,
116
97
  max_researcher_steps: int = 20,
117
- max_search_results: int = 10,
118
- max_content_length: int = 50_000,
98
+ max_concurrent_researchers: int = 5,
99
+ max_subtopics: int = 5,
100
+ max_page_chars: int = 50_000,
119
101
  ) -> Agent:
120
102
  """
121
103
  Create the deep research agent.
122
104
 
123
- :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.
124
109
  Defaults to `OpenAIResponsesChatGenerator("gpt-5.4")`.
125
- :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.
126
114
  Defaults to `OpenAIResponsesChatGenerator("gpt-5.4")`.
127
115
  :param researcher_llm: LLM that drives each sub-researcher's search/read/think loop.
128
116
  Defaults to `OpenAIResponsesChatGenerator("gpt-5.4-mini")`.
129
- :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
130
120
  the question. Defaults to `OpenAIResponsesChatGenerator("gpt-5.4-mini")`.
131
- :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.
132
122
  Defaults to `OpenAIResponsesChatGenerator("gpt-5.4")`.
133
- :param max_subtopics: Maximum number of sub-questions the orchestrator may delegate (breadth).
134
- :param max_concurrent_researchers: Maximum number of sub-researchers that run at the same time.
135
- :param max_orchestrator_steps: Maximum steps for the orchestrator's agent loop (reflect -> delegate rounds).
136
123
  :param max_researcher_steps: Maximum steps for each sub-researcher's agent loop.
137
- :param max_search_results: Number of results returned per `web_search` call.
138
- :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.
139
127
  :returns: The deep research `Agent`. Call it with the question as a user message,
140
128
  `agent.run(messages=[ChatMessage.from_user(question)])`; it returns a dict whose main output is
141
129
  `report` (the final markdown report, a `str`). The dict also carries the intermediate `brief`
142
130
  (`str`) and `notes` (`list[str]`), plus the standard Agent outputs `messages`, `last_message`,
143
131
  `step_count`, `token_usage` and `tool_call_counts`.
144
132
  """
145
- scope_llm = scope_llm or _default_llm("gpt-5.4")
146
- 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")
147
135
  researcher_llm = researcher_llm or _default_llm("gpt-5.4-mini")
148
- summarizer_llm = summarizer_llm or _default_llm("gpt-5.4-mini")
149
- 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")
150
138
 
151
139
  researcher = _make_researcher_agent(
152
140
  researcher_llm=researcher_llm,
153
- summarizer_llm=summarizer_llm,
141
+ page_summary_llm=page_summary_llm,
142
+ search_tool=search_tool,
154
143
  max_researcher_steps=max_researcher_steps,
155
- max_search_results=max_search_results,
156
- max_content_length=max_content_length,
144
+ max_page_chars=max_page_chars,
157
145
  )
158
146
 
159
- research_subtopic = ComponentTool(
160
- component=researcher,
147
+ research_subtopic = AgentTool(
148
+ agent=researcher,
161
149
  name="research_subtopic",
162
150
  description=(
163
151
  "Research ONE focused sub-question in an isolated context. "
164
152
  "Pass the sub-question as a user message. "
165
153
  "Returns a compressed, cited summary of the findings."
166
154
  ),
167
- parameters=_RESEARCH_SUBTOPIC_PARAMS,
168
- outputs_to_string={"source": "last_message"},
169
155
  outputs_to_state={"notes": {"source": "last_message", "handler": _collect_note}},
170
156
  )
171
157
 
172
158
  scope = ScopeHook(
173
- generator=scope_llm,
159
+ generator=brief_llm,
174
160
  prompt_builder=ChatPromptBuilder(template=prompts.SCOPE_TEMPLATE, required_variables=["query"]),
175
161
  )
176
162
  write = WriteHook(
177
- generator=writer_llm,
163
+ generator=report_llm,
178
164
  prompt_builder=ChatPromptBuilder(template=prompts.WRITER_TEMPLATE, required_variables=["replies", "notes"]),
179
165
  )
180
166
 
181
- 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))
182
168
 
183
169
  return Agent(
184
- chat_generator=orchestrator_llm,
170
+ chat_generator=llm,
185
171
  system_prompt=system_prompt,
186
172
  tools=[research_subtopic, think_tool],
187
173
  exit_conditions=["text"],
188
- max_agent_steps=max_orchestrator_steps,
174
+ max_agent_steps=max_agent_steps,
189
175
  tool_concurrency_limit=max_concurrent_researchers,
190
176
  state_schema={"notes": {"type": list}, "brief": {"type": str}, "report": {"type": str}},
191
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,6 +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, tool
8
9
 
9
10
  from haystack_integrations.agent_pack.deep_research.agent import (
10
11
  _collect_note,
@@ -46,10 +47,10 @@ def test_collect_note_skips_empty_message():
46
47
  def test_make_researcher_agent_wires_the_three_tools():
47
48
  researcher = _make_researcher_agent(
48
49
  researcher_llm=MockChatGenerator(),
49
- summarizer_llm=MockChatGenerator(),
50
+ page_summary_llm=MockChatGenerator(),
51
+ search_tool=None,
50
52
  max_researcher_steps=12,
51
- max_search_results=4,
52
- max_content_length=1000,
53
+ max_page_chars=1000,
53
54
  )
54
55
  assert isinstance(researcher, Agent)
55
56
  assert [t.name for t in researcher.tools] == ["web_search", "read_url", "think_tool"]
@@ -57,18 +58,37 @@ def test_make_researcher_agent_wires_the_three_tools():
57
58
  assert researcher.max_agent_steps == 12
58
59
 
59
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
+
60
77
  def test_create_deep_research_agent():
61
78
  agent = create_deep_research_agent(
62
- scope_llm=MockChatGenerator(),
63
- orchestrator_llm=MockChatGenerator(),
79
+ llm=MockChatGenerator(),
80
+ brief_llm=MockChatGenerator(),
64
81
  researcher_llm=MockChatGenerator(),
65
- summarizer_llm=MockChatGenerator(),
66
- writer_llm=MockChatGenerator(),
82
+ page_summary_llm=MockChatGenerator(),
83
+ report_llm=MockChatGenerator(),
67
84
  max_subtopics=3,
68
85
  max_concurrent_researchers=2,
69
- max_orchestrator_steps=6,
86
+ max_agent_steps=6,
70
87
  )
71
88
  assert [t.name for t in agent.tools] == ["research_subtopic", "think_tool"]
89
+ assert isinstance(agent.tools[0], AgentTool)
90
+ assert agent.tools[0].parameters["properties"]["messages"]["minItems"] == 1
91
+ assert agent.tools[0].parameters["properties"]["messages"]["maxItems"] == 1
72
92
  assert agent.exit_conditions == ["text"]
73
93
  assert agent.max_agent_steps == 6
74
94
  assert agent.tool_concurrency_limit == 2
@@ -82,15 +102,28 @@ def test_create_deep_research_agent():
82
102
  assert default_agent.chat_generator.model == "gpt-5.4"
83
103
 
84
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
+
85
118
  def test_deep_research_run(monkeypatch):
86
119
  # agent.run() warms up the TavilyWebSearch tool component, which resolves TAVILY_API_KEY
87
120
  monkeypatch.setenv("TAVILY_API_KEY", "test-key")
88
121
  agent = create_deep_research_agent(
89
- scope_llm=MockChatGenerator("Brief: investigate the causes of X."),
90
- 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."),
91
124
  researcher_llm=MockChatGenerator("X is caused by Y (https://example.com/x).\nSources:\nhttps://example.com/x"),
92
- summarizer_llm=MockChatGenerator(),
93
- 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)."),
94
127
  max_subtopics=1,
95
128
  )
96
129
  result = agent.run(messages=[ChatMessage.from_user("Tell me about X")])
@@ -104,17 +137,18 @@ def test_deep_research_run(monkeypatch):
104
137
  def test_create_deep_research_agent_serialization_roundtrip():
105
138
  allow_deserialization_module("haystack_integrations.agent_pack.*")
106
139
  agent = create_deep_research_agent(
107
- scope_llm=MockChatGenerator(),
108
- orchestrator_llm=MockChatGenerator(),
140
+ llm=MockChatGenerator(),
141
+ brief_llm=MockChatGenerator(),
109
142
  researcher_llm=MockChatGenerator(),
110
- summarizer_llm=MockChatGenerator(),
111
- writer_llm=MockChatGenerator(),
143
+ page_summary_llm=MockChatGenerator(),
144
+ report_llm=MockChatGenerator(),
112
145
  )
113
146
 
114
147
  restored = Agent.from_dict(agent.to_dict())
115
148
 
116
149
  assert isinstance(restored, Agent)
117
150
  assert [t.name for t in restored.tools] == ["research_subtopic", "think_tool"]
151
+ assert isinstance(restored.tools[0], AgentTool)
118
152
  assert [type(h) for h in restored.hooks["before_run"]] == [ScopeHook]
119
153
  assert [type(h) for h in restored.hooks["after_run"]] == [WriteHook]
120
154
  assert set(restored.state_schema) >= {"notes", "brief", "report"}
@@ -126,7 +160,7 @@ def test_create_deep_research_agent_serialization_roundtrip():
126
160
  reason="OPENAI_API_KEY and TAVILY_API_KEY required",
127
161
  )
128
162
  def test_deep_research_live_run():
129
- 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)
130
164
  result = agent.run(messages=[ChatMessage.from_user("What are the main causes and effects of ocean acidification?")])
131
165
 
132
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"]}