onetool-mcp 1.0.0b1__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.
- bench/__init__.py +5 -0
- bench/cli.py +69 -0
- bench/harness/__init__.py +66 -0
- bench/harness/client.py +692 -0
- bench/harness/config.py +397 -0
- bench/harness/csv_writer.py +109 -0
- bench/harness/evaluate.py +512 -0
- bench/harness/metrics.py +283 -0
- bench/harness/runner.py +899 -0
- bench/py.typed +0 -0
- bench/reporter.py +629 -0
- bench/run.py +487 -0
- bench/secrets.py +101 -0
- bench/utils.py +16 -0
- onetool/__init__.py +4 -0
- onetool/cli.py +391 -0
- onetool/py.typed +0 -0
- onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
- onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
- onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
- onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
- ot/__init__.py +37 -0
- ot/__main__.py +6 -0
- ot/_cli.py +107 -0
- ot/_tui.py +53 -0
- ot/config/__init__.py +46 -0
- ot/config/defaults/bench.yaml +4 -0
- ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
- ot/config/defaults/diagram-templates/c4-context.puml +30 -0
- ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
- ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
- ot/config/defaults/diagram-templates/microservices.d2 +81 -0
- ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
- ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
- ot/config/defaults/onetool.yaml +25 -0
- ot/config/defaults/prompts.yaml +97 -0
- ot/config/defaults/servers.yaml +7 -0
- ot/config/defaults/snippets.yaml +4 -0
- ot/config/defaults/tool_templates/__init__.py +7 -0
- ot/config/defaults/tool_templates/extension.py +52 -0
- ot/config/defaults/tool_templates/isolated.py +61 -0
- ot/config/dynamic.py +121 -0
- ot/config/global_templates/__init__.py +2 -0
- ot/config/global_templates/bench-secrets-template.yaml +6 -0
- ot/config/global_templates/bench.yaml +9 -0
- ot/config/global_templates/onetool.yaml +27 -0
- ot/config/global_templates/secrets-template.yaml +44 -0
- ot/config/global_templates/servers.yaml +18 -0
- ot/config/global_templates/snippets.yaml +235 -0
- ot/config/loader.py +1087 -0
- ot/config/mcp.py +145 -0
- ot/config/secrets.py +190 -0
- ot/config/tool_config.py +125 -0
- ot/decorators.py +116 -0
- ot/executor/__init__.py +35 -0
- ot/executor/base.py +16 -0
- ot/executor/fence_processor.py +83 -0
- ot/executor/linter.py +142 -0
- ot/executor/pack_proxy.py +260 -0
- ot/executor/param_resolver.py +140 -0
- ot/executor/pep723.py +288 -0
- ot/executor/result_store.py +369 -0
- ot/executor/runner.py +496 -0
- ot/executor/simple.py +163 -0
- ot/executor/tool_loader.py +396 -0
- ot/executor/validator.py +398 -0
- ot/executor/worker_pool.py +388 -0
- ot/executor/worker_proxy.py +189 -0
- ot/http_client.py +145 -0
- ot/logging/__init__.py +37 -0
- ot/logging/config.py +315 -0
- ot/logging/entry.py +213 -0
- ot/logging/format.py +188 -0
- ot/logging/span.py +349 -0
- ot/meta.py +1555 -0
- ot/paths.py +453 -0
- ot/prompts.py +218 -0
- ot/proxy/__init__.py +21 -0
- ot/proxy/manager.py +396 -0
- ot/py.typed +0 -0
- ot/registry/__init__.py +189 -0
- ot/registry/models.py +57 -0
- ot/registry/parser.py +269 -0
- ot/registry/registry.py +413 -0
- ot/server.py +315 -0
- ot/shortcuts/__init__.py +15 -0
- ot/shortcuts/aliases.py +87 -0
- ot/shortcuts/snippets.py +258 -0
- ot/stats/__init__.py +35 -0
- ot/stats/html.py +250 -0
- ot/stats/jsonl_writer.py +283 -0
- ot/stats/reader.py +354 -0
- ot/stats/timing.py +57 -0
- ot/support.py +63 -0
- ot/tools.py +114 -0
- ot/utils/__init__.py +81 -0
- ot/utils/batch.py +161 -0
- ot/utils/cache.py +120 -0
- ot/utils/deps.py +403 -0
- ot/utils/exceptions.py +23 -0
- ot/utils/factory.py +179 -0
- ot/utils/format.py +65 -0
- ot/utils/http.py +202 -0
- ot/utils/platform.py +45 -0
- ot/utils/sanitize.py +130 -0
- ot/utils/truncate.py +69 -0
- ot_tools/__init__.py +4 -0
- ot_tools/_convert/__init__.py +12 -0
- ot_tools/_convert/excel.py +279 -0
- ot_tools/_convert/pdf.py +254 -0
- ot_tools/_convert/powerpoint.py +268 -0
- ot_tools/_convert/utils.py +358 -0
- ot_tools/_convert/word.py +283 -0
- ot_tools/brave_search.py +604 -0
- ot_tools/code_search.py +736 -0
- ot_tools/context7.py +495 -0
- ot_tools/convert.py +614 -0
- ot_tools/db.py +415 -0
- ot_tools/diagram.py +1604 -0
- ot_tools/diagram.yaml +167 -0
- ot_tools/excel.py +1372 -0
- ot_tools/file.py +1348 -0
- ot_tools/firecrawl.py +732 -0
- ot_tools/grounding_search.py +646 -0
- ot_tools/package.py +604 -0
- ot_tools/py.typed +0 -0
- ot_tools/ripgrep.py +544 -0
- ot_tools/scaffold.py +471 -0
- ot_tools/transform.py +213 -0
- ot_tools/web_fetch.py +384 -0
ot_tools/brave_search.py
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
"""Brave Search API tools.
|
|
2
|
+
|
|
3
|
+
Provides web search, local search, news search, image search, and video search
|
|
4
|
+
via the Brave Search API. Requires BRAVE_API_KEY secret in secrets.yaml.
|
|
5
|
+
|
|
6
|
+
API docs: https://api-dashboard.search.brave.com/app/documentation
|
|
7
|
+
Reference: https://github.com/brave/brave-search-mcp-server
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
# Pack for dot notation: brave.search(), brave.news(), etc.
|
|
13
|
+
pack = "brave"
|
|
14
|
+
|
|
15
|
+
__all__ = ["image", "local", "news", "search", "search_batch", "video"]
|
|
16
|
+
|
|
17
|
+
# Dependency declarations for CLI validation
|
|
18
|
+
__ot_requires__ = {
|
|
19
|
+
"secrets": ["BRAVE_API_KEY"],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
from typing import Any, Literal
|
|
23
|
+
|
|
24
|
+
import httpx
|
|
25
|
+
from pydantic import BaseModel, Field
|
|
26
|
+
|
|
27
|
+
from ot.config import get_secret, get_tool_config
|
|
28
|
+
from ot.logging import LogSpan
|
|
29
|
+
from ot.utils import batch_execute, format_batch_results, lazy_client, normalize_items
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Config(BaseModel):
|
|
33
|
+
"""Pack configuration - discovered by registry."""
|
|
34
|
+
|
|
35
|
+
timeout: float = Field(
|
|
36
|
+
default=60.0,
|
|
37
|
+
ge=1.0,
|
|
38
|
+
le=300.0,
|
|
39
|
+
description="Request timeout in seconds",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
BRAVE_API_BASE = "https://api.search.brave.com/res/v1"
|
|
43
|
+
|
|
44
|
+
# Truncation length for video descriptions (147 chars + "..." = 150 total)
|
|
45
|
+
VIDEO_DESC_MAX_LENGTH = 150
|
|
46
|
+
|
|
47
|
+
def _create_http_client() -> httpx.Client:
|
|
48
|
+
"""Create HTTP client for Brave API requests."""
|
|
49
|
+
return httpx.Client(
|
|
50
|
+
base_url=BRAVE_API_BASE,
|
|
51
|
+
timeout=60.0,
|
|
52
|
+
headers={"Accept": "application/json", "Accept-Encoding": "gzip"},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Thread-safe lazy client using SDK utility
|
|
57
|
+
_get_http_client = lazy_client(_create_http_client)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_config() -> Config:
|
|
61
|
+
"""Get brave pack configuration."""
|
|
62
|
+
return get_tool_config("brave", Config)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_api_key() -> str:
|
|
66
|
+
"""Get Brave API key from secrets."""
|
|
67
|
+
return get_secret("BRAVE_API_KEY") or ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _get_headers(api_key: str) -> dict[str, str]:
|
|
71
|
+
"""Get headers for Brave API requests.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
api_key: Pre-fetched Brave API key
|
|
75
|
+
"""
|
|
76
|
+
return {
|
|
77
|
+
"X-Subscription-Token": api_key,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _make_request(
|
|
82
|
+
endpoint: str,
|
|
83
|
+
params: dict[str, Any],
|
|
84
|
+
timeout: float | None = None,
|
|
85
|
+
) -> tuple[bool, dict[str, Any] | str]:
|
|
86
|
+
"""Make HTTP GET request to Brave API.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
endpoint: API endpoint path (e.g., "/web/search")
|
|
90
|
+
params: Query parameters
|
|
91
|
+
timeout: Request timeout in seconds
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Tuple of (success, result). If success, result is parsed JSON dict.
|
|
95
|
+
If failure, result is error message string.
|
|
96
|
+
"""
|
|
97
|
+
api_key = _get_api_key()
|
|
98
|
+
if not api_key:
|
|
99
|
+
return False, "Error: BRAVE_API_KEY secret not configured"
|
|
100
|
+
|
|
101
|
+
if timeout is None:
|
|
102
|
+
timeout = _get_config().timeout
|
|
103
|
+
|
|
104
|
+
with LogSpan(
|
|
105
|
+
span="brave.request", endpoint=endpoint, query=params.get("q", "")
|
|
106
|
+
) as span:
|
|
107
|
+
try:
|
|
108
|
+
client = _get_http_client()
|
|
109
|
+
if client is None:
|
|
110
|
+
return False, "Error: HTTP client not initialized"
|
|
111
|
+
response = client.get(
|
|
112
|
+
endpoint,
|
|
113
|
+
params=params,
|
|
114
|
+
headers=_get_headers(api_key),
|
|
115
|
+
timeout=timeout,
|
|
116
|
+
)
|
|
117
|
+
response.raise_for_status()
|
|
118
|
+
|
|
119
|
+
result = response.json()
|
|
120
|
+
span.add(status=response.status_code)
|
|
121
|
+
return True, result
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
error_type = type(e).__name__
|
|
125
|
+
span.add(error=f"{error_type}: {e}")
|
|
126
|
+
|
|
127
|
+
if hasattr(e, "response"):
|
|
128
|
+
status = getattr(e.response, "status_code", "unknown")
|
|
129
|
+
text = getattr(e.response, "text", "")[:200]
|
|
130
|
+
return False, f"HTTP error ({status}): {text}"
|
|
131
|
+
return False, f"Request failed: {e}"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _format_web_results(data: dict[str, Any]) -> str:
|
|
135
|
+
"""Format web search results for display."""
|
|
136
|
+
lines: list[str] = []
|
|
137
|
+
|
|
138
|
+
web = data.get("web", {})
|
|
139
|
+
results = web.get("results", [])
|
|
140
|
+
|
|
141
|
+
if not results:
|
|
142
|
+
return "No results found."
|
|
143
|
+
|
|
144
|
+
for i, result in enumerate(results, 1):
|
|
145
|
+
title = result.get("title", "No title")
|
|
146
|
+
url = result.get("url", "")
|
|
147
|
+
description = result.get("description", "")
|
|
148
|
+
|
|
149
|
+
lines.append(f"{i}. {title}")
|
|
150
|
+
lines.append(f" {url}")
|
|
151
|
+
if description:
|
|
152
|
+
lines.append(f" {description}")
|
|
153
|
+
lines.append("")
|
|
154
|
+
|
|
155
|
+
return "\n".join(lines)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _format_news_results(data: dict[str, Any]) -> str:
|
|
159
|
+
"""Format news search results for display."""
|
|
160
|
+
lines: list[str] = []
|
|
161
|
+
|
|
162
|
+
results = data.get("results", [])
|
|
163
|
+
|
|
164
|
+
if not results:
|
|
165
|
+
return "No news results found."
|
|
166
|
+
|
|
167
|
+
for i, result in enumerate(results, 1):
|
|
168
|
+
title = result.get("title", "No title")
|
|
169
|
+
url = result.get("url", "")
|
|
170
|
+
source = result.get("meta_url", {}).get("hostname", "")
|
|
171
|
+
age = result.get("age", "")
|
|
172
|
+
breaking = result.get("breaking", False)
|
|
173
|
+
|
|
174
|
+
prefix = "[BREAKING] " if breaking else ""
|
|
175
|
+
lines.append(f"{i}. {prefix}{title}")
|
|
176
|
+
if source:
|
|
177
|
+
lines.append(f" Source: {source} ({age})")
|
|
178
|
+
lines.append(f" {url}")
|
|
179
|
+
lines.append("")
|
|
180
|
+
|
|
181
|
+
return "\n".join(lines)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _format_local_results_from_web(data: dict[str, Any]) -> str:
|
|
185
|
+
"""Format local results from web search with locations filter."""
|
|
186
|
+
lines: list[str] = []
|
|
187
|
+
|
|
188
|
+
locations = data.get("locations", {})
|
|
189
|
+
results = locations.get("results", [])
|
|
190
|
+
|
|
191
|
+
if not results:
|
|
192
|
+
# Inform user about fallback instead of silent switch
|
|
193
|
+
web_results = _format_web_results(data)
|
|
194
|
+
if web_results == "No results found.":
|
|
195
|
+
return web_results
|
|
196
|
+
return f"No local business data found. Showing web results:\n\n{web_results}"
|
|
197
|
+
|
|
198
|
+
for i, result in enumerate(results, 1):
|
|
199
|
+
name = result.get("title", "No name")
|
|
200
|
+
|
|
201
|
+
lines.append(f"{i}. {name}")
|
|
202
|
+
|
|
203
|
+
# Address
|
|
204
|
+
address = result.get("address", {})
|
|
205
|
+
if address:
|
|
206
|
+
addr_parts = [
|
|
207
|
+
address.get("streetAddress", ""),
|
|
208
|
+
address.get("addressLocality", ""),
|
|
209
|
+
address.get("addressRegion", ""),
|
|
210
|
+
]
|
|
211
|
+
addr_str = ", ".join(p for p in addr_parts if p)
|
|
212
|
+
if addr_str:
|
|
213
|
+
lines.append(f" Address: {addr_str}")
|
|
214
|
+
|
|
215
|
+
# Rating
|
|
216
|
+
rating = result.get("rating", {})
|
|
217
|
+
if rating:
|
|
218
|
+
stars = rating.get("ratingValue", "")
|
|
219
|
+
count = rating.get("ratingCount", "")
|
|
220
|
+
if stars:
|
|
221
|
+
lines.append(f" Rating: {stars}/5 ({count} reviews)")
|
|
222
|
+
|
|
223
|
+
# Phone
|
|
224
|
+
phone = result.get("phone", "")
|
|
225
|
+
if phone:
|
|
226
|
+
lines.append(f" Phone: {phone}")
|
|
227
|
+
|
|
228
|
+
lines.append("")
|
|
229
|
+
|
|
230
|
+
return "\n".join(lines)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _format_image_results(data: dict[str, Any]) -> str:
|
|
234
|
+
"""Format image search results for display."""
|
|
235
|
+
lines: list[str] = []
|
|
236
|
+
|
|
237
|
+
results = data.get("results", [])
|
|
238
|
+
|
|
239
|
+
if not results:
|
|
240
|
+
return "No image results found."
|
|
241
|
+
|
|
242
|
+
for i, result in enumerate(results, 1):
|
|
243
|
+
title = result.get("title", "No title")
|
|
244
|
+
url = result.get("url", "")
|
|
245
|
+
source = result.get("source", "")
|
|
246
|
+
width = result.get("properties", {}).get("width", "")
|
|
247
|
+
height = result.get("properties", {}).get("height", "")
|
|
248
|
+
|
|
249
|
+
lines.append(f"{i}. {title}")
|
|
250
|
+
if width and height:
|
|
251
|
+
lines.append(f" Size: {width}x{height}")
|
|
252
|
+
if source:
|
|
253
|
+
lines.append(f" Source: {source}")
|
|
254
|
+
lines.append(f" {url}")
|
|
255
|
+
lines.append("")
|
|
256
|
+
|
|
257
|
+
return "\n".join(lines)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _format_video_results(data: dict[str, Any]) -> str:
|
|
261
|
+
"""Format video search results for display."""
|
|
262
|
+
lines: list[str] = []
|
|
263
|
+
|
|
264
|
+
results = data.get("results", [])
|
|
265
|
+
|
|
266
|
+
if not results:
|
|
267
|
+
return "No video results found."
|
|
268
|
+
|
|
269
|
+
for i, result in enumerate(results, 1):
|
|
270
|
+
title = result.get("title", "No title")
|
|
271
|
+
url = result.get("url", "")
|
|
272
|
+
description = result.get("description", "")
|
|
273
|
+
creator = result.get("meta_url", {}).get("hostname", "")
|
|
274
|
+
duration = result.get("video", {}).get("duration", "")
|
|
275
|
+
views = result.get("video", {}).get("views", "")
|
|
276
|
+
|
|
277
|
+
lines.append(f"{i}. {title}")
|
|
278
|
+
if creator:
|
|
279
|
+
lines.append(f" Channel: {creator}")
|
|
280
|
+
if duration:
|
|
281
|
+
lines.append(f" Duration: {duration}")
|
|
282
|
+
if views:
|
|
283
|
+
lines.append(f" Views: {views}")
|
|
284
|
+
if description:
|
|
285
|
+
if len(description) > VIDEO_DESC_MAX_LENGTH:
|
|
286
|
+
description = description[: VIDEO_DESC_MAX_LENGTH - 3] + "..."
|
|
287
|
+
lines.append(f" {description}")
|
|
288
|
+
lines.append(f" {url}")
|
|
289
|
+
lines.append("")
|
|
290
|
+
|
|
291
|
+
return "\n".join(lines)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _clamp(value: int, min_val: int, max_val: int) -> int:
|
|
295
|
+
"""Clamp a value between min and max."""
|
|
296
|
+
return min(max(value, min_val), max_val)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _validate_query(query: str) -> str | None:
|
|
300
|
+
"""Validate query against Brave API limits.
|
|
301
|
+
|
|
302
|
+
Returns None if valid, error message if invalid.
|
|
303
|
+
"""
|
|
304
|
+
if not query or not query.strip():
|
|
305
|
+
return "Error: Query cannot be empty"
|
|
306
|
+
if len(query) > 400:
|
|
307
|
+
return f"Error: Query exceeds 400 character limit ({len(query)} chars)"
|
|
308
|
+
word_count = len(query.split())
|
|
309
|
+
if word_count > 50:
|
|
310
|
+
return f"Error: Query exceeds 50 word limit ({word_count} words)"
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def search(
|
|
315
|
+
*,
|
|
316
|
+
query: str,
|
|
317
|
+
count: int = 10,
|
|
318
|
+
offset: int = 0,
|
|
319
|
+
country: str = "US",
|
|
320
|
+
search_lang: str = "en",
|
|
321
|
+
safesearch: Literal["off", "moderate", "strict"] = "moderate",
|
|
322
|
+
freshness: Literal["pd", "pw", "pm", "py"] | None = None,
|
|
323
|
+
) -> str:
|
|
324
|
+
"""Search the web using Brave Search API.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
query: Search query (max 400 chars, 50 words)
|
|
328
|
+
count: Number of results to return (1-20, default: 10)
|
|
329
|
+
offset: Pagination offset (0-9, default: 0)
|
|
330
|
+
country: 2-letter country code for results (default: "US")
|
|
331
|
+
search_lang: Language code for results (default: "en")
|
|
332
|
+
safesearch: Content filter - "off", "moderate", "strict" (default: "moderate")
|
|
333
|
+
freshness: Time filter - "pd" (day), "pw" (week), "pm" (month), "py" (year)
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
YAML flow style search results or error message
|
|
337
|
+
|
|
338
|
+
Example:
|
|
339
|
+
# Basic search
|
|
340
|
+
brave.search(query="Python async best practices")
|
|
341
|
+
|
|
342
|
+
# Recent results only
|
|
343
|
+
brave.search(query="AI news", freshness="pw", count=5)
|
|
344
|
+
"""
|
|
345
|
+
if error := _validate_query(query):
|
|
346
|
+
return error
|
|
347
|
+
|
|
348
|
+
params: dict[str, Any] = {
|
|
349
|
+
"q": query,
|
|
350
|
+
"count": _clamp(count, 1, 20),
|
|
351
|
+
"offset": _clamp(offset, 0, 9),
|
|
352
|
+
"country": country,
|
|
353
|
+
"search_lang": search_lang,
|
|
354
|
+
"safesearch": safesearch,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if freshness:
|
|
358
|
+
params["freshness"] = freshness
|
|
359
|
+
|
|
360
|
+
success, result = _make_request("/web/search", params)
|
|
361
|
+
|
|
362
|
+
if not success:
|
|
363
|
+
return str(result)
|
|
364
|
+
|
|
365
|
+
return _format_web_results(result) # type: ignore[arg-type]
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def news(
|
|
369
|
+
*,
|
|
370
|
+
query: str,
|
|
371
|
+
count: int = 10,
|
|
372
|
+
offset: int = 0,
|
|
373
|
+
country: str = "US",
|
|
374
|
+
search_lang: str = "en",
|
|
375
|
+
freshness: Literal["pd", "pw", "pm"] | None = None,
|
|
376
|
+
) -> str:
|
|
377
|
+
"""Search news articles using Brave Search API.
|
|
378
|
+
|
|
379
|
+
Uses the dedicated /news/search endpoint for better news results.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
query: Search query for news
|
|
383
|
+
count: Number of results (1-20, default: 10)
|
|
384
|
+
offset: Pagination offset (0-9, default: 0)
|
|
385
|
+
country: 2-letter country code (default: "US")
|
|
386
|
+
search_lang: Language code (default: "en")
|
|
387
|
+
freshness: Time filter - "pd" (day), "pw" (week), "pm" (month)
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Formatted news results or error message
|
|
391
|
+
|
|
392
|
+
Example:
|
|
393
|
+
# Today's tech news
|
|
394
|
+
brave.news(query="artificial intelligence", freshness="pd")
|
|
395
|
+
|
|
396
|
+
# UK news
|
|
397
|
+
brave.news(query="technology", country="GB", count=5)
|
|
398
|
+
"""
|
|
399
|
+
if error := _validate_query(query):
|
|
400
|
+
return error
|
|
401
|
+
|
|
402
|
+
params: dict[str, Any] = {
|
|
403
|
+
"q": query,
|
|
404
|
+
"count": _clamp(count, 1, 20),
|
|
405
|
+
"offset": _clamp(offset, 0, 9),
|
|
406
|
+
"country": country,
|
|
407
|
+
"search_lang": search_lang,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if freshness:
|
|
411
|
+
params["freshness"] = freshness
|
|
412
|
+
|
|
413
|
+
success, result = _make_request("/news/search", params)
|
|
414
|
+
|
|
415
|
+
if not success:
|
|
416
|
+
return str(result)
|
|
417
|
+
return _format_news_results(result) # type: ignore[arg-type]
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def local(
|
|
421
|
+
*,
|
|
422
|
+
query: str,
|
|
423
|
+
count: int = 5,
|
|
424
|
+
country: str = "US",
|
|
425
|
+
) -> str:
|
|
426
|
+
"""Search for local businesses and places using Brave Search API.
|
|
427
|
+
|
|
428
|
+
Performs web search optimized for local queries. Returns location results
|
|
429
|
+
if available, otherwise falls back to web results.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
query: Local search query (e.g., "pizza near Central Park")
|
|
433
|
+
count: Number of results (1-20, default: 5)
|
|
434
|
+
country: 2-letter country code (default: "US")
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Formatted local results with addresses, ratings, phone numbers
|
|
438
|
+
|
|
439
|
+
Example:
|
|
440
|
+
brave.local(query="coffee shops near Times Square")
|
|
441
|
+
brave.local(query="restaurants in San Francisco", count=10)
|
|
442
|
+
"""
|
|
443
|
+
if error := _validate_query(query):
|
|
444
|
+
return error
|
|
445
|
+
|
|
446
|
+
params: dict[str, Any] = {
|
|
447
|
+
"q": query,
|
|
448
|
+
"count": _clamp(count, 1, 20),
|
|
449
|
+
"country": country,
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
success, result = _make_request("/web/search", params)
|
|
453
|
+
|
|
454
|
+
if not success:
|
|
455
|
+
return str(result)
|
|
456
|
+
return _format_local_results_from_web(result) # type: ignore[arg-type]
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def image(
|
|
460
|
+
*,
|
|
461
|
+
query: str,
|
|
462
|
+
count: int = 10,
|
|
463
|
+
country: str = "US",
|
|
464
|
+
search_lang: str = "en",
|
|
465
|
+
safesearch: Literal["off", "strict"] = "strict",
|
|
466
|
+
) -> str:
|
|
467
|
+
"""Search for images using Brave Search API.
|
|
468
|
+
|
|
469
|
+
Uses the dedicated /images/search endpoint.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
query: Search query for images
|
|
473
|
+
count: Number of results (1-20, default: 10)
|
|
474
|
+
country: 2-letter country code (default: "US")
|
|
475
|
+
search_lang: Language code (default: "en")
|
|
476
|
+
safesearch: Content filter - "off" or "strict" (default: "strict")
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Formatted image results with URLs, sizes, and sources
|
|
480
|
+
|
|
481
|
+
Example:
|
|
482
|
+
brave.image(query="Python programming logo")
|
|
483
|
+
brave.image(query="architecture diagrams", count=5)
|
|
484
|
+
"""
|
|
485
|
+
if error := _validate_query(query):
|
|
486
|
+
return error
|
|
487
|
+
|
|
488
|
+
params: dict[str, Any] = {
|
|
489
|
+
"q": query,
|
|
490
|
+
"count": _clamp(count, 1, 20),
|
|
491
|
+
"country": country,
|
|
492
|
+
"search_lang": search_lang,
|
|
493
|
+
"safesearch": safesearch,
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
success, result = _make_request("/images/search", params)
|
|
497
|
+
|
|
498
|
+
if not success:
|
|
499
|
+
return str(result)
|
|
500
|
+
return _format_image_results(result) # type: ignore[arg-type]
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def video(
|
|
504
|
+
*,
|
|
505
|
+
query: str,
|
|
506
|
+
count: int = 10,
|
|
507
|
+
country: str = "US",
|
|
508
|
+
search_lang: str = "en",
|
|
509
|
+
freshness: Literal["pd", "pw", "pm", "py"] | None = None,
|
|
510
|
+
) -> str:
|
|
511
|
+
"""Search for videos using Brave Search API.
|
|
512
|
+
|
|
513
|
+
Uses the dedicated /videos/search endpoint.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
query: Search query for videos
|
|
517
|
+
count: Number of results (1-20, default: 10)
|
|
518
|
+
country: 2-letter country code (default: "US")
|
|
519
|
+
search_lang: Language code (default: "en")
|
|
520
|
+
freshness: Time filter - "pd" (day), "pw" (week), "pm" (month), "py" (year)
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
Formatted video results with titles, channels, durations, and URLs
|
|
524
|
+
|
|
525
|
+
Example:
|
|
526
|
+
brave.video(query="Python tutorial for beginners")
|
|
527
|
+
brave.video(query="tech conference keynote", freshness="pm", count=5)
|
|
528
|
+
"""
|
|
529
|
+
if error := _validate_query(query):
|
|
530
|
+
return error
|
|
531
|
+
|
|
532
|
+
params: dict[str, Any] = {
|
|
533
|
+
"q": query,
|
|
534
|
+
"count": _clamp(count, 1, 20),
|
|
535
|
+
"country": country,
|
|
536
|
+
"search_lang": search_lang,
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if freshness:
|
|
540
|
+
params["freshness"] = freshness
|
|
541
|
+
|
|
542
|
+
success, result = _make_request("/videos/search", params)
|
|
543
|
+
|
|
544
|
+
if not success:
|
|
545
|
+
return str(result)
|
|
546
|
+
return _format_video_results(result) # type: ignore[arg-type]
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def search_batch(
|
|
550
|
+
*,
|
|
551
|
+
queries: list[tuple[str, str] | str],
|
|
552
|
+
count: int = 2,
|
|
553
|
+
country: str = "US",
|
|
554
|
+
search_lang: str = "en",
|
|
555
|
+
) -> str:
|
|
556
|
+
"""Execute multiple web searches concurrently and return combined results.
|
|
557
|
+
|
|
558
|
+
Queries are executed in parallel using threads for better performance.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
queries: List of queries. Each item can be:
|
|
562
|
+
- A string (query text, used as both query and label)
|
|
563
|
+
- A tuple of (query, label) for custom labeling
|
|
564
|
+
count: Number of results per query (1-20, default: 2)
|
|
565
|
+
country: 2-letter country code for results (default: "US")
|
|
566
|
+
search_lang: Language code for results (default: "en")
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
Combined formatted results with labels
|
|
570
|
+
|
|
571
|
+
Example:
|
|
572
|
+
# Simple list of queries
|
|
573
|
+
brave.search_batch(["scipy", "sqlalchemy", "jupyterlab"])
|
|
574
|
+
|
|
575
|
+
# With custom labels
|
|
576
|
+
brave.search_batch([
|
|
577
|
+
("current Gold price USD/oz today", "Gold (USD/oz)"),
|
|
578
|
+
("current Silver price USD/oz today", "Silver (USD/oz)"),
|
|
579
|
+
("current Copper price USD/lb today", "Copper (USD/lb)"),
|
|
580
|
+
])
|
|
581
|
+
"""
|
|
582
|
+
normalized = normalize_items(queries)
|
|
583
|
+
|
|
584
|
+
if not normalized:
|
|
585
|
+
return "Error: No queries provided"
|
|
586
|
+
|
|
587
|
+
with LogSpan(span="brave.batch", query_count=len(normalized), count=count) as s:
|
|
588
|
+
|
|
589
|
+
def _search_one(query: str, label: str) -> tuple[str, str]:
|
|
590
|
+
"""Execute a single search and return (label, result)."""
|
|
591
|
+
result = search(
|
|
592
|
+
query=query,
|
|
593
|
+
count=count,
|
|
594
|
+
country=country,
|
|
595
|
+
search_lang=search_lang,
|
|
596
|
+
)
|
|
597
|
+
return label, result
|
|
598
|
+
|
|
599
|
+
results = batch_execute(_search_one, normalized, max_workers=len(normalized))
|
|
600
|
+
output = format_batch_results(results, normalized)
|
|
601
|
+
s.add(outputLen=len(output))
|
|
602
|
+
return output
|
|
603
|
+
|
|
604
|
+
|