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
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
"""Grounding search tools.
|
|
2
|
+
|
|
3
|
+
Provides web search with Google's grounding capabilities via Gemini API.
|
|
4
|
+
Supports general search, developer resources, documentation, and Reddit searches.
|
|
5
|
+
Requires GEMINI_API_KEY in secrets.yaml.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import functools
|
|
11
|
+
|
|
12
|
+
# Pack for dot notation: ground.search(), ground.dev(), etc.
|
|
13
|
+
pack = "ground"
|
|
14
|
+
|
|
15
|
+
__all__ = ["dev", "docs", "reddit", "search", "search_batch"]
|
|
16
|
+
|
|
17
|
+
from typing import Any, Literal
|
|
18
|
+
|
|
19
|
+
# Type alias for output format
|
|
20
|
+
OutputFormat = Literal["full", "text_only", "sources_only"]
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
|
+
|
|
24
|
+
from ot.config import get_tool_config
|
|
25
|
+
from ot.config.secrets import get_secret
|
|
26
|
+
from ot.logging import LogSpan
|
|
27
|
+
from ot.utils import batch_execute, format_batch_results, normalize_items
|
|
28
|
+
|
|
29
|
+
# Dependency declarations for CLI validation
|
|
30
|
+
__ot_requires__ = {
|
|
31
|
+
"lib": [{"name": "google-genai", "import_name": "google.genai", "install": "pip install google-genai"}],
|
|
32
|
+
"secrets": ["GEMINI_API_KEY"],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Config(BaseModel):
|
|
37
|
+
"""Pack configuration - discovered by registry."""
|
|
38
|
+
|
|
39
|
+
model: str = Field(
|
|
40
|
+
default="gemini-2.5-flash",
|
|
41
|
+
description="Gemini model for grounding search (e.g., gemini-2.5-flash)",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
from google import genai
|
|
46
|
+
from google.genai import types
|
|
47
|
+
except ImportError as e:
|
|
48
|
+
raise ImportError(
|
|
49
|
+
"google-genai is required for grounding_search. "
|
|
50
|
+
"Install with: pip install google-genai"
|
|
51
|
+
) from e
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_api_key() -> str:
|
|
55
|
+
"""Get Gemini API key from secrets."""
|
|
56
|
+
return get_secret("GEMINI_API_KEY") or ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@functools.lru_cache(maxsize=1)
|
|
60
|
+
def _get_cached_client(api_key: str) -> genai.Client:
|
|
61
|
+
"""Get or create a cached Gemini client.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
api_key: The Gemini API key (used as cache key)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Cached Gemini client instance
|
|
68
|
+
"""
|
|
69
|
+
return genai.Client(api_key=api_key)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _create_client() -> genai.Client:
|
|
73
|
+
"""Create a Gemini client with API key (cached)."""
|
|
74
|
+
api_key = _get_api_key()
|
|
75
|
+
if not api_key:
|
|
76
|
+
raise ValueError("GEMINI_API_KEY not set in secrets.yaml")
|
|
77
|
+
return _get_cached_client(api_key)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _extract_sources(response: Any) -> list[dict[str, str]]:
|
|
81
|
+
"""Extract grounding sources from Gemini response.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
response: Gemini API response object
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
List of source dicts with 'title' and 'url' keys
|
|
88
|
+
"""
|
|
89
|
+
sources: list[dict[str, str]] = []
|
|
90
|
+
|
|
91
|
+
# Navigate to grounding metadata
|
|
92
|
+
if not hasattr(response, "candidates") or not response.candidates:
|
|
93
|
+
return sources
|
|
94
|
+
|
|
95
|
+
candidate = response.candidates[0]
|
|
96
|
+
metadata = getattr(candidate, "grounding_metadata", None)
|
|
97
|
+
if not metadata:
|
|
98
|
+
return sources
|
|
99
|
+
|
|
100
|
+
# Extract from grounding_chunks
|
|
101
|
+
chunks = getattr(metadata, "grounding_chunks", None)
|
|
102
|
+
if not chunks:
|
|
103
|
+
return sources
|
|
104
|
+
|
|
105
|
+
for chunk in chunks:
|
|
106
|
+
web = getattr(chunk, "web", None)
|
|
107
|
+
if not web:
|
|
108
|
+
continue
|
|
109
|
+
uri = getattr(web, "uri", "") or ""
|
|
110
|
+
if uri:
|
|
111
|
+
title = getattr(web, "title", "") or ""
|
|
112
|
+
sources.append({"title": title, "url": uri})
|
|
113
|
+
|
|
114
|
+
return sources
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _format_response(
|
|
118
|
+
response: Any,
|
|
119
|
+
*,
|
|
120
|
+
output_format: OutputFormat = "full",
|
|
121
|
+
max_sources: int | None = None,
|
|
122
|
+
) -> str:
|
|
123
|
+
"""Format Gemini response with content and sources.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
response: Gemini API response object
|
|
127
|
+
output_format: Output format - "full" (default), "text_only", or "sources_only"
|
|
128
|
+
max_sources: Maximum number of sources to include (None for unlimited)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Formatted string with content and/or source citations
|
|
132
|
+
"""
|
|
133
|
+
# Extract text content
|
|
134
|
+
text = ""
|
|
135
|
+
if hasattr(response, "text"):
|
|
136
|
+
text = response.text or ""
|
|
137
|
+
elif hasattr(response, "candidates") and response.candidates:
|
|
138
|
+
candidate = response.candidates[0]
|
|
139
|
+
if hasattr(candidate, "content") and candidate.content:
|
|
140
|
+
content = candidate.content
|
|
141
|
+
if hasattr(content, "parts") and content.parts:
|
|
142
|
+
text = "".join(getattr(part, "text", "") for part in content.parts)
|
|
143
|
+
|
|
144
|
+
# Extract sources
|
|
145
|
+
sources = _extract_sources(response)
|
|
146
|
+
|
|
147
|
+
# Handle output format
|
|
148
|
+
if output_format == "sources_only":
|
|
149
|
+
if not sources:
|
|
150
|
+
return "No sources found."
|
|
151
|
+
return _format_sources(sources, max_sources=max_sources)
|
|
152
|
+
|
|
153
|
+
if not text:
|
|
154
|
+
return "No results found."
|
|
155
|
+
|
|
156
|
+
if output_format == "text_only":
|
|
157
|
+
return text
|
|
158
|
+
|
|
159
|
+
# Full format: text + sources
|
|
160
|
+
if sources:
|
|
161
|
+
text += "\n\n## Sources\n"
|
|
162
|
+
text += _format_sources(sources, max_sources=max_sources)
|
|
163
|
+
|
|
164
|
+
return text
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _format_sources(sources: list[dict[str, str]], *, max_sources: int | None = None) -> str:
|
|
168
|
+
"""Format source citations with deduplication and optional limit.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
sources: List of source dicts with 'title' and 'url' keys
|
|
172
|
+
max_sources: Maximum number of sources to include (None for unlimited)
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Formatted source list with numbered markdown links
|
|
176
|
+
"""
|
|
177
|
+
result = ""
|
|
178
|
+
seen_urls: set[str] = set()
|
|
179
|
+
display_num = 0
|
|
180
|
+
|
|
181
|
+
for source in sources:
|
|
182
|
+
url = source["url"]
|
|
183
|
+
if url in seen_urls:
|
|
184
|
+
continue
|
|
185
|
+
seen_urls.add(url)
|
|
186
|
+
display_num += 1
|
|
187
|
+
|
|
188
|
+
if max_sources is not None and display_num > max_sources:
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
title = source["title"] or url
|
|
192
|
+
result += f"{display_num}. [{title}]({url})\n"
|
|
193
|
+
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _format_error(e: Exception) -> str:
|
|
198
|
+
"""Format error message with helpful context.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
e: The exception that occurred
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
User-friendly error message
|
|
205
|
+
"""
|
|
206
|
+
error_str = str(e).lower()
|
|
207
|
+
|
|
208
|
+
if "quota" in error_str or "rate" in error_str:
|
|
209
|
+
return "Error: API quota exceeded. Try again later."
|
|
210
|
+
elif "authentication" in error_str or "api key" in error_str or "unauthorized" in error_str:
|
|
211
|
+
return "Error: Invalid GEMINI_API_KEY. Check secrets.yaml."
|
|
212
|
+
elif "timeout" in error_str:
|
|
213
|
+
return "Error: Request timed out. Try a simpler query or increase timeout."
|
|
214
|
+
else:
|
|
215
|
+
return f"Search failed: {e}"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _grounded_search(
|
|
219
|
+
prompt: str,
|
|
220
|
+
*,
|
|
221
|
+
span_name: str,
|
|
222
|
+
model: str | None = None,
|
|
223
|
+
timeout: float = 30.0,
|
|
224
|
+
output_format: OutputFormat = "full",
|
|
225
|
+
max_sources: int | None = None,
|
|
226
|
+
**log_extras: Any,
|
|
227
|
+
) -> str:
|
|
228
|
+
"""Execute a grounded search query.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
prompt: The search prompt to send to Gemini
|
|
232
|
+
span_name: Name for the log span
|
|
233
|
+
model: Gemini model to use (defaults to config)
|
|
234
|
+
timeout: Request timeout in seconds (default: 30.0)
|
|
235
|
+
output_format: Output format - "full", "text_only", or "sources_only"
|
|
236
|
+
max_sources: Maximum number of sources to include (None for unlimited)
|
|
237
|
+
**log_extras: Additional fields to log
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Formatted search results with sources
|
|
241
|
+
"""
|
|
242
|
+
with LogSpan(span=span_name, **log_extras) as s:
|
|
243
|
+
try:
|
|
244
|
+
if model is None:
|
|
245
|
+
model = get_tool_config("ground", Config).model
|
|
246
|
+
client = _create_client()
|
|
247
|
+
|
|
248
|
+
# Configure grounding with Google Search
|
|
249
|
+
google_search_tool = types.Tool(google_search=types.GoogleSearch())
|
|
250
|
+
|
|
251
|
+
# Build config with timeout
|
|
252
|
+
config = types.GenerateContentConfig(
|
|
253
|
+
tools=[google_search_tool],
|
|
254
|
+
http_options={"timeout": timeout * 1000}, # Convert to milliseconds
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
response = client.models.generate_content(
|
|
258
|
+
model=model,
|
|
259
|
+
contents=prompt,
|
|
260
|
+
config=config,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
result = _format_response(
|
|
264
|
+
response,
|
|
265
|
+
output_format=output_format,
|
|
266
|
+
max_sources=max_sources,
|
|
267
|
+
)
|
|
268
|
+
s.add("hasResults", bool(result and result not in ("No results found.", "No sources found.")))
|
|
269
|
+
s.add("resultLen", len(result))
|
|
270
|
+
return result
|
|
271
|
+
|
|
272
|
+
except Exception as e:
|
|
273
|
+
s.add("error", str(e))
|
|
274
|
+
return _format_error(e)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def search(
|
|
278
|
+
*,
|
|
279
|
+
query: str,
|
|
280
|
+
context: str = "",
|
|
281
|
+
focus: Literal["general", "code", "documentation", "troubleshooting"] = "general",
|
|
282
|
+
model: str | None = None,
|
|
283
|
+
timeout: float = 30.0,
|
|
284
|
+
max_sources: int | None = None,
|
|
285
|
+
output_format: OutputFormat = "full",
|
|
286
|
+
) -> str:
|
|
287
|
+
"""Search the web using Google Gemini with grounding.
|
|
288
|
+
|
|
289
|
+
Performs a grounded web search using Google Search via Gemini.
|
|
290
|
+
Results include content and source citations.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
query: The search query (cannot be empty)
|
|
294
|
+
context: Additional context to refine the search (e.g., "Python async")
|
|
295
|
+
focus: Search focus mode:
|
|
296
|
+
- "general": General purpose search (default)
|
|
297
|
+
- "code": Focus on code examples and implementations
|
|
298
|
+
- "documentation": Focus on official documentation
|
|
299
|
+
- "troubleshooting": Focus on solving problems and debugging
|
|
300
|
+
model: Gemini model to use (defaults to config, e.g., "gemini-2.5-flash")
|
|
301
|
+
timeout: Request timeout in seconds (default: 30.0)
|
|
302
|
+
max_sources: Maximum number of sources to include (None for unlimited)
|
|
303
|
+
output_format: Output format - "full" (default), "text_only", or "sources_only"
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Search results with content and source citations
|
|
307
|
+
|
|
308
|
+
Raises:
|
|
309
|
+
ValueError: If query is empty or whitespace-only
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
# Basic search
|
|
313
|
+
ground.search(query="Python asyncio best practices")
|
|
314
|
+
|
|
315
|
+
# With context
|
|
316
|
+
ground.search(
|
|
317
|
+
query="how to handle timeouts",
|
|
318
|
+
context="Python async programming"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Focus on code examples
|
|
322
|
+
ground.search(query="fastapi middleware", focus="code")
|
|
323
|
+
|
|
324
|
+
# Use a specific model
|
|
325
|
+
ground.search(query="latest AI news", model="gemini-3.0-flash")
|
|
326
|
+
|
|
327
|
+
# Get only sources
|
|
328
|
+
ground.search(query="Python tutorials", output_format="sources_only")
|
|
329
|
+
|
|
330
|
+
# Limit sources
|
|
331
|
+
ground.search(query="machine learning", max_sources=5)
|
|
332
|
+
"""
|
|
333
|
+
if not query or not query.strip():
|
|
334
|
+
raise ValueError("query cannot be empty")
|
|
335
|
+
|
|
336
|
+
# Build the search prompt
|
|
337
|
+
focus_instructions = {
|
|
338
|
+
"general": "Provide a comprehensive answer with relevant information.",
|
|
339
|
+
"code": "Focus on code examples, implementations, and technical details.",
|
|
340
|
+
"documentation": "Focus on official documentation and API references.",
|
|
341
|
+
"troubleshooting": "Focus on solutions, debugging tips, and common issues.",
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
prompt_parts = [query]
|
|
345
|
+
|
|
346
|
+
if context:
|
|
347
|
+
prompt_parts.append(f"\nContext: {context}")
|
|
348
|
+
|
|
349
|
+
prompt_parts.append(f"\n{focus_instructions[focus]}")
|
|
350
|
+
|
|
351
|
+
prompt = "".join(prompt_parts)
|
|
352
|
+
|
|
353
|
+
return _grounded_search(
|
|
354
|
+
prompt,
|
|
355
|
+
span_name="ground.search",
|
|
356
|
+
model=model,
|
|
357
|
+
timeout=timeout,
|
|
358
|
+
output_format=output_format,
|
|
359
|
+
max_sources=max_sources,
|
|
360
|
+
query=query,
|
|
361
|
+
focus=focus,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def search_batch(
|
|
366
|
+
*,
|
|
367
|
+
queries: list[tuple[str, str] | str],
|
|
368
|
+
context: str = "",
|
|
369
|
+
focus: Literal["general", "code", "documentation", "troubleshooting"] = "general",
|
|
370
|
+
model: str | None = None,
|
|
371
|
+
timeout: float = 30.0,
|
|
372
|
+
max_sources: int | None = None,
|
|
373
|
+
output_format: OutputFormat = "full",
|
|
374
|
+
) -> str:
|
|
375
|
+
"""Execute multiple grounded searches concurrently and return combined results.
|
|
376
|
+
|
|
377
|
+
Queries are executed in parallel using threads for better performance.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
queries: List of queries. Each item can be:
|
|
381
|
+
- A string (query text, used as both query and label)
|
|
382
|
+
- A tuple of (query, label) for custom labeling
|
|
383
|
+
context: Additional context to refine all searches (e.g., "Python async")
|
|
384
|
+
focus: Search focus mode for all queries:
|
|
385
|
+
- "general": General purpose search (default)
|
|
386
|
+
- "code": Focus on code examples and implementations
|
|
387
|
+
- "documentation": Focus on official documentation
|
|
388
|
+
- "troubleshooting": Focus on solving problems and debugging
|
|
389
|
+
model: Gemini model to use (defaults to config)
|
|
390
|
+
timeout: Request timeout in seconds (default: 30.0)
|
|
391
|
+
max_sources: Maximum number of sources per query (None for unlimited)
|
|
392
|
+
output_format: Output format - "full" (default), "text_only", or "sources_only"
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Combined formatted results with labels
|
|
396
|
+
|
|
397
|
+
Raises:
|
|
398
|
+
ValueError: If queries list is empty
|
|
399
|
+
|
|
400
|
+
Example:
|
|
401
|
+
# Simple list of queries
|
|
402
|
+
ground.search_batch(queries=["fastapi", "django", "flask"])
|
|
403
|
+
|
|
404
|
+
# With custom labels
|
|
405
|
+
ground.search_batch(queries=[
|
|
406
|
+
("Python async best practices", "Async"),
|
|
407
|
+
("Python type hints guide", "Types"),
|
|
408
|
+
("Python testing frameworks", "Testing"),
|
|
409
|
+
])
|
|
410
|
+
|
|
411
|
+
# With context and focus
|
|
412
|
+
ground.search_batch(
|
|
413
|
+
queries=["error handling", "logging", "debugging"],
|
|
414
|
+
context="Python web development",
|
|
415
|
+
focus="code"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# With model and timeout
|
|
419
|
+
ground.search_batch(
|
|
420
|
+
queries=["AI news", "ML trends"],
|
|
421
|
+
model="gemini-3.0-flash",
|
|
422
|
+
timeout=60.0
|
|
423
|
+
)
|
|
424
|
+
"""
|
|
425
|
+
normalized = normalize_items(queries)
|
|
426
|
+
|
|
427
|
+
if not normalized:
|
|
428
|
+
raise ValueError("queries list cannot be empty")
|
|
429
|
+
|
|
430
|
+
with LogSpan(span="ground.batch", query_count=len(normalized), focus=focus) as s:
|
|
431
|
+
|
|
432
|
+
def _search_one(query: str, label: str) -> tuple[str, str]:
|
|
433
|
+
"""Execute a single search and return (label, result)."""
|
|
434
|
+
result = search(
|
|
435
|
+
query=query,
|
|
436
|
+
context=context,
|
|
437
|
+
focus=focus,
|
|
438
|
+
model=model,
|
|
439
|
+
timeout=timeout,
|
|
440
|
+
max_sources=max_sources,
|
|
441
|
+
output_format=output_format,
|
|
442
|
+
)
|
|
443
|
+
return label, result
|
|
444
|
+
|
|
445
|
+
results = batch_execute(_search_one, normalized, max_workers=len(normalized))
|
|
446
|
+
output = format_batch_results(results, normalized)
|
|
447
|
+
s.add(outputLen=len(output))
|
|
448
|
+
return output
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def dev(
|
|
452
|
+
*,
|
|
453
|
+
query: str,
|
|
454
|
+
language: str = "",
|
|
455
|
+
framework: str = "",
|
|
456
|
+
timeout: float = 30.0,
|
|
457
|
+
max_sources: int | None = None,
|
|
458
|
+
output_format: OutputFormat = "full",
|
|
459
|
+
) -> str:
|
|
460
|
+
"""Search for developer resources and documentation.
|
|
461
|
+
|
|
462
|
+
Searches for developer-focused content including GitHub repositories,
|
|
463
|
+
Stack Overflow discussions, and technical documentation.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
query: The technical search query (cannot be empty)
|
|
467
|
+
language: Programming language to prioritize (e.g., "Python", "TypeScript")
|
|
468
|
+
framework: Framework to prioritize (e.g., "FastAPI", "React")
|
|
469
|
+
timeout: Request timeout in seconds (default: 30.0)
|
|
470
|
+
max_sources: Maximum number of sources to include (None for unlimited)
|
|
471
|
+
output_format: Output format - "full" (default), "text_only", or "sources_only"
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
Developer resources with content and source citations
|
|
475
|
+
|
|
476
|
+
Raises:
|
|
477
|
+
ValueError: If query is empty or whitespace-only
|
|
478
|
+
|
|
479
|
+
Example:
|
|
480
|
+
# Basic developer search
|
|
481
|
+
ground.dev(query="websocket connection handling")
|
|
482
|
+
|
|
483
|
+
# Language-specific search
|
|
484
|
+
ground.dev(query="parse JSON", language="Python")
|
|
485
|
+
|
|
486
|
+
# Framework-specific search
|
|
487
|
+
ground.dev(query="dependency injection", framework="FastAPI")
|
|
488
|
+
"""
|
|
489
|
+
if not query or not query.strip():
|
|
490
|
+
raise ValueError("query cannot be empty")
|
|
491
|
+
|
|
492
|
+
prompt_parts = [
|
|
493
|
+
f"Developer search: {query}",
|
|
494
|
+
"\nFocus on: GitHub repositories, Stack Overflow, technical documentation, "
|
|
495
|
+
"and developer resources.",
|
|
496
|
+
]
|
|
497
|
+
|
|
498
|
+
if language:
|
|
499
|
+
prompt_parts.append(f"\nProgramming language: {language}")
|
|
500
|
+
|
|
501
|
+
if framework:
|
|
502
|
+
prompt_parts.append(f"\nFramework/Library: {framework}")
|
|
503
|
+
|
|
504
|
+
prompt_parts.append("\nProvide code examples and technical details where relevant.")
|
|
505
|
+
|
|
506
|
+
prompt = "".join(prompt_parts)
|
|
507
|
+
|
|
508
|
+
return _grounded_search(
|
|
509
|
+
prompt,
|
|
510
|
+
span_name="ground.dev",
|
|
511
|
+
timeout=timeout,
|
|
512
|
+
output_format=output_format,
|
|
513
|
+
max_sources=max_sources,
|
|
514
|
+
query=query,
|
|
515
|
+
language=language or None,
|
|
516
|
+
framework=framework or None,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def docs(
|
|
521
|
+
*,
|
|
522
|
+
query: str,
|
|
523
|
+
technology: str = "",
|
|
524
|
+
timeout: float = 30.0,
|
|
525
|
+
max_sources: int | None = None,
|
|
526
|
+
output_format: OutputFormat = "full",
|
|
527
|
+
) -> str:
|
|
528
|
+
"""Search for official documentation.
|
|
529
|
+
|
|
530
|
+
Searches specifically for official documentation and API references.
|
|
531
|
+
Prioritizes authoritative sources.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
query: The documentation search query (cannot be empty)
|
|
535
|
+
technology: Technology/library name to focus on (e.g., "React", "Django")
|
|
536
|
+
timeout: Request timeout in seconds (default: 30.0)
|
|
537
|
+
max_sources: Maximum number of sources to include (None for unlimited)
|
|
538
|
+
output_format: Output format - "full" (default), "text_only", or "sources_only"
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
Documentation content with source citations
|
|
542
|
+
|
|
543
|
+
Raises:
|
|
544
|
+
ValueError: If query is empty or whitespace-only
|
|
545
|
+
|
|
546
|
+
Example:
|
|
547
|
+
# Basic documentation search
|
|
548
|
+
ground.docs(query="async context managers")
|
|
549
|
+
|
|
550
|
+
# Technology-specific docs
|
|
551
|
+
ground.docs(query="hooks lifecycle", technology="React")
|
|
552
|
+
"""
|
|
553
|
+
if not query or not query.strip():
|
|
554
|
+
raise ValueError("query cannot be empty")
|
|
555
|
+
|
|
556
|
+
prompt_parts = [f"Documentation search: {query}"]
|
|
557
|
+
|
|
558
|
+
if technology:
|
|
559
|
+
prompt_parts.append(f"\nTechnology: {technology}")
|
|
560
|
+
prompt_parts.append(
|
|
561
|
+
f"\nSearch specifically in {technology} official documentation "
|
|
562
|
+
"and authoritative API references."
|
|
563
|
+
)
|
|
564
|
+
else:
|
|
565
|
+
prompt_parts.append(
|
|
566
|
+
"\nFocus on official documentation, API references, and authoritative "
|
|
567
|
+
"technical guides."
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
prompt = "".join(prompt_parts)
|
|
571
|
+
|
|
572
|
+
return _grounded_search(
|
|
573
|
+
prompt,
|
|
574
|
+
span_name="ground.docs",
|
|
575
|
+
timeout=timeout,
|
|
576
|
+
output_format=output_format,
|
|
577
|
+
max_sources=max_sources,
|
|
578
|
+
query=query,
|
|
579
|
+
technology=technology or None,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def reddit(
|
|
584
|
+
*,
|
|
585
|
+
query: str,
|
|
586
|
+
subreddit: str = "",
|
|
587
|
+
timeout: float = 30.0,
|
|
588
|
+
max_sources: int | None = None,
|
|
589
|
+
output_format: OutputFormat = "full",
|
|
590
|
+
) -> str:
|
|
591
|
+
"""Search Reddit discussions.
|
|
592
|
+
|
|
593
|
+
Searches indexed Reddit posts and comments for community discussions,
|
|
594
|
+
opinions, and real-world experiences.
|
|
595
|
+
|
|
596
|
+
Tips:
|
|
597
|
+
- Use shorter, more general queries for better results
|
|
598
|
+
- Always specify a relevant subreddit for technical topics;
|
|
599
|
+
the subreddit parameter acts as important context for the grounding model
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
query: The Reddit search query (cannot be empty)
|
|
603
|
+
subreddit: Specific subreddit to search (e.g., "programming", "python")
|
|
604
|
+
timeout: Request timeout in seconds (default: 30.0)
|
|
605
|
+
max_sources: Maximum number of sources to include (None for unlimited)
|
|
606
|
+
output_format: Output format - "full" (default), "text_only", or "sources_only"
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Reddit discussion content with source citations
|
|
610
|
+
|
|
611
|
+
Raises:
|
|
612
|
+
ValueError: If query is empty or whitespace-only
|
|
613
|
+
|
|
614
|
+
Example:
|
|
615
|
+
# General Reddit search
|
|
616
|
+
ground.reddit(query="best Python web framework 2024")
|
|
617
|
+
|
|
618
|
+
# Subreddit-specific search
|
|
619
|
+
ground.reddit(query="FastAPI vs Flask", subreddit="python")
|
|
620
|
+
"""
|
|
621
|
+
if not query or not query.strip():
|
|
622
|
+
raise ValueError("query cannot be empty")
|
|
623
|
+
|
|
624
|
+
prompt_parts = [f"Reddit search: {query}"]
|
|
625
|
+
|
|
626
|
+
if subreddit:
|
|
627
|
+
prompt_parts.append(f"\nSearch in r/{subreddit} subreddit.")
|
|
628
|
+
else:
|
|
629
|
+
prompt_parts.append("\nSearch Reddit discussions, posts, and comments.")
|
|
630
|
+
|
|
631
|
+
prompt_parts.append(
|
|
632
|
+
"\nInclude community opinions, real-world experiences, and discussions. "
|
|
633
|
+
"Cite specific Reddit threads when relevant."
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
prompt = "".join(prompt_parts)
|
|
637
|
+
|
|
638
|
+
return _grounded_search(
|
|
639
|
+
prompt,
|
|
640
|
+
span_name="ground.reddit",
|
|
641
|
+
timeout=timeout,
|
|
642
|
+
output_format=output_format,
|
|
643
|
+
max_sources=max_sources,
|
|
644
|
+
query=query,
|
|
645
|
+
subreddit=subreddit or None,
|
|
646
|
+
)
|