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
ot_tools/context7.py ADDED
@@ -0,0 +1,495 @@
1
+ """Context7 API tools for library search and documentation.
2
+
3
+ These built-in tools provide access to the Context7 documentation API
4
+ for fetching up-to-date library documentation and code examples.
5
+
6
+ Based on context7 by Upstash (MIT License).
7
+ https://github.com/upstash/context7
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ # Pack for dot notation: context7.search(), context7.doc()
13
+ pack = "context7"
14
+
15
+ __all__ = ["doc", "search"]
16
+
17
+ # Dependency declarations for CLI validation
18
+ __ot_requires__ = {
19
+ "secrets": ["CONTEXT7_API_KEY"],
20
+ }
21
+
22
+ import re
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 cache
30
+
31
+
32
+ class Config(BaseModel):
33
+ """Pack configuration - discovered by registry."""
34
+
35
+ timeout: float = Field(
36
+ default=30.0,
37
+ ge=1.0,
38
+ le=120.0,
39
+ description="Request timeout in seconds",
40
+ )
41
+ docs_limit: int = Field(
42
+ default=10,
43
+ ge=1,
44
+ le=20,
45
+ description="Maximum number of documentation items to return",
46
+ )
47
+
48
+ # Context7 REST API configuration
49
+ CONTEXT7_SEARCH_URL = "https://context7.com/api/v2/search"
50
+ CONTEXT7_DOCS_CODE_URL = "https://context7.com/api/v2/docs/code"
51
+ CONTEXT7_DOCS_INFO_URL = "https://context7.com/api/v2/docs/info"
52
+
53
+
54
+ def _get_config() -> Config:
55
+ """Get context7 pack configuration."""
56
+ return get_tool_config("context7", Config)
57
+
58
+
59
+ # Shared HTTP client for connection pooling
60
+ _client = httpx.Client(timeout=30.0)
61
+
62
+
63
+ def _get_api_key() -> str:
64
+ """Get Context7 API key from secrets."""
65
+ return get_secret("CONTEXT7_API_KEY") or ""
66
+
67
+
68
+ def _get_headers() -> dict[str, str]:
69
+ """Get authorization headers for Context7 API."""
70
+ api_key = _get_api_key()
71
+ if api_key:
72
+ return {"Authorization": f"Bearer {api_key}"}
73
+ return {}
74
+
75
+
76
+ def _make_request(
77
+ url: str,
78
+ params: dict[str, str | int] | None = None,
79
+ timeout: float | None = None,
80
+ ) -> tuple[bool, str | dict]:
81
+ """Make HTTP GET request to Context7 API.
82
+
83
+ Args:
84
+ url: Full URL to request
85
+ params: Query parameters
86
+ timeout: Request timeout in seconds (defaults to config)
87
+
88
+ Returns:
89
+ Tuple of (success, result). If success, result is parsed JSON or text.
90
+ If failure, result is error message string.
91
+ """
92
+ api_key = _get_api_key()
93
+ if not api_key:
94
+ return False, "[Context7 API key not configured]"
95
+
96
+ if timeout is None:
97
+ timeout = _get_config().timeout
98
+
99
+ with LogSpan(span="context7.request", url=url) as span:
100
+ try:
101
+ response = _client.get(
102
+ url,
103
+ params=params,
104
+ headers=_get_headers(),
105
+ timeout=timeout,
106
+ )
107
+ response.raise_for_status()
108
+
109
+ content_type = response.headers.get("content-type", "")
110
+ span.add(status=response.status_code)
111
+ if "application/json" in content_type:
112
+ return True, response.json()
113
+ return True, response.text
114
+
115
+ except Exception as e:
116
+ error_type = type(e).__name__
117
+ span.add(error=f"{error_type}: {e}")
118
+ if hasattr(e, "response"):
119
+ status = getattr(e.response, "status_code", "unknown")
120
+ return False, f"HTTP error ({status}): {error_type}"
121
+ return False, f"Request failed: {e}"
122
+
123
+
124
+ @cache(ttl=3600) # Cache library key resolutions for 1 hour
125
+ def _normalize_library_key(library_key: str) -> str:
126
+ """Normalize library key to Context7 API format.
127
+
128
+ Handles various input formats and common issues:
129
+ - "/vercel/next.js/v16.0.3" -> "vercel/next.js"
130
+ - "/vercel/next.js" -> "vercel/next.js"
131
+ - "vercel/next.js" -> "vercel/next.js"
132
+ - "next.js" -> "next.js" (search will be needed)
133
+ - "https://github.com/vercel/next.js" -> "vercel/next.js"
134
+ - Stray quotes: '"vercel/next.js"' -> "vercel/next.js"
135
+ - Double slashes: "vercel//next.js" -> "vercel/next.js"
136
+ - Trailing slashes: "vercel/next.js/" -> "vercel/next.js"
137
+
138
+ Args:
139
+ library_key: Raw library key from user input
140
+
141
+ Returns:
142
+ Normalized org/repo format for Context7 API
143
+ """
144
+ key = library_key.strip()
145
+
146
+ # Remove stray quotes (single or double)
147
+ key = key.strip("\"'")
148
+
149
+ # Handle GitHub URLs
150
+ github_match = re.match(r"https?://(?:www\.)?github\.com/([^/]+)/([^/]+)/?.*", key)
151
+ if github_match:
152
+ return f"{github_match.group(1)}/{github_match.group(2)}"
153
+
154
+ # Handle Context7 URLs
155
+ context7_match = re.match(
156
+ r"https?://(?:www\.)?context7\.com/([^/]+)/([^/]+)/?.*", key
157
+ )
158
+ if context7_match:
159
+ return f"{context7_match.group(1)}/{context7_match.group(2)}"
160
+
161
+ # Fix double slashes
162
+ while "//" in key:
163
+ key = key.replace("//", "/")
164
+
165
+ # Strip leading and trailing slashes
166
+ key = key.strip("/")
167
+
168
+ # Extract org/repo (ignore version suffix like /v16.0.3)
169
+ parts = key.split("/")
170
+ if len(parts) >= 2:
171
+ # Check if third part looks like a version
172
+ if len(parts) > 2 and re.match(r"v?\d+", parts[2]):
173
+ return f"{parts[0]}/{parts[1]}"
174
+ # Otherwise just take first two parts
175
+ return f"{parts[0]}/{parts[1]}"
176
+
177
+ return key
178
+
179
+
180
+ def _normalize_topic(topic: str) -> str:
181
+ """Normalize topic string for search.
182
+
183
+ Handles:
184
+ - Stray quotes: '"PPR"' -> "PPR"
185
+ - Placeholder syntax: "<relevant topic>" -> ""
186
+ - Path-like topics: "app/partial-pre-rendering/index" -> "partial pre-rendering"
187
+ - Extra whitespace
188
+ - Escaped quotes: '\\"topic\\"' -> "topic"
189
+
190
+ Args:
191
+ topic: Raw topic from user input
192
+
193
+ Returns:
194
+ Cleaned topic string
195
+ """
196
+ topic = topic.strip()
197
+
198
+ # Remove stray quotes (single or double)
199
+ topic = topic.strip("\"'")
200
+
201
+ # Remove escaped quotes
202
+ topic = topic.replace('\\"', "").replace("\\'", "")
203
+
204
+ # Remove placeholder markers
205
+ if topic.startswith("<") and topic.endswith(">"):
206
+ topic = topic[1:-1].strip()
207
+
208
+ # If it's a placeholder like "relevant topic", return empty to get general docs
209
+ if topic.lower() in ("relevant topic", "topic", "extract from question", ""):
210
+ return ""
211
+
212
+ # Convert path-like topics to search terms
213
+ # "app/partial-pre-rendering/index" -> "partial pre-rendering"
214
+ if "/" in topic and not topic.startswith("http"):
215
+ # Take the most specific part (usually the last meaningful segment)
216
+ parts = [p for p in topic.split("/") if p and p != "index"]
217
+ if parts:
218
+ topic = parts[-1]
219
+
220
+ # Convert kebab-case to spaces
221
+ topic = topic.replace("-", " ").replace("_", " ")
222
+
223
+ # Clean up whitespace
224
+ topic = " ".join(topic.split())
225
+
226
+ return topic
227
+
228
+
229
+ def search(*, query: str, output_format: str = "str") -> str | dict | list:
230
+ """Search for libraries by name in Context7.
231
+
232
+ Args:
233
+ query: The search query (e.g., 'next.js', 'react', 'vue')
234
+ output_format: Response format - 'str' for string (default), 'dict' for raw dict/list
235
+
236
+ Returns:
237
+ Search results with matching libraries and their IDs.
238
+ If output_format='dict', returns the raw API response (dict or list).
239
+ If output_format='str', returns a string representation.
240
+
241
+ Example:
242
+ context7.search(query="fastapi")
243
+ context7.search(query="react hooks")
244
+ context7.search(query="flask", output_format="dict") # Returns raw dict
245
+ """
246
+ if output_format not in ("str", "dict"):
247
+ return f"Invalid output_format '{output_format}'. Valid options: 'str', 'dict'."
248
+
249
+ with LogSpan(span="context7.search", query=query, output_format=output_format) as s:
250
+ success, result = _make_request(CONTEXT7_SEARCH_URL, params={"query": query})
251
+
252
+ s.add(success=success)
253
+ if not success:
254
+ return f"{result} query={query}"
255
+
256
+ if output_format == "dict":
257
+ s.add(resultType=type(result).__name__)
258
+ return result
259
+
260
+ result_str = str(result)
261
+ s.add(resultLen=len(result_str))
262
+ return result_str
263
+
264
+
265
+ def _pick_best_library(data: dict | list | None, query: str) -> str | None:
266
+ """Pick the best library from Context7 search response.
267
+
268
+ Handles {"results": [...]} and [...] response formats.
269
+
270
+ Scoring:
271
+ - Exact title match: +100
272
+ - Title contains query: +50
273
+ - VIP library: +30
274
+ - Verified: +20
275
+ - Trust score: +trustScore * 2
276
+ - Large corpus (>100k tokens): +5
277
+ """
278
+ # Unwrap response
279
+ if isinstance(data, dict):
280
+ results = data.get("results", [])
281
+ elif isinstance(data, list):
282
+ results = data
283
+ else:
284
+ return None
285
+
286
+ if not results:
287
+ return None
288
+
289
+ query_lower = query.lower()
290
+
291
+ def score_result(r: dict) -> float:
292
+ score = 0.0
293
+ title = r.get("title", "").lower()
294
+
295
+ if title == query_lower:
296
+ score += 100
297
+ elif query_lower in title:
298
+ score += 50
299
+
300
+ if r.get("vip"):
301
+ score += 30
302
+ if r.get("verified"):
303
+ score += 20
304
+
305
+ trust = r.get("trustScore", 0)
306
+ if trust > 0:
307
+ score += trust * 2
308
+
309
+ if r.get("totalTokens", 0) > 100000:
310
+ score += 5
311
+
312
+ # Star-based scoring (capped at 20 bonus points)
313
+ stars = r.get("stars", 0)
314
+ if stars > 0:
315
+ score += min(stars / 1000, 20)
316
+
317
+ # Benchmark score contribution (max ~10 bonus points)
318
+ benchmark = r.get("benchmarkScore", 0)
319
+ if benchmark > 0:
320
+ score += benchmark / 10
321
+
322
+ return score
323
+
324
+ sorted_results = sorted(results, key=score_result, reverse=True)
325
+ best = sorted_results[0]
326
+ lib_id = best.get("id", "").lstrip("/")
327
+
328
+ return lib_id if "/" in lib_id else None
329
+
330
+
331
+ @cache(ttl=3600) # Cache library key resolutions for 1 hour
332
+ def _resolve_library_key(library_key: str) -> tuple[str, bool, bool]:
333
+ """Resolve a library key, searching if needed.
334
+
335
+ If the key doesn't look like a valid org/repo format,
336
+ search Context7 to find the best match.
337
+
338
+ Args:
339
+ library_key: Raw or partial library key
340
+
341
+ Returns:
342
+ Tuple of (resolved org/repo library key, was_searched, found_match).
343
+ was_searched is True if a search was performed to resolve the key.
344
+ found_match is True if search found a valid library match.
345
+ """
346
+ normalized = _normalize_library_key(library_key)
347
+
348
+ # If it looks like a valid org/repo, use it directly
349
+ if "/" in normalized and len(normalized.split("/")) == 2:
350
+ org, repo = normalized.split("/")
351
+ if org and repo and not org.startswith("http"):
352
+ return normalized, False, True
353
+
354
+ # Otherwise, search for the library
355
+ success, data = _make_request(CONTEXT7_SEARCH_URL, params={"query": normalized})
356
+
357
+ if not success:
358
+ return normalized, True, False
359
+
360
+ # Use smart scoring to pick the best match
361
+ best = _pick_best_library(data, normalized)
362
+ if best:
363
+ return best, True, True
364
+ return normalized, True, False
365
+
366
+
367
+ def doc(
368
+ *,
369
+ library_key: str,
370
+ topic: str = "",
371
+ mode: str = "info",
372
+ page: int = 1,
373
+ limit: int | None = None,
374
+ doc_type: str = "txt",
375
+ version: str | None = None,
376
+ ) -> str:
377
+ """Fetch documentation for a library from Context7.
378
+
379
+ Args:
380
+ library_key: The library key - can be flexible format:
381
+ - Full: 'vercel/next.js'
382
+ - With version: '/vercel/next.js/v16.0.3'
383
+ - Shorthand: 'next.js', 'nextjs', 'react'
384
+ - URL: 'https://github.com/vercel/next.js'
385
+ topic: Topic to focus documentation on (e.g., 'routing', 'hooks', 'ssr').
386
+ Default: empty string for general docs
387
+ mode: Documentation mode - 'info' for conceptual guides and narrative documentation (default),
388
+ 'code' for API references and code examples
389
+ page: Page number for pagination (default: 1, max: 10)
390
+ limit: Number of results per page (defaults to config, max: config docs_limit)
391
+ doc_type: Response format 'txt' or 'json' (default: 'txt')
392
+ version: Optional version suffix (e.g., 'v16.0.3'). If provided, appended to library key.
393
+
394
+ Returns:
395
+ Documentation content and code examples for the requested topic
396
+
397
+ Example:
398
+ # Get general docs
399
+ context7.doc(library_key="fastapi/fastapi")
400
+
401
+ # Get docs on a specific topic
402
+ context7.doc(library_key="vercel/next.js", topic="routing")
403
+
404
+ # Get code examples
405
+ context7.doc(library_key="pallets/flask", topic="blueprints", mode="code")
406
+
407
+ # Get version-specific docs
408
+ context7.doc(library_key="vercel/next.js", topic="routing", version="v14")
409
+ """
410
+ # Validate mode parameter
411
+ if mode not in ("info", "code"):
412
+ return f"Invalid mode '{mode}'. Valid options: 'info', 'code'."
413
+
414
+ # Validate doc_type parameter
415
+ if doc_type not in ("txt", "json"):
416
+ return f"Invalid doc_type '{doc_type}'. Valid options: 'txt', 'json'."
417
+
418
+ with LogSpan(span="context7.doc", library_key=library_key, topic=topic, mode=mode) as s:
419
+ # Normalize and resolve library key (searches if needed)
420
+ resolved_key, was_searched, found_match = _resolve_library_key(library_key)
421
+ s.add(resolvedKey=resolved_key, wasSearched=was_searched, foundMatch=found_match)
422
+
423
+ # If search was performed but found no match, library doesn't exist
424
+ if was_searched and not found_match:
425
+ return (
426
+ f"Library '{library_key}' not found. "
427
+ f"Use context7.search(query=\"{library_key}\") to find available libraries."
428
+ )
429
+
430
+ # Validate resolved key has org/repo format
431
+ if "/" not in resolved_key:
432
+ return (
433
+ f"Could not resolve library '{library_key}' to org/repo format. "
434
+ f"Please use full format like 'facebook/react' or 'vercel/next.js'. "
435
+ f"Use context7.search(query=\"{library_key}\") to find the correct library key."
436
+ )
437
+
438
+ # Append version suffix if provided
439
+ api_key = resolved_key
440
+ if version:
441
+ # Ensure version has 'v' prefix if it starts with a number
442
+ if version[0].isdigit():
443
+ version = f"v{version}"
444
+ api_key = f"{resolved_key}/{version}"
445
+ s.add(apiKey=api_key)
446
+
447
+ # Normalize topic
448
+ normalized_topic = _normalize_topic(topic)
449
+
450
+ # Clamp page and limit to valid ranges
451
+ config_docs_limit = _get_config().docs_limit
452
+ page = max(1, min(page, 10))
453
+ if limit is None:
454
+ limit = config_docs_limit
455
+ limit = max(1, min(limit, config_docs_limit))
456
+
457
+ # Select endpoint based on mode
458
+ base_url = CONTEXT7_DOCS_INFO_URL if mode == "info" else CONTEXT7_DOCS_CODE_URL
459
+ url = f"{base_url}/{api_key}"
460
+ params: dict[str, str | int] = {
461
+ "type": doc_type,
462
+ "page": page,
463
+ "limit": limit,
464
+ }
465
+ # Only include topic if non-empty
466
+ if normalized_topic:
467
+ params["topic"] = normalized_topic
468
+
469
+ success, data = _make_request(url, params=params)
470
+ s.add(success=success)
471
+
472
+ if not success:
473
+ return f"{data} library_key={library_key}"
474
+
475
+ # Handle response
476
+ if isinstance(data, str):
477
+ # Check for "no content" responses
478
+ if data in ("No content available", "No context data available", ""):
479
+ other_mode = "info" if mode == "code" else "code"
480
+ topic_hint = f" on topic '{topic}'" if topic else ""
481
+ return (
482
+ f"No {mode} documentation found for '{resolved_key}'{topic_hint}. "
483
+ f"Try mode='{other_mode}' or a different topic."
484
+ )
485
+ s.add(resultLen=len(data))
486
+ return data
487
+
488
+ if isinstance(data, dict):
489
+ result = str(data.get("content", data.get("text", str(data))))
490
+ s.add(resultLen=len(result))
491
+ return result
492
+
493
+ result = str(data)
494
+ s.add(resultLen=len(result))
495
+ return result