Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ graph TB

subgraph IntelligentAgents["智能体 Agents (skills/agents/, 7个)"]
A_Info["info_agent<br/>信息查询助手<br/>(18个工具)<br/>• weather_query<br/>• *hot 热搜<br/>• bilibili_*<br/>• arxiv_search<br/>• whois"]
A_Web["web_agent<br/>网络搜索助手<br/>(3个工具 + MCP)<br/>• web_search<br/>• crawl_webpage<br/>• Playwright MCP"]
A_Web["web_agent<br/>网络搜索助手<br/>(4个工具 + MCP)<br/>• grok_search<br/>• firecrawl_search<br/>• web_search<br/>• crawl_webpage<br/>• Playwright MCP"]
A_File["file_analysis_agent<br/>文件分析助手<br/>• extract_* (PDF/Word/Excel/PPT)<br/>• describe_pdf_page<br/>• analyze_code<br/>• analyze_multimodal"]
A_Naga["naga_code_analysis_agent<br/>NagaAgent 代码分析<br/>(7个工具)<br/>• read_file / glob<br/>• search_file_content"]
A_Self["undefined_self_code_agent<br/>Undefined 自身代码查阅<br/>(4个工具)<br/>• read_file / list_directory<br/>• glob / search_file_content"]
Expand Down
20 changes: 18 additions & 2 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -1002,13 +1002,29 @@ show_uptime = true
# zh: 搜索服务配置。
# en: Search service config.
[search]
# zh: web_agent 搜索工具优先级。关闭的工具会从可用工具中隐藏;开启后模型按该顺序优先考虑,但不会被代码硬性路由。
# en: Search tool priority for web_agent. Disabled tools are hidden; enabled tools are preferred in this order by prompt guidance, not hard routing.
priority = ["grok_search", "firecrawl_search", "web_search"]
# zh: SearxNG 搜索服务地址,例如 http://127.0.0.1:8849。
# en: SearxNG service URL, e.g. http://127.0.0.1:8849.
searxng_url = ""
# zh: 是否在 web_agent 中启用 grok_search。启用后该工具会优先于 web_search 暴露给模型
# en: Enable grok_search in web_agent. When enabled, this tool is exposed with higher priority than web_search.
# zh: 是否在 web_agent 中启用 grok_search。关闭时该工具会从 web_agent 工具列表中隐藏
# en: Enable grok_search in web_agent. When disabled, this tool is hidden from the web_agent tool list.
grok_search_enabled = false

# zh: Firecrawl 搜索服务配置。
# en: Firecrawl search service config.
[search.firecrawl]
# zh: 是否在 web_agent 中启用 firecrawl_search。默认关闭;关闭时该工具会隐藏。
# en: Enable firecrawl_search in web_agent. Disabled by default; when disabled, this tool is hidden.
enabled = false
# zh: Firecrawl API Key。为空时使用 Firecrawl keyless 搜索;填写后会发送 Authorization: Bearer。
# en: Firecrawl API key. Leave empty for Firecrawl keyless search; when set, Authorization: Bearer is sent.
api_key = ""
# zh: Firecrawl API 基础地址。
# en: Firecrawl API base URL.
base_url = "https://api.firecrawl.dev"

# zh: 代理设置(可选)。
# en: Proxy settings (optional).
[proxy]
Expand Down
19 changes: 17 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -648,12 +648,22 @@ Prompt caching 补充:

| 字段 | 默认值 | 说明 |
|---|---:|---|
| `priority` | `["grok_search", "firecrawl_search", "web_search"]` | `web_agent` 搜索工具优先级;关闭的工具会隐藏,开启后仅通过提示词引导选择 |
| `searxng_url` | `""` | SearXNG 地址;为空则禁用搜索包装器 |
| `grok_search_enabled` | `false` | 是否在 `web_agent` 中暴露 `grok_search`;启用后该工具优先于 `web_search` |
| `grok_search_enabled` | `false` | 是否在 `web_agent` 中暴露 `grok_search`;关闭时隐藏该工具 |

#### `search.firecrawl`

| 字段 | 默认值 | 说明 |
|---|---:|---|
| `enabled` | `false` | 是否在 `web_agent` 中暴露 `firecrawl_search`;关闭时隐藏该工具 |
| `api_key` | `""` | Firecrawl API Key;为空时使用 keyless 搜索 |
| `base_url` | `"https://api.firecrawl.dev"` | Firecrawl API 基础地址 |

补充:
- `searxng_url` 可热更新,运行时会重建搜索客户端。
- `grok_search_enabled` 不需要重建客户端;它只影响 `web_agent` 的工具暴露。
- `grok_search_enabled`、`search.firecrawl.*`、`priority` 不需要重建客户端;它们影响 `web_agent` 的工具暴露和提示词优先级。
- `firecrawl_search` 调用 Firecrawl `POST /v2/search`;配置 `api_key` 时发送 `Authorization: Bearer`,为空则走 Firecrawl keyless。

---

Expand Down Expand Up @@ -1087,6 +1097,7 @@ Prompt caching 补充:
- `render.browser_max_concurrency` 会在当前渲染任务空闲后重建渲染并发信号量。
- `skills.intro_autogen_*`(Agent intro 生成器配置刷新)
- `search.searxng_url`(搜索客户端刷新)
- `search.priority` / `search.firecrawl.*` / `search.grok_search_enabled` 会随运行时配置更新,用于后续 `web_agent` 工具暴露和提示词优先级;无需重启。
- `skills.hot_reload*`(技能热重载任务重启)
- `skills.hot_reload_interval/debounce`(配置热更新监听器自身重启)

Expand Down Expand Up @@ -1293,7 +1304,11 @@ Prompt caching 补充:

| TOML 路径 | 环境变量 |
|-----------|----------|
| `search.priority` | `SEARCH_PRIORITY` |
| `search.searxng_url` | `SEARXNG_URL` |
| `search.firecrawl.enabled` | `FIRECRAWL_SEARCH_ENABLED` |
| `search.firecrawl.api_key` | `FIRECRAWL_API_KEY` |
| `search.firecrawl.base_url` | `FIRECRAWL_BASE_URL` |

#### `skills`

Expand Down
4 changes: 3 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ Undefined 搭载了基于 ChromaDB 向量数据库的后台认知系统,无需

负责网页搜索和网页内容爬取,能够获取互联网上的实时最新信息。

**子工具**:`grok_search`(Grok 搜索)、`web_search`(通用搜索)、`crawl_webpage`(网页内容提取)
**子工具**:`grok_search`(Grok 搜索)、`firecrawl_search`(Firecrawl 搜索)、`web_search`(SearXNG 搜索)、`crawl_webpage`(网页内容提取)

启用 `grok_search` 后,工具会在调用 Grok 模型时注入检索约束:以服务端提供的当前时间为准,先调用搜索能力,使用多组搜索查询或多个搜索工具进行交叉检索,禁止编造,并在输出中给出来源。

搜索工具优先级由 `[search].priority` 配置并注入 `web_agent` 提示词;关闭的搜索工具会从工具列表中隐藏。`firecrawl_search` 支持 Firecrawl API Key,未配置 Key 时使用 keyless 搜索。

**示例:**
> *"请搜索最近三天关于 DeepSeek 的最新动态并生成摘要。"*
> *"帮我爬取这个网页的主要内容并整理成结构化笔记。"*
Expand Down
30 changes: 28 additions & 2 deletions src/Undefined/ai/prompts/system_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

from typing import Any

from Undefined.config.search import (
DEFAULT_SEARCH_PRIORITY,
SEARCH_TOOL_FIRECRAWL,
SEARCH_TOOL_GROK,
SEARCH_TOOL_SEARXNG,
order_by_priority,
)


def select_system_prompt_path(
*,
Expand Down Expand Up @@ -79,8 +87,26 @@ def build_model_config_info(runtime_config: Any) -> str:
knowledge_enabled = bool(getattr(runtime_config, "knowledge_enabled", False))
parts.append(f"- 知识库: {'已启用' if knowledge_enabled else '未启用'}")

grok_search_enabled = bool(getattr(runtime_config, "grok_search_enabled", False))
parts.append(f"- 联网搜索: {'已启用' if grok_search_enabled else '未启用'}")
search_priority = list(
getattr(runtime_config, "search_priority", []) or DEFAULT_SEARCH_PRIORITY
)
enabled_search_tools: list[str] = []
if bool(getattr(runtime_config, "grok_search_enabled", False)):
enabled_search_tools.append(SEARCH_TOOL_GROK)
if bool(getattr(runtime_config, "firecrawl_search_enabled", False)):
enabled_search_tools.append(SEARCH_TOOL_FIRECRAWL)
if str(getattr(runtime_config, "searxng_url", "") or "").strip():
enabled_search_tools.append(SEARCH_TOOL_SEARXNG)
ordered_enabled_search_tools = order_by_priority(
search_priority,
set(enabled_search_tools),
)
if ordered_enabled_search_tools:
parts.append(
f"- 联网搜索: 已启用(优先级={' > '.join(ordered_enabled_search_tools)})"
)
else:
parts.append("- 联网搜索: 未启用")

memes = getattr(runtime_config, "memes", None)
if memes is not None:
Expand Down
6 changes: 5 additions & 1 deletion src/Undefined/config/coercers.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def _coerce_int_list(value: Any) -> list[int]:
return []


def _coerce_str_list(value: Any) -> list[str]:
def coerce_str_list(value: Any) -> list[str]:
if value is None:
return []
if isinstance(value, list):
Expand All @@ -120,6 +120,10 @@ def _coerce_str_list(value: Any) -> list[str]:
return []


def _coerce_str_list(value: Any) -> list[str]:
return coerce_str_list(value)


def _coerce_request_params(value: Any) -> dict[str, Any]:
return normalize_request_params(value)

Expand Down
4 changes: 4 additions & 0 deletions src/Undefined/config/config_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,12 @@ class Config:
agent_intro_autogen_queue_interval: float
agent_intro_autogen_max_tokens: int
agent_intro_hash_path: str
search_priority: list[str]
searxng_url: str
grok_search_enabled: bool
firecrawl_search_enabled: bool
firecrawl_api_key: str
firecrawl_base_url: str
use_proxy: bool
http_proxy: str
https_proxy: str
Expand Down
4 changes: 4 additions & 0 deletions src/Undefined/config/env_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@
("onebot", "token"): "ONEBOT_TOKEN",
("onebot", "ws_url"): "ONEBOT_WS_URL",
("proxy", "use_proxy"): "USE_PROXY",
("search", "firecrawl", "api_key"): "FIRECRAWL_API_KEY",
("search", "firecrawl", "base_url"): "FIRECRAWL_BASE_URL",
("search", "firecrawl", "enabled"): "FIRECRAWL_SEARCH_ENABLED",
("search", "priority"): "SEARCH_PRIORITY",
("search", "searxng_url"): "SEARXNG_URL",
("skills", "hot_reload"): "SKILLS_HOT_RELOAD",
("skills", "intro_hash_path"): "AGENT_INTRO_HASH_PATH",
Expand Down
35 changes: 35 additions & 0 deletions src/Undefined/config/load_sections/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@
_normalize_base_url,
_warn_env_fallback,
)
from ..search import normalize_search_priority

logger = logging.getLogger(__name__)


def load_network(
data: dict[str, Any], *, config_path: Optional[Path] = None
) -> dict[str, Any]:
search_priority = normalize_search_priority(
_get_value(data, ("search", "priority"), "SEARCH_PRIORITY")
)
searxng_url = _coerce_str(
_get_value(data, ("search", "searxng_url"), "SEARXNG_URL"), ""
)
Expand All @@ -36,6 +40,33 @@ def load_network(
),
False,
)
firecrawl_search_enabled = _coerce_bool(
_get_value(
data,
("search", "firecrawl", "enabled"),
"FIRECRAWL_SEARCH_ENABLED",
),
False,
)
firecrawl_api_key = _coerce_str(
_get_value(
data,
("search", "firecrawl", "api_key"),
"FIRECRAWL_API_KEY",
),
"",
)
firecrawl_base_url = _normalize_base_url(
_coerce_str(
_get_value(
data,
("search", "firecrawl", "base_url"),
"FIRECRAWL_BASE_URL",
),
"https://api.firecrawl.dev",
),
"https://api.firecrawl.dev",
)

use_proxy = _coerce_bool(
_get_value(data, ("proxy", "use_proxy"), "USE_PROXY"), True
Expand Down Expand Up @@ -143,8 +174,12 @@ def load_network(

# Bilibili 配置
return {
"search_priority": search_priority,
"searxng_url": searxng_url,
"grok_search_enabled": grok_search_enabled,
"firecrawl_search_enabled": firecrawl_search_enabled,
"firecrawl_api_key": firecrawl_api_key,
"firecrawl_base_url": firecrawl_base_url,
"use_proxy": use_proxy,
"http_proxy": http_proxy,
"https_proxy": https_proxy,
Expand Down
53 changes: 53 additions & 0 deletions src/Undefined/config/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Search tool configuration helpers."""

from __future__ import annotations

from typing import Any, Final

from .coercers import coerce_str_list

SEARCH_TOOL_GROK: Final = "grok_search"
SEARCH_TOOL_FIRECRAWL: Final = "firecrawl_search"
SEARCH_TOOL_SEARXNG: Final = "web_search"

DEFAULT_SEARCH_PRIORITY: Final[tuple[str, ...]] = (
SEARCH_TOOL_GROK,
SEARCH_TOOL_FIRECRAWL,
SEARCH_TOOL_SEARXNG,
)
KNOWN_SEARCH_TOOLS: Final[frozenset[str]] = frozenset(DEFAULT_SEARCH_PRIORITY)


def normalize_search_priority(value: Any) -> list[str]:
"""Return a stable ordered search tool list from TOML/env input."""

raw_items = coerce_str_list(value)
normalized: list[str] = []
for item in raw_items:
if item not in KNOWN_SEARCH_TOOLS or item in normalized:
continue
normalized.append(item)

if not normalized:
return list(DEFAULT_SEARCH_PRIORITY)

for item in DEFAULT_SEARCH_PRIORITY:
if item not in normalized:
normalized.append(item)
return normalized


def order_by_priority(
priority: list[str] | tuple[str, ...],
available: set[str],
) -> list[str]:
"""Order available search tools by configured priority, then append leftovers."""

configured = list(priority or DEFAULT_SEARCH_PRIORITY)
ordered = [name for name in configured if name in available]
ordered.extend(
name
for name in DEFAULT_SEARCH_PRIORITY
if name in available and name not in ordered
)
return ordered
5 changes: 3 additions & 2 deletions src/Undefined/skills/agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,9 @@ mv skills/tools/my_tool skills/agents/my_agent/tools/
- **功能**:联网搜索、网页阅读、来源核验和最新信息获取。
- **适用场景**:新闻/公告/资料搜索、指定 URL 摘要、多来源对比、时效性问题核验。
- **不适用**:天气、金价、热搜、Whois、B 站、arXiv 检索等结构化查询;用户附件或文件解析。
- **子工具**:`grok_search`, `web_search`, `crawl_webpage`。
- **grok_search 参数**:优先使用 `search_request`,用自然语言完整叙述搜索要求,不要只传关键词。
- **子工具**:`grok_search`, `firecrawl_search`, `web_search`, `crawl_webpage`。
- **搜索优先级**:由 `[search].priority` 注入提示词引导,关闭的搜索工具会从 `web_agent` 工具列表中隐藏。
- **grok_search 参数**:使用 `search_request`,用自然语言完整叙述搜索要求,不要只传关键词。

### file_analysis_agent(文件分析助手)
- **功能**:分析用户提供的附件、内部 UID、URL、legacy file_id、arXiv 论文标识或 Bilibili 视频标识,提取文件内容。
Expand Down
53 changes: 53 additions & 0 deletions src/Undefined/skills/agents/runner/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import aiofiles

from Undefined.config.models import AgentModelConfig
from Undefined.config.search import KNOWN_SEARCH_TOOLS, order_by_priority
from Undefined.skills.agents.agent_tool_registry import AgentToolRegistry
from Undefined.skills.anthropic_skills import AnthropicSkillRegistry

Expand All @@ -25,6 +26,52 @@ async def load_prompt_text(agent_dir: Path, default_prompt: str) -> str:
return default_prompt


def _tool_names(tools: list[dict[str, Any]]) -> set[str]:
names: set[str] = set()
for tool in tools:
function = tool.get("function") if isinstance(tool, dict) else None
name = function.get("name") if isinstance(function, dict) else None
if isinstance(name, str) and name:
names.add(name)
return names


def _build_web_agent_search_priority_prompt(
runtime_config: Any | None,
tools: list[dict[str, Any]],
) -> str:
available_names = _tool_names(tools)
priority = list(getattr(runtime_config, "search_priority", []) or [])
ordered = order_by_priority(priority, available_names)
if not ordered:
return ""

return "\n".join(
[
"【搜索工具优先级】",
f"- 当前可用搜索工具优先级:{' > '.join(ordered)}。",
"- 搜索类任务优先考虑排在前面的工具;当前一个工具不可用、不适合、结果不足或需要交叉验证时,再使用后面的工具。",
"- 关闭的搜索工具不会出现在可用工具列表中;不要提议或假装调用未提供的工具。",
]
)


def _append_web_agent_runtime_prompt(
agent_name: str,
system_prompt: str,
runtime_config: Any | None,
tools: list[dict[str, Any]],
) -> str:
if agent_name != "web_agent":
return system_prompt
if not (_tool_names(tools) & KNOWN_SEARCH_TOOLS):
return system_prompt
priority_prompt = _build_web_agent_search_priority_prompt(runtime_config, tools)
if not priority_prompt:
return system_prompt
return f"{system_prompt.rstrip()}\n\n{priority_prompt}"


@dataclass
# 类:PreparedAgentRun
class PreparedAgentRun:
Expand Down Expand Up @@ -92,6 +139,12 @@ async def prepare_agent_run(
global_enabled=global_enabled,
)
system_prompt = await load_prompt_text(agent_dir, default_prompt)
system_prompt = _append_web_agent_runtime_prompt(
agent_name,
system_prompt,
runtime_config,
tools,
)

if agent_skill_registry and agent_skill_registry.has_skills():
skills_xml = agent_skill_registry.build_metadata_xml()
Expand Down
Loading
Loading