# retoor <retoor@molodetz.nl>
from __future__ import annotations
import json
import logging
from typing import Any
import httpx
from ..config import Settings
from ..errors import NetworkError, ToolInputError, UpstreamError
logger = logging.getLogger("devii.rsearch")
USER_AGENT = "devii/0.1"
def _flag(value: Any) -> str:
if isinstance(value, bool):
return "true" if value else "false"
return "true" if str(value).strip().lower() in ("1", "true", "yes", "on") else "false"
class RsearchController:
def __init__(self, settings: Settings) -> None:
self._settings = settings
async def dispatch(self, name: str, arguments: dict[str, Any]) -> str:
if not self._settings.rsearch_enabled:
raise ToolInputError(
"Web search tools are disabled by the administrator; use platform tools instead."
)
if name == "rsearch":
return await self._search(arguments)
if name == "rsearch_answer":
return await self._answer(arguments)
if name == "rsearch_chat":
return await self._chat(arguments)
if name == "rsearch_describe_image":
return await self._describe(arguments)
raise ToolInputError(f"Unknown rsearch tool: {name}")
async def _request(self, path: str, params: dict[str, Any]) -> dict[str, Any]:
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
timeout = httpx.Timeout(self._settings.rsearch_timeout_seconds, connect=30.0)
try:
async with httpx.AsyncClient(
base_url=self._settings.rsearch_url,
headers=headers,
follow_redirects=True,
timeout=timeout,
) as client:
response = await client.get(path, params=params)
except httpx.TimeoutException as exc:
raise NetworkError(f"rsearch timed out calling {path}", path=path) from exc
except httpx.HTTPError as exc:
raise NetworkError(f"rsearch request failed: {exc}", path=path) from exc
if response.status_code >= 400:
raise UpstreamError(
f"rsearch returned {response.status_code} for {path}",
status=response.status_code,
path=path,
)
try:
return response.json()
except ValueError as exc:
raise UpstreamError("rsearch returned a non-JSON response", path=path) from exc
def _require_text(self, arguments: dict[str, Any], key: str) -> str:
value = str(arguments.get(key, "")).strip()
if not value:
raise ToolInputError(f"rsearch requires a non-empty '{key}'.")
return value
def _count(self, arguments: dict[str, Any]) -> int:
try:
count = int(arguments.get("count", 10))
except (TypeError, ValueError):
count = 10
return max(1, min(count, 100))
async def _search(self, arguments: dict[str, Any]) -> str:
query = self._require_text(arguments, "query")
result_type = str(arguments.get("type", "web")).strip().lower()
if result_type not in ("web", "images"):
result_type = "web"
params: dict[str, Any] = {
"query": query,
"count": self._count(arguments),
"content": _flag(arguments.get("content")),
"deep": _flag(arguments.get("deep")),
}
if result_type == "images":
params["type"] = "images"
data = await self._request("/search", params)
results = data.get("results") or []
compact = [
{
"title": item.get("title"),
"url": item.get("url"),
"description": item.get("description"),
"source": item.get("source"),
"content": item.get("content"),
}
for item in results
]
return json.dumps({
"status": "success",
"query": data.get("query", query),
"type": result_type,
"source": data.get("source"),
"count": len(compact),
"results": compact,
}, ensure_ascii=False)
async def _answer(self, arguments: dict[str, Any]) -> str:
query = self._require_text(arguments, "query")
params = {
"query": query,
"ai": "true",
"content": _flag(arguments.get("content")),
"count": self._count(arguments),
}
data = await self._request("/search", params)
results = data.get("results") or []
return json.dumps({
"status": "success",
"query": query,
"answer": data.get("ai_response"),
"answer_error": data.get("ai_error"),
"sources": [
{"title": item.get("title"), "url": item.get("url")}
for item in results
],
}, ensure_ascii=False)
async def _chat(self, arguments: dict[str, Any]) -> str:
prompt = self._require_text(arguments, "prompt")
params: dict[str, Any] = {"prompt": prompt, "json": _flag(arguments.get("json"))}
system = str(arguments.get("system", "")).strip()
if system:
params["system"] = system
data = await self._request("/chat", params)
return json.dumps({
"status": "success",
"response": data.get("response"),
"json_mode": data.get("json_mode", False),
}, ensure_ascii=False)
async def _describe(self, arguments: dict[str, Any]) -> str:
url = self._require_text(arguments, "url")
data = await self._request("/describe", {"url": url})
return json.dumps({
"status": "success",
"url": url,
"mime_type": data.get("mime_type"),
"description": data.get("description"),
}, ensure_ascii=False)