mirror of https://github.com/openclaw/openclaw.git
287 lines
8.9 KiB
TypeScript
287 lines
8.9 KiB
TypeScript
import { Type } from "@sinclair/typebox";
|
|
import {
|
|
DEFAULT_CACHE_TTL_MINUTES,
|
|
DEFAULT_TIMEOUT_SECONDS,
|
|
formatCliCommand,
|
|
getScopedCredentialValue,
|
|
mergeScopedSearchConfig,
|
|
normalizeCacheKey,
|
|
readCache,
|
|
readNumberParam,
|
|
readStringParam,
|
|
resolveCacheTtlMs,
|
|
resolveProviderWebSearchPluginConfig,
|
|
resolveTimeoutSeconds,
|
|
resolveWebSearchProviderCredential,
|
|
setProviderWebSearchPluginConfigValue,
|
|
setScopedCredentialValue,
|
|
type SearchConfigRecord,
|
|
type WebSearchProviderSetupContext,
|
|
type WebSearchProviderPlugin,
|
|
writeCache,
|
|
} from "openclaw/plugin-sdk/provider-web-search";
|
|
import {
|
|
buildXaiWebSearchPayload,
|
|
extractXaiWebSearchContent,
|
|
requestXaiWebSearch,
|
|
resolveXaiInlineCitations,
|
|
resolveXaiWebSearchModel,
|
|
} from "./src/web-search-shared.js";
|
|
import { XAI_DEFAULT_X_SEARCH_MODEL } from "./src/x-search-shared.js";
|
|
|
|
const XAI_WEB_SEARCH_CACHE = new Map<
|
|
string,
|
|
{ value: Record<string, unknown>; insertedAt: number; expiresAt: number }
|
|
>();
|
|
|
|
const X_SEARCH_MODEL_OPTIONS = [
|
|
{
|
|
value: XAI_DEFAULT_X_SEARCH_MODEL,
|
|
label: XAI_DEFAULT_X_SEARCH_MODEL,
|
|
hint: "default · fast, no reasoning",
|
|
},
|
|
{
|
|
value: "grok-4-1-fast",
|
|
label: "grok-4-1-fast",
|
|
hint: "fast with reasoning",
|
|
},
|
|
] as const;
|
|
|
|
function resolveXSearchConfigRecord(
|
|
config?: WebSearchProviderSetupContext["config"],
|
|
): Record<string, unknown> | undefined {
|
|
const xSearch = config?.tools?.web?.x_search;
|
|
return xSearch && typeof xSearch === "object" ? (xSearch as Record<string, unknown>) : undefined;
|
|
}
|
|
|
|
function applyXSearchSetupConfig(
|
|
config: WebSearchProviderSetupContext["config"],
|
|
params: { enabled: boolean; model: string },
|
|
): WebSearchProviderSetupContext["config"] {
|
|
return {
|
|
...config,
|
|
tools: {
|
|
...config.tools,
|
|
web: {
|
|
...config.tools?.web,
|
|
x_search: {
|
|
...config.tools?.web?.x_search,
|
|
enabled: params.enabled,
|
|
model: params.model,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
async function runXaiSearchProviderSetup(
|
|
ctx: WebSearchProviderSetupContext,
|
|
): Promise<WebSearchProviderSetupContext["config"]> {
|
|
const existingXSearch = resolveXSearchConfigRecord(ctx.config);
|
|
if (existingXSearch?.enabled === false) {
|
|
return ctx.config;
|
|
}
|
|
|
|
await ctx.prompter.note(
|
|
[
|
|
"x_search lets your agent search X (formerly Twitter) posts via xAI.",
|
|
"It reuses the same xAI API key you just configured for Grok web search.",
|
|
`You can change this later with ${formatCliCommand("openclaw configure --section web")}.`,
|
|
].join("\n"),
|
|
"X search",
|
|
);
|
|
|
|
const enableChoice = await ctx.prompter.select<"yes" | "skip">({
|
|
message: "Enable x_search too?",
|
|
options: [
|
|
{
|
|
value: "yes",
|
|
label: "Yes, enable x_search",
|
|
hint: "Search X posts with the same xAI key",
|
|
},
|
|
{
|
|
value: "skip",
|
|
label: "Skip for now",
|
|
hint: "Keep Grok web_search only",
|
|
},
|
|
],
|
|
initialValue: existingXSearch?.enabled === true || ctx.quickstartDefaults ? "yes" : "skip",
|
|
});
|
|
|
|
if (enableChoice === "skip") {
|
|
return ctx.config;
|
|
}
|
|
|
|
const existingModel =
|
|
typeof existingXSearch?.model === "string" && existingXSearch.model.trim()
|
|
? existingXSearch.model.trim()
|
|
: "";
|
|
const knownModel = X_SEARCH_MODEL_OPTIONS.find((entry) => entry.value === existingModel)?.value;
|
|
const modelPick = await ctx.prompter.select<string>({
|
|
message: "Grok model for x_search",
|
|
options: [
|
|
...X_SEARCH_MODEL_OPTIONS,
|
|
{ value: "__custom__", label: "Enter custom model name", hint: "" },
|
|
],
|
|
initialValue: knownModel ?? XAI_DEFAULT_X_SEARCH_MODEL,
|
|
});
|
|
|
|
let model = modelPick;
|
|
if (modelPick === "__custom__") {
|
|
const customModel = await ctx.prompter.text({
|
|
message: "Custom Grok model name",
|
|
initialValue: existingModel || XAI_DEFAULT_X_SEARCH_MODEL,
|
|
placeholder: XAI_DEFAULT_X_SEARCH_MODEL,
|
|
});
|
|
model = customModel.trim() || XAI_DEFAULT_X_SEARCH_MODEL;
|
|
}
|
|
|
|
return applyXSearchSetupConfig(ctx.config, {
|
|
enabled: true,
|
|
model: model || XAI_DEFAULT_X_SEARCH_MODEL,
|
|
});
|
|
}
|
|
|
|
function runXaiWebSearch(params: {
|
|
query: string;
|
|
model: string;
|
|
apiKey: string;
|
|
timeoutSeconds: number;
|
|
inlineCitations: boolean;
|
|
cacheTtlMs: number;
|
|
}): Promise<Record<string, unknown>> {
|
|
const cacheKey = normalizeCacheKey(
|
|
`grok:${params.model}:${String(params.inlineCitations)}:${params.query}`,
|
|
);
|
|
const cached = readCache(XAI_WEB_SEARCH_CACHE, cacheKey);
|
|
if (cached) {
|
|
return Promise.resolve({ ...cached.value, cached: true });
|
|
}
|
|
|
|
return (async () => {
|
|
const startedAt = Date.now();
|
|
const result = await requestXaiWebSearch({
|
|
query: params.query,
|
|
model: params.model,
|
|
apiKey: params.apiKey,
|
|
timeoutSeconds: params.timeoutSeconds,
|
|
inlineCitations: params.inlineCitations,
|
|
});
|
|
const payload = buildXaiWebSearchPayload({
|
|
query: params.query,
|
|
provider: "grok",
|
|
model: params.model,
|
|
tookMs: Date.now() - startedAt,
|
|
content: result.content,
|
|
citations: result.citations,
|
|
inlineCitations: result.inlineCitations,
|
|
});
|
|
|
|
writeCache(XAI_WEB_SEARCH_CACHE, cacheKey, payload, params.cacheTtlMs);
|
|
return payload;
|
|
})();
|
|
}
|
|
|
|
function resolveXaiToolSearchConfig(ctx: {
|
|
config?: Record<string, unknown>;
|
|
searchConfig?: Record<string, unknown>;
|
|
}): SearchConfigRecord | undefined {
|
|
return mergeScopedSearchConfig(
|
|
ctx.searchConfig as SearchConfigRecord | undefined,
|
|
"grok",
|
|
resolveProviderWebSearchPluginConfig(ctx.config, "xai"),
|
|
) as SearchConfigRecord | undefined;
|
|
}
|
|
|
|
function resolveXaiWebSearchCredential(searchConfig?: SearchConfigRecord): string | undefined {
|
|
return resolveWebSearchProviderCredential({
|
|
credentialValue: getScopedCredentialValue(searchConfig, "grok"),
|
|
path: "tools.web.search.grok.apiKey",
|
|
envVars: ["XAI_API_KEY"],
|
|
});
|
|
}
|
|
|
|
export function createXaiWebSearchProvider(): WebSearchProviderPlugin {
|
|
return {
|
|
id: "grok",
|
|
label: "Grok (xAI)",
|
|
hint: "Requires xAI API key · xAI web-grounded responses",
|
|
onboardingScopes: ["text-inference"],
|
|
credentialLabel: "xAI API key",
|
|
envVars: ["XAI_API_KEY"],
|
|
placeholder: "xai-...",
|
|
signupUrl: "https://console.x.ai/",
|
|
docsUrl: "https://docs.openclaw.ai/tools/web",
|
|
autoDetectOrder: 30,
|
|
credentialPath: "plugins.entries.xai.config.webSearch.apiKey",
|
|
inactiveSecretPaths: ["plugins.entries.xai.config.webSearch.apiKey"],
|
|
getCredentialValue: (searchConfig?: Record<string, unknown>) =>
|
|
getScopedCredentialValue(searchConfig, "grok"),
|
|
setCredentialValue: (searchConfigTarget: Record<string, unknown>, value: unknown) =>
|
|
setScopedCredentialValue(searchConfigTarget, "grok", value),
|
|
getConfiguredCredentialValue: (config) =>
|
|
resolveProviderWebSearchPluginConfig(config, "xai")?.apiKey,
|
|
setConfiguredCredentialValue: (configTarget, value) => {
|
|
setProviderWebSearchPluginConfigValue(configTarget, "xai", "apiKey", value);
|
|
},
|
|
runSetup: runXaiSearchProviderSetup,
|
|
createTool: (ctx) => {
|
|
const searchConfig = resolveXaiToolSearchConfig(ctx);
|
|
return {
|
|
description:
|
|
"Search the web using xAI Grok. Returns AI-synthesized answers with citations from real-time web search.",
|
|
parameters: Type.Object({
|
|
query: Type.String({ description: "Search query string." }),
|
|
count: Type.Optional(
|
|
Type.Number({
|
|
description: "Number of results to return (1-10).",
|
|
minimum: 1,
|
|
maximum: 10,
|
|
}),
|
|
),
|
|
}),
|
|
execute: async (args: Record<string, unknown>) => {
|
|
const apiKey = resolveXaiWebSearchCredential(searchConfig);
|
|
|
|
if (!apiKey) {
|
|
return {
|
|
error: "missing_xai_api_key",
|
|
message:
|
|
"web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.",
|
|
docs: "https://docs.openclaw.ai/tools/web",
|
|
};
|
|
}
|
|
|
|
const query = readStringParam(args, "query", { required: true });
|
|
void readNumberParam(args, "count", { integer: true });
|
|
|
|
return await runXaiWebSearch({
|
|
query,
|
|
model: resolveXaiWebSearchModel(searchConfig),
|
|
apiKey,
|
|
timeoutSeconds: resolveTimeoutSeconds(
|
|
(searchConfig?.timeoutSeconds as number | undefined) ?? undefined,
|
|
DEFAULT_TIMEOUT_SECONDS,
|
|
),
|
|
inlineCitations: resolveXaiInlineCitations(searchConfig),
|
|
cacheTtlMs: resolveCacheTtlMs(
|
|
(searchConfig?.cacheTtlMinutes as number | undefined) ?? undefined,
|
|
DEFAULT_CACHE_TTL_MINUTES,
|
|
),
|
|
});
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
export const __testing = {
|
|
buildXaiWebSearchPayload,
|
|
extractXaiWebSearchContent,
|
|
resolveXaiToolSearchConfig,
|
|
resolveXaiInlineCitations,
|
|
resolveXaiWebSearchCredential,
|
|
resolveXaiWebSearchModel,
|
|
requestXaiWebSearch,
|
|
};
|