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.
Files changed (132) hide show
  1. bench/__init__.py +5 -0
  2. bench/cli.py +69 -0
  3. bench/harness/__init__.py +66 -0
  4. bench/harness/client.py +692 -0
  5. bench/harness/config.py +397 -0
  6. bench/harness/csv_writer.py +109 -0
  7. bench/harness/evaluate.py +512 -0
  8. bench/harness/metrics.py +283 -0
  9. bench/harness/runner.py +899 -0
  10. bench/py.typed +0 -0
  11. bench/reporter.py +629 -0
  12. bench/run.py +487 -0
  13. bench/secrets.py +101 -0
  14. bench/utils.py +16 -0
  15. onetool/__init__.py +4 -0
  16. onetool/cli.py +391 -0
  17. onetool/py.typed +0 -0
  18. onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
  19. onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
  20. onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
  21. onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
  22. onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
  23. onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
  24. ot/__init__.py +37 -0
  25. ot/__main__.py +6 -0
  26. ot/_cli.py +107 -0
  27. ot/_tui.py +53 -0
  28. ot/config/__init__.py +46 -0
  29. ot/config/defaults/bench.yaml +4 -0
  30. ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
  31. ot/config/defaults/diagram-templates/c4-context.puml +30 -0
  32. ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
  33. ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
  34. ot/config/defaults/diagram-templates/microservices.d2 +81 -0
  35. ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
  36. ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
  37. ot/config/defaults/onetool.yaml +25 -0
  38. ot/config/defaults/prompts.yaml +97 -0
  39. ot/config/defaults/servers.yaml +7 -0
  40. ot/config/defaults/snippets.yaml +4 -0
  41. ot/config/defaults/tool_templates/__init__.py +7 -0
  42. ot/config/defaults/tool_templates/extension.py +52 -0
  43. ot/config/defaults/tool_templates/isolated.py +61 -0
  44. ot/config/dynamic.py +121 -0
  45. ot/config/global_templates/__init__.py +2 -0
  46. ot/config/global_templates/bench-secrets-template.yaml +6 -0
  47. ot/config/global_templates/bench.yaml +9 -0
  48. ot/config/global_templates/onetool.yaml +27 -0
  49. ot/config/global_templates/secrets-template.yaml +44 -0
  50. ot/config/global_templates/servers.yaml +18 -0
  51. ot/config/global_templates/snippets.yaml +235 -0
  52. ot/config/loader.py +1087 -0
  53. ot/config/mcp.py +145 -0
  54. ot/config/secrets.py +190 -0
  55. ot/config/tool_config.py +125 -0
  56. ot/decorators.py +116 -0
  57. ot/executor/__init__.py +35 -0
  58. ot/executor/base.py +16 -0
  59. ot/executor/fence_processor.py +83 -0
  60. ot/executor/linter.py +142 -0
  61. ot/executor/pack_proxy.py +260 -0
  62. ot/executor/param_resolver.py +140 -0
  63. ot/executor/pep723.py +288 -0
  64. ot/executor/result_store.py +369 -0
  65. ot/executor/runner.py +496 -0
  66. ot/executor/simple.py +163 -0
  67. ot/executor/tool_loader.py +396 -0
  68. ot/executor/validator.py +398 -0
  69. ot/executor/worker_pool.py +388 -0
  70. ot/executor/worker_proxy.py +189 -0
  71. ot/http_client.py +145 -0
  72. ot/logging/__init__.py +37 -0
  73. ot/logging/config.py +315 -0
  74. ot/logging/entry.py +213 -0
  75. ot/logging/format.py +188 -0
  76. ot/logging/span.py +349 -0
  77. ot/meta.py +1555 -0
  78. ot/paths.py +453 -0
  79. ot/prompts.py +218 -0
  80. ot/proxy/__init__.py +21 -0
  81. ot/proxy/manager.py +396 -0
  82. ot/py.typed +0 -0
  83. ot/registry/__init__.py +189 -0
  84. ot/registry/models.py +57 -0
  85. ot/registry/parser.py +269 -0
  86. ot/registry/registry.py +413 -0
  87. ot/server.py +315 -0
  88. ot/shortcuts/__init__.py +15 -0
  89. ot/shortcuts/aliases.py +87 -0
  90. ot/shortcuts/snippets.py +258 -0
  91. ot/stats/__init__.py +35 -0
  92. ot/stats/html.py +250 -0
  93. ot/stats/jsonl_writer.py +283 -0
  94. ot/stats/reader.py +354 -0
  95. ot/stats/timing.py +57 -0
  96. ot/support.py +63 -0
  97. ot/tools.py +114 -0
  98. ot/utils/__init__.py +81 -0
  99. ot/utils/batch.py +161 -0
  100. ot/utils/cache.py +120 -0
  101. ot/utils/deps.py +403 -0
  102. ot/utils/exceptions.py +23 -0
  103. ot/utils/factory.py +179 -0
  104. ot/utils/format.py +65 -0
  105. ot/utils/http.py +202 -0
  106. ot/utils/platform.py +45 -0
  107. ot/utils/sanitize.py +130 -0
  108. ot/utils/truncate.py +69 -0
  109. ot_tools/__init__.py +4 -0
  110. ot_tools/_convert/__init__.py +12 -0
  111. ot_tools/_convert/excel.py +279 -0
  112. ot_tools/_convert/pdf.py +254 -0
  113. ot_tools/_convert/powerpoint.py +268 -0
  114. ot_tools/_convert/utils.py +358 -0
  115. ot_tools/_convert/word.py +283 -0
  116. ot_tools/brave_search.py +604 -0
  117. ot_tools/code_search.py +736 -0
  118. ot_tools/context7.py +495 -0
  119. ot_tools/convert.py +614 -0
  120. ot_tools/db.py +415 -0
  121. ot_tools/diagram.py +1604 -0
  122. ot_tools/diagram.yaml +167 -0
  123. ot_tools/excel.py +1372 -0
  124. ot_tools/file.py +1348 -0
  125. ot_tools/firecrawl.py +732 -0
  126. ot_tools/grounding_search.py +646 -0
  127. ot_tools/package.py +604 -0
  128. ot_tools/py.typed +0 -0
  129. ot_tools/ripgrep.py +544 -0
  130. ot_tools/scaffold.py +471 -0
  131. ot_tools/transform.py +213 -0
  132. 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
+ )