Z User commited on
Commit
4036608
·
1 Parent(s): 47dfb43

feat: add DuckDuckGo free fallback for web_search (no API key needed)

Browse files
.env.example CHANGED
File without changes
.gitignore CHANGED
File without changes
Dockerfile CHANGED
@@ -20,7 +20,7 @@ RUN git clone --depth 1 --branch v2026.4.30 https://github.com/NousResearch/herm
20
  RUN python3 -m venv /app/venv
21
  ENV PATH="/app/venv/bin:$PATH"
22
  RUN pip install --quiet --upgrade pip && \
23
- pip install --quiet psutil networkx && \
24
  pip install --quiet -e "/app/hermes-agent[feishu,mcp,cron,pty]" && \
25
  pip install --quiet aiohttp cryptography 2>&1 | tail -10
26
 
@@ -46,6 +46,10 @@ RUN python3 /tmp/patch_resolve_media_paths.py; rm -f /tmp/patch_resolve_media_pa
46
  COPY scripts/patch_weixin_cross_loop.py /tmp/patch_weixin_cross_loop.py
47
  RUN python3 /tmp/patch_weixin_cross_loop.py; rm -f /tmp/patch_weixin_cross_loop.py
48
 
 
 
 
 
49
  # Install Node.js 23
50
  RUN ARCH=$(dpkg --print-architecture) \
51
  && if [ "$ARCH" = "amd64" ]; then NODE_ARCH="x64"; else NODE_ARCH="$ARCH"; fi \
 
20
  RUN python3 -m venv /app/venv
21
  ENV PATH="/app/venv/bin:$PATH"
22
  RUN pip install --quiet --upgrade pip && \
23
+ pip install --quiet psutil networkx duckduckgo-search && \
24
  pip install --quiet -e "/app/hermes-agent[feishu,mcp,cron,pty]" && \
25
  pip install --quiet aiohttp cryptography 2>&1 | tail -10
26
 
 
46
  COPY scripts/patch_weixin_cross_loop.py /tmp/patch_weixin_cross_loop.py
47
  RUN python3 /tmp/patch_weixin_cross_loop.py; rm -f /tmp/patch_weixin_cross_loop.py
48
 
49
+ # Patch: DuckDuckGo free fallback for web_search (no API key needed)
50
+ COPY scripts/patch_web_search_fallback.py /tmp/patch_web_search_fallback.py
51
+ RUN python3 /tmp/patch_web_search_fallback.py; rm -f /tmp/patch_web_search_fallback.py
52
+
53
  # Install Node.js 23
54
  RUN ARCH=$(dpkg --print-architecture) \
55
  && if [ "$ARCH" = "amd64" ]; then NODE_ARCH="x64"; else NODE_ARCH="$ARCH"; fi \
custom-agents/philosophy/philosophy-sunyata-wisdom-advisor.md CHANGED
File without changes
healthcheck.sh CHANGED
File without changes
patches/hermes-agent/agent/prompt_builder.py CHANGED
File without changes
patches/hermes-agent/tools/send_message_tool.py CHANGED
File without changes
plugins/pollinations/__init__.py CHANGED
File without changes
plugins/pollinations/plugin.yaml CHANGED
File without changes
scripts/dream_mode.py CHANGED
File without changes
scripts/knowledge_graph.py CHANGED
File without changes
scripts/patch_file_delivery.py CHANGED
File without changes
scripts/patch_web_search_fallback.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ patch_web_search_fallback.py — DuckDuckGo 免费 fallback
4
+
5
+ 当所有付费搜索后端(Firecrawl / Tavily / Exa / Parallel)都不可用时,
6
+ 自动 fallback 到 DuckDuckGo 搜索,无需任何 API key。
7
+
8
+ Monkey-patch 目标(tools/web_tools.py):
9
+ - _get_backend() : 无可用后端时返回 "duckduckgo"
10
+ - web_search_tool() : 分发 duckduckgo 后端
11
+ - _is_backend_available(): duckduckgo 始终返回 True
12
+ """
13
+
14
+ import json
15
+ import logging
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def _ddg_search(query: str, limit: int = 5) -> dict:
21
+ """通过 DuckDuckGo 搜索,返回标准格式结果。"""
22
+ try:
23
+ from duckduckgo_search import DDGS
24
+
25
+ results = []
26
+ with DDGS() as ddgs:
27
+ for i, r in enumerate(ddgs.text(query, max_results=limit)):
28
+ results.append({
29
+ "title": r.get("title", ""),
30
+ "url": r.get("href", ""),
31
+ "description": r.get("body", ""),
32
+ "position": i + 1,
33
+ })
34
+
35
+ return {"success": True, "data": {"web": results}}
36
+ except Exception as e:
37
+ logger.error("DuckDuckGo search failed: %s", e)
38
+ return {"success": False, "error": f"DuckDuckGo search failed: {e}"}
39
+
40
+
41
+ def patch():
42
+ """Apply the DuckDuckGo fallback patch to web_tools."""
43
+ import tools.web_tools as wt
44
+
45
+ # ── 1. 让 _get_backend() 在无可用后端时返回 "duckduckgo" ──
46
+ _original_get_backend = wt._get_backend
47
+
48
+ def _get_backend_patched():
49
+ configured = (wt._load_web_config().get("backend") or "").lower().strip()
50
+ # 用户显式指定了某个后端 → 尊重配置(即使 key 可能无效)
51
+ if configured in ("parallel", "firecrawl", "tavily", "exa"):
52
+ return configured
53
+
54
+ # 按优先级检查哪个后端可用
55
+ for backend in ("firecrawl", "parallel", "tavily", "exa"):
56
+ if wt._is_backend_available(backend):
57
+ return backend
58
+
59
+ # 全都不可用 → DuckDuckGo 兜底
60
+ return "duckduckgo"
61
+
62
+ wt._get_backend = _get_backend_patched
63
+
64
+ # ── 2. 在 web_search_tool() 中添加 duckduckgo 分发 ──
65
+ _original_web_search = wt.web_search_tool
66
+
67
+ def web_search_patched(query: str, limit: int = 5) -> str:
68
+ backend = wt._get_backend()
69
+
70
+ if backend == "duckduckgo":
71
+ logger.info("DuckDuckGo search (free fallback): '%s' (limit: %d)", query, limit)
72
+ response_data = _ddg_search(query, limit)
73
+ return json.dumps(response_data, indent=2, ensure_ascii=False)
74
+
75
+ # 其他后端走原逻辑
76
+ return _original_web_search(query, limit)
77
+
78
+ wt.web_search_tool = web_search_patched
79
+
80
+ # ── 3. 让 _is_backend_available("duckduckgo") 始终返回 True ──
81
+ _original_is_backend_available = wt._is_backend_available
82
+
83
+ def _is_backend_available_patched(backend: str) -> bool:
84
+ if backend == "duckduckgo":
85
+ return True
86
+ return _original_is_backend_available(backend)
87
+
88
+ wt._is_backend_available = _is_backend_available_patched
89
+
90
+ # ── 4. 让 web_extract 也能在 duckduckgo 后端下工作 ──
91
+ # web_extract 不依赖搜索后端(它直接抓 URL),但如果 _get_backend
92
+ # 返回 "duckduckgo" 且 web_extract 内部也调用 _get_backend,需要兼容。
93
+ # 目前 web_extract 不调用 _get_backend,所以不需要 patch。
94
+
95
+ logger.info(
96
+ "Web search: DuckDuckGo free fallback enabled "
97
+ "(no API key required, auto-activates when paid backends unavailable)"
98
+ )
99
+
100
+
101
+ if __name__ == "__main__":
102
+ patch()
scripts/patch_weixin_cross_loop.py CHANGED
File without changes
scripts/selfheal.py CHANGED
File without changes
start.sh CHANGED
@@ -545,6 +545,12 @@ update_hermes_agent_background() {
545
  if [ -f "/app/scripts/patch_resolve_media_paths.py" ]; then
546
  python3 /app/scripts/patch_resolve_media_paths.py 2>/dev/null
547
  fi
 
 
 
 
 
 
548
  # Copy patch files if they exist
549
  for patch_file in prompt_builder.py send_message_tool.py; do
550
  if [ -f "/app/patches/hermes-agent/agent/$patch_file" ] && [ -f "$AGENT_DIR/agent/$patch_file" ]; then
 
545
  if [ -f "/app/scripts/patch_resolve_media_paths.py" ]; then
546
  python3 /app/scripts/patch_resolve_media_paths.py 2>/dev/null
547
  fi
548
+ if [ -f "/app/scripts/patch_weixin_cross_loop.py" ]; then
549
+ python3 /app/scripts/patch_weixin_cross_loop.py 2>/dev/null
550
+ fi
551
+ if [ -f "/app/scripts/patch_web_search_fallback.py" ]; then
552
+ python3 /app/scripts/patch_web_search_fallback.py 2>/dev/null
553
+ fi
554
  # Copy patch files if they exist
555
  for patch_file in prompt_builder.py send_message_tool.py; do
556
  if [ -f "/app/patches/hermes-agent/agent/$patch_file" ] && [ -f "$AGENT_DIR/agent/$patch_file" ]; then