Spaces:
Running
Running
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 +0 -0
- .gitignore +0 -0
- Dockerfile +5 -1
- custom-agents/philosophy/philosophy-sunyata-wisdom-advisor.md +0 -0
- healthcheck.sh +0 -0
- patches/hermes-agent/agent/prompt_builder.py +0 -0
- patches/hermes-agent/tools/send_message_tool.py +0 -0
- plugins/pollinations/__init__.py +0 -0
- plugins/pollinations/plugin.yaml +0 -0
- scripts/dream_mode.py +0 -0
- scripts/knowledge_graph.py +0 -0
- scripts/patch_file_delivery.py +0 -0
- scripts/patch_web_search_fallback.py +102 -0
- scripts/patch_weixin_cross_loop.py +0 -0
- scripts/selfheal.py +0 -0
- start.sh +6 -0
.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
|