feat(websearch): add DeepSeek search and page fetching (#10599) · AstrBotDevs/AstrBot@ff22dac · GitHub
Skip to content

Commit ff22dac

Browse files
authored
feat(websearch): add DeepSeek search and page fetching (#10599)
* feat(websearch): add DeepSeek search and page fetching Add DeepSeek Search and Fetch builtin tools with validated inputs, API key rotation, and WebChat source citations. Expose the provider in WebUI configuration and update localized metadata, documentation, and tests. Refs #10598 * fix(websearch): validate ports in DeepSeek fetch URLs * docs(websearch): add DeepSeek query examples and clarify fetching
1 parent 8c99c2c commit ff22dac

17 files changed

Lines changed: 784 additions & 27 deletions

File tree

‎astrbot/core/astr_main_agent.py‎

Lines changed: 6 additions & 0 deletions

‎astrbot/core/config/default.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def get_local_permission_defaults(system: str | None = None) -> dict:
148148
"websearch_firecrawl_key": [],
149149
"websearch_exa_key": [],
150150
"websearch_anysearch_key": [],
151+
"websearch_deepseek_key": [],
151152
"web_search_link": False,
152153
"display_reasoning_text": False,
153154
"identifier": False,
@@ -3678,6 +3679,7 @@ def get_local_permission_defaults(system: str | None = None) -> dict:
36783679
"firecrawl",
36793680
"exa",
36803681
"anysearch",
3682+
"deepseek",
36813683
],
36823684
"condition": {
36833685
"provider_settings.web_search": True,
@@ -3758,6 +3760,17 @@ def get_local_permission_defaults(system: str | None = None) -> dict:
37583760
"provider_settings.web_search": True,
37593761
},
37603762
},
3763+
"provider_settings.websearch_deepseek_key": {
3764+
"description": "DeepSeek API Key",
3765+
"type": "list",
3766+
"items": {"type": "string"},
3767+
"hint": "Configure a DeepSeek API key for search and page extraction. Multiple keys rotate; keys from the same account share rate limits.",
3768+
"secret": True,
3769+
"condition": {
3770+
"provider_settings.websearch_provider": "deepseek",
3771+
"provider_settings.web_search": True,
3772+
},
3773+
},
37613774
"provider_settings.web_search_link": {
37623775
"description": "显示来源引用",
37633776
"type": "bool",

‎astrbot/core/tools/web_search_tools.py‎

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import uuid
44
from dataclasses import dataclass as std_dataclass
55
from dataclasses import field
6+
from urllib.parse import urlsplit
67

78
import aiohttp
89
from pydantic import Field
@@ -24,6 +25,8 @@
2425
"web_search_exa",
2526
"exa_get_contents",
2627
"web_search_anysearch",
28+
"web_search_deepseek",
29+
"deepseek_fetch_web_page",
2730
]
2831
_TAVILY_WEB_SEARCH_TOOL_CONFIG = {
2932
"provider_settings.web_search": True,
@@ -53,6 +56,10 @@
5356
"provider_settings.web_search": True,
5457
"provider_settings.websearch_provider": "anysearch",
5558
}
59+
_DEEPSEEK_WEB_SEARCH_TOOL_CONFIG = {
60+
"provider_settings.web_search": True,
61+
"provider_settings.websearch_provider": "deepseek",
62+
}
5663

5764

5865
@std_dataclass
@@ -117,6 +124,7 @@ async def get(self, provider_settings: dict) -> str:
117124
_FIRECRAWL_KEY_ROTATOR = _KeyRotator("websearch_firecrawl_key", "Firecrawl")
118125
_EXA_KEY_ROTATOR = _KeyRotator("websearch_exa_key", "Exa")
119126
_ANYSEARCH_KEY_ROTATOR = _KeyRotator("websearch_anysearch_key", "AnySearch")
127+
_DEEPSEEK_KEY_ROTATOR = _KeyRotator("websearch_deepseek_key", "DeepSeek")
120128

121129

122130
def normalize_legacy_web_search_config(cfg) -> None:
@@ -1442,11 +1450,225 @@ async def call(self, context, **kwargs) -> ToolExecResult:
14421450
return _search_result_payload(results)
14431451

14441452

1453+
@builtin_tool(config=_DEEPSEEK_WEB_SEARCH_TOOL_CONFIG)
1454+
@pydantic_dataclass
1455+
class DeepSeekWebSearchTool(FunctionTool[AstrAgentContext]):
1456+
name: str = "web_search_deepseek"
1457+
description: str = (
1458+
"Search the web using DeepSeek and return summaries with source references. "
1459+
"Use deepseek_fetch_web_page for full page content when needed."
1460+
)
1461+
parameters: dict = Field(
1462+
default_factory=lambda: {
1463+
"type": "object",
1464+
"properties": {
1465+
"query": {
1466+
"type": "string",
1467+
"minLength": 1,
1468+
"maxLength": 128,
1469+
"description": (
1470+
"Search query, 1-128 Unicode characters including filters, "
1471+
"not all whitespace. Filters can be combined. Examples:\n"
1472+
"Find DeepSeek API references on GitHub or Hugging Face -> "
1473+
'DeepSeek "API reference" site:github.com OR site:huggingface.co\n'
1474+
"Find Chinese or English DeepSeek news published after 2026-01-01 "
1475+
"and before 2027-01-01 -> "
1476+
"DeepSeek news lang:zh,en after:2026-01-01 before:2027-01-01\n"
1477+
"Find DeepSeek information excluding ads, example.com and the phrase "
1478+
'"release notes" -> DeepSeek -Ads -site:example.com -"release notes"\n'
1479+
"Date filters accept YYYY or YYYY-MM-DD."
1480+
),
1481+
},
1482+
"count": {
1483+
"type": "integer",
1484+
"minimum": 1,
1485+
"maximum": 50,
1486+
"default": 10,
1487+
"description": "Optional. Number of results, 1-50. Default is 10.",
1488+
},
1489+
"ori_question": {
1490+
"type": "string",
1491+
"minLength": 1,
1492+
"maxLength": 128,
1493+
"description": "Optional. Original user question, 1-128 Unicode characters.",
1494+
},
1495+
"need_content": {
1496+
"type": "boolean",
1497+
"default": False,
1498+
"description": "Optional. Include full page content in results. Default is false.",
1499+
},
1500+
},
1501+
"required": ["query"],
1502+
}
1503+
)
1504+
1505+
async def call(self, context, **kwargs) -> ToolExecResult:
1506+
"""Search DeepSeek and normalize summaries and optional page content.
1507+
1508+
Args:
1509+
context: Current agent execution context.
1510+
**kwargs: Search query and optional count, ori_question, need_content.
1511+
1512+
Returns:
1513+
Search results with citation indices, or an input/configuration error.
1514+
1515+
Raises:
1516+
RuntimeError: If the DeepSeek API returns an HTTP error.
1517+
"""
1518+
_, provider_settings, _ = _get_runtime(context)
1519+
if not provider_settings.get("websearch_deepseek_key", []):
1520+
return "Error: DeepSeek API key is not configured in AstrBot."
1521+
1522+
query = kwargs.get("query")
1523+
if (
1524+
not isinstance(query, str)
1525+
or not query.strip()
1526+
or not 1 <= len(query) <= 128
1527+
):
1528+
return (
1529+
"Error: query must be a non-blank string of 1-128 Unicode characters."
1530+
)
1531+
count = kwargs.get("count", 10)
1532+
if type(count) is not int or not 1 <= count <= 50:
1533+
return "Error: count must be an integer between 1 and 50."
1534+
ori_question = kwargs.get("ori_question")
1535+
if ori_question is not None and (
1536+
not isinstance(ori_question, str) or not 1 <= len(ori_question) <= 128
1537+
):
1538+
return "Error: ori_question must be a string of 1-128 Unicode characters."
1539+
need_content = kwargs.get("need_content", False)
1540+
if not isinstance(need_content, bool):
1541+
return "Error: need_content must be a boolean."
1542+
1543+
payload = {"query": query, "count": count, "need_content": need_content}
1544+
if ori_question is not None:
1545+
payload["ori_question"] = ori_question
1546+
key = await _DEEPSEEK_KEY_ROTATOR.get(provider_settings)
1547+
headers = {
1548+
"Authorization": f"Bearer {key}",
1549+
"Content-Type": "application/json",
1550+
}
1551+
# Account-level limits and safety refusals cannot be resolved by rotating keys.
1552+
async with aiohttp.ClientSession(trust_env=True) as session:
1553+
async with session.post(
1554+
"https://api.deepseek.com/v1/tools/search",
1555+
json=payload,
1556+
headers=headers,
1557+
) as response:
1558+
if response.status != 200:
1559+
reason = await response.text()
1560+
raise RuntimeError(
1561+
f"DeepSeek web search failed: {reason}, status: {response.status}"
1562+
)
1563+
data = await response.json()
1564+
1565+
results = []
1566+
for item in data.get("results", []):
1567+
snippet = item.get("summary") or item.get("snippet") or ""
1568+
if need_content and item.get("content"):
1569+
snippet = "\n\n".join(filter(None, [snippet, item["content"]]))
1570+
results.append(
1571+
SearchResult(
1572+
title=item.get("title", ""),
1573+
url=item.get("url", ""),
1574+
snippet=snippet,
1575+
favicon=item.get("site_icon"),
1576+
)
1577+
)
1578+
if not results:
1579+
return "Error: DeepSeek web search does not return any results."
1580+
return _search_result_payload(results)
1581+
1582+
1583+
@builtin_tool(config=_DEEPSEEK_WEB_SEARCH_TOOL_CONFIG)
1584+
@pydantic_dataclass
1585+
class DeepSeekFetchWebPageTool(FunctionTool[AstrAgentContext]):
1586+
name: str = "deepseek_fetch_web_page"
1587+
description: str = (
1588+
"Fetch a single web page using DeepSeek and return its title and Markdown content. "
1589+
"Use this to read a known URL, including a page found through web search."
1590+
)
1591+
parameters: dict = Field(
1592+
default_factory=lambda: {
1593+
"type": "object",
1594+
"properties": {
1595+
"url": {
1596+
"type": "string",
1597+
"minLength": 1,
1598+
"maxLength": 8192,
1599+
"description": "Required. HTTP or HTTPS URL with a host, no embedded credentials, and at most 8192 characters.",
1600+
},
1601+
},
1602+
"required": ["url"],
1603+
}
1604+
)
1605+
1606+
async def call(self, context, **kwargs) -> ToolExecResult:
1607+
"""Fetch Markdown content from the DeepSeek Fetch API.
1608+
1609+
Args:
1610+
context: Current agent execution context.
1611+
**kwargs: The HTTP or HTTPS page URL to fetch.
1612+
1613+
Returns:
1614+
The page URL, title and Markdown content, or an input/configuration error.
1615+
1616+
Raises:
1617+
RuntimeError: If the DeepSeek API returns an HTTP error.
1618+
"""
1619+
_, provider_settings, _ = _get_runtime(context)
1620+
if not provider_settings.get("websearch_deepseek_key", []):
1621+
return "Error: DeepSeek API key is not configured in AstrBot."
1622+
1623+
url = kwargs.get("url")
1624+
if not isinstance(url, str) or not 1 <= len(url) <= 8192:
1625+
return "Error: url must be a string of 1-8192 characters."
1626+
try:
1627+
parsed = urlsplit(url)
1628+
# Port syntax and range are validated only when this property is read.
1629+
_ = parsed.port
1630+
valid_url = (
1631+
parsed.scheme in {"http", "https"}
1632+
and bool(parsed.hostname)
1633+
and parsed.username is None
1634+
and parsed.password is None
1635+
and not any(char.isspace() for char in url)
1636+
)
1637+
except ValueError:
1638+
valid_url = False
1639+
if not valid_url:
1640+
return "Error: url must be an HTTP or HTTPS URL with a host and no embedded credentials."
1641+
1642+
key = await _DEEPSEEK_KEY_ROTATOR.get(provider_settings)
1643+
headers = {
1644+
"Authorization": f"Bearer {key}",
1645+
"Content-Type": "application/json",
1646+
}
1647+
async with aiohttp.ClientSession(trust_env=True) as session:
1648+
async with session.post(
1649+
"https://api.deepseek.com/v1/tools/fetch",
1650+
json={"url": url},
1651+
headers=headers,
1652+
) as response:
1653+
if response.status != 200:
1654+
reason = await response.text()
1655+
raise RuntimeError(
1656+
f"DeepSeek fetch failed: {reason}, status: {response.status}"
1657+
)
1658+
data = await response.json()
1659+
1660+
if not data.get("content"):
1661+
return "Error: DeepSeek fetch does not return any content."
1662+
return f"URL: {url}\nTitle: {data.get('title', '')}\nContent: {data['content']}"
1663+
1664+
14451665
__all__ = [
14461666
"AnySearchWebSearchTool",
14471667
"BaiduWebSearchTool",
14481668
"BochaWebSearchTool",
14491669
"BraveWebSearchTool",
1670+
"DeepSeekFetchWebPageTool",
1671+
"DeepSeekWebSearchTool",
14501672
"ExaGetContentsTool",
14511673
"ExaWebSearchTool",
14521674
"TavilyExtractWebPageTool",

‎astrbot/dashboard/services/chat_service.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ def extract_web_search_refs(accumulated_text: str, accumulated_parts: list) -> d
276276
"web_search_tavily",
277277
"web_search_bocha",
278278
"web_search_brave",
279+
"web_search_deepseek",
279280
]
280281
web_search_results = {}
281282
tool_call_parts = [

‎astrbot/dashboard/services/live_chat_service.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ def extract_web_search_refs(accumulated_text: str, accumulated_parts: list) -> d
287287
"web_search_tavily",
288288
"web_search_bocha",
289289
"web_search_brave",
290+
"web_search_deepseek",
290291
]
291292
web_search_results = {}
292293
tool_call_parts = [

‎dashboard/src/i18n/locales/en-US/features/config-metadata.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@
204204
"websearch_anysearch_key": {
205205
"description": "AnySearch API Key",
206206
"hint": "Multiple keys can be added for rotation. Leave empty to use anonymous mode with a daily free quota."
207+
},
208+
"websearch_deepseek_key": {
209+
"description": "DeepSeek API Key",
210+
"hint": "Enter your DeepSeek API key. Can use the same key as the chat model (must be entered separately). Supports multiple keys for rotation; keys under the same account share QPS limits."
207211
}
208212
}
209213
},

‎dashboard/src/i18n/locales/ja-JP/features/config-metadata.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@
292292
"websearch_exa_key": {
293293
"description": "Exa API Key",
294294
"hint": "複数の Key を追加してローテーションできます。Key の取得: https://dashboard.exa.ai"
295+
},
296+
"websearch_deepseek_key": {
297+
"description": "DeepSeek API Key",
298+
"hint": "DeepSeek API Key を入力します。チャットモデルと同じ Key も使用可能(個別に入力が必要)。複数 Key のローテーションに対応していますが、同一アカウントでは QPS 制限を共有します。"
295299
}
296300
}
297301
},

‎dashboard/src/i18n/locales/ru-RU/features/config-metadata.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@
292292
"websearch_anysearch_key": {
293293
"description": "AnySearch API-ключ",
294294
"hint": "Можно добавить несколько ключей для ротации. Оставьте пустым для анонимного режима с ежедневной бесплатной квотой."
295+
},
296+
"websearch_deepseek_key": {
297+
"description": "API-ключ DeepSeek",
298+
"hint": "Введите API-ключ DeepSeek. Можно использовать тот же ключ, что и для чат-модели (не подставляется автоматически). Поддерживается ротация нескольких ключей, но лимит QPS общий на аккаунт."
295299
}
296300
}
297301
},

‎dashboard/src/i18n/locales/zh-CN/features/config-metadata.json‎

Lines changed: 4 additions & 0 deletions

0 commit comments

Comments
 (0)