mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
refactor: dedupe xai x search tool helpers
This commit is contained in:
@@ -18,6 +18,10 @@ import { resolveEffectiveXSearchConfig } from "./src/x-search-config.js";
|
||||
import { wrapXaiProviderStream } from "./stream.js";
|
||||
import { buildXaiVideoGenerationProvider } from "./video-generation-provider.js";
|
||||
import { createXaiWebSearchProvider } from "./web-search.js";
|
||||
import {
|
||||
buildMissingXSearchApiKeyPayload,
|
||||
createXSearchToolDefinition,
|
||||
} from "./x-search-tool-shared.js";
|
||||
|
||||
const PROVIDER_ID = "xai";
|
||||
const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({
|
||||
@@ -117,53 +121,17 @@ function createLazyXSearchTool(ctx: {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
label: "X Search",
|
||||
name: "x_search",
|
||||
description:
|
||||
"Search X (formerly Twitter) using xAI, including targeted post or thread lookups. For per-post stats like reposts, replies, bookmarks, or views, prefer the exact post URL or status ID.",
|
||||
parameters: Type.Object({
|
||||
query: Type.String({ description: "X search query string." }),
|
||||
allowed_x_handles: Type.Optional(
|
||||
Type.Array(Type.String({ minLength: 1 }), {
|
||||
description: "Only include posts from these X handles.",
|
||||
}),
|
||||
),
|
||||
excluded_x_handles: Type.Optional(
|
||||
Type.Array(Type.String({ minLength: 1 }), {
|
||||
description: "Exclude posts from these X handles.",
|
||||
}),
|
||||
),
|
||||
from_date: Type.Optional(
|
||||
Type.String({ description: "Only include posts on or after this date (YYYY-MM-DD)." }),
|
||||
),
|
||||
to_date: Type.Optional(
|
||||
Type.String({ description: "Only include posts on or before this date (YYYY-MM-DD)." }),
|
||||
),
|
||||
enable_image_understanding: Type.Optional(
|
||||
Type.Boolean({ description: "Allow xAI to inspect images attached to matching posts." }),
|
||||
),
|
||||
enable_video_understanding: Type.Optional(
|
||||
Type.Boolean({ description: "Allow xAI to inspect videos attached to matching posts." }),
|
||||
),
|
||||
}),
|
||||
execute: async (toolCallId: string, args: Record<string, unknown>) => {
|
||||
const { createXSearchTool } = await import("./x-search.js");
|
||||
const tool = createXSearchTool({
|
||||
config: ctx.config as never,
|
||||
runtimeConfig: (ctx.runtimeConfig as never) ?? null,
|
||||
});
|
||||
if (!tool) {
|
||||
return jsonResult({
|
||||
error: "missing_xai_api_key",
|
||||
message:
|
||||
"x_search 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",
|
||||
});
|
||||
}
|
||||
return await tool.execute(toolCallId, args);
|
||||
},
|
||||
};
|
||||
return createXSearchToolDefinition(async (toolCallId: string, args: Record<string, unknown>) => {
|
||||
const { createXSearchTool } = await import("./x-search.js");
|
||||
const tool = createXSearchTool({
|
||||
config: ctx.config as never,
|
||||
runtimeConfig: (ctx.runtimeConfig as never) ?? null,
|
||||
});
|
||||
if (!tool) {
|
||||
return jsonResult(buildMissingXSearchApiKeyPayload());
|
||||
}
|
||||
return await tool.execute(toolCallId, args);
|
||||
});
|
||||
}
|
||||
|
||||
export default defineSingleProviderPluginEntry({
|
||||
|
||||
47
extensions/xai/x-search-tool-shared.ts
Normal file
47
extensions/xai/x-search-tool-shared.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
export function buildMissingXSearchApiKeyPayload() {
|
||||
return {
|
||||
error: "missing_xai_api_key",
|
||||
message:
|
||||
"x_search 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",
|
||||
};
|
||||
}
|
||||
|
||||
export function createXSearchToolDefinition(
|
||||
execute: (toolCallId: string, args: Record<string, unknown>) => Promise<unknown>,
|
||||
) {
|
||||
return {
|
||||
label: "X Search",
|
||||
name: "x_search",
|
||||
description:
|
||||
"Search X (formerly Twitter) using xAI, including targeted post or thread lookups. For per-post stats like reposts, replies, bookmarks, or views, prefer the exact post URL or status ID.",
|
||||
parameters: Type.Object({
|
||||
query: Type.String({ description: "X search query string." }),
|
||||
allowed_x_handles: Type.Optional(
|
||||
Type.Array(Type.String({ minLength: 1 }), {
|
||||
description: "Only include posts from these X handles.",
|
||||
}),
|
||||
),
|
||||
excluded_x_handles: Type.Optional(
|
||||
Type.Array(Type.String({ minLength: 1 }), {
|
||||
description: "Exclude posts from these X handles.",
|
||||
}),
|
||||
),
|
||||
from_date: Type.Optional(
|
||||
Type.String({ description: "Only include posts on or after this date (YYYY-MM-DD)." }),
|
||||
),
|
||||
to_date: Type.Optional(
|
||||
Type.String({ description: "Only include posts on or before this date (YYYY-MM-DD)." }),
|
||||
),
|
||||
enable_image_understanding: Type.Optional(
|
||||
Type.Boolean({ description: "Allow xAI to inspect images attached to matching posts." }),
|
||||
),
|
||||
enable_video_understanding: Type.Optional(
|
||||
Type.Boolean({ description: "Allow xAI to inspect videos attached to matching posts." }),
|
||||
),
|
||||
}),
|
||||
execute,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { getRuntimeConfigSnapshot, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
jsonResult,
|
||||
@@ -19,6 +18,10 @@ import {
|
||||
resolveXaiXSearchModel,
|
||||
type XaiXSearchOptions,
|
||||
} from "./src/x-search-shared.js";
|
||||
import {
|
||||
buildMissingXSearchApiKeyPayload,
|
||||
createXSearchToolDefinition,
|
||||
} from "./x-search-tool-shared.js";
|
||||
|
||||
class PluginToolInputError extends Error {
|
||||
constructor(message: string) {
|
||||
@@ -132,116 +135,80 @@ export function createXSearchTool(options?: {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
label: "X Search",
|
||||
name: "x_search",
|
||||
description:
|
||||
"Search X (formerly Twitter) using xAI, including targeted post or thread lookups. For per-post stats like reposts, replies, bookmarks, or views, prefer the exact post URL or status ID.",
|
||||
parameters: Type.Object({
|
||||
query: Type.String({ description: "X search query string." }),
|
||||
allowed_x_handles: Type.Optional(
|
||||
Type.Array(Type.String({ minLength: 1 }), {
|
||||
description: "Only include posts from these X handles.",
|
||||
}),
|
||||
),
|
||||
excluded_x_handles: Type.Optional(
|
||||
Type.Array(Type.String({ minLength: 1 }), {
|
||||
description: "Exclude posts from these X handles.",
|
||||
}),
|
||||
),
|
||||
from_date: Type.Optional(
|
||||
Type.String({ description: "Only include posts on or after this date (YYYY-MM-DD)." }),
|
||||
),
|
||||
to_date: Type.Optional(
|
||||
Type.String({ description: "Only include posts on or before this date (YYYY-MM-DD)." }),
|
||||
),
|
||||
enable_image_understanding: Type.Optional(
|
||||
Type.Boolean({ description: "Allow xAI to inspect images attached to matching posts." }),
|
||||
),
|
||||
enable_video_understanding: Type.Optional(
|
||||
Type.Boolean({ description: "Allow xAI to inspect videos attached to matching posts." }),
|
||||
),
|
||||
}),
|
||||
execute: async (_toolCallId: string, args: Record<string, unknown>) => {
|
||||
const apiKey = resolveXSearchApiKey({
|
||||
sourceConfig: options?.config,
|
||||
runtimeConfig: runtimeConfig ?? undefined,
|
||||
});
|
||||
if (!apiKey) {
|
||||
return jsonResult({
|
||||
error: "missing_xai_api_key",
|
||||
message:
|
||||
"x_search 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",
|
||||
});
|
||||
}
|
||||
return createXSearchToolDefinition(async (_toolCallId: string, args: Record<string, unknown>) => {
|
||||
const apiKey = resolveXSearchApiKey({
|
||||
sourceConfig: options?.config,
|
||||
runtimeConfig: runtimeConfig ?? undefined,
|
||||
});
|
||||
if (!apiKey) {
|
||||
return jsonResult(buildMissingXSearchApiKeyPayload());
|
||||
}
|
||||
|
||||
const query = readStringParam(args, "query", { required: true });
|
||||
const allowedXHandles = readStringArrayParam(args, "allowed_x_handles");
|
||||
const excludedXHandles = readStringArrayParam(args, "excluded_x_handles");
|
||||
const fromDate = normalizeOptionalIsoDate(readStringParam(args, "from_date"), "from_date");
|
||||
const toDate = normalizeOptionalIsoDate(readStringParam(args, "to_date"), "to_date");
|
||||
if (fromDate && toDate && fromDate > toDate) {
|
||||
throw new PluginToolInputError("from_date must be on or before to_date");
|
||||
}
|
||||
const query = readStringParam(args, "query", { required: true });
|
||||
const allowedXHandles = readStringArrayParam(args, "allowed_x_handles");
|
||||
const excludedXHandles = readStringArrayParam(args, "excluded_x_handles");
|
||||
const fromDate = normalizeOptionalIsoDate(readStringParam(args, "from_date"), "from_date");
|
||||
const toDate = normalizeOptionalIsoDate(readStringParam(args, "to_date"), "to_date");
|
||||
if (fromDate && toDate && fromDate > toDate) {
|
||||
throw new PluginToolInputError("from_date must be on or before to_date");
|
||||
}
|
||||
|
||||
const xSearchOptions: XaiXSearchOptions = {
|
||||
query,
|
||||
const xSearchOptions: XaiXSearchOptions = {
|
||||
query,
|
||||
allowedXHandles,
|
||||
excludedXHandles,
|
||||
fromDate,
|
||||
toDate,
|
||||
enableImageUnderstanding: args.enable_image_understanding === true,
|
||||
enableVideoUnderstanding: args.enable_video_understanding === true,
|
||||
};
|
||||
const xSearchConfigRecord = xSearchConfig;
|
||||
const model = resolveXaiXSearchModel(xSearchConfigRecord);
|
||||
const inlineCitations = resolveXaiXSearchInlineCitations(xSearchConfigRecord);
|
||||
const maxTurns = resolveXaiXSearchMaxTurns(xSearchConfigRecord);
|
||||
const cacheKey = buildXSearchCacheKey({
|
||||
query,
|
||||
model,
|
||||
inlineCitations,
|
||||
maxTurns,
|
||||
options: {
|
||||
allowedXHandles,
|
||||
excludedXHandles,
|
||||
fromDate,
|
||||
toDate,
|
||||
enableImageUnderstanding: args.enable_image_understanding === true,
|
||||
enableVideoUnderstanding: args.enable_video_understanding === true,
|
||||
};
|
||||
const xSearchConfigRecord = xSearchConfig;
|
||||
const model = resolveXaiXSearchModel(xSearchConfigRecord);
|
||||
const inlineCitations = resolveXaiXSearchInlineCitations(xSearchConfigRecord);
|
||||
const maxTurns = resolveXaiXSearchMaxTurns(xSearchConfigRecord);
|
||||
const cacheKey = buildXSearchCacheKey({
|
||||
query,
|
||||
model,
|
||||
inlineCitations,
|
||||
maxTurns,
|
||||
options: {
|
||||
allowedXHandles,
|
||||
excludedXHandles,
|
||||
fromDate,
|
||||
toDate,
|
||||
enableImageUnderstanding: xSearchOptions.enableImageUnderstanding,
|
||||
enableVideoUnderstanding: xSearchOptions.enableVideoUnderstanding,
|
||||
},
|
||||
});
|
||||
const cached = readCache(X_SEARCH_CACHE, cacheKey);
|
||||
if (cached) {
|
||||
return jsonResult({ ...cached.value, cached: true });
|
||||
}
|
||||
enableImageUnderstanding: xSearchOptions.enableImageUnderstanding,
|
||||
enableVideoUnderstanding: xSearchOptions.enableVideoUnderstanding,
|
||||
},
|
||||
});
|
||||
const cached = readCache(X_SEARCH_CACHE, cacheKey);
|
||||
if (cached) {
|
||||
return jsonResult({ ...cached.value, cached: true });
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
const result = await requestXaiXSearch({
|
||||
apiKey,
|
||||
model,
|
||||
timeoutSeconds: resolveTimeoutSeconds(xSearchConfig?.timeoutSeconds, 30),
|
||||
inlineCitations,
|
||||
maxTurns,
|
||||
options: xSearchOptions,
|
||||
});
|
||||
const payload = buildXaiXSearchPayload({
|
||||
query,
|
||||
model,
|
||||
tookMs: Date.now() - startedAt,
|
||||
content: result.content,
|
||||
citations: result.citations,
|
||||
inlineCitations: result.inlineCitations,
|
||||
options: xSearchOptions,
|
||||
});
|
||||
writeCache(
|
||||
X_SEARCH_CACHE,
|
||||
cacheKey,
|
||||
payload,
|
||||
resolveCacheTtlMs(xSearchConfig?.cacheTtlMinutes, 15),
|
||||
);
|
||||
return jsonResult(payload);
|
||||
},
|
||||
};
|
||||
const startedAt = Date.now();
|
||||
const result = await requestXaiXSearch({
|
||||
apiKey,
|
||||
model,
|
||||
timeoutSeconds: resolveTimeoutSeconds(xSearchConfig?.timeoutSeconds, 30),
|
||||
inlineCitations,
|
||||
maxTurns,
|
||||
options: xSearchOptions,
|
||||
});
|
||||
const payload = buildXaiXSearchPayload({
|
||||
query,
|
||||
model,
|
||||
tookMs: Date.now() - startedAt,
|
||||
content: result.content,
|
||||
citations: result.citations,
|
||||
inlineCitations: result.inlineCitations,
|
||||
options: xSearchOptions,
|
||||
});
|
||||
writeCache(
|
||||
X_SEARCH_CACHE,
|
||||
cacheKey,
|
||||
payload,
|
||||
resolveCacheTtlMs(xSearchConfig?.cacheTtlMinutes, 15),
|
||||
);
|
||||
return jsonResult(payload);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user