mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
feat(pi-web-search): 统一 web_search/web_fetch 并由主模型路由搜索厂商
7 个按厂商命名的搜索工具收敛为 2 个统一工具,schema 与提示词不再暴露 provider 概念:web_search 由当前主模型按内置规则在 Tavily/Exa/Keenable 间路由(会话内 LRU 缓存,失败按可用性降级),web_fetch 固定走 Exa 并 静默降级。三家均改为纯 fetch 直连——Exa 直接对 mcp.exa.ai 发 JSON-RPC 并缓存会话,彻底移除 pi-mcp-adapter、@tavily/pi-extension 与 @keenable/pi-search 依赖。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
6fe6cb29ae
commit
ee1e6999d7
@@ -1,45 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createExaToolApiOverride, mapExaToolName, prefixExaTool } from "../extensions/exa-tool-prefix.ts";
|
||||
|
||||
test("Exa tools use a provider prefix followed by the original semantic name", () => {
|
||||
assert.equal(mapExaToolName("web_search_exa"), "exa_web_search");
|
||||
assert.equal(mapExaToolName("web_fetch_exa"), "exa_web_fetch");
|
||||
assert.equal(mapExaToolName("web_search_advanced_exa"), "exa_web_search_advanced");
|
||||
assert.equal(mapExaToolName("unrelated_tool"), "unrelated_tool");
|
||||
|
||||
const tool = prefixExaTool({ name: "web_search_exa", label: "MCP: web_search_exa" });
|
||||
assert.equal(tool.name, "exa_web_search");
|
||||
assert.equal(tool.label, "Exa Web Search");
|
||||
});
|
||||
|
||||
test("Exa API override keeps registered and active tool names aligned", () => {
|
||||
const registered: Array<{ name: string; label: string }> = [];
|
||||
let active: string[] = [];
|
||||
const unregistered: string[] = [];
|
||||
const api = createExaToolApiOverride({
|
||||
registerTool(tool: unknown) {
|
||||
registered.push(tool as { name: string; label: string });
|
||||
},
|
||||
setActiveTools(names: string[]) {
|
||||
active = names;
|
||||
},
|
||||
unregisterTool(name: string) {
|
||||
unregistered.push(name);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
api.registerTool({ name: "web_fetch_exa", label: "MCP: web_fetch_exa" });
|
||||
api.registerTool({ name: "codegraph_explore", label: "MCP: codegraph_explore" });
|
||||
api.setActiveTools(["read", "web_fetch_exa", "web_fetch_exa", "codegraph_explore"]);
|
||||
api.unregisterTool("web_fetch_exa");
|
||||
api.unregisterTool("codegraph_explore");
|
||||
|
||||
assert.equal(registered[0]?.name, "exa_web_fetch");
|
||||
assert.equal(registered[0]?.label, "Exa Web Fetch");
|
||||
assert.equal(registered[1]?.name, "codegraph_explore");
|
||||
assert.deepEqual(active, ["read", "exa_web_fetch", "codegraph_explore"]);
|
||||
assert.deepEqual(unregistered, ["exa_web_fetch", "codegraph_explore"]);
|
||||
});
|
||||
@@ -35,6 +35,11 @@ test("all package extensions load together without global registration conflicts
|
||||
packageJson.pi.extensions.includes("./extensions/codegraph.ts"),
|
||||
"the bundle must expose the native codegraph_explore tool",
|
||||
);
|
||||
assert.ok(
|
||||
packageJson.pi.extensions.includes("./pi-web-search/index.ts"),
|
||||
"the bundle must load the unified pi-web-search tools",
|
||||
);
|
||||
assert.equal(packageJson.dependencies["pi-web-search"], "file:./pi-web-search");
|
||||
assert.equal(
|
||||
packageJson.pi.extensions.includes("./extensions/plugin-init.ts"),
|
||||
false,
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
import { createMcpConfig, EXA_TOOLS } from "../extensions/mcp-config.ts";
|
||||
|
||||
test("the shared MCP adapter config only connects Exa", () => {
|
||||
assert.deepEqual(createMcpConfig({}), {
|
||||
settings: {
|
||||
disableProxyTool: true,
|
||||
scriptMode: false,
|
||||
},
|
||||
mcpServers: {
|
||||
exa: {
|
||||
url: `https://mcp.exa.ai/mcp?tools=${EXA_TOOLS.join(",")}`,
|
||||
protocolVersion: "auto",
|
||||
lifecycle: "eager",
|
||||
directTools: EXA_TOOLS,
|
||||
includeTools: EXA_TOOLS,
|
||||
toolPrefix: "none",
|
||||
approveTools: false,
|
||||
exposeResources: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Exa API key is resolved from the environment into a header and never placed in the URL", () => {
|
||||
const config = createMcpConfig({ EXA_API_KEY: "exa-test-key" });
|
||||
assert.deepEqual(config.mcpServers.exa.headers, { "x-api-key": "$env:EXA_API_KEY" });
|
||||
assert.equal(config.mcpServers.exa.url?.includes("exa-test-key"), false);
|
||||
assert.equal(JSON.stringify(config).includes("exa-test-key"), false);
|
||||
});
|
||||
|
||||
test("the package has exactly one local MCP adapter owner", async () => {
|
||||
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
|
||||
pi: { extensions: string[] };
|
||||
};
|
||||
const localEntries = packageJson.pi.extensions.filter((entry) => !entry.startsWith("./node_modules/"));
|
||||
const adapterOwners: string[] = [];
|
||||
for (const entry of localEntries) {
|
||||
const source = await readFile(new URL(`../${entry.slice(2)}`, import.meta.url), "utf8");
|
||||
if (source.includes("createMcpAdapter") || source.includes('from "pi-mcp-adapter"')) {
|
||||
adapterOwners.push(entry);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(adapterOwners, ["./extensions/mcp.ts"]);
|
||||
});
|
||||
|
||||
test("search credentials load before all search providers", async () => {
|
||||
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
|
||||
pi: { extensions: string[] };
|
||||
};
|
||||
const entries = packageJson.pi.extensions;
|
||||
const loaderIndex = entries.indexOf("./extensions/search-config.ts");
|
||||
for (const provider of [
|
||||
"./extensions/tavily-override.ts",
|
||||
"./node_modules/@keenable/pi-search/src/index.ts",
|
||||
"./extensions/mcp.ts",
|
||||
]) {
|
||||
assert.ok(loaderIndex >= 0 && loaderIndex < entries.indexOf(provider), `${provider} must load after search config`);
|
||||
}
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { prefixTavilyTool } from "../extensions/tavily-tool-prefix.ts";
|
||||
|
||||
test("Tavily override prefixes tool identity and internal tool references", () => {
|
||||
const tool = prefixTavilyTool({
|
||||
name: "web_search",
|
||||
label: "Web Search",
|
||||
description: "Use web_search, then web_fetch with Tavily.",
|
||||
promptSnippet: "Call web_search.",
|
||||
promptGuidelines: ["Use web_fetch after web_search."],
|
||||
parameters: {} as never,
|
||||
execute: async () => ({ content: [{ type: "text" as const, text: "ok" }] }),
|
||||
});
|
||||
|
||||
assert.equal(tool.name, "tavily_web_search");
|
||||
assert.equal(tool.label, "Tavily Web Search");
|
||||
assert.equal(tool.description, "Use tavily_web_search, then tavily_web_fetch with Tavily.");
|
||||
assert.equal(tool.promptSnippet, "Call tavily_web_search.");
|
||||
assert.deepEqual(tool.promptGuidelines, ["Use tavily_web_fetch after tavily_web_search."]);
|
||||
});
|
||||
@@ -73,21 +73,15 @@ test("LSP routing covers navigation and post-edit diagnostics", () => {
|
||||
assert.match(section, /post-edit diagnostics/);
|
||||
});
|
||||
|
||||
test("online search routing selects providers by task", () => {
|
||||
const tavily = buildToolRoutingSection(["tavily_web_search", "tavily_web_fetch"]);
|
||||
assert.match(tavily, /broad discovery when keywords are uncertain/);
|
||||
assert.match(tavily, /news\/recent\/trending topics/);
|
||||
assert.doesNotMatch(tavily, /official or API documentation/);
|
||||
test("online search routing uses the unified web tools without naming providers", () => {
|
||||
const section = buildToolRoutingSection(["web_search", "web_fetch"]);
|
||||
assert.match(section, /Use web_search for current, real-world, or citable web information/);
|
||||
assert.match(section, /provider selection is handled internally/);
|
||||
assert.match(section, /read selected pages with web_fetch/);
|
||||
assert.doesNotMatch(section, /tavily|exa|keenable/iu);
|
||||
|
||||
const exa = buildToolRoutingSection(["exa_web_search", "exa_web_fetch", "exa_web_search_advanced"]);
|
||||
assert.match(exa, /official or API documentation/);
|
||||
assert.match(exa, /explicitly bound the result count and returned text length/);
|
||||
assert.doesNotMatch(exa, /Chinese-language pages/);
|
||||
|
||||
const keenable = buildToolRoutingSection(["keenable_search", "keenable_fetch"]);
|
||||
assert.match(keenable, /Chinese-language pages, policies and government notices/);
|
||||
assert.match(keenable, /site-restricted search/);
|
||||
assert.match(keenable, /publication-date filtering/);
|
||||
const searchOnly = buildToolRoutingSection(["web_search"]);
|
||||
assert.match(searchOnly, /Use web_search/);
|
||||
});
|
||||
|
||||
test("routing includes only guidance for active optional tools", () => {
|
||||
@@ -98,8 +92,7 @@ test("routing includes only guidance for active optional tools", () => {
|
||||
assert.doesNotMatch(section, /Use Context Mode/);
|
||||
assert.doesNotMatch(section, /ctx_execute_file is confined/);
|
||||
assert.doesNotMatch(section, /fresh LINE#HASH anchors/);
|
||||
assert.doesNotMatch(section, /Choose one online search provider/);
|
||||
assert.doesNotMatch(section, /broad discovery when keywords are uncertain/);
|
||||
assert.doesNotMatch(section, /Use web_search for current/);
|
||||
assert.match(section, /Use read only when exact source is needed/);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
|
||||
dependencies: Record<string, string>;
|
||||
files: string[];
|
||||
pi: { extensions: string[]; skills: string[] };
|
||||
};
|
||||
|
||||
test("pi-web-search is the single search surface, loading after search-config", () => {
|
||||
const entries = packageJson.pi.extensions;
|
||||
assert.ok(entries.includes("./pi-web-search/index.ts"), "the bundle must load pi-web-search");
|
||||
assert.ok(
|
||||
entries.indexOf("./extensions/search-config.ts") < entries.indexOf("./pi-web-search/index.ts"),
|
||||
"search credentials must load before pi-web-search",
|
||||
);
|
||||
for (const retired of [
|
||||
"./extensions/mcp.ts",
|
||||
"./extensions/tavily-override.ts",
|
||||
"./node_modules/@keenable/pi-search/src/index.ts",
|
||||
]) {
|
||||
assert.equal(entries.includes(retired), false, `${retired} must no longer be loaded`);
|
||||
}
|
||||
});
|
||||
|
||||
test("pi-web-search ships as a maintained file: dependency without upstream search packages", () => {
|
||||
assert.equal(packageJson.dependencies["pi-web-search"], "file:./pi-web-search");
|
||||
assert.ok(packageJson.files.includes("pi-web-search"));
|
||||
assert.equal(packageJson.dependencies["@tavily/pi-extension"], undefined);
|
||||
assert.equal(packageJson.dependencies["@keenable/pi-search"], undefined);
|
||||
assert.equal(packageJson.dependencies["pi-mcp-adapter"], undefined, "Exa is a direct client now — no MCP adapter");
|
||||
});
|
||||
|
||||
test("no extension in the bundle loads the MCP adapter or upstream search extensions", async () => {
|
||||
for (const entry of packageJson.pi.extensions.filter((e) => !e.startsWith("./node_modules/"))) {
|
||||
const source = await readFile(new URL(`../${entry.slice(2)}`, import.meta.url), "utf8");
|
||||
assert.equal(source.includes("pi-mcp-adapter"), false, `${entry} must not import pi-mcp-adapter`);
|
||||
assert.equal(source.includes("@tavily/pi-extension"), false, `${entry} must not import @tavily/pi-extension`);
|
||||
}
|
||||
});
|
||||
|
||||
test("the keenable skill path is gone with the upstream package", () => {
|
||||
for (const skillPath of packageJson.pi.skills) {
|
||||
assert.equal(skillPath.includes("@keenable"), false);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user