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.
- agent_pack_haystack-0.1.0/CHANGELOG.md +29 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/PKG-INFO +4 -3
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/README.md +1 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/pyproject.toml +2 -1
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/agent.py +6 -28
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/agent.py +51 -65
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/tools.py +5 -5
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/advanced_rag/test_agent.py +1 -45
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/deep_research/test_agent.py +51 -17
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/deep_research/test_tools.py +1 -1
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/.gitignore +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/LICENSE.txt +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/examples/advanced_rag_eval.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/pydoc/config_docusaurus.yml +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/__init__.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/__init__.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/hooks.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/prompts.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/advanced_rag/tools.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/__init__.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/hooks.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/deep_research/prompts.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/src/haystack_integrations/agent_pack/py.typed +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/__init__.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/advanced_rag/__init__.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/advanced_rag/test_hooks.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/advanced_rag/test_tools.py +0 -0
- {agent_pack_haystack-0.0.1 → agent_pack_haystack-0.1.0}/tests/deep_research/__init__.py +0 -0
- {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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-pack-haystack
|
|
3
|
-
Version: 0.0
|
|
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.
|
|
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
|
[](https://pypi.org/project/agent-pack-haystack)
|
|
29
29
|
[](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
|
[](https://pypi.org/project/agent-pack-haystack)
|
|
4
4
|
[](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.
|
|
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
|
|
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={
|
|
167
|
-
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
|
|
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
|
-
|
|
57
|
+
page_summary_llm: ChatGenerator,
|
|
58
|
+
search_tool: Tool | None,
|
|
58
59
|
max_researcher_steps: int,
|
|
59
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
make_read_url_tool(
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
138
|
-
:param
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
141
|
+
page_summary_llm=page_summary_llm,
|
|
142
|
+
search_tool=search_tool,
|
|
154
143
|
max_researcher_steps=max_researcher_steps,
|
|
155
|
-
|
|
156
|
-
max_content_length=max_content_length,
|
|
144
|
+
max_page_chars=max_page_chars,
|
|
157
145
|
)
|
|
158
146
|
|
|
159
|
-
research_subtopic =
|
|
160
|
-
|
|
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=
|
|
159
|
+
generator=brief_llm,
|
|
174
160
|
prompt_builder=ChatPromptBuilder(template=prompts.SCOPE_TEMPLATE, required_variables=["query"]),
|
|
175
161
|
)
|
|
176
162
|
write = WriteHook(
|
|
177
|
-
generator=
|
|
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=
|
|
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=
|
|
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(*,
|
|
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
|
|
73
|
-
:param
|
|
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(
|
|
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",
|
|
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.
|
|
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
|
-
|
|
50
|
+
page_summary_llm=MockChatGenerator(),
|
|
51
|
+
search_tool=None,
|
|
50
52
|
max_researcher_steps=12,
|
|
51
|
-
|
|
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
|
-
|
|
63
|
-
|
|
79
|
+
llm=MockChatGenerator(),
|
|
80
|
+
brief_llm=MockChatGenerator(),
|
|
64
81
|
researcher_llm=MockChatGenerator(),
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
page_summary_llm=MockChatGenerator(),
|
|
83
|
+
report_llm=MockChatGenerator(),
|
|
67
84
|
max_subtopics=3,
|
|
68
85
|
max_concurrent_researchers=2,
|
|
69
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
108
|
-
|
|
140
|
+
llm=MockChatGenerator(),
|
|
141
|
+
brief_llm=MockChatGenerator(),
|
|
109
142
|
researcher_llm=MockChatGenerator(),
|
|
110
|
-
|
|
111
|
-
|
|
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,
|
|
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(
|
|
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"]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|