mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
fix(tools): improve web search provider fallback (#2629)
- centralize web search provider readiness and resolution logic - fall back when the configured provider is unavailable or invalid - allow native-search-capable models to use built-in search without the client tool - simplify the tools page and add direct access to web search settings - add backend, agent, and integration tests for the new selection behavior
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { IconSearch } from "@tabler/icons-react"
|
||||
import { IconSearch, IconSettings } from "@tabler/icons-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import type { ToolSupportItem } from "@/api/tools"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
@@ -29,6 +30,7 @@ interface ToolLibraryTabProps {
|
||||
pendingToolName: string | null
|
||||
onSearchQueryChange: (value: string) => void
|
||||
onStatusFilterChange: (value: ToolStatusFilter) => void
|
||||
onOpenWebSearchSettings: () => void
|
||||
onToggleTool: (name: string, enabled: boolean) => void
|
||||
}
|
||||
|
||||
@@ -43,6 +45,7 @@ export function ToolLibraryTab({
|
||||
pendingToolName,
|
||||
onSearchQueryChange,
|
||||
onStatusFilterChange,
|
||||
onOpenWebSearchSettings,
|
||||
onToggleTool,
|
||||
}: ToolLibraryTabProps) {
|
||||
const { t } = useTranslation()
|
||||
@@ -131,6 +134,7 @@ export function ToolLibraryTab({
|
||||
key={tool.name}
|
||||
tool={tool}
|
||||
isPending={pendingToolName === tool.name}
|
||||
onOpenWebSearchSettings={onOpenWebSearchSettings}
|
||||
onToggleTool={onToggleTool}
|
||||
/>
|
||||
))}
|
||||
@@ -146,10 +150,12 @@ export function ToolLibraryTab({
|
||||
function ToolCard({
|
||||
tool,
|
||||
isPending,
|
||||
onOpenWebSearchSettings,
|
||||
onToggleTool,
|
||||
}: {
|
||||
tool: ToolSupportItem
|
||||
isPending: boolean
|
||||
onOpenWebSearchSettings: () => void
|
||||
onToggleTool: (name: string, enabled: boolean) => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
@@ -157,8 +163,10 @@ function ToolCard({
|
||||
? t(`pages.agent.tools.reasons.${tool.reason_code}`)
|
||||
: ""
|
||||
const isEnabled = tool.status === "enabled"
|
||||
const isToggledOn = tool.status !== "disabled"
|
||||
const isDisabled = tool.status === "disabled"
|
||||
const isBlocked = tool.status === "blocked"
|
||||
const isWebSearchTool = tool.name === "web_search"
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -170,23 +178,40 @@ function ToolCard({
|
||||
isDisabled && "opacity-[0.80] hover:opacity-100",
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex h-full flex-col p-6">
|
||||
<div className="mb-3 flex items-start justify-between gap-4">
|
||||
<CardContent className="flex h-full flex-col px-5 py-1">
|
||||
<div className="mb-0.5 flex items-start justify-between gap-4">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<h4 className="text-foreground/90 min-w-0 break-all font-mono text-sm font-semibold tracking-tight">
|
||||
<h4 className="text-foreground/90 min-w-0 font-mono text-sm font-semibold tracking-tight break-all">
|
||||
{tool.name}
|
||||
</h4>
|
||||
<ToolStatusBadge status={tool.status} />
|
||||
</div>
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
disabled={isPending}
|
||||
onCheckedChange={(checked) => onToggleTool(tool.name, checked)}
|
||||
className={cn(
|
||||
"shrink-0",
|
||||
isEnabled && "shadow-xs ring-1 ring-emerald-500/20",
|
||||
<div className="flex h-8 shrink-0 items-center gap-2">
|
||||
{isWebSearchTool && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onOpenWebSearchSettings}
|
||||
className="text-muted-foreground hover:text-foreground hover:bg-muted/60 size-8 rounded-lg"
|
||||
aria-label={t(
|
||||
"pages.agent.tools.web_search.open_settings",
|
||||
"Open Settings",
|
||||
)}
|
||||
>
|
||||
<IconSettings className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<Switch
|
||||
checked={isToggledOn}
|
||||
disabled={isPending}
|
||||
onCheckedChange={(checked) => onToggleTool(tool.name, checked)}
|
||||
className={cn(
|
||||
"shrink-0",
|
||||
isEnabled && "shadow-xs ring-1 ring-emerald-500/20",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground/80 flex-1 text-[14px] leading-relaxed">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useLayoutEffect, useRef } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
|
||||
@@ -8,9 +9,9 @@ import { WebSearchTab } from "./web-search-tab"
|
||||
|
||||
export function ToolsPage() {
|
||||
const { t } = useTranslation()
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const {
|
||||
activeTab,
|
||||
currentProviderLabel,
|
||||
expandedProvider,
|
||||
groupedTools,
|
||||
pendingToolName,
|
||||
@@ -34,12 +35,19 @@ export function ToolsPage() {
|
||||
updateWebSearchDraft,
|
||||
} = useToolsPage()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
scrollContainerRef.current?.scrollTo({ top: 0 })
|
||||
}, [activeTab])
|
||||
|
||||
return (
|
||||
<div className="bg-background flex h-full flex-col">
|
||||
<PageHeader title={t("navigation.tools", "Tools")} />
|
||||
<ToolsTabs activeTab={activeTab} onChange={setActiveTab} />
|
||||
|
||||
<div className="flex-1 overflow-auto px-6 py-6 pb-20">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="flex-1 overflow-auto px-6 py-6 pb-20"
|
||||
>
|
||||
<div className="mx-auto w-full max-w-6xl">
|
||||
{activeTab === "library" ? (
|
||||
<ToolLibraryTab
|
||||
@@ -53,12 +61,12 @@ export function ToolsPage() {
|
||||
pendingToolName={pendingToolName}
|
||||
onSearchQueryChange={setSearchQuery}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
onOpenWebSearchSettings={() => setActiveTab("web-search")}
|
||||
onToggleTool={toggleTool}
|
||||
/>
|
||||
) : (
|
||||
<WebSearchTab
|
||||
draft={webSearchDraft}
|
||||
currentProviderLabel={currentProviderLabel}
|
||||
providerLabelMap={providerLabelMap}
|
||||
expandedProvider={expandedProvider}
|
||||
isLoading={isWebSearchLoading}
|
||||
|
||||
@@ -132,11 +132,6 @@ export function useToolsPage() {
|
||||
return new Map(providers.map((provider) => [provider.id, provider.label]))
|
||||
}, [webSearchDraft])
|
||||
|
||||
const currentProviderLabel = webSearchDraft?.current_service
|
||||
? (providerLabelMap.get(webSearchDraft.current_service) ??
|
||||
webSearchDraft.current_service)
|
||||
: t("pages.agent.tools.web_search.none", "None")
|
||||
|
||||
const pendingToolName = toggleToolMutation.isPending
|
||||
? (toggleToolMutation.variables?.name ?? null)
|
||||
: null
|
||||
@@ -168,7 +163,6 @@ export function useToolsPage() {
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
currentProviderLabel,
|
||||
expandedProvider,
|
||||
groupedTools: groupedTools.groupedTools,
|
||||
pendingToolName,
|
||||
|
||||
@@ -36,7 +36,7 @@ export function WebSearchGeneralSettings({
|
||||
label={t("pages.agent.tools.web_search.provider", "Primary Provider")}
|
||||
description={t(
|
||||
"pages.agent.tools.web_search.provider_description",
|
||||
"Select the default search engine that agents will fallback to.",
|
||||
"Select the default provider to use when the web search tool handles a request.",
|
||||
)}
|
||||
>
|
||||
<Select
|
||||
@@ -95,7 +95,7 @@ export function WebSearchGeneralSettings({
|
||||
)}
|
||||
description={t(
|
||||
"pages.agent.tools.web_search.prefer_native_hint",
|
||||
"Bypass external providers if the agent inherently supports web search tools.",
|
||||
"When enabled, the model may use its built-in search capability instead of the configured provider list.",
|
||||
)}
|
||||
>
|
||||
<Switch
|
||||
|
||||
@@ -10,7 +10,6 @@ import { WebSearchProviderSettings } from "./web-search-provider-settings"
|
||||
|
||||
interface WebSearchTabProps {
|
||||
draft: WebSearchConfigResponse | null
|
||||
currentProviderLabel: string
|
||||
providerLabelMap: Map<string, string>
|
||||
expandedProvider: string | null
|
||||
isLoading: boolean
|
||||
@@ -23,7 +22,6 @@ interface WebSearchTabProps {
|
||||
|
||||
export function WebSearchTab({
|
||||
draft,
|
||||
currentProviderLabel,
|
||||
providerLabelMap,
|
||||
expandedProvider,
|
||||
isLoading,
|
||||
@@ -52,21 +50,16 @@ export function WebSearchTab({
|
||||
<>
|
||||
<div className="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="max-w-xl space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-foreground/90 text-2xl font-semibold tracking-tight">
|
||||
{t(
|
||||
"pages.agent.tools.web_search.title",
|
||||
"Web Search Configuration",
|
||||
)}
|
||||
</h1>
|
||||
<div className="rounded-full bg-emerald-500/10 px-2.5 py-0.5 text-[11px] font-semibold tracking-wide text-emerald-600 uppercase dark:text-emerald-400">
|
||||
{currentProviderLabel}
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-foreground/90 text-2xl font-semibold tracking-tight">
|
||||
{t(
|
||||
"pages.agent.tools.web_search.title",
|
||||
"Web Search Configuration",
|
||||
)}
|
||||
</h1>
|
||||
<p className="text-muted-foreground/80 text-[14px] leading-relaxed">
|
||||
{t(
|
||||
"pages.agent.tools.web_search.description",
|
||||
"Provide web search capability for agents to find the latest real-world info. Automatically routes to the optimal active provider.",
|
||||
"Configure how the web search tool behaves by default, including whether the model may use its built-in search capability.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -555,16 +555,15 @@
|
||||
"providers_config": "Integrations",
|
||||
"load_error": "Failed to load web search configuration.",
|
||||
"save": "Save Changes",
|
||||
"open_settings": "Open Settings",
|
||||
"save_success": "Settings saved successfully.",
|
||||
"save_error": "Failed to save settings.",
|
||||
"current_active": "Active: ",
|
||||
"current_service": "Current Service",
|
||||
"provider": "Primary Provider",
|
||||
"provider_description": "Select the default search engine that agents will fallback to.",
|
||||
"provider_description": "Select the default provider to use when the web search tool handles a request.",
|
||||
"proxy": "HTTPS Proxy",
|
||||
"proxy_description": "Optional global HTTP/S proxy for underlying web requests.",
|
||||
"prefer_native": "Prefer Native Search",
|
||||
"prefer_native_hint": "Bypass external providers if the agent inherently supports web search tools.",
|
||||
"prefer_native_hint": "When enabled, the model may use its built-in search capability instead of the configured provider list.",
|
||||
"provider_hint": "Enable this provider and fill any required connection settings.",
|
||||
"max_results": "Max Results",
|
||||
"base_url": "Base URL",
|
||||
@@ -592,7 +591,8 @@
|
||||
"requires_linux": "This tool only works on Linux hosts with the required device files exposed.",
|
||||
"requires_skills": "Enable `tools.skills` before this skill-registry tool can be used.",
|
||||
"requires_subagent": "Enable `tools.subagent` before the spawn tool can delegate work.",
|
||||
"requires_mcp_discovery": "Enable `tools.mcp.discovery` before MCP discovery tools become available."
|
||||
"requires_mcp_discovery": "Enable `tools.mcp.discovery` before MCP discovery tools become available.",
|
||||
"requires_web_search_provider": "Configure at least one ready external web-search provider."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -555,16 +555,15 @@
|
||||
"providers_config": "集成",
|
||||
"load_error": "加载 Web Search 配置失败。",
|
||||
"save": "保存更改",
|
||||
"open_settings": "打开设置",
|
||||
"save_success": "设置保存成功。",
|
||||
"save_error": "保存设置失败。",
|
||||
"current_active": "活动: ",
|
||||
"current_service": "当前服务",
|
||||
"provider": "首选服务",
|
||||
"provider_description": "选择智能体在默认情况下进行网络搜索的回退引擎。",
|
||||
"provider_description": "选择在由 Web Search 工具处理请求时默认使用的搜索引擎。",
|
||||
"proxy": "HTTPS 代理",
|
||||
"proxy_description": "用于底层网页请求的可选全局代理配置。",
|
||||
"prefer_native": "优先使用模型搜索",
|
||||
"prefer_native_hint": "如果当前模型本身支持联网功能,则直接使用模型自带的搜索能力",
|
||||
"prefer_native_hint": "启用后,模型在支持时可以直接使用自身搜索能力,而不必走已配置的搜索引擎列表。",
|
||||
"provider_hint": "启用该服务后,可继续填写所需的连接参数。",
|
||||
"max_results": "最大获取结果数",
|
||||
"base_url": "API 请求地址",
|
||||
@@ -592,7 +591,8 @@
|
||||
"requires_linux": "该工具仅在 Linux 主机上可用,并且需要暴露对应的设备文件。",
|
||||
"requires_skills": "需要先启用 `tools.skills`,该技能注册表工具才能使用。",
|
||||
"requires_subagent": "需要先启用 `tools.subagent`,`spawn` 才能委派任务。",
|
||||
"requires_mcp_discovery": "需要先启用 `tools.mcp.discovery`,MCP 发现工具才会可用。"
|
||||
"requires_mcp_discovery": "需要先启用 `tools.mcp.discovery`,MCP 发现工具才会可用。",
|
||||
"requires_web_search_provider": "请至少配置一个可用的外部网络搜索 provider。"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user