mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: add routed online search providers
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
const EXA_TOOL_ALIASES: Record<string, string> = {
|
||||
web_search_exa: "exa_web_search",
|
||||
web_fetch_exa: "exa_web_fetch",
|
||||
web_search_advanced_exa: "exa_web_search_advanced",
|
||||
};
|
||||
|
||||
export type ExaToolDefinition = {
|
||||
name: string;
|
||||
label: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export function mapExaToolName(name: string): string {
|
||||
return EXA_TOOL_ALIASES[name] ?? name;
|
||||
}
|
||||
|
||||
export function prefixExaTool<T extends ExaToolDefinition>(definition: T): T {
|
||||
const name = mapExaToolName(definition.name);
|
||||
if (name === definition.name) return definition;
|
||||
|
||||
const displayName = name
|
||||
.slice("exa_".length)
|
||||
.split("_")
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
return {
|
||||
...definition,
|
||||
name,
|
||||
label: `Exa ${displayName}`,
|
||||
} as T;
|
||||
}
|
||||
|
||||
type ToolApi = {
|
||||
registerTool: (tool: unknown) => unknown;
|
||||
setActiveTools: (names: string[]) => unknown;
|
||||
unregisterTool?: (name: string) => boolean;
|
||||
};
|
||||
|
||||
export function createExaToolApiOverride<T extends object>(pi: T): T {
|
||||
const api = pi as T & ToolApi;
|
||||
return new Proxy(pi, {
|
||||
get(target, property) {
|
||||
if (property === "registerTool") {
|
||||
return (definition: ExaToolDefinition) => api.registerTool(prefixExaTool(definition));
|
||||
}
|
||||
if (property === "setActiveTools") {
|
||||
return (names: string[]) => api.setActiveTools([...new Set(names.map(mapExaToolName))]);
|
||||
}
|
||||
if (property === "unregisterTool") {
|
||||
return api.unregisterTool ? (name: string) => api.unregisterTool?.(mapExaToolName(name)) : undefined;
|
||||
}
|
||||
|
||||
const value = Reflect.get(target, property, target);
|
||||
return typeof value === "function" ? value.bind(target) : value;
|
||||
},
|
||||
}) as T;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { createMcpAdapter } from "pi-mcp-adapter";
|
||||
import { createExaToolApiOverride } from "./exa-tool-prefix.ts";
|
||||
|
||||
const EXA_TOOLS = ["web_search_exa", "web_fetch_exa", "web_search_advanced_exa"];
|
||||
const EXA_MCP_URL = `https://mcp.exa.ai/mcp?tools=${EXA_TOOLS.join(",")}`;
|
||||
|
||||
const exaAdapter = createMcpAdapter({
|
||||
config: {
|
||||
settings: {
|
||||
disableProxyTool: true,
|
||||
scriptMode: false,
|
||||
},
|
||||
mcpServers: {
|
||||
exa: {
|
||||
url: EXA_MCP_URL,
|
||||
protocolVersion: "auto",
|
||||
lifecycle: "eager",
|
||||
directTools: EXA_TOOLS,
|
||||
includeTools: EXA_TOOLS,
|
||||
toolPrefix: "none",
|
||||
approveTools: false,
|
||||
exposeResources: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** Connect Pi to Exa while exposing every direct tool as `exa_<original-name>`. */
|
||||
export default function exaExtension(pi: ExtensionAPI): void {
|
||||
exaAdapter(createExaToolApiOverride(pi));
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import tavilyExtension from "../node_modules/@tavily/pi-extension/index.ts";
|
||||
import { prefixTavilyTool, type TavilyToolDefinition } from "./tavily-tool-prefix.ts";
|
||||
|
||||
|
||||
/** Load Tavily while making the provider explicit in every registered tool name. */
|
||||
export default function tavilyOverrideExtension(pi: ExtensionAPI): void {
|
||||
const prefixedPi = new Proxy(pi, {
|
||||
get(target, property) {
|
||||
if (property === "registerTool") {
|
||||
return (definition: TavilyToolDefinition) =>
|
||||
(target.registerTool as (tool: unknown) => void)(prefixTavilyTool(definition));
|
||||
}
|
||||
|
||||
const value = Reflect.get(target, property, target);
|
||||
return typeof value === "function" ? value.bind(target) : value;
|
||||
},
|
||||
});
|
||||
|
||||
tavilyExtension(prefixedPi);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
const TAVILY_TOOL_PREFIX = "tavily_";
|
||||
|
||||
export type TavilyToolDefinition = {
|
||||
name: string;
|
||||
label: string;
|
||||
description: string;
|
||||
promptSnippet?: string;
|
||||
promptGuidelines?: string[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
function rewriteToolReferences(text: string): string {
|
||||
return text.replace(/\bweb_search\b/g, "tavily_web_search").replace(/\bweb_fetch\b/g, "tavily_web_fetch");
|
||||
}
|
||||
|
||||
export function prefixTavilyTool<T extends TavilyToolDefinition>(definition: T): T {
|
||||
return {
|
||||
...definition,
|
||||
name: `${TAVILY_TOOL_PREFIX}${definition.name}`,
|
||||
label: definition.label.startsWith("Tavily ") ? definition.label : `Tavily ${definition.label}`,
|
||||
description: rewriteToolReferences(definition.description),
|
||||
promptSnippet: definition.promptSnippet ? rewriteToolReferences(definition.promptSnippet) : undefined,
|
||||
promptGuidelines: definition.promptGuidelines?.map(rewriteToolReferences),
|
||||
} as T;
|
||||
}
|
||||
@@ -11,6 +11,10 @@ function hasTool(selectedTools: SelectedTools, name: string): boolean {
|
||||
return selectedTools === undefined || selectedTools.includes(name);
|
||||
}
|
||||
|
||||
function hasAnyTool(selectedTools: SelectedTools, names: string[]): boolean {
|
||||
return names.some((name) => hasTool(selectedTools, name));
|
||||
}
|
||||
|
||||
export function buildToolRoutingSection(selectedTools: SelectedTools): string {
|
||||
const rules: string[] = [];
|
||||
|
||||
@@ -40,6 +44,30 @@ export function buildToolRoutingSection(selectedTools: SelectedTools): string {
|
||||
);
|
||||
}
|
||||
|
||||
const tavilyTools = ["tavily_web_search", "tavily_web_fetch"];
|
||||
const exaTools = ["exa_web_search", "exa_web_fetch", "exa_web_search_advanced"];
|
||||
const keenableTools = ["keenable_search", "keenable_fetch"];
|
||||
if (hasAnyTool(selectedTools, [...tavilyTools, ...exaTools, ...keenableTools])) {
|
||||
rules.push(
|
||||
"- Choose one online search provider based on the task instead of querying Tavily, Exa, and Keenable by default; cross-check with another provider only when source quality or uncertainty warrants it.",
|
||||
);
|
||||
}
|
||||
if (hasAnyTool(selectedTools, tavilyTools)) {
|
||||
rules.push(
|
||||
"- Use Tavily tools for broad discovery when keywords are uncertain, for news/recent/trending topics, or when many candidate sources are useful. Tavily results can mix official pages, blogs, forums, and secondary sources, so keep max_results modest, avoid raw content unless needed, and fetch only selected pages when depth matters.",
|
||||
);
|
||||
}
|
||||
if (hasAnyTool(selectedTools, exaTools)) {
|
||||
rules.push(
|
||||
"- Use Exa tools when source precision matters: official or API documentation, release/version information, academic papers, research material, and high-quality fact confirmation. Use exa_web_fetch to continue reading selected pages; for exa_web_search_advanced, explicitly bound the result count and returned text length to prevent oversized responses.",
|
||||
);
|
||||
}
|
||||
if (hasAnyTool(selectedTools, keenableTools)) {
|
||||
rules.push(
|
||||
"- Use Keenable tools for Chinese-language pages, policies and government notices, site-restricted search, and publication-date filtering. Prefer its compact search results and fetch only selected pages to keep returned content controlled; KEENABLE_API_KEY is optional but raises rate limits.",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasTool(selectedTools, "ctx_execute") || hasTool(selectedTools, "ctx_execute_file")) {
|
||||
rules.push(
|
||||
"- Use Context Mode for logs, test/build output, generated data, large-file exploration/analysis/summarization, and any command whose output may be large or unpredictable. Prefer ctx_execute_file over read when deriving an answer from a large file inside the workspace, and print only the derived answer needed for the task.",
|
||||
|
||||
Reference in New Issue
Block a user