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.
Files changed (126) hide show
  1. voidx/__init__.py +3 -0
  2. voidx/agent/__init__.py +0 -0
  3. voidx/agent/agents.py +439 -0
  4. voidx/agent/attachments.py +235 -0
  5. voidx/agent/graph.py +463 -0
  6. voidx/agent/graph_components/__init__.py +1 -0
  7. voidx/agent/graph_components/compaction.py +268 -0
  8. voidx/agent/graph_components/permissions.py +139 -0
  9. voidx/agent/graph_components/run_loop.py +532 -0
  10. voidx/agent/graph_components/runtime.py +14 -0
  11. voidx/agent/graph_components/streaming.py +351 -0
  12. voidx/agent/graph_components/subagent.py +278 -0
  13. voidx/agent/graph_components/tool_execution.py +208 -0
  14. voidx/agent/runtime_context.py +368 -0
  15. voidx/agent/slash.py +466 -0
  16. voidx/agent/slash_components/__init__.py +1 -0
  17. voidx/agent/slash_components/code_ide.py +68 -0
  18. voidx/agent/slash_components/lsp.py +105 -0
  19. voidx/agent/slash_components/mcp.py +332 -0
  20. voidx/agent/slash_components/model.py +419 -0
  21. voidx/agent/slash_components/runtime.py +55 -0
  22. voidx/agent/slash_components/skills.py +94 -0
  23. voidx/agent/state.py +32 -0
  24. voidx/agent/task_state.py +278 -0
  25. voidx/agent/tool_filters.py +27 -0
  26. voidx/config.py +707 -0
  27. voidx/llm/__init__.py +0 -0
  28. voidx/llm/catalog.py +188 -0
  29. voidx/llm/compaction.py +267 -0
  30. voidx/llm/context.py +43 -0
  31. voidx/llm/instruction.py +220 -0
  32. voidx/llm/provider.py +312 -0
  33. voidx/llm/usage.py +341 -0
  34. voidx/lsp/__init__.py +30 -0
  35. voidx/lsp/client.py +259 -0
  36. voidx/lsp/config.py +172 -0
  37. voidx/lsp/detector.py +512 -0
  38. voidx/lsp/errors.py +19 -0
  39. voidx/lsp/manager.py +280 -0
  40. voidx/lsp/schema.py +179 -0
  41. voidx/lsp/service.py +103 -0
  42. voidx/main.py +154 -0
  43. voidx/mcp/__init__.py +33 -0
  44. voidx/mcp/client.py +458 -0
  45. voidx/mcp/manager.py +267 -0
  46. voidx/mcp/schema.py +112 -0
  47. voidx/mcp/tool.py +122 -0
  48. voidx/mcp_servers/__init__.py +1 -0
  49. voidx/mcp_servers/web.py +104 -0
  50. voidx/memory/__init__.py +0 -0
  51. voidx/memory/context_frames.py +188 -0
  52. voidx/memory/model_profiles.py +98 -0
  53. voidx/memory/runtime_state.py +240 -0
  54. voidx/memory/session.py +272 -0
  55. voidx/memory/store.py +245 -0
  56. voidx/memory/transcript.py +137 -0
  57. voidx/permission/__init__.py +28 -0
  58. voidx/permission/engine.py +430 -0
  59. voidx/permission/evaluate.py +114 -0
  60. voidx/permission/sandbox.py +280 -0
  61. voidx/permission/schema.py +24 -0
  62. voidx/permission/service.py +314 -0
  63. voidx/permission/wildcard.py +34 -0
  64. voidx/skills/__init__.py +18 -0
  65. voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
  66. voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
  67. voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
  68. voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
  69. voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
  70. voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
  71. voidx/skills/policy.py +97 -0
  72. voidx/skills/registry.py +162 -0
  73. voidx/skills/schema.py +47 -0
  74. voidx/skills/service.py +199 -0
  75. voidx/tools/__init__.py +0 -0
  76. voidx/tools/agent.py +81 -0
  77. voidx/tools/base.py +86 -0
  78. voidx/tools/bash.py +105 -0
  79. voidx/tools/file_ops.py +193 -0
  80. voidx/tools/lsp.py +155 -0
  81. voidx/tools/registry.py +104 -0
  82. voidx/tools/repomap.py +238 -0
  83. voidx/tools/search.py +162 -0
  84. voidx/tools/task_status.py +57 -0
  85. voidx/tools/task_tracker.py +81 -0
  86. voidx/tools/todo.py +82 -0
  87. voidx/tools/web_content.py +357 -0
  88. voidx/tools/web_mcp.py +107 -0
  89. voidx/tools/webfetch.py +155 -0
  90. voidx/tools/websearch.py +276 -0
  91. voidx/ui/__init__.py +0 -0
  92. voidx/ui/app.py +1033 -0
  93. voidx/ui/app_components/__init__.py +1 -0
  94. voidx/ui/app_components/clipboard_image.py +245 -0
  95. voidx/ui/app_components/commands.py +18 -0
  96. voidx/ui/app_components/controls.py +29 -0
  97. voidx/ui/app_components/file_picker.py +115 -0
  98. voidx/ui/app_components/formatting.py +187 -0
  99. voidx/ui/app_components/git_changes.py +51 -0
  100. voidx/ui/app_components/rendering.py +1169 -0
  101. voidx/ui/browse.py +160 -0
  102. voidx/ui/capture.py +169 -0
  103. voidx/ui/code_ide.py +251 -0
  104. voidx/ui/commands.py +83 -0
  105. voidx/ui/console.py +381 -0
  106. voidx/ui/console_components/__init__.py +1 -0
  107. voidx/ui/console_components/formatting.py +96 -0
  108. voidx/ui/console_components/streaming.py +253 -0
  109. voidx/ui/diff.py +331 -0
  110. voidx/ui/dock.py +372 -0
  111. voidx/ui/dock_components/__init__.py +1 -0
  112. voidx/ui/dock_components/formatting.py +123 -0
  113. voidx/ui/dock_components/nodes.py +401 -0
  114. voidx/ui/dock_components/state.py +51 -0
  115. voidx/ui/event_components/__init__.py +1 -0
  116. voidx/ui/event_components/schema.py +249 -0
  117. voidx/ui/events.py +341 -0
  118. voidx/ui/session_changes.py +163 -0
  119. voidx/ui/startup.py +161 -0
  120. voidx/ui/transcript.py +148 -0
  121. voidx/ui/tree.py +316 -0
  122. voidx-1.0.0.dist-info/METADATA +59 -0
  123. voidx-1.0.0.dist-info/RECORD +126 -0
  124. voidx-1.0.0.dist-info/WHEEL +5 -0
  125. voidx-1.0.0.dist-info/entry_points.txt +2 -0
  126. voidx-1.0.0.dist-info/top_level.txt +1 -0
@@ -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