feat: add support for SearXNG HTTP basic authentication

This commit is contained in:
Michael
2026-06-30 22:37:05 +02:00
parent 5203cf89a0
commit 031c3461df
3 changed files with 41 additions and 7 deletions
+7
View File
@@ -14,6 +14,8 @@ class AgentSettings(BaseSettings):
agent_model_id: str = "qwen/qwen3.6-35b-a3b" agent_model_id: str = "qwen/qwen3.6-35b-a3b"
agent_max_retries: int = 2 agent_max_retries: int = 2
searxng_url: str = "" searxng_url: str = ""
searxng_username: str = ""
searxng_password: str = ""
def get_settings() -> AgentSettings: def get_settings() -> AgentSettings:
@@ -30,9 +32,14 @@ GITEA_REPOS_ROOT: str = _agent_settings.gitea_repos_root
AGENT_MODEL_ID: str = _agent_settings.agent_model_id AGENT_MODEL_ID: str = _agent_settings.agent_model_id
AGENT_MAX_RETRIES: int = _agent_settings.agent_max_retries AGENT_MAX_RETRIES: int = _agent_settings.agent_max_retries
SEARXNG_URL: str = _agent_settings.searxng_url SEARXNG_URL: str = _agent_settings.searxng_url
SEARXNG_USERNAME: str = _agent_settings.searxng_username
SEARXNG_PASSWORD: str = _agent_settings.searxng_password
import os import os
os.environ["GITEA_SERVER_URL"] = GITEA_URL os.environ["GITEA_SERVER_URL"] = GITEA_URL
os.environ["GITEA_SERVER_TOKEN"] = GITEA_TOKEN os.environ["GITEA_SERVER_TOKEN"] = GITEA_TOKEN
os.environ["SEARXNG_URL"] = SEARXNG_URL os.environ["SEARXNG_URL"] = SEARXNG_URL
os.environ["SEARXNG_USERNAME"] = SEARXNG_USERNAME
os.environ["SEARXNG_PASSWORD"] = SEARXNG_PASSWORD
+9 -2
View File
@@ -26,7 +26,7 @@ from duckduckgo_search.exceptions import ( # type: ignore[import-untyped]
RatelimitException, RatelimitException,
) )
from gitea.config import SEARXNG_URL from gitea.config import SEARXNG_URL, SEARXNG_USERNAME, SEARXNG_PASSWORD
logger: logging.Logger = logging.getLogger("research-tools") logger: logging.Logger = logging.getLogger("research-tools")
@@ -38,6 +38,9 @@ _USER_AGENT: str = (
) )
# SearXNG instance # SearXNG instance
_SEARXNG_URL: str = SEARXNG_URL _SEARXNG_URL: str = SEARXNG_URL
_SEARXNG_USERNAME: str = SEARXNG_USERNAME
_SEARXNG_PASSWORD: str = SEARXNG_PASSWORD
@@ -196,9 +199,13 @@ class ResearchTools:
if time_range: if time_range:
params["time_range"] = time_range params["time_range"] = time_range
auth = None
if _SEARXNG_USERNAME and _SEARXNG_PASSWORD:
auth = (_SEARXNG_USERNAME, _SEARXNG_PASSWORD)
try: try:
with httpx.Client( with httpx.Client(
timeout=_DEFAULT_TIMEOUT, follow_redirects=True timeout=_DEFAULT_TIMEOUT, follow_redirects=True, auth=auth
) as client: ) as client:
response = client.get( response = client.get(
f"{_SEARXNG_URL}/search", f"{_SEARXNG_URL}/search",
+25 -5
View File
@@ -191,6 +191,21 @@ class TestSearchSearxng:
params = call_kwargs[1].get("params", call_kwargs[0][1] if len(call_kwargs[0]) > 1 else {}) params = call_kwargs[1].get("params", call_kwargs[0][1] if len(call_kwargs[0]) > 1 else {})
assert params.get("time_range") == "month" assert params.get("time_range") == "month"
@patch("gitea.tools.research_tools.httpx.Client")
def test_passes_basic_auth_if_configured(
self, mock_cls: MagicMock, tools: ResearchTools
) -> None:
mock_cls.return_value.__enter__.return_value.get.return_value = (
self._make_searxng_response([])
)
with patch("gitea.tools.research_tools._SEARXNG_USERNAME", "user"), \
patch("gitea.tools.research_tools._SEARXNG_PASSWORD", "pass"):
tools._search_searxng("q", num_results=5)
mock_cls.assert_called_once()
kwargs = mock_cls.call_args[1]
assert kwargs.get("auth") == ("user", "pass")
@patch("gitea.tools.research_tools.httpx.Client") @patch("gitea.tools.research_tools.httpx.Client")
def test_web_search_uses_searxng_first( def test_web_search_uses_searxng_first(
self, mock_cls: MagicMock, tools: ResearchTools self, mock_cls: MagicMock, tools: ResearchTools
@@ -228,7 +243,8 @@ class TestWebSearch:
self, _mock_sleep: MagicMock, tools: ResearchTools self, _mock_sleep: MagicMock, tools: ResearchTools
) -> None: ) -> None:
mock_cls = MagicMock(return_value=self._mock_ddgs(self._make_ddgs_result())) mock_cls = MagicMock(return_value=self._mock_ddgs(self._make_ddgs_result()))
with patch("gitea.tools.research_tools.DDGS", mock_cls): with patch("gitea.tools.research_tools.ResearchTools._search_searxng", return_value=None), \
patch("gitea.tools.research_tools.DDGS", mock_cls):
result = tools.web_search("python httpx") result = tools.web_search("python httpx")
assert "[1]" in result assert "[1]" in result
assert "foo.com" in result assert "foo.com" in result
@@ -238,14 +254,16 @@ class TestWebSearch:
self, _mock_sleep: MagicMock, tools: ResearchTools self, _mock_sleep: MagicMock, tools: ResearchTools
) -> None: ) -> None:
mock_cls = MagicMock(return_value=self._mock_ddgs([])) mock_cls = MagicMock(return_value=self._mock_ddgs([]))
with patch("gitea.tools.research_tools.DDGS", mock_cls): with patch("gitea.tools.research_tools.ResearchTools._search_searxng", return_value=None), \
patch("gitea.tools.research_tools.DDGS", mock_cls):
result = tools.web_search("xyzzy-not-real") result = tools.web_search("xyzzy-not-real")
assert "No results" in result assert "No results" in result
def test_clamps_num_results_max(self, tools: ResearchTools) -> None: def test_clamps_num_results_max(self, tools: ResearchTools) -> None:
ddgs_mock = self._mock_ddgs(self._make_ddgs_result()) ddgs_mock = self._mock_ddgs(self._make_ddgs_result())
mock_cls = MagicMock(return_value=ddgs_mock) mock_cls = MagicMock(return_value=ddgs_mock)
with patch("gitea.tools.research_tools.DDGS", mock_cls): with patch("gitea.tools.research_tools.ResearchTools._search_searxng", return_value=None), \
patch("gitea.tools.research_tools.DDGS", mock_cls):
tools.web_search("q", num_results=999) tools.web_search("q", num_results=999)
# DDGS.text should be called with max_results clamped to 20 # DDGS.text should be called with max_results clamped to 20
ddgs_mock.text.assert_called_once() ddgs_mock.text.assert_called_once()
@@ -255,7 +273,8 @@ class TestWebSearch:
def test_clamps_num_results_min(self, tools: ResearchTools) -> None: def test_clamps_num_results_min(self, tools: ResearchTools) -> None:
ddgs_mock = self._mock_ddgs(self._make_ddgs_result()) ddgs_mock = self._mock_ddgs(self._make_ddgs_result())
mock_cls = MagicMock(return_value=ddgs_mock) mock_cls = MagicMock(return_value=ddgs_mock)
with patch("gitea.tools.research_tools.DDGS", mock_cls): with patch("gitea.tools.research_tools.ResearchTools._search_searxng", return_value=None), \
patch("gitea.tools.research_tools.DDGS", mock_cls):
tools.web_search("q", num_results=0) tools.web_search("q", num_results=0)
_, kwargs = ddgs_mock.text.call_args _, kwargs = ddgs_mock.text.call_args
assert kwargs.get("max_results", 0) >= 1 assert kwargs.get("max_results", 0) >= 1
@@ -271,7 +290,8 @@ class TestWebSearch:
ddgs_mock.__exit__ = MagicMock(return_value=False) ddgs_mock.__exit__ = MagicMock(return_value=False)
ddgs_mock.text.side_effect = rate_exc ddgs_mock.text.side_effect = rate_exc
with patch("gitea.tools.research_tools.DDGS", return_value=ddgs_mock), \ with patch("gitea.tools.research_tools.ResearchTools._search_searxng", return_value=None), \
patch("gitea.tools.research_tools.DDGS", return_value=ddgs_mock), \
patch("gitea.tools.research_tools.RatelimitException", type(rate_exc)), \ patch("gitea.tools.research_tools.RatelimitException", type(rate_exc)), \
patch("gitea.tools.research_tools.DuckDuckGoSearchException", ValueError): patch("gitea.tools.research_tools.DuckDuckGoSearchException", ValueError):
result = tools.web_search("q") result = tools.web_search("q")