superqode 0.1.5__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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Web Tools - Search and Fetch Web Content.
|
|
3
|
+
|
|
4
|
+
Provides web search and content fetching capabilities for agents:
|
|
5
|
+
- Web search with multiple provider support
|
|
6
|
+
- URL content fetching with summarization
|
|
7
|
+
- HTML to markdown conversion
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- DuckDuckGo search (no API key required)
|
|
11
|
+
- Optional Tavily/SerpAPI integration
|
|
12
|
+
- Content extraction and summarization
|
|
13
|
+
- Configurable result limits
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import ssl
|
|
22
|
+
import urllib.request
|
|
23
|
+
import urllib.error
|
|
24
|
+
import urllib.parse
|
|
25
|
+
from html.parser import HTMLParser
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
|
|
29
|
+
from .base import Tool, ToolResult, ToolContext
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ============================================================================
|
|
33
|
+
# HTML Processing Utilities
|
|
34
|
+
# ============================================================================
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class HTMLToMarkdown(HTMLParser):
|
|
38
|
+
"""Convert HTML to Markdown format."""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
super().__init__()
|
|
42
|
+
self._output: List[str] = []
|
|
43
|
+
self._skip_tags = {"script", "style", "head", "meta", "link", "noscript"}
|
|
44
|
+
self._in_skip = False
|
|
45
|
+
self._tag_stack: List[str] = []
|
|
46
|
+
self._list_depth = 0
|
|
47
|
+
self._in_code = False
|
|
48
|
+
self._href = ""
|
|
49
|
+
|
|
50
|
+
def handle_starttag(self, tag: str, attrs: List[tuple]):
|
|
51
|
+
tag = tag.lower()
|
|
52
|
+
self._tag_stack.append(tag)
|
|
53
|
+
|
|
54
|
+
if tag in self._skip_tags:
|
|
55
|
+
self._in_skip = True
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
attrs_dict = dict(attrs)
|
|
59
|
+
|
|
60
|
+
if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
|
|
61
|
+
level = int(tag[1])
|
|
62
|
+
self._output.append("\n" + "#" * level + " ")
|
|
63
|
+
elif tag == "p":
|
|
64
|
+
self._output.append("\n\n")
|
|
65
|
+
elif tag == "br":
|
|
66
|
+
self._output.append("\n")
|
|
67
|
+
elif tag in ("strong", "b"):
|
|
68
|
+
self._output.append("**")
|
|
69
|
+
elif tag in ("em", "i"):
|
|
70
|
+
self._output.append("*")
|
|
71
|
+
elif tag == "a":
|
|
72
|
+
self._href = attrs_dict.get("href", "")
|
|
73
|
+
self._output.append("[")
|
|
74
|
+
elif tag == "code":
|
|
75
|
+
if self._in_code:
|
|
76
|
+
return
|
|
77
|
+
self._output.append("`")
|
|
78
|
+
self._in_code = True
|
|
79
|
+
elif tag == "pre":
|
|
80
|
+
self._output.append("\n```\n")
|
|
81
|
+
self._in_code = True
|
|
82
|
+
elif tag == "ul":
|
|
83
|
+
self._list_depth += 1
|
|
84
|
+
self._output.append("\n")
|
|
85
|
+
elif tag == "ol":
|
|
86
|
+
self._list_depth += 1
|
|
87
|
+
self._output.append("\n")
|
|
88
|
+
elif tag == "li":
|
|
89
|
+
indent = " " * (self._list_depth - 1)
|
|
90
|
+
self._output.append(f"{indent}- ")
|
|
91
|
+
elif tag == "blockquote":
|
|
92
|
+
self._output.append("\n> ")
|
|
93
|
+
elif tag == "hr":
|
|
94
|
+
self._output.append("\n---\n")
|
|
95
|
+
|
|
96
|
+
def handle_endtag(self, tag: str):
|
|
97
|
+
tag = tag.lower()
|
|
98
|
+
|
|
99
|
+
if self._tag_stack and self._tag_stack[-1] == tag:
|
|
100
|
+
self._tag_stack.pop()
|
|
101
|
+
|
|
102
|
+
if tag in self._skip_tags:
|
|
103
|
+
self._in_skip = False
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
|
|
107
|
+
self._output.append("\n")
|
|
108
|
+
elif tag == "p":
|
|
109
|
+
self._output.append("\n")
|
|
110
|
+
elif tag in ("strong", "b"):
|
|
111
|
+
self._output.append("**")
|
|
112
|
+
elif tag in ("em", "i"):
|
|
113
|
+
self._output.append("*")
|
|
114
|
+
elif tag == "a":
|
|
115
|
+
if self._href:
|
|
116
|
+
self._output.append(f"]({self._href})")
|
|
117
|
+
else:
|
|
118
|
+
self._output.append("]")
|
|
119
|
+
self._href = ""
|
|
120
|
+
elif tag == "code":
|
|
121
|
+
self._output.append("`")
|
|
122
|
+
self._in_code = False
|
|
123
|
+
elif tag == "pre":
|
|
124
|
+
self._output.append("\n```\n")
|
|
125
|
+
self._in_code = False
|
|
126
|
+
elif tag == "ul" or tag == "ol":
|
|
127
|
+
self._list_depth = max(0, self._list_depth - 1)
|
|
128
|
+
self._output.append("\n")
|
|
129
|
+
elif tag == "li":
|
|
130
|
+
self._output.append("\n")
|
|
131
|
+
|
|
132
|
+
def handle_data(self, data: str):
|
|
133
|
+
if self._in_skip:
|
|
134
|
+
return
|
|
135
|
+
text = data.strip() if not self._in_code else data
|
|
136
|
+
if text:
|
|
137
|
+
self._output.append(text)
|
|
138
|
+
|
|
139
|
+
def get_markdown(self) -> str:
|
|
140
|
+
"""Get the converted markdown."""
|
|
141
|
+
result = "".join(self._output)
|
|
142
|
+
# Clean up extra whitespace
|
|
143
|
+
result = re.sub(r"\n{3,}", "\n\n", result)
|
|
144
|
+
return result.strip()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TextExtractor(HTMLParser):
|
|
148
|
+
"""Extract plain text from HTML."""
|
|
149
|
+
|
|
150
|
+
def __init__(self):
|
|
151
|
+
super().__init__()
|
|
152
|
+
self._text: List[str] = []
|
|
153
|
+
self._skip_tags = {"script", "style", "head", "meta", "link", "noscript"}
|
|
154
|
+
self._in_skip = False
|
|
155
|
+
|
|
156
|
+
def handle_starttag(self, tag: str, attrs: List[tuple]):
|
|
157
|
+
if tag.lower() in self._skip_tags:
|
|
158
|
+
self._in_skip = True
|
|
159
|
+
|
|
160
|
+
def handle_endtag(self, tag: str):
|
|
161
|
+
if tag.lower() in self._skip_tags:
|
|
162
|
+
self._in_skip = False
|
|
163
|
+
|
|
164
|
+
def handle_data(self, data: str):
|
|
165
|
+
if not self._in_skip:
|
|
166
|
+
text = data.strip()
|
|
167
|
+
if text:
|
|
168
|
+
self._text.append(text)
|
|
169
|
+
|
|
170
|
+
def get_text(self) -> str:
|
|
171
|
+
return "\n".join(self._text)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ============================================================================
|
|
175
|
+
# Web Search Tool
|
|
176
|
+
# ============================================================================
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class SearchResult:
|
|
181
|
+
"""A search result."""
|
|
182
|
+
|
|
183
|
+
title: str
|
|
184
|
+
url: str
|
|
185
|
+
snippet: str
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class WebSearchTool(Tool):
|
|
189
|
+
"""
|
|
190
|
+
Search the web for information.
|
|
191
|
+
|
|
192
|
+
Uses DuckDuckGo by default (no API key required).
|
|
193
|
+
Can be configured to use Tavily or SerpAPI with API keys.
|
|
194
|
+
|
|
195
|
+
Features:
|
|
196
|
+
- Multiple search providers
|
|
197
|
+
- Configurable result count
|
|
198
|
+
- Search type options (fast, deep)
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
202
|
+
DEFAULT_TIMEOUT = 15
|
|
203
|
+
MAX_RESULTS = 10
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def name(self) -> str:
|
|
207
|
+
return "web_search"
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def description(self) -> str:
|
|
211
|
+
return """Search the web and return relevant results.
|
|
212
|
+
|
|
213
|
+
Returns a list of search results with titles, URLs, and snippets.
|
|
214
|
+
Useful for finding documentation, examples, recent information, etc."""
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def parameters(self) -> Dict[str, Any]:
|
|
218
|
+
return {
|
|
219
|
+
"type": "object",
|
|
220
|
+
"properties": {
|
|
221
|
+
"query": {"type": "string", "description": "Search query"},
|
|
222
|
+
"num_results": {
|
|
223
|
+
"type": "integer",
|
|
224
|
+
"description": "Number of results to return (default: 5, max: 10)",
|
|
225
|
+
},
|
|
226
|
+
"search_type": {
|
|
227
|
+
"type": "string",
|
|
228
|
+
"enum": ["auto", "fast", "deep"],
|
|
229
|
+
"description": "Search type: fast (quick results), deep (more comprehensive)",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
"required": ["query"],
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
|
|
236
|
+
query = args.get("query", "")
|
|
237
|
+
num_results = min(args.get("num_results", 5), self.MAX_RESULTS)
|
|
238
|
+
search_type = args.get("search_type", "auto")
|
|
239
|
+
|
|
240
|
+
if not query:
|
|
241
|
+
return ToolResult(success=False, output="", error="Search query is required")
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
# Try DuckDuckGo first (no API key needed)
|
|
245
|
+
results = await self._search_duckduckgo(query, num_results)
|
|
246
|
+
|
|
247
|
+
if not results:
|
|
248
|
+
return ToolResult(
|
|
249
|
+
success=True,
|
|
250
|
+
output=f"No results found for: {query}",
|
|
251
|
+
metadata={"query": query, "count": 0},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Format results
|
|
255
|
+
output_lines = [f"Search results for: {query}\n"]
|
|
256
|
+
|
|
257
|
+
for i, result in enumerate(results, 1):
|
|
258
|
+
output_lines.append(f"{i}. {result.title}")
|
|
259
|
+
output_lines.append(f" URL: {result.url}")
|
|
260
|
+
if result.snippet:
|
|
261
|
+
# Truncate long snippets
|
|
262
|
+
snippet = (
|
|
263
|
+
result.snippet[:200] + "..."
|
|
264
|
+
if len(result.snippet) > 200
|
|
265
|
+
else result.snippet
|
|
266
|
+
)
|
|
267
|
+
output_lines.append(f" {snippet}")
|
|
268
|
+
output_lines.append("")
|
|
269
|
+
|
|
270
|
+
return ToolResult(
|
|
271
|
+
success=True,
|
|
272
|
+
output="\n".join(output_lines),
|
|
273
|
+
metadata={"query": query, "count": len(results), "search_type": search_type},
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
except Exception as e:
|
|
277
|
+
return ToolResult(success=False, output="", error=f"Search failed: {str(e)}")
|
|
278
|
+
|
|
279
|
+
async def _search_duckduckgo(self, query: str, num_results: int) -> List[SearchResult]:
|
|
280
|
+
"""Search using DuckDuckGo HTML."""
|
|
281
|
+
loop = asyncio.get_event_loop()
|
|
282
|
+
return await loop.run_in_executor(
|
|
283
|
+
None, lambda: self._sync_search_duckduckgo(query, num_results)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def _sync_search_duckduckgo(self, query: str, num_results: int) -> List[SearchResult]:
|
|
287
|
+
"""Synchronous DuckDuckGo search implementation."""
|
|
288
|
+
try:
|
|
289
|
+
# Use DuckDuckGo HTML search
|
|
290
|
+
encoded_query = urllib.parse.quote_plus(query)
|
|
291
|
+
url = f"https://html.duckduckgo.com/html/?q={encoded_query}"
|
|
292
|
+
|
|
293
|
+
req = urllib.request.Request(url)
|
|
294
|
+
req.add_header("User-Agent", self.USER_AGENT)
|
|
295
|
+
|
|
296
|
+
ctx = ssl.create_default_context()
|
|
297
|
+
|
|
298
|
+
with urllib.request.urlopen(req, timeout=self.DEFAULT_TIMEOUT, context=ctx) as response:
|
|
299
|
+
html = response.read().decode("utf-8", errors="replace")
|
|
300
|
+
|
|
301
|
+
# Parse results
|
|
302
|
+
results = self._parse_ddg_results(html, num_results)
|
|
303
|
+
return results
|
|
304
|
+
|
|
305
|
+
except Exception as e:
|
|
306
|
+
# Fallback to instant answer API
|
|
307
|
+
try:
|
|
308
|
+
return self._search_ddg_instant(query, num_results)
|
|
309
|
+
except Exception:
|
|
310
|
+
raise e
|
|
311
|
+
|
|
312
|
+
def _parse_ddg_results(self, html: str, num_results: int) -> List[SearchResult]:
|
|
313
|
+
"""Parse DuckDuckGo HTML results."""
|
|
314
|
+
results = []
|
|
315
|
+
|
|
316
|
+
# Find result blocks
|
|
317
|
+
result_pattern = re.compile(
|
|
318
|
+
r'<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([^<]*)</a>', re.DOTALL
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
snippet_pattern = re.compile(r'<a[^>]*class="result__snippet"[^>]*>([^<]*)</a>', re.DOTALL)
|
|
322
|
+
|
|
323
|
+
# Find all result links
|
|
324
|
+
link_matches = result_pattern.findall(html)
|
|
325
|
+
snippet_matches = snippet_pattern.findall(html)
|
|
326
|
+
|
|
327
|
+
for i, (url, title) in enumerate(link_matches[:num_results]):
|
|
328
|
+
# Clean URL (DuckDuckGo uses redirect URLs)
|
|
329
|
+
if "uddg=" in url:
|
|
330
|
+
try:
|
|
331
|
+
parsed = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
|
|
332
|
+
url = parsed.get("uddg", [url])[0]
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
# Get snippet if available
|
|
337
|
+
snippet = snippet_matches[i] if i < len(snippet_matches) else ""
|
|
338
|
+
|
|
339
|
+
# Clean HTML entities
|
|
340
|
+
title = self._clean_html_entities(title)
|
|
341
|
+
snippet = self._clean_html_entities(snippet)
|
|
342
|
+
|
|
343
|
+
results.append(SearchResult(title=title.strip(), url=url, snippet=snippet.strip()))
|
|
344
|
+
|
|
345
|
+
return results
|
|
346
|
+
|
|
347
|
+
def _search_ddg_instant(self, query: str, num_results: int) -> List[SearchResult]:
|
|
348
|
+
"""Search using DuckDuckGo instant answer API."""
|
|
349
|
+
encoded_query = urllib.parse.quote_plus(query)
|
|
350
|
+
url = f"https://api.duckduckgo.com/?q={encoded_query}&format=json&no_redirect=1"
|
|
351
|
+
|
|
352
|
+
req = urllib.request.Request(url)
|
|
353
|
+
req.add_header("User-Agent", self.USER_AGENT)
|
|
354
|
+
|
|
355
|
+
ctx = ssl.create_default_context()
|
|
356
|
+
|
|
357
|
+
with urllib.request.urlopen(req, timeout=self.DEFAULT_TIMEOUT, context=ctx) as response:
|
|
358
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
359
|
+
|
|
360
|
+
results = []
|
|
361
|
+
|
|
362
|
+
# Abstract result
|
|
363
|
+
if data.get("AbstractURL") and data.get("AbstractText"):
|
|
364
|
+
results.append(
|
|
365
|
+
SearchResult(
|
|
366
|
+
title=data.get("Heading", query),
|
|
367
|
+
url=data["AbstractURL"],
|
|
368
|
+
snippet=data["AbstractText"][:200],
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Related topics
|
|
373
|
+
for topic in data.get("RelatedTopics", [])[: num_results - len(results)]:
|
|
374
|
+
if isinstance(topic, dict) and topic.get("FirstURL"):
|
|
375
|
+
results.append(
|
|
376
|
+
SearchResult(
|
|
377
|
+
title=topic.get("Text", "")[:100],
|
|
378
|
+
url=topic["FirstURL"],
|
|
379
|
+
snippet=topic.get("Text", "")[:200],
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return results
|
|
384
|
+
|
|
385
|
+
def _clean_html_entities(self, text: str) -> str:
|
|
386
|
+
"""Clean HTML entities from text."""
|
|
387
|
+
import html
|
|
388
|
+
|
|
389
|
+
return html.unescape(text)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ============================================================================
|
|
393
|
+
# Web Fetch Tool (Enhanced)
|
|
394
|
+
# ============================================================================
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class WebFetchTool(Tool):
|
|
398
|
+
"""
|
|
399
|
+
Fetch and analyze web content.
|
|
400
|
+
|
|
401
|
+
Features:
|
|
402
|
+
- HTML to markdown conversion
|
|
403
|
+
- Text extraction
|
|
404
|
+
- Optional summarization
|
|
405
|
+
- Configurable output format
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
USER_AGENT = "SuperQode/1.0 (AI Coding Assistant)"
|
|
409
|
+
DEFAULT_TIMEOUT = 30
|
|
410
|
+
MAX_SIZE = 2 * 1024 * 1024 # 2MB
|
|
411
|
+
|
|
412
|
+
@property
|
|
413
|
+
def name(self) -> str:
|
|
414
|
+
return "web_fetch"
|
|
415
|
+
|
|
416
|
+
@property
|
|
417
|
+
def description(self) -> str:
|
|
418
|
+
return """Fetch content from a URL and optionally process it.
|
|
419
|
+
|
|
420
|
+
Supports:
|
|
421
|
+
- HTML pages (converts to markdown or extracts text)
|
|
422
|
+
- JSON APIs (formatted output)
|
|
423
|
+
- Plain text content
|
|
424
|
+
|
|
425
|
+
Useful for reading documentation, API responses, web pages, etc."""
|
|
426
|
+
|
|
427
|
+
@property
|
|
428
|
+
def parameters(self) -> Dict[str, Any]:
|
|
429
|
+
return {
|
|
430
|
+
"type": "object",
|
|
431
|
+
"properties": {
|
|
432
|
+
"url": {"type": "string", "description": "URL to fetch (http or https)"},
|
|
433
|
+
"format": {
|
|
434
|
+
"type": "string",
|
|
435
|
+
"enum": ["auto", "markdown", "text", "json", "raw"],
|
|
436
|
+
"description": "Output format: markdown (HTML to MD), text (plain text), json, raw",
|
|
437
|
+
},
|
|
438
|
+
"extract_main": {
|
|
439
|
+
"type": "boolean",
|
|
440
|
+
"description": "Try to extract main content only (skip navigation, ads)",
|
|
441
|
+
},
|
|
442
|
+
"max_length": {
|
|
443
|
+
"type": "integer",
|
|
444
|
+
"description": "Maximum content length in characters (default: 50000)",
|
|
445
|
+
},
|
|
446
|
+
"selector": {
|
|
447
|
+
"type": "string",
|
|
448
|
+
"description": "CSS-like selector to extract specific content (e.g., 'article', 'main')",
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
"required": ["url"],
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
|
|
455
|
+
url = args.get("url", "")
|
|
456
|
+
format_type = args.get("format", "auto")
|
|
457
|
+
extract_main = args.get("extract_main", True)
|
|
458
|
+
max_length = args.get("max_length", 50000)
|
|
459
|
+
selector = args.get("selector", "")
|
|
460
|
+
|
|
461
|
+
if not url:
|
|
462
|
+
return ToolResult(success=False, output="", error="URL is required")
|
|
463
|
+
|
|
464
|
+
if not url.startswith(("http://", "https://")):
|
|
465
|
+
return ToolResult(
|
|
466
|
+
success=False, output="", error="Only http:// and https:// URLs are supported"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
loop = asyncio.get_event_loop()
|
|
471
|
+
result = await asyncio.wait_for(
|
|
472
|
+
loop.run_in_executor(None, lambda: self._sync_fetch(url)),
|
|
473
|
+
timeout=self.DEFAULT_TIMEOUT + 5,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
if result.get("error"):
|
|
477
|
+
return ToolResult(success=False, output="", error=result["error"])
|
|
478
|
+
|
|
479
|
+
content = result["content"]
|
|
480
|
+
content_type = result.get("content_type", "")
|
|
481
|
+
|
|
482
|
+
# Process content based on format
|
|
483
|
+
output = self._process_content(
|
|
484
|
+
content, content_type, format_type, extract_main, selector
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Truncate if needed
|
|
488
|
+
if len(output) > max_length:
|
|
489
|
+
output = output[:max_length] + f"\n\n[Content truncated at {max_length} characters]"
|
|
490
|
+
|
|
491
|
+
return ToolResult(
|
|
492
|
+
success=True,
|
|
493
|
+
output=output,
|
|
494
|
+
metadata={
|
|
495
|
+
"url": url,
|
|
496
|
+
"content_type": content_type,
|
|
497
|
+
"original_size": len(content),
|
|
498
|
+
"output_size": len(output),
|
|
499
|
+
"format": format_type,
|
|
500
|
+
},
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
except asyncio.TimeoutError:
|
|
504
|
+
return ToolResult(
|
|
505
|
+
success=False,
|
|
506
|
+
output="",
|
|
507
|
+
error=f"Request timed out after {self.DEFAULT_TIMEOUT} seconds",
|
|
508
|
+
)
|
|
509
|
+
except Exception as e:
|
|
510
|
+
return ToolResult(success=False, output="", error=f"Fetch error: {str(e)}")
|
|
511
|
+
|
|
512
|
+
def _sync_fetch(self, url: str) -> Dict[str, Any]:
|
|
513
|
+
"""Synchronous fetch implementation."""
|
|
514
|
+
try:
|
|
515
|
+
req = urllib.request.Request(url)
|
|
516
|
+
req.add_header("User-Agent", self.USER_AGENT)
|
|
517
|
+
req.add_header(
|
|
518
|
+
"Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
ctx = ssl.create_default_context()
|
|
522
|
+
|
|
523
|
+
with urllib.request.urlopen(req, timeout=self.DEFAULT_TIMEOUT, context=ctx) as response:
|
|
524
|
+
content_type = response.headers.get("Content-Type", "")
|
|
525
|
+
|
|
526
|
+
# Read with size limit
|
|
527
|
+
content = response.read(self.MAX_SIZE)
|
|
528
|
+
|
|
529
|
+
# Check for truncation
|
|
530
|
+
extra = response.read(1)
|
|
531
|
+
truncated = bool(extra)
|
|
532
|
+
|
|
533
|
+
# Decode
|
|
534
|
+
charset = self._get_charset(content_type)
|
|
535
|
+
try:
|
|
536
|
+
text = content.decode(charset, errors="replace")
|
|
537
|
+
except (UnicodeDecodeError, LookupError):
|
|
538
|
+
text = content.decode("utf-8", errors="replace")
|
|
539
|
+
|
|
540
|
+
if truncated:
|
|
541
|
+
text += f"\n\n[Content truncated at {self.MAX_SIZE} bytes]"
|
|
542
|
+
|
|
543
|
+
return {"content": text, "content_type": content_type, "truncated": truncated}
|
|
544
|
+
|
|
545
|
+
except urllib.error.HTTPError as e:
|
|
546
|
+
return {"error": f"HTTP {e.code}: {e.reason}"}
|
|
547
|
+
except urllib.error.URLError as e:
|
|
548
|
+
return {"error": f"URL Error: {str(e.reason)}"}
|
|
549
|
+
except Exception as e:
|
|
550
|
+
return {"error": str(e)}
|
|
551
|
+
|
|
552
|
+
def _get_charset(self, content_type: str) -> str:
|
|
553
|
+
"""Extract charset from Content-Type header."""
|
|
554
|
+
if not content_type:
|
|
555
|
+
return "utf-8"
|
|
556
|
+
|
|
557
|
+
match = re.search(r"charset=([^\s;]+)", content_type, re.I)
|
|
558
|
+
if match:
|
|
559
|
+
return match.group(1).strip("\"'")
|
|
560
|
+
|
|
561
|
+
return "utf-8"
|
|
562
|
+
|
|
563
|
+
def _process_content(
|
|
564
|
+
self, content: str, content_type: str, format_type: str, extract_main: bool, selector: str
|
|
565
|
+
) -> str:
|
|
566
|
+
"""Process content based on format type."""
|
|
567
|
+
# Auto-detect format
|
|
568
|
+
if format_type == "auto":
|
|
569
|
+
if "application/json" in content_type:
|
|
570
|
+
format_type = "json"
|
|
571
|
+
elif "text/html" in content_type:
|
|
572
|
+
format_type = "markdown"
|
|
573
|
+
else:
|
|
574
|
+
format_type = "raw"
|
|
575
|
+
|
|
576
|
+
if format_type == "json":
|
|
577
|
+
try:
|
|
578
|
+
data = json.loads(content)
|
|
579
|
+
return json.dumps(data, indent=2)
|
|
580
|
+
except json.JSONDecodeError:
|
|
581
|
+
return content
|
|
582
|
+
|
|
583
|
+
elif format_type == "markdown":
|
|
584
|
+
# Convert HTML to Markdown
|
|
585
|
+
try:
|
|
586
|
+
# Optionally extract main content first
|
|
587
|
+
if extract_main:
|
|
588
|
+
content = self._extract_main_content(content, selector)
|
|
589
|
+
|
|
590
|
+
parser = HTMLToMarkdown()
|
|
591
|
+
parser.feed(content)
|
|
592
|
+
return parser.get_markdown()
|
|
593
|
+
except Exception:
|
|
594
|
+
return content
|
|
595
|
+
|
|
596
|
+
elif format_type == "text":
|
|
597
|
+
# Extract plain text
|
|
598
|
+
try:
|
|
599
|
+
parser = TextExtractor()
|
|
600
|
+
parser.feed(content)
|
|
601
|
+
return parser.get_text()
|
|
602
|
+
except Exception:
|
|
603
|
+
return content
|
|
604
|
+
|
|
605
|
+
else: # raw
|
|
606
|
+
return content
|
|
607
|
+
|
|
608
|
+
def _extract_main_content(self, html: str, selector: str) -> str:
|
|
609
|
+
"""Extract main content from HTML."""
|
|
610
|
+
# Simple extraction based on common patterns
|
|
611
|
+
# Priority: article, main, .content, .post, #content, body
|
|
612
|
+
|
|
613
|
+
patterns = [
|
|
614
|
+
(r"<article[^>]*>(.*?)</article>", "article"),
|
|
615
|
+
(r"<main[^>]*>(.*?)</main>", "main"),
|
|
616
|
+
(r'<div[^>]*class="[^"]*content[^"]*"[^>]*>(.*?)</div>', ".content"),
|
|
617
|
+
(r'<div[^>]*id="content"[^>]*>(.*?)</div>', "#content"),
|
|
618
|
+
]
|
|
619
|
+
|
|
620
|
+
if selector:
|
|
621
|
+
# Custom selector (simplified)
|
|
622
|
+
if selector.startswith("."):
|
|
623
|
+
class_name = selector[1:]
|
|
624
|
+
patterns.insert(
|
|
625
|
+
0, (rf'<[^>]*class="[^"]*{class_name}[^"]*"[^>]*>(.*?)</\w+>', selector)
|
|
626
|
+
)
|
|
627
|
+
elif selector.startswith("#"):
|
|
628
|
+
id_name = selector[1:]
|
|
629
|
+
patterns.insert(0, (rf'<[^>]*id="{id_name}"[^>]*>(.*?)</\w+>', selector))
|
|
630
|
+
else:
|
|
631
|
+
patterns.insert(0, (rf"<{selector}[^>]*>(.*?)</{selector}>", selector))
|
|
632
|
+
|
|
633
|
+
for pattern, name in patterns:
|
|
634
|
+
match = re.search(pattern, html, re.DOTALL | re.IGNORECASE)
|
|
635
|
+
if match:
|
|
636
|
+
return match.group(1)
|
|
637
|
+
|
|
638
|
+
# Return original if no main content found
|
|
639
|
+
return html
|