neorag 0.2.0__py3-none-any.whl
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.
- neorag/__init__.py +7 -0
- neorag/agent/__init__.py +0 -0
- neorag/agent/direct.py +17 -0
- neorag/agent/fast.py +66 -0
- neorag/agent/prompts.py +53 -0
- neorag/agent/react.py +208 -0
- neorag/agent/tools.py +80 -0
- neorag/cli.py +272 -0
- neorag/config.py +232 -0
- neorag/data/__init__.py +0 -0
- neorag/data/chunker.py +51 -0
- neorag/data/loaders.py +135 -0
- neorag/data/qa_dataset.py +153 -0
- neorag/embeddings/__init__.py +0 -0
- neorag/embeddings/embedder.py +76 -0
- neorag/env.py +22 -0
- neorag/eval/__init__.py +0 -0
- neorag/eval/metrics.py +51 -0
- neorag/llm/__init__.py +0 -0
- neorag/llm/base.py +38 -0
- neorag/llm/local_hf.py +110 -0
- neorag/llm/openai_compat.py +114 -0
- neorag/logging_utils.py +20 -0
- neorag/pipeline.py +65 -0
- neorag/retrieval/__init__.py +0 -0
- neorag/retrieval/passage.py +26 -0
- neorag/retrieval/reranker.py +55 -0
- neorag/retrieval/retriever.py +40 -0
- neorag/store/__init__.py +0 -0
- neorag/store/vector_store.py +74 -0
- neorag/training/__init__.py +0 -0
- neorag/training/data_format.py +77 -0
- neorag/training/sft.py +168 -0
- neorag/ui.py +276 -0
- neorag/web/__init__.py +0 -0
- neorag/web/scraper.py +119 -0
- neorag/web/search.py +130 -0
- neorag/web/websearch.py +197 -0
- neorag-0.2.0.dist-info/METADATA +316 -0
- neorag-0.2.0.dist-info/RECORD +44 -0
- neorag-0.2.0.dist-info/WHEEL +5 -0
- neorag-0.2.0.dist-info/entry_points.txt +2 -0
- neorag-0.2.0.dist-info/licenses/LICENSE +21 -0
- neorag-0.2.0.dist-info/top_level.txt +1 -0
neorag/__init__.py
ADDED
neorag/agent/__init__.py
ADDED
|
File without changes
|
neorag/agent/direct.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from neorag.data.qa_dataset import RAG_QA_SYSTEM_PROMPT
|
|
4
|
+
from neorag.retrieval.passage import Passage, format_context
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def direct_answer(llm, retriever, question: str) -> tuple[str, list[Passage]]:
|
|
8
|
+
passages = retriever.retrieve(question)
|
|
9
|
+
if not passages:
|
|
10
|
+
return "I could not find the answer in the provided documents.", []
|
|
11
|
+
context, _ = format_context(passages)
|
|
12
|
+
messages = [
|
|
13
|
+
{"role": "system", "content": RAG_QA_SYSTEM_PROMPT},
|
|
14
|
+
{"role": "user", "content": f"CONTEXT:\n{context}\n\nQUESTION: {question}"},
|
|
15
|
+
]
|
|
16
|
+
answer = llm.generate(messages)
|
|
17
|
+
return answer, passages
|
neorag/agent/fast.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
|
|
5
|
+
from neorag.data.qa_dataset import RAG_QA_SYSTEM_PROMPT
|
|
6
|
+
from neorag.retrieval.passage import Passage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _build_context(passages: list[Passage], max_chars: int) -> tuple[str, int]:
|
|
10
|
+
lines = []
|
|
11
|
+
used = 0
|
|
12
|
+
count = 0
|
|
13
|
+
for i, p in enumerate(passages, start=1):
|
|
14
|
+
header = f"[{i}] (source: {p.metadata.get('source', 'unknown')})"
|
|
15
|
+
block = f"{header}\n{p.text}"
|
|
16
|
+
if used + len(block) > max_chars:
|
|
17
|
+
break
|
|
18
|
+
lines.append(block)
|
|
19
|
+
used += len(block)
|
|
20
|
+
count = i
|
|
21
|
+
return "\n\n".join(lines), count
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fast_answer(
|
|
25
|
+
llm,
|
|
26
|
+
retriever,
|
|
27
|
+
web_searcher,
|
|
28
|
+
question: str,
|
|
29
|
+
max_context_chars: int = 12000,
|
|
30
|
+
on_phase=None,
|
|
31
|
+
) -> tuple[str, list[Passage]]:
|
|
32
|
+
phases = on_phase or (lambda s: None)
|
|
33
|
+
|
|
34
|
+
def fetch_web() -> list[dict]:
|
|
35
|
+
if web_searcher is None:
|
|
36
|
+
return []
|
|
37
|
+
try:
|
|
38
|
+
return web_searcher.fetch_pages(question)
|
|
39
|
+
except Exception:
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
phases("searching local docs and the web")
|
|
43
|
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
44
|
+
local_future = pool.submit(retriever.retrieve, question)
|
|
45
|
+
web_future = pool.submit(fetch_web)
|
|
46
|
+
local_passages = list(local_future.result() or [])
|
|
47
|
+
web_pages = web_future.result() or []
|
|
48
|
+
|
|
49
|
+
phases(f"reading {len(local_passages) + len(web_pages)} sources")
|
|
50
|
+
web_passages = (
|
|
51
|
+
web_searcher.rank_pages_to_passages(question, web_pages) if web_searcher else []
|
|
52
|
+
)
|
|
53
|
+
passages = local_passages + web_passages
|
|
54
|
+
if not passages:
|
|
55
|
+
return "I could not find the answer in the provided documents or on the web.", []
|
|
56
|
+
|
|
57
|
+
context, count = _build_context(passages, max_context_chars)
|
|
58
|
+
passages = passages[:count]
|
|
59
|
+
|
|
60
|
+
phases("generating answer")
|
|
61
|
+
messages = [
|
|
62
|
+
{"role": "system", "content": RAG_QA_SYSTEM_PROMPT},
|
|
63
|
+
{"role": "user", "content": f"CONTEXT:\n{context}\n\nQUESTION: {question}"},
|
|
64
|
+
]
|
|
65
|
+
answer = llm.generate(messages)
|
|
66
|
+
return answer, passages
|
neorag/agent/prompts.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from neorag.agent.tools import Tool
|
|
7
|
+
|
|
8
|
+
_INTRO = """You are NeoRAG, a retrieval-grounded answering agent. You answer only from information returned by tools, never from imagination. The tools may search a private document collection, the public web, or both."""
|
|
9
|
+
|
|
10
|
+
_ACTION_FORMAT = """Each turn you must output exactly ONE JSON object and nothing else. No markdown, no extra text. Format:
|
|
11
|
+
|
|
12
|
+
{"thought": "<your reasoning>", "action": "<tool name or answer/give_up>", "action_input": {<tool-specific input>}}"""
|
|
13
|
+
|
|
14
|
+
_RULES = """Rules:
|
|
15
|
+
1. After each tool call you will receive an OBSERVATION containing numbered passages like [1], [2].
|
|
16
|
+
2. In the final "answer", cite the numbers of the passages you used, e.g. "citations": [1, 3].
|
|
17
|
+
3. Never invent facts. If observations do not contain the answer, try another tool or rephrase (up to the step limit) or choose "give_up".
|
|
18
|
+
4. Keep queries short and keyword-like; rewrite failed queries using synonyms."""
|
|
19
|
+
|
|
20
|
+
_TOOL_DOC_TEMPLATE = 'Tool "{name}": {description}\nInput example: {input_example}'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _default_input_example(tool: Any) -> str:
|
|
24
|
+
if "query" in tool.description.lower():
|
|
25
|
+
return '{"query": "<search words>"}'
|
|
26
|
+
return '{"value": "<input>"}'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_system_prompt(tools: list[Tool]) -> str:
|
|
30
|
+
tool_docs = [
|
|
31
|
+
_TOOL_DOC_TEMPLATE.format(
|
|
32
|
+
name=t.name, description=t.description, input_example=_default_input_example(t)
|
|
33
|
+
)
|
|
34
|
+
for t in tools
|
|
35
|
+
]
|
|
36
|
+
tool_section = "\n\n".join(tool_docs) if tool_docs else "No tools are available; answer or give_up directly."
|
|
37
|
+
return f"{_INTRO}\n\n{_ACTION_FORMAT}\n\n{_RULES}\n\nAvailable tools:\n{tool_section}\n"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
SYSTEM_PROMPT = (
|
|
41
|
+
f"{_INTRO}\n\n{_ACTION_FORMAT}\n\n{_RULES}\n\n"
|
|
42
|
+
'Tools:\nTool "search": Search the private document collection. Input example: {"query": "<search words>"}\n'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
OBSERVATION_HEADER = "OBSERVATION (numbered passages retrieved for your query)"
|
|
46
|
+
|
|
47
|
+
LAST_CHANCE_PROMPT = """Based only on the OBSERVATIONS above, now respond with exactly ONE JSON object using the "answer" action:
|
|
48
|
+
|
|
49
|
+
{"thought": "<reasoning>", "action": "answer", "action_input": {"answer": "<final answer grounded in the passages>", "citations": [<numbers>]}}
|
|
50
|
+
|
|
51
|
+
If the passages truly do not contain the answer, use the "give_up" action instead."""
|
|
52
|
+
|
|
53
|
+
NO_RESULTS_OBSERVATION = "No passages matched this query. Try different or simpler keywords."
|
neorag/agent/react.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from neorag.agent.prompts import (
|
|
8
|
+
LAST_CHANCE_PROMPT,
|
|
9
|
+
NO_RESULTS_OBSERVATION,
|
|
10
|
+
OBSERVATION_HEADER,
|
|
11
|
+
build_system_prompt,
|
|
12
|
+
)
|
|
13
|
+
from neorag.agent.tools import Tool
|
|
14
|
+
from neorag.logging_utils import logger
|
|
15
|
+
from neorag.retrieval.passage import Passage
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AgentResult:
|
|
20
|
+
answer: str
|
|
21
|
+
citations: list[int] = field(default_factory=list)
|
|
22
|
+
passages: dict[int, Passage] = field(default_factory=dict)
|
|
23
|
+
steps_used: int = 0
|
|
24
|
+
gave_up: bool = False
|
|
25
|
+
trace: list[dict] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def extract_json_object(text: str) -> dict | None:
|
|
29
|
+
text = re.sub(r"```(?:json)?", "", text).strip()
|
|
30
|
+
start = text.find("{")
|
|
31
|
+
while start != -1:
|
|
32
|
+
depth = 0
|
|
33
|
+
in_string = False
|
|
34
|
+
escape = False
|
|
35
|
+
for i in range(start, len(text)):
|
|
36
|
+
ch = text[i]
|
|
37
|
+
if in_string:
|
|
38
|
+
if escape:
|
|
39
|
+
escape = False
|
|
40
|
+
elif ch == "\\":
|
|
41
|
+
escape = True
|
|
42
|
+
elif ch == '"':
|
|
43
|
+
in_string = False
|
|
44
|
+
continue
|
|
45
|
+
if ch == '"':
|
|
46
|
+
in_string = True
|
|
47
|
+
elif ch == "{":
|
|
48
|
+
depth += 1
|
|
49
|
+
elif ch == "}":
|
|
50
|
+
depth -= 1
|
|
51
|
+
if depth == 0:
|
|
52
|
+
candidate = text[start : i + 1]
|
|
53
|
+
try:
|
|
54
|
+
obj = json.loads(candidate)
|
|
55
|
+
return obj if isinstance(obj, dict) else None
|
|
56
|
+
except json.JSONDecodeError:
|
|
57
|
+
break
|
|
58
|
+
start = text.find("{", start + 1)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_action(text: str) -> dict:
|
|
63
|
+
obj = extract_json_object(text)
|
|
64
|
+
if not obj:
|
|
65
|
+
return {"thought": "", "action": "invalid", "action_input": {}}
|
|
66
|
+
action = str(obj.get("action", "invalid")).strip().lower()
|
|
67
|
+
action_input = obj.get("action_input", obj.get("action_input:", {}))
|
|
68
|
+
if not isinstance(action_input, dict):
|
|
69
|
+
action_input = {"value": action_input}
|
|
70
|
+
thought = str(obj.get("thought", ""))
|
|
71
|
+
return {"thought": thought, "action": action, "action_input": action_input}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _format_observation(fresh: list[tuple[int, Passage]]) -> str:
|
|
75
|
+
if not fresh:
|
|
76
|
+
return NO_RESULTS_OBSERVATION
|
|
77
|
+
lines = [
|
|
78
|
+
f"[{n}] (source: {p.metadata.get('source', 'unknown')}, score: {p.score:.3f})\n{p.text}"
|
|
79
|
+
for n, p in fresh
|
|
80
|
+
]
|
|
81
|
+
return "\n\n".join(lines)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class RAGAgent:
|
|
85
|
+
def __init__(self, llm, tools: list[Tool], max_steps: int = 5, system_prompt: str | None = None):
|
|
86
|
+
self.llm = llm
|
|
87
|
+
self.tools = {t.name: t for t in tools}
|
|
88
|
+
self.max_steps = max_steps
|
|
89
|
+
self.system_prompt = system_prompt
|
|
90
|
+
|
|
91
|
+
def run(self, question: str, on_step=None) -> AgentResult:
|
|
92
|
+
system_content = self.system_prompt or build_system_prompt(list(self.tools.values()))
|
|
93
|
+
messages: list[dict[str, str]] = [
|
|
94
|
+
{"role": "system", "content": system_content},
|
|
95
|
+
{"role": "user", "content": question},
|
|
96
|
+
]
|
|
97
|
+
seen_ids: set[str] = set()
|
|
98
|
+
passage_registry: dict[int, Passage] = {}
|
|
99
|
+
next_number = 1
|
|
100
|
+
trace: list[dict] = []
|
|
101
|
+
gave_up = False
|
|
102
|
+
answer: str | None = None
|
|
103
|
+
citations: list[int] = []
|
|
104
|
+
tool_names = sorted(self.tools.keys())
|
|
105
|
+
consecutive_failures = 0
|
|
106
|
+
|
|
107
|
+
for step in range(1, self.max_steps + 1):
|
|
108
|
+
raw = self.llm.generate(messages)
|
|
109
|
+
parsed = parse_action(raw)
|
|
110
|
+
action = parsed["action"]
|
|
111
|
+
action_input = parsed["action_input"]
|
|
112
|
+
trace.append({"step": step, "thought": parsed["thought"], "action": action, "raw": raw})
|
|
113
|
+
if on_step is not None:
|
|
114
|
+
try:
|
|
115
|
+
on_step({"step": step, "action": action, "thought": parsed["thought"]})
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
logger.info(f"[agent step {step}] action={action} thought={parsed['thought'][:120]}")
|
|
119
|
+
|
|
120
|
+
if action == "answer":
|
|
121
|
+
candidate = str(action_input.get("answer", "")).strip()
|
|
122
|
+
if candidate:
|
|
123
|
+
answer = candidate
|
|
124
|
+
citations = self._validate_citations(action_input.get("citations"), passage_registry)
|
|
125
|
+
break
|
|
126
|
+
consecutive_failures += 1
|
|
127
|
+
if consecutive_failures >= 2:
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
if action == "give_up":
|
|
131
|
+
gave_up = True
|
|
132
|
+
answer = str(action_input.get("answer", "")).strip() or (
|
|
133
|
+
"I could not find the answer in the provided documents."
|
|
134
|
+
)
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
tool = self.tools.get(action)
|
|
138
|
+
if tool is not None:
|
|
139
|
+
result = tool.fn(action_input)
|
|
140
|
+
consecutive_failures = 0
|
|
141
|
+
fresh = [p for p in result.passages if p.id not in seen_ids]
|
|
142
|
+
for p in fresh:
|
|
143
|
+
seen_ids.add(p.id)
|
|
144
|
+
numbered = []
|
|
145
|
+
for p in fresh:
|
|
146
|
+
passage_registry[next_number] = p
|
|
147
|
+
numbered.append((next_number, p))
|
|
148
|
+
next_number += 1
|
|
149
|
+
observation = _format_observation(numbered)
|
|
150
|
+
messages.append({"role": "assistant", "content": raw})
|
|
151
|
+
messages.append({"role": "user", "content": f"{OBSERVATION_HEADER}:\n{observation}"})
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
messages.append({"role": "assistant", "content": raw})
|
|
155
|
+
consecutive_failures += 1
|
|
156
|
+
if consecutive_failures >= 2:
|
|
157
|
+
break
|
|
158
|
+
messages.append(
|
|
159
|
+
{
|
|
160
|
+
"role": "user",
|
|
161
|
+
"content": (
|
|
162
|
+
"That was not a valid action. Respond with exactly one JSON object using "
|
|
163
|
+
f'one of these actions: {", ".join(tool_names + ["answer", "give_up"])}. '
|
|
164
|
+
'Example: {"thought": "...", "action": "...", "action_input": {...}}'
|
|
165
|
+
),
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if answer is None:
|
|
170
|
+
logger.info("Step limit reached; forcing a final grounded answer.")
|
|
171
|
+
messages.append({"role": "user", "content": LAST_CHANCE_PROMPT})
|
|
172
|
+
raw = self.llm.generate(messages)
|
|
173
|
+
parsed = parse_action(raw)
|
|
174
|
+
answer = str(parsed["action_input"].get("answer", "")).strip()
|
|
175
|
+
citations = self._validate_citations(
|
|
176
|
+
parsed["action_input"].get("citations"), passage_registry
|
|
177
|
+
)
|
|
178
|
+
if parsed["action"] == "give_up":
|
|
179
|
+
gave_up = True
|
|
180
|
+
if not answer:
|
|
181
|
+
gave_up = True
|
|
182
|
+
answer = "I could not find the answer in the provided documents."
|
|
183
|
+
|
|
184
|
+
return AgentResult(
|
|
185
|
+
answer=answer,
|
|
186
|
+
citations=citations,
|
|
187
|
+
passages=passage_registry,
|
|
188
|
+
steps_used=len(trace),
|
|
189
|
+
gave_up=gave_up,
|
|
190
|
+
trace=trace,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def _validate_citations(
|
|
194
|
+
self, raw_citations: object, registry: dict[int, Passage]
|
|
195
|
+
) -> list[int]:
|
|
196
|
+
if isinstance(raw_citations, (int, float)):
|
|
197
|
+
raw_citations = [raw_citations]
|
|
198
|
+
if not isinstance(raw_citations, list):
|
|
199
|
+
return []
|
|
200
|
+
valid = []
|
|
201
|
+
for c in raw_citations:
|
|
202
|
+
try:
|
|
203
|
+
n = int(c)
|
|
204
|
+
except (TypeError, ValueError):
|
|
205
|
+
continue
|
|
206
|
+
if n in registry:
|
|
207
|
+
valid.append(n)
|
|
208
|
+
return sorted(set(valid))
|
neorag/agent/tools.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from neorag.agent.prompts import NO_RESULTS_OBSERVATION
|
|
7
|
+
from neorag.retrieval.passage import Passage, format_context
|
|
8
|
+
from neorag.retrieval.retriever import Retriever
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ToolResult:
|
|
13
|
+
observation: str
|
|
14
|
+
passages: list[Passage]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Tool:
|
|
19
|
+
name: str
|
|
20
|
+
description: str
|
|
21
|
+
fn: Callable[[dict], ToolResult]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SearchDocumentsTool:
|
|
25
|
+
def __init__(self, retriever: Retriever, top_k: int | None = None):
|
|
26
|
+
self.retriever = retriever
|
|
27
|
+
self.top_k = top_k
|
|
28
|
+
|
|
29
|
+
def __call__(self, action_input: dict) -> ToolResult:
|
|
30
|
+
query = str(action_input.get("query", "")).strip()
|
|
31
|
+
if not query:
|
|
32
|
+
return ToolResult(observation=NO_RESULTS_OBSERVATION, passages=[])
|
|
33
|
+
retriever = self.retriever
|
|
34
|
+
if self.top_k:
|
|
35
|
+
original_final = retriever.final_k
|
|
36
|
+
retriever.final_k = self.top_k
|
|
37
|
+
try:
|
|
38
|
+
passages = retriever.retrieve(query)
|
|
39
|
+
finally:
|
|
40
|
+
retriever.final_k = original_final
|
|
41
|
+
else:
|
|
42
|
+
passages = retriever.retrieve(query)
|
|
43
|
+
if not passages:
|
|
44
|
+
return ToolResult(observation=NO_RESULTS_OBSERVATION, passages=[])
|
|
45
|
+
text, _ = format_context(passages)
|
|
46
|
+
return ToolResult(observation=text, passages=passages)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_web_tool(web_searcher, top_k: int | None = None) -> Tool:
|
|
50
|
+
from neorag.web.websearch import WebSearchTool
|
|
51
|
+
|
|
52
|
+
searcher = WebSearchTool(web_searcher, top_k=top_k)
|
|
53
|
+
return Tool(
|
|
54
|
+
name="web_search",
|
|
55
|
+
description=(
|
|
56
|
+
"Search the public web for fresh information. "
|
|
57
|
+
'Input: {"query": "<search words>"}. Returns numbered passages with URLs as sources.'
|
|
58
|
+
),
|
|
59
|
+
fn=searcher,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def default_tools(
|
|
64
|
+
retriever: Retriever,
|
|
65
|
+
web_searcher=None,
|
|
66
|
+
search_top_k: int | None = None,
|
|
67
|
+
web_top_k: int | None = None,
|
|
68
|
+
) -> list[Tool]:
|
|
69
|
+
search = Tool(
|
|
70
|
+
name="search",
|
|
71
|
+
description=(
|
|
72
|
+
"Search the private document collection. "
|
|
73
|
+
'Input: {"query": "<search words>"}. Returns numbered passages.'
|
|
74
|
+
),
|
|
75
|
+
fn=SearchDocumentsTool(retriever, top_k=search_top_k),
|
|
76
|
+
)
|
|
77
|
+
tools = [search]
|
|
78
|
+
if web_searcher is not None:
|
|
79
|
+
tools.append(build_web_tool(web_searcher, top_k=web_top_k))
|
|
80
|
+
return tools
|