regcode 0.5.1__tar.gz → 0.6.0__tar.gz
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.
- {regcode-0.5.1 → regcode-0.6.0}/GOALS.md +5 -1
- {regcode-0.5.1 → regcode-0.6.0}/PKG-INFO +1 -1
- {regcode-0.5.1 → regcode-0.6.0}/pyproject.toml +1 -1
- {regcode-0.5.1 → regcode-0.6.0}/regcode/__init__.py +1 -1
- {regcode-0.5.1 → regcode-0.6.0}/regcode/config.py +7 -1
- {regcode-0.5.1 → regcode-0.6.0}/regcode/tools/builtins.py +3 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/tools/web_crawl.py +1 -1
- regcode-0.6.0/regcode/tools/web_search.py +158 -0
- {regcode-0.5.1 → regcode-0.6.0}/tests/test_api.py +0 -1
- {regcode-0.5.1 → regcode-0.6.0}/tests/test_cli.py +0 -1
- regcode-0.6.0/tests/test_web_search.py +206 -0
- {regcode-0.5.1 → regcode-0.6.0}/uv.lock +2 -2
- {regcode-0.5.1 → regcode-0.6.0}/.gitignore +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/.python-version +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/.qwen/settings.json +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/AGENTS.md +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/README.md +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/config.yaml.template +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/main.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/cli.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/conversation_manager.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/dependencies.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/main.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/monty_sandbox.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/permissions.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/sandbox.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/tools/__init__.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/tools/base.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/tools/registry.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/tools/review_notes.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/tracing.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/regcode/tui.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/tests/__init__.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/tests/conftest.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/tests/test_config.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/tests/test_context_manager.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/tests/test_dependencies.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/tests/test_main.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/tests/test_monty_sandbox.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/tests/test_osv_cve.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/tests/test_provider_extra.py +0 -0
- {regcode-0.5.1 → regcode-0.6.0}/tests/test_review_notes.py +0 -0
|
@@ -499,7 +499,11 @@ All phases complete. 50/50 tests passing. Lint clean.
|
|
|
499
499
|
|
|
500
500
|
2. ✅ **TUI complete overhaul** — `tui.py` rewritten from scratch. New layout: (1) top status bar showing `[RegCode <version>] [model] [$cost] [tokens]`, (2) large chat history area with clearly separated user/agent/tool/system message blocks, (3) bottom bar with progress indicator and input field. Streaming works correctly: text chunks stream live, then on completion full Markdown is rendered. Tool calls are interleaved after streaming text (stream buffer flushed before status rendering). Context compaction shows status indicators. All 137 tests pass, lint clean.
|
|
501
501
|
|
|
502
|
+
3. ✅ **WebSearchTool** — New `regcode/tools/web_search.py` tool connecting to SearXNG instances. Defaults to `https://search.regmi.dev`, supports custom SearXNG URL, language code, and result count (1-20). Uses only stdlib `urllib` — no extra dependencies. Registered in `create_default_tools()`. 15 tests pass.
|
|
503
|
+
|
|
504
|
+
## Completed (2026-06-30)
|
|
505
|
+
|
|
506
|
+
- ✅ WebSearchTool: connects to SearXNG, defaults to https://search.regmi.dev, no extra deps needed
|
|
502
507
|
|
|
503
508
|
## To Dos:
|
|
504
|
-
- implement web crawl tool with https://github.com/unclecode/crawl4AI, the api should be straight forward. Use its inbuilt markdown generation and markdown integration.
|
|
505
509
|
- If current Dir has `Agents.MD`, or `AGENTS.md` or `agents.md`, read it and provide it clearly marked as content of Agents.md to the model as extra context about the project.
|
|
@@ -43,6 +43,12 @@ default_system_prompt = dedent(
|
|
|
43
43
|
|
|
44
44
|
logger = logging.getLogger(__name__)
|
|
45
45
|
|
|
46
|
+
class ToolSetConfig(BaseModel):
|
|
47
|
+
"""Configuration for a set of tools."""
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(extra="allow")
|
|
50
|
+
default_searxng_url: str = "https://search.regmi.dev"
|
|
51
|
+
|
|
46
52
|
class AgentConfig(BaseModel):
|
|
47
53
|
"""Configuration specific to the agent's behavior (not provider params).
|
|
48
54
|
|
|
@@ -57,7 +63,6 @@ class AgentConfig(BaseModel):
|
|
|
57
63
|
compaction_threshold: float = 0.8 # Compact when context > 80% of window
|
|
58
64
|
enable_compaction: bool = True
|
|
59
65
|
|
|
60
|
-
|
|
61
66
|
class ProviderConfig(BaseModel):
|
|
62
67
|
model_config = ConfigDict(extra="allow")
|
|
63
68
|
base_url: str = "https://api.openai.com/v1"
|
|
@@ -119,6 +124,7 @@ class Config(BaseModel):
|
|
|
119
124
|
sandbox: SandboxConfig = SandboxConfig()
|
|
120
125
|
tool_budget: int = 20
|
|
121
126
|
permissions: list[AgentPermission] = [AgentPermission.READ, AgentPermission.EXECUTE]
|
|
127
|
+
toolset_config: ToolSetConfig = ToolSetConfig()
|
|
122
128
|
|
|
123
129
|
@classmethod
|
|
124
130
|
def load(cls, path: str | None = None) -> "Config":
|
|
@@ -18,6 +18,8 @@ from typing import Any
|
|
|
18
18
|
from regcode.monty_sandbox import MontySandbox
|
|
19
19
|
from regcode.tools.base import BaseTool, ToolParam, ToolResult
|
|
20
20
|
from regcode.tools.web_crawl import WebCrawlerTool
|
|
21
|
+
from regcode.tools.web_search import WebSearchTool
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
class PythonCodeTool(BaseTool):
|
|
23
25
|
"""Execute Python code in a Monty sandbox."""
|
|
@@ -1368,4 +1370,5 @@ def create_default_tools() -> list[BaseTool]:
|
|
|
1368
1370
|
FetchGitDiffTool(),
|
|
1369
1371
|
OSVCVETool(),
|
|
1370
1372
|
WebCrawlerTool(),
|
|
1373
|
+
WebSearchTool(),
|
|
1371
1374
|
]
|
|
@@ -119,7 +119,7 @@ class WebCrawlerTool(BaseTool):
|
|
|
119
119
|
) -> ToolResult:
|
|
120
120
|
"""Async crawl implementation using crawl4AI."""
|
|
121
121
|
try:
|
|
122
|
-
async with AsyncWebCrawler(verbose=False) as crawler:
|
|
122
|
+
async with AsyncWebCrawler(verbose=False) as crawler: # type: ignore
|
|
123
123
|
if markdown:
|
|
124
124
|
config = CrawlerRunConfig( # type: ignore[union-attr]
|
|
125
125
|
markdown_generator=DefaultMarkdownGenerator(), # type: ignore[union-attr]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Web search tool using SearXNG instances for privacy-respecting search."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.parse
|
|
8
|
+
import urllib.request
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from regcode import Config
|
|
12
|
+
from regcode.tools.base import BaseTool, ToolParam, ToolResult
|
|
13
|
+
|
|
14
|
+
_c = Config.load()
|
|
15
|
+
DEFAULT_SEARXNG_URL = _c.toolset_config.default_searxng_url
|
|
16
|
+
|
|
17
|
+
class WebSearchTool(BaseTool):
|
|
18
|
+
"""Search the web via a SearXNG instance for privacy-respecting results."""
|
|
19
|
+
|
|
20
|
+
name = "web_search"
|
|
21
|
+
description = (
|
|
22
|
+
"Search the web using a SearXNG instance. Returns a list of search "
|
|
23
|
+
"results with titles, URLs, and snippets. Defaults to "
|
|
24
|
+
f"{DEFAULT_SEARXNG_URL} but allows custom SearXNG URLs."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def params(self) -> list[ToolParam]:
|
|
29
|
+
return [
|
|
30
|
+
ToolParam(
|
|
31
|
+
"query",
|
|
32
|
+
"string",
|
|
33
|
+
"Search query string",
|
|
34
|
+
required=True,
|
|
35
|
+
),
|
|
36
|
+
ToolParam(
|
|
37
|
+
"language",
|
|
38
|
+
"string",
|
|
39
|
+
"Language code for search results (e.g. 'en', 'de', 'zh-CN'). "
|
|
40
|
+
"Defaults to the instance default.",
|
|
41
|
+
required=False,
|
|
42
|
+
default="",
|
|
43
|
+
),
|
|
44
|
+
ToolParam(
|
|
45
|
+
"results_count",
|
|
46
|
+
"integer",
|
|
47
|
+
"Number of results to return (max 20). Defaults to 10.",
|
|
48
|
+
required=False,
|
|
49
|
+
default=10,
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
def execute(self, **kwargs: Any) -> ToolResult:
|
|
54
|
+
query = kwargs.get("query")
|
|
55
|
+
url = kwargs.get("url", DEFAULT_SEARXNG_URL)
|
|
56
|
+
language = kwargs.get("language", "")
|
|
57
|
+
results_count = kwargs.get("results_count", 10)
|
|
58
|
+
|
|
59
|
+
if not query:
|
|
60
|
+
return ToolResult(
|
|
61
|
+
output="Missing required parameter: query",
|
|
62
|
+
error=True,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Sanitise the count
|
|
66
|
+
try:
|
|
67
|
+
results_count = int(results_count)
|
|
68
|
+
except (TypeError, ValueError):
|
|
69
|
+
results_count = 10
|
|
70
|
+
results_count = max(1, min(results_count, 20))
|
|
71
|
+
|
|
72
|
+
return self._search(query, url, language, results_count)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _search(
|
|
76
|
+
query: str,
|
|
77
|
+
url: str,
|
|
78
|
+
language: str,
|
|
79
|
+
results_count: int,
|
|
80
|
+
) -> ToolResult:
|
|
81
|
+
"""Perform the SearXNG search via plain HTTP (no extra deps)."""
|
|
82
|
+
params = urllib.parse.urlencode({
|
|
83
|
+
"q": query,
|
|
84
|
+
"format": "json",
|
|
85
|
+
"pageno": "1",
|
|
86
|
+
})
|
|
87
|
+
if language:
|
|
88
|
+
params += "&language=" + urllib.parse.quote(language)
|
|
89
|
+
|
|
90
|
+
headers = {
|
|
91
|
+
"Accept": "application/json",
|
|
92
|
+
"User-Agent": "regcode-agent/0.5.1",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
request_url = url.rstrip("/") + "/search?" + params
|
|
96
|
+
try:
|
|
97
|
+
req = urllib.request.Request(request_url, headers=headers)
|
|
98
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
99
|
+
body = resp.read()
|
|
100
|
+
data = json.loads(body)
|
|
101
|
+
except urllib.error.HTTPError as e:
|
|
102
|
+
return ToolResult(
|
|
103
|
+
output=f"SearXNG HTTP error {e.code}: {e.reason}",
|
|
104
|
+
error=True,
|
|
105
|
+
exit_code=e.code,
|
|
106
|
+
)
|
|
107
|
+
except urllib.error.URLError as e:
|
|
108
|
+
return ToolResult(
|
|
109
|
+
output=f"SearXNG connection failed: {e.reason}",
|
|
110
|
+
error=True,
|
|
111
|
+
exit_code=1,
|
|
112
|
+
)
|
|
113
|
+
except json.JSONDecodeError:
|
|
114
|
+
return ToolResult(
|
|
115
|
+
output="SearXNG returned invalid JSON",
|
|
116
|
+
error=True,
|
|
117
|
+
exit_code=1,
|
|
118
|
+
)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return ToolResult(
|
|
121
|
+
output=f"Search failed: {str(e)}",
|
|
122
|
+
error=True,
|
|
123
|
+
exit_code=1,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
results = data.get("results", [])
|
|
127
|
+
if not results:
|
|
128
|
+
return ToolResult(
|
|
129
|
+
output=f"No results found for '{query}' from {url}",
|
|
130
|
+
exit_code=0,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Limit to requested count
|
|
134
|
+
results = results[:results_count]
|
|
135
|
+
|
|
136
|
+
lines: list[str] = [f"Search results for '{query}' (from {url}):"]
|
|
137
|
+
lines.append(f"Found {len(results)} results:")
|
|
138
|
+
lines.append("")
|
|
139
|
+
|
|
140
|
+
for i, r in enumerate(results, 1):
|
|
141
|
+
title = r.get("title", "No title")
|
|
142
|
+
url_result = r.get("url", "")
|
|
143
|
+
content = r.get("content", "")
|
|
144
|
+
engine = r.get("engine", "")
|
|
145
|
+
lines.append(f" {i}. {title}")
|
|
146
|
+
lines.append(f" URL: {url_result}")
|
|
147
|
+
if engine:
|
|
148
|
+
lines.append(f" Engine: {engine}")
|
|
149
|
+
if content:
|
|
150
|
+
lines.append(f" {content}")
|
|
151
|
+
lines.append("")
|
|
152
|
+
|
|
153
|
+
return ToolResult(output="\n".join(lines), exit_code=0)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def create_web_search_tool() -> WebSearchTool | None:
|
|
157
|
+
"""Create a web search tool (always available — no extra deps needed)."""
|
|
158
|
+
return WebSearchTool()
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Tests for the WebSearchTool connecting to SearXNG instances."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
from regcode.tools.web_search import WebSearchTool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestWebSearchTool:
|
|
10
|
+
"""Tests for the WebSearchTool."""
|
|
11
|
+
|
|
12
|
+
def setup_method(self):
|
|
13
|
+
self.tool = WebSearchTool()
|
|
14
|
+
|
|
15
|
+
def test_tool_name_and_description(self):
|
|
16
|
+
assert self.tool.name == "web_search"
|
|
17
|
+
assert "SearXNG" in self.tool.description
|
|
18
|
+
|
|
19
|
+
def test_tool_params(self):
|
|
20
|
+
params = self.tool.params
|
|
21
|
+
assert len(params) == 3
|
|
22
|
+
names = {p.name for p in params}
|
|
23
|
+
assert names == {"query", "language", "results_count"}
|
|
24
|
+
|
|
25
|
+
query_param = next(p for p in params if p.name == "query")
|
|
26
|
+
assert query_param.required is True
|
|
27
|
+
assert query_param.type == "string"
|
|
28
|
+
|
|
29
|
+
count_param = next(p for p in params if p.name == "results_count")
|
|
30
|
+
assert count_param.default == 10
|
|
31
|
+
|
|
32
|
+
def test_execute_missing_query(self):
|
|
33
|
+
result = self.tool.execute()
|
|
34
|
+
assert result.error is True
|
|
35
|
+
assert "Missing required parameter: query" in result.output
|
|
36
|
+
|
|
37
|
+
def test_execute_empty_query(self):
|
|
38
|
+
result = self.tool.execute(query="")
|
|
39
|
+
assert result.error is True
|
|
40
|
+
assert "Missing required parameter: query" in result.output
|
|
41
|
+
|
|
42
|
+
@patch("regcode.tools.web_search.urllib.request.urlopen")
|
|
43
|
+
@patch("regcode.tools.web_search.urllib.request.Request")
|
|
44
|
+
def test_execute_success_mock(self, mock_request, mock_urlopen):
|
|
45
|
+
"""Test successful search with mocked HTTP response."""
|
|
46
|
+
mock_response = MagicMock()
|
|
47
|
+
results_list = [
|
|
48
|
+
{
|
|
49
|
+
"title": "Test",
|
|
50
|
+
"url": "https://example.com",
|
|
51
|
+
"content": "A test result",
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
mock_response.read.return_value = json.dumps(
|
|
55
|
+
{"results": results_list}
|
|
56
|
+
).encode()
|
|
57
|
+
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
|
58
|
+
mock_response.__exit__ = MagicMock(return_value=False)
|
|
59
|
+
mock_urlopen.return_value = mock_response
|
|
60
|
+
|
|
61
|
+
result = self.tool.execute(query="test query")
|
|
62
|
+
assert result.error is False
|
|
63
|
+
assert "test query" in result.output
|
|
64
|
+
assert "Test" in result.output
|
|
65
|
+
assert "https://example.com" in result.output
|
|
66
|
+
mock_request.assert_called_once()
|
|
67
|
+
call_args = mock_request.call_args
|
|
68
|
+
assert "format=json" in call_args[0][0]
|
|
69
|
+
assert "q=test+query" in call_args[0][0]
|
|
70
|
+
|
|
71
|
+
@patch("regcode.tools.web_search.urllib.request.urlopen")
|
|
72
|
+
def test_execute_custom_url(self, mock_urlopen):
|
|
73
|
+
"""Test that custom URL is used."""
|
|
74
|
+
mock_response = MagicMock()
|
|
75
|
+
mock_response.read.return_value = json.dumps({"results": []}).encode()
|
|
76
|
+
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
|
77
|
+
mock_response.__exit__ = MagicMock(return_value=False)
|
|
78
|
+
mock_urlopen.return_value = mock_response
|
|
79
|
+
|
|
80
|
+
result = self.tool.execute(query="test", url="https://custom.search.org")
|
|
81
|
+
assert result.error is False
|
|
82
|
+
|
|
83
|
+
@patch("regcode.tools.web_search.urllib.request.urlopen")
|
|
84
|
+
def test_execute_no_results(self, mock_urlopen):
|
|
85
|
+
"""Test when SearXNG returns no results."""
|
|
86
|
+
mock_response = MagicMock()
|
|
87
|
+
mock_response.read.return_value = json.dumps({"results": []}).encode()
|
|
88
|
+
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
|
89
|
+
mock_response.__exit__ = MagicMock(return_value=False)
|
|
90
|
+
mock_urlopen.return_value = mock_response
|
|
91
|
+
|
|
92
|
+
result = self.tool.execute(query="xyz_nonexistent_query_12345")
|
|
93
|
+
assert result.error is False
|
|
94
|
+
assert "No results found" in result.output
|
|
95
|
+
|
|
96
|
+
@patch("regcode.tools.web_search.urllib.request.urlopen")
|
|
97
|
+
def test_execute_http_error(self, mock_urlopen):
|
|
98
|
+
"""Test handling of HTTP errors from SearXNG."""
|
|
99
|
+
import urllib.error
|
|
100
|
+
|
|
101
|
+
mock_urlopen.side_effect = urllib.error.HTTPError(
|
|
102
|
+
"https://search.regmi.dev/search",
|
|
103
|
+
500,
|
|
104
|
+
"Internal Server Error",
|
|
105
|
+
{},
|
|
106
|
+
None,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
result = self.tool.execute(query="test")
|
|
110
|
+
assert result.error is True
|
|
111
|
+
assert "500" in result.output
|
|
112
|
+
|
|
113
|
+
@patch("regcode.tools.web_search.urllib.request.urlopen")
|
|
114
|
+
def test_execute_connection_error(self, mock_urlopen):
|
|
115
|
+
"""Test handling of connection errors."""
|
|
116
|
+
import urllib.error
|
|
117
|
+
|
|
118
|
+
mock_urlopen.side_effect = urllib.error.URLError("Connection refused")
|
|
119
|
+
|
|
120
|
+
result = self.tool.execute(query="test")
|
|
121
|
+
assert result.error is True
|
|
122
|
+
assert "connection failed" in result.output.lower()
|
|
123
|
+
|
|
124
|
+
@patch("regcode.tools.web_search.urllib.request.urlopen")
|
|
125
|
+
def test_execute_invalid_json(self, mock_urlopen):
|
|
126
|
+
"""Test handling of invalid JSON response."""
|
|
127
|
+
mock_urlopen.return_value = MagicMock(
|
|
128
|
+
read=MagicMock(return_value=b"not json")
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
result = self.tool.execute(query="test")
|
|
132
|
+
assert result.error is True
|
|
133
|
+
assert "Search failed" in result.output
|
|
134
|
+
|
|
135
|
+
@patch("regcode.tools.web_search.urllib.request.urlopen")
|
|
136
|
+
def test_execute_multiple_results(self, mock_urlopen):
|
|
137
|
+
"""Test that multiple results are formatted correctly."""
|
|
138
|
+
mock_response = MagicMock()
|
|
139
|
+
results = [
|
|
140
|
+
{
|
|
141
|
+
"title": "Result " + str(i),
|
|
142
|
+
"url": "https://example" + str(i) + ".com",
|
|
143
|
+
"content": "Content " + str(i),
|
|
144
|
+
"engine": "google",
|
|
145
|
+
}
|
|
146
|
+
for i in range(5)
|
|
147
|
+
]
|
|
148
|
+
json_body = json.dumps({"results": results}).encode()
|
|
149
|
+
mock_response.read.return_value = json_body
|
|
150
|
+
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
|
151
|
+
mock_response.__exit__ = MagicMock(return_value=False)
|
|
152
|
+
mock_urlopen.return_value = mock_response
|
|
153
|
+
|
|
154
|
+
result = self.tool.execute(query="multi search")
|
|
155
|
+
assert result.error is False
|
|
156
|
+
assert "Result 0" in result.output
|
|
157
|
+
assert "Result 4" in result.output
|
|
158
|
+
assert "google" in result.output
|
|
159
|
+
|
|
160
|
+
def test_execute_results_count_clamped(self):
|
|
161
|
+
"""Test that results_count is clamped to max 20 and min 1."""
|
|
162
|
+
tool = WebSearchTool()
|
|
163
|
+
params = tool.params
|
|
164
|
+
count_param = next(p for p in params if p.name == "results_count")
|
|
165
|
+
assert count_param.default == 10
|
|
166
|
+
|
|
167
|
+
@patch("regcode.tools.web_search.urllib.request.urlopen")
|
|
168
|
+
def test_execute_language_parameter(self, mock_urlopen):
|
|
169
|
+
"""Test that language parameter is passed in the URL."""
|
|
170
|
+
mock_response = MagicMock()
|
|
171
|
+
mock_response.read.return_value = json.dumps({"results": []}).encode()
|
|
172
|
+
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
|
173
|
+
mock_response.__exit__ = MagicMock(return_value=False)
|
|
174
|
+
mock_urlopen.return_value = mock_response
|
|
175
|
+
|
|
176
|
+
result = self.tool.execute(query="test", language="de")
|
|
177
|
+
assert result.error is False
|
|
178
|
+
call_args = mock_urlopen.call_args
|
|
179
|
+
request = call_args[0][0]
|
|
180
|
+
assert "language=de" in request.full_url
|
|
181
|
+
|
|
182
|
+
@patch("regcode.tools.web_search.urllib.request.urlopen")
|
|
183
|
+
def test_execute_results_count_limit(self, mock_urlopen):
|
|
184
|
+
"""Test that results are limited by results_count."""
|
|
185
|
+
mock_response = MagicMock()
|
|
186
|
+
results = [
|
|
187
|
+
{
|
|
188
|
+
"title": "Result " + str(i),
|
|
189
|
+
"url": "https://example" + str(i) + ".com",
|
|
190
|
+
"content": "Content " + str(i),
|
|
191
|
+
}
|
|
192
|
+
for i in range(15)
|
|
193
|
+
]
|
|
194
|
+
json_body = json.dumps({"results": results}).encode()
|
|
195
|
+
mock_response.read.return_value = json_body
|
|
196
|
+
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
|
197
|
+
mock_response.__exit__ = MagicMock(return_value=False)
|
|
198
|
+
mock_urlopen.return_value = mock_response
|
|
199
|
+
|
|
200
|
+
result = self.tool.execute(query="test", results_count=5)
|
|
201
|
+
assert result.error is False
|
|
202
|
+
# Should only show 5 results (indices 0-4 after slicing)
|
|
203
|
+
for i in range(5):
|
|
204
|
+
assert "Result " + str(i) in result.output
|
|
205
|
+
# Result 5+ should not appear
|
|
206
|
+
assert "Result 5" not in result.output
|
|
@@ -967,7 +967,7 @@ name = "exceptiongroup"
|
|
|
967
967
|
version = "1.3.1"
|
|
968
968
|
source = { registry = "https://pypi.org/simple" }
|
|
969
969
|
dependencies = [
|
|
970
|
-
{ name = "typing-extensions", marker = "python_full_version < '3.
|
|
970
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
|
971
971
|
]
|
|
972
972
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
973
973
|
wheels = [
|
|
@@ -3805,7 +3805,7 @@ wheels = [
|
|
|
3805
3805
|
|
|
3806
3806
|
[[package]]
|
|
3807
3807
|
name = "regcode"
|
|
3808
|
-
version = "0.
|
|
3808
|
+
version = "0.6.0"
|
|
3809
3809
|
source = { editable = "." }
|
|
3810
3810
|
dependencies = [
|
|
3811
3811
|
{ name = "click" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|