voidx 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voidx/__init__.py +3 -0
- voidx/agent/__init__.py +0 -0
- voidx/agent/agents.py +439 -0
- voidx/agent/attachments.py +235 -0
- voidx/agent/graph.py +463 -0
- voidx/agent/graph_components/__init__.py +1 -0
- voidx/agent/graph_components/compaction.py +268 -0
- voidx/agent/graph_components/permissions.py +139 -0
- voidx/agent/graph_components/run_loop.py +532 -0
- voidx/agent/graph_components/runtime.py +14 -0
- voidx/agent/graph_components/streaming.py +351 -0
- voidx/agent/graph_components/subagent.py +278 -0
- voidx/agent/graph_components/tool_execution.py +208 -0
- voidx/agent/runtime_context.py +368 -0
- voidx/agent/slash.py +466 -0
- voidx/agent/slash_components/__init__.py +1 -0
- voidx/agent/slash_components/code_ide.py +68 -0
- voidx/agent/slash_components/lsp.py +105 -0
- voidx/agent/slash_components/mcp.py +332 -0
- voidx/agent/slash_components/model.py +419 -0
- voidx/agent/slash_components/runtime.py +55 -0
- voidx/agent/slash_components/skills.py +94 -0
- voidx/agent/state.py +32 -0
- voidx/agent/task_state.py +278 -0
- voidx/agent/tool_filters.py +27 -0
- voidx/config.py +707 -0
- voidx/llm/__init__.py +0 -0
- voidx/llm/catalog.py +188 -0
- voidx/llm/compaction.py +267 -0
- voidx/llm/context.py +43 -0
- voidx/llm/instruction.py +220 -0
- voidx/llm/provider.py +312 -0
- voidx/llm/usage.py +341 -0
- voidx/lsp/__init__.py +30 -0
- voidx/lsp/client.py +259 -0
- voidx/lsp/config.py +172 -0
- voidx/lsp/detector.py +512 -0
- voidx/lsp/errors.py +19 -0
- voidx/lsp/manager.py +280 -0
- voidx/lsp/schema.py +179 -0
- voidx/lsp/service.py +103 -0
- voidx/main.py +154 -0
- voidx/mcp/__init__.py +33 -0
- voidx/mcp/client.py +458 -0
- voidx/mcp/manager.py +267 -0
- voidx/mcp/schema.py +112 -0
- voidx/mcp/tool.py +122 -0
- voidx/mcp_servers/__init__.py +1 -0
- voidx/mcp_servers/web.py +104 -0
- voidx/memory/__init__.py +0 -0
- voidx/memory/context_frames.py +188 -0
- voidx/memory/model_profiles.py +98 -0
- voidx/memory/runtime_state.py +240 -0
- voidx/memory/session.py +272 -0
- voidx/memory/store.py +245 -0
- voidx/memory/transcript.py +137 -0
- voidx/permission/__init__.py +28 -0
- voidx/permission/engine.py +430 -0
- voidx/permission/evaluate.py +114 -0
- voidx/permission/sandbox.py +280 -0
- voidx/permission/schema.py +24 -0
- voidx/permission/service.py +314 -0
- voidx/permission/wildcard.py +34 -0
- voidx/skills/__init__.py +18 -0
- voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
- voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
- voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
- voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
- voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
- voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
- voidx/skills/policy.py +97 -0
- voidx/skills/registry.py +162 -0
- voidx/skills/schema.py +47 -0
- voidx/skills/service.py +199 -0
- voidx/tools/__init__.py +0 -0
- voidx/tools/agent.py +81 -0
- voidx/tools/base.py +86 -0
- voidx/tools/bash.py +105 -0
- voidx/tools/file_ops.py +193 -0
- voidx/tools/lsp.py +155 -0
- voidx/tools/registry.py +104 -0
- voidx/tools/repomap.py +238 -0
- voidx/tools/search.py +162 -0
- voidx/tools/task_status.py +57 -0
- voidx/tools/task_tracker.py +81 -0
- voidx/tools/todo.py +82 -0
- voidx/tools/web_content.py +357 -0
- voidx/tools/web_mcp.py +107 -0
- voidx/tools/webfetch.py +155 -0
- voidx/tools/websearch.py +276 -0
- voidx/ui/__init__.py +0 -0
- voidx/ui/app.py +1033 -0
- voidx/ui/app_components/__init__.py +1 -0
- voidx/ui/app_components/clipboard_image.py +245 -0
- voidx/ui/app_components/commands.py +18 -0
- voidx/ui/app_components/controls.py +29 -0
- voidx/ui/app_components/file_picker.py +115 -0
- voidx/ui/app_components/formatting.py +187 -0
- voidx/ui/app_components/git_changes.py +51 -0
- voidx/ui/app_components/rendering.py +1169 -0
- voidx/ui/browse.py +160 -0
- voidx/ui/capture.py +169 -0
- voidx/ui/code_ide.py +251 -0
- voidx/ui/commands.py +83 -0
- voidx/ui/console.py +381 -0
- voidx/ui/console_components/__init__.py +1 -0
- voidx/ui/console_components/formatting.py +96 -0
- voidx/ui/console_components/streaming.py +253 -0
- voidx/ui/diff.py +331 -0
- voidx/ui/dock.py +372 -0
- voidx/ui/dock_components/__init__.py +1 -0
- voidx/ui/dock_components/formatting.py +123 -0
- voidx/ui/dock_components/nodes.py +401 -0
- voidx/ui/dock_components/state.py +51 -0
- voidx/ui/event_components/__init__.py +1 -0
- voidx/ui/event_components/schema.py +249 -0
- voidx/ui/events.py +341 -0
- voidx/ui/session_changes.py +163 -0
- voidx/ui/startup.py +161 -0
- voidx/ui/transcript.py +148 -0
- voidx/ui/tree.py +316 -0
- voidx-1.0.0.dist-info/METADATA +59 -0
- voidx-1.0.0.dist-info/RECORD +126 -0
- voidx-1.0.0.dist-info/WHEEL +5 -0
- voidx-1.0.0.dist-info/entry_points.txt +2 -0
- voidx-1.0.0.dist-info/top_level.txt +1 -0
voidx/tools/websearch.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""WebSearch tool — search the web. Tavily > DuckDuckGo fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from html.parser import HTMLParser
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from voidx.tools.base import BaseTool, model_to_json_schema, ToolContext, ToolResult
|
|
11
|
+
from voidx.tools.web_content import (
|
|
12
|
+
WEB_TOOL_CACHE,
|
|
13
|
+
cached_tool_result,
|
|
14
|
+
matches_domain,
|
|
15
|
+
normalize_search_results,
|
|
16
|
+
search_cache_key,
|
|
17
|
+
)
|
|
18
|
+
from voidx.tools.web_mcp import call_mcp_web_tool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WebSearchInput(BaseModel):
|
|
22
|
+
query: str = Field(description="Search query, max 70 characters")
|
|
23
|
+
allowed_domains: list[str] | None = Field(
|
|
24
|
+
default=None,
|
|
25
|
+
description="Only include search results from these domains",
|
|
26
|
+
)
|
|
27
|
+
blocked_domains: list[str] | None = Field(
|
|
28
|
+
default=None,
|
|
29
|
+
description="Never include search results from these domains",
|
|
30
|
+
)
|
|
31
|
+
max_results: int = Field(default=10, ge=1, le=20, description="Maximum number of results to return")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ── DuckDuckGo HTML parser ──────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
class _DDGResultParser(HTMLParser):
|
|
37
|
+
"""Extract search results from DuckDuckGo HTML page."""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
super().__init__()
|
|
41
|
+
self._results: list[dict[str, str]] = []
|
|
42
|
+
self._current: dict[str, str] | None = None
|
|
43
|
+
self._capture = ""
|
|
44
|
+
self._in_result_link = False
|
|
45
|
+
self._in_snippet = False
|
|
46
|
+
|
|
47
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]):
|
|
48
|
+
attr = dict(attrs)
|
|
49
|
+
cls = attr.get("class", "")
|
|
50
|
+
|
|
51
|
+
if tag == "a" and "result__a" in cls:
|
|
52
|
+
self._current = {"url": attr.get("href", ""), "title": "", "snippet": ""}
|
|
53
|
+
self._in_result_link = True
|
|
54
|
+
self._capture = ""
|
|
55
|
+
|
|
56
|
+
if tag == "a" and "result__snippet" in cls and self._current is not None:
|
|
57
|
+
self._in_snippet = True
|
|
58
|
+
self._capture = ""
|
|
59
|
+
|
|
60
|
+
def handle_endtag(self, tag: str):
|
|
61
|
+
if tag == "a" and self._in_result_link:
|
|
62
|
+
if self._current is not None:
|
|
63
|
+
self._current["title"] = self._capture.strip()
|
|
64
|
+
self._in_result_link = False
|
|
65
|
+
|
|
66
|
+
if tag == "a" and self._in_snippet:
|
|
67
|
+
if self._current is not None:
|
|
68
|
+
self._current["snippet"] = self._capture.strip()
|
|
69
|
+
self._results.append(self._current)
|
|
70
|
+
self._current = None
|
|
71
|
+
self._in_snippet = False
|
|
72
|
+
|
|
73
|
+
def handle_data(self, data: str):
|
|
74
|
+
if self._in_result_link or self._in_snippet:
|
|
75
|
+
self._capture += data
|
|
76
|
+
|
|
77
|
+
def results(self) -> list[dict[str, str]]:
|
|
78
|
+
return self._results
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _parse_duckduckgo_html(html: str) -> list[dict[str, str]]:
|
|
82
|
+
"""Parse DuckDuckGo HTML page using HTMLParser (not fragile regex)."""
|
|
83
|
+
parser = _DDGResultParser()
|
|
84
|
+
try:
|
|
85
|
+
parser.feed(html)
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
return parser.results()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ── Tavily API backend ──────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
async def _search_tavily(
|
|
94
|
+
query: str,
|
|
95
|
+
api_key: str,
|
|
96
|
+
allowed_domains: list[str] | None = None,
|
|
97
|
+
blocked_domains: list[str] | None = None,
|
|
98
|
+
max_results: int = 10,
|
|
99
|
+
) -> list[dict[str, str]]:
|
|
100
|
+
"""Search via Tavily API. Returns list of {url, title, snippet}."""
|
|
101
|
+
import httpx
|
|
102
|
+
|
|
103
|
+
url = "https://api.tavily.com/search"
|
|
104
|
+
headers = {
|
|
105
|
+
"Authorization": f"Bearer {api_key}",
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
}
|
|
108
|
+
payload: dict = {
|
|
109
|
+
"query": query,
|
|
110
|
+
"max_results": max_results,
|
|
111
|
+
"include_answer": False,
|
|
112
|
+
"search_depth": "basic",
|
|
113
|
+
}
|
|
114
|
+
if allowed_domains:
|
|
115
|
+
payload["include_domains"] = allowed_domains
|
|
116
|
+
if blocked_domains:
|
|
117
|
+
payload["exclude_domains"] = blocked_domains
|
|
118
|
+
|
|
119
|
+
async with httpx.AsyncClient(timeout=15) as client:
|
|
120
|
+
resp = await client.post(url, json=payload, headers=headers)
|
|
121
|
+
resp.raise_for_status()
|
|
122
|
+
data = resp.json()
|
|
123
|
+
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
"url": item.get("url", ""),
|
|
127
|
+
"title": item.get("title", ""),
|
|
128
|
+
"snippet": item.get("content", ""),
|
|
129
|
+
}
|
|
130
|
+
for item in data.get("results", [])
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── DuckDuckGo fallback backend ─────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
async def _search_duckduckgo(
|
|
137
|
+
query: str,
|
|
138
|
+
allowed_domains: list[str] | None = None,
|
|
139
|
+
blocked_domains: list[str] | None = None,
|
|
140
|
+
max_results: int = 10,
|
|
141
|
+
) -> list[dict[str, str]]:
|
|
142
|
+
"""Search via DuckDuckGo HTML scraping. Returns list of {url, title, snippet}."""
|
|
143
|
+
import httpx
|
|
144
|
+
|
|
145
|
+
search_url = "https://html.duckduckgo.com/html/"
|
|
146
|
+
async with httpx.AsyncClient(timeout=15) as client:
|
|
147
|
+
resp = await client.post(
|
|
148
|
+
search_url,
|
|
149
|
+
data={"q": query},
|
|
150
|
+
headers={"User-Agent": "voidx/0.1"},
|
|
151
|
+
)
|
|
152
|
+
resp.raise_for_status()
|
|
153
|
+
|
|
154
|
+
results = _parse_duckduckgo_html(resp.text)
|
|
155
|
+
|
|
156
|
+
if allowed_domains:
|
|
157
|
+
results = [r for r in results if any(matches_domain(r["url"], domain) for domain in allowed_domains)]
|
|
158
|
+
if blocked_domains:
|
|
159
|
+
results = [r for r in results if not any(matches_domain(r["url"], domain) for domain in blocked_domains)]
|
|
160
|
+
|
|
161
|
+
return results[:max_results]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ── Tool ────────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
class WebSearchTool(BaseTool):
|
|
167
|
+
id = "websearch"
|
|
168
|
+
description = "Search the web. Returns titles, URLs, and snippets."
|
|
169
|
+
|
|
170
|
+
def __init__(self, settings=None):
|
|
171
|
+
self._settings = settings
|
|
172
|
+
|
|
173
|
+
def parameters_schema(self) -> dict:
|
|
174
|
+
return model_to_json_schema(WebSearchInput)
|
|
175
|
+
|
|
176
|
+
async def execute(self, args: dict, ctx: ToolContext) -> ToolResult:
|
|
177
|
+
inp = WebSearchInput.model_validate(args)
|
|
178
|
+
mcp_result = await call_mcp_web_tool(
|
|
179
|
+
kind="search",
|
|
180
|
+
settings=self._settings,
|
|
181
|
+
ctx=ctx,
|
|
182
|
+
arguments=inp.model_dump(exclude_none=True),
|
|
183
|
+
title=f"Search: {inp.query}",
|
|
184
|
+
)
|
|
185
|
+
if mcp_result is not None:
|
|
186
|
+
return mcp_result
|
|
187
|
+
|
|
188
|
+
tavily_key = self._get_tavily_key()
|
|
189
|
+
cache_key = search_cache_key(
|
|
190
|
+
query=inp.query,
|
|
191
|
+
allowed_domains=inp.allowed_domains,
|
|
192
|
+
blocked_domains=inp.blocked_domains,
|
|
193
|
+
max_results=inp.max_results,
|
|
194
|
+
backend="tavily" if tavily_key else "duckduckgo",
|
|
195
|
+
)
|
|
196
|
+
cached = WEB_TOOL_CACHE.get(cache_key)
|
|
197
|
+
if isinstance(cached, ToolResult):
|
|
198
|
+
return cached_tool_result(cached)
|
|
199
|
+
|
|
200
|
+
fallback_errors: list[str] = []
|
|
201
|
+
if tavily_key:
|
|
202
|
+
try:
|
|
203
|
+
results = await _search_tavily(
|
|
204
|
+
inp.query,
|
|
205
|
+
tavily_key,
|
|
206
|
+
inp.allowed_domains,
|
|
207
|
+
inp.blocked_domains,
|
|
208
|
+
inp.max_results,
|
|
209
|
+
)
|
|
210
|
+
if results:
|
|
211
|
+
result = self._format_results(inp.query, results, "tavily", fallback_errors)
|
|
212
|
+
WEB_TOOL_CACHE.set(cache_key, result, ttl_seconds=600)
|
|
213
|
+
return result
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
fallback_errors.append(f"tavily: {exc}")
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
results = await _search_duckduckgo(
|
|
219
|
+
inp.query,
|
|
220
|
+
inp.allowed_domains,
|
|
221
|
+
inp.blocked_domains,
|
|
222
|
+
inp.max_results,
|
|
223
|
+
)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
return ToolResult(
|
|
226
|
+
output=f"Search failed: {e}. Query: {inp.query}",
|
|
227
|
+
metadata={"query": inp.query, "error": str(e), "fallback_errors": fallback_errors},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if not results:
|
|
231
|
+
return ToolResult(
|
|
232
|
+
output=f"No results found for: {inp.query}",
|
|
233
|
+
metadata={
|
|
234
|
+
"query": inp.query,
|
|
235
|
+
"results": 0,
|
|
236
|
+
"backend": "duckduckgo",
|
|
237
|
+
"fallback_errors": fallback_errors,
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
result = self._format_results(inp.query, results, "duckduckgo", fallback_errors)
|
|
242
|
+
WEB_TOOL_CACHE.set(cache_key, result, ttl_seconds=600)
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
def _get_tavily_key(self) -> str | None:
|
|
246
|
+
env_key = os.environ.get("TAVILY_API_KEY")
|
|
247
|
+
if env_key:
|
|
248
|
+
return env_key
|
|
249
|
+
if self._settings:
|
|
250
|
+
return self._settings.get_tavily_api_key()
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def _format_results(
|
|
255
|
+
query: str,
|
|
256
|
+
results: list[dict[str, str]],
|
|
257
|
+
backend: str,
|
|
258
|
+
fallback_errors: list[str] | None = None,
|
|
259
|
+
) -> ToolResult:
|
|
260
|
+
normalized = normalize_search_results(results)
|
|
261
|
+
formatted = []
|
|
262
|
+
for r in normalized:
|
|
263
|
+
snippet = f"\n {r['snippet']}" if r["snippet"] else ""
|
|
264
|
+
formatted.append(f"- [{r['title']}]({r['url']}){snippet}")
|
|
265
|
+
return ToolResult(
|
|
266
|
+
title=f"Search: {query}",
|
|
267
|
+
output="\n\n".join(formatted),
|
|
268
|
+
metadata={
|
|
269
|
+
"query": query,
|
|
270
|
+
"results": len(normalized),
|
|
271
|
+
"backend": backend,
|
|
272
|
+
"items": normalized,
|
|
273
|
+
"cached": False,
|
|
274
|
+
"fallback_errors": fallback_errors or [],
|
|
275
|
+
},
|
|
276
|
+
)
|
voidx/ui/__init__.py
ADDED
|
File without changes
|