UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

357 lines (356 loc) 15.3 kB
import { c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js"; import { b as parseStrictPositiveInteger } from "./number-coercion-CJQ8TR--.js"; import { b as readStringParam, g as readPositiveIntegerParam } from "./common-C4yy9V-D.js"; import "./number-runtime-DBLVDypr.js"; import { a as wrapWebContent } from "./external-content-pX-Pk1Iu.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import { S as writeCachedSearchPayload, _ as resolveSearchTimeoutSeconds, f as readCachedSearchPayload, h as resolveSearchCacheTtlMs, i as buildSearchCacheKey, l as parseIsoDateRange, m as readProviderEnvValue, p as readConfiguredSecretString, v as resolveSiteName, x as withTrustedWebSearchEndpoint } from "./web-search-provider-common-DisZTXTb.js"; import { i as resolveProviderWebSearchPluginConfig, r as mergeScopedSearchConfig } from "./web-search-provider-config-BQzMMhw8.js"; import "./provider-web-search-9G87vZMY.js"; //#region extensions/exa/src/exa-web-search-provider.runtime.ts const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search"; const EXA_SEARCH_TYPES = [ "auto", "neural", "fast", "deep", "deep-reasoning", "instant" ]; const EXA_FRESHNESS_VALUES = [ "day", "week", "month", "year" ]; const EXA_MAX_SEARCH_COUNT = 100; async function readExaSearchResults(response) { try { return normalizeExaResults(await response.json()); } catch (cause) { throw new Error("Exa API returned malformed JSON", { cause }); } } function normalizeExaFreshness(value) { const trimmed = normalizeOptionalLowercaseString(value); if (!trimmed) return; return EXA_FRESHNESS_VALUES.includes(trimmed) ? trimmed : void 0; } function resolveExaConfig(searchConfig) { const exa = searchConfig?.exa; return exa && typeof exa === "object" && !Array.isArray(exa) ? exa : {}; } function resolveExaApiKey(exa) { return readConfiguredSecretString(exa?.apiKey, "tools.web.search.exa.apiKey") ?? readProviderEnvValue(["EXA_API_KEY"]); } function invalidBaseUrlPayload(value) { return { error: "invalid_base_url", message: `plugins.entries.exa.config.webSearch.baseUrl must be a valid http(s) URL. Got: ${value}`, docs: "https://docs.openclaw.ai/tools/exa-search" }; } function resolveExaSearchEndpoint(exa) { const configured = normalizeOptionalString(exa?.baseUrl); if (!configured) return { endpoint: EXA_SEARCH_ENDPOINT }; if (/^[a-z][a-z0-9+.-]*:\/\//i.test(configured) && !/^https?:\/\//i.test(configured)) return invalidBaseUrlPayload(configured); const candidate = /^https?:\/\//i.test(configured) ? configured : `https://${configured}`; let parsed; try { parsed = new URL(candidate); } catch { return invalidBaseUrlPayload(configured); } if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return invalidBaseUrlPayload(configured); const pathname = parsed.pathname.replace(/\/+$/, ""); parsed.pathname = pathname.endsWith("/search") ? pathname : `${pathname === "" ? "" : pathname}/search`; parsed.hash = ""; return { endpoint: parsed.toString() }; } function resolveExaDescription(result) { const highlights = result.highlights; if (Array.isArray(highlights)) { const highlightText = highlights.map((entry) => normalizeOptionalString(entry)).filter((entry) => Boolean(entry)).join("\n"); if (highlightText) return highlightText; } const summary = normalizeOptionalString(result.summary); if (summary) return summary; return normalizeOptionalString(result.text) ?? ""; } function parsePositiveInteger(value) { return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0; } function invalidContentsPayload(message) { return { error: "invalid_contents", message, docs: "https://docs.openclaw.ai/tools/web" }; } function isErrorPayload(value) { return Boolean(value && typeof value === "object" && "error" in value && "message" in value && "docs" in value); } function resolveExaSearchCount(value, fallback) { const parsed = parseStrictPositiveInteger(value); if (parsed === void 0) return fallback; return Math.min(EXA_MAX_SEARCH_COUNT, parsed); } function parseExaContents(rawContents) { if (rawContents === void 0) return { value: void 0 }; if (!rawContents || typeof rawContents !== "object" || Array.isArray(rawContents)) return invalidContentsPayload("contents must be an object with optional text, highlights, and summary fields."); const raw = rawContents; const allowedKeys = new Set([ "text", "highlights", "summary" ]); for (const key of Object.keys(raw)) if (!allowedKeys.has(key)) return invalidContentsPayload(`contents has unknown field "${key}". Only "text", "highlights", and "summary" are allowed.`); const parsed = {}; const parseText = (value) => { if (typeof value === "boolean") return value; if (!value || typeof value !== "object" || Array.isArray(value)) return invalidContentsPayload("contents.text must be a boolean or an object."); const obj = value; for (const key of Object.keys(obj)) if (key !== "maxCharacters") return invalidContentsPayload(`contents.text has unknown field "${key}". Only "maxCharacters" is allowed.`); if ("maxCharacters" in obj && parsePositiveInteger(obj.maxCharacters) === void 0) return invalidContentsPayload("contents.text.maxCharacters must be a positive integer."); return parsePositiveInteger(obj.maxCharacters) ? { maxCharacters: parsePositiveInteger(obj.maxCharacters) } : {}; }; const parseHighlights = (value) => { if (typeof value === "boolean") return value; if (!value || typeof value !== "object" || Array.isArray(value)) return invalidContentsPayload("contents.highlights must be a boolean or an object."); const obj = value; const allowed = new Set([ "maxCharacters", "query", "numSentences", "highlightsPerUrl" ]); for (const key of Object.keys(obj)) if (!allowed.has(key)) return invalidContentsPayload(`contents.highlights has unknown field "${key}". Allowed fields are "maxCharacters", "query", "numSentences", and "highlightsPerUrl".`); if ("maxCharacters" in obj && parsePositiveInteger(obj.maxCharacters) === void 0) return invalidContentsPayload("contents.highlights.maxCharacters must be a positive integer."); if ("numSentences" in obj && parsePositiveInteger(obj.numSentences) === void 0) return invalidContentsPayload("contents.highlights.numSentences must be a positive integer."); if ("highlightsPerUrl" in obj && parsePositiveInteger(obj.highlightsPerUrl) === void 0) return invalidContentsPayload("contents.highlights.highlightsPerUrl must be a positive integer."); if ("query" in obj && typeof obj.query !== "string") return invalidContentsPayload("contents.highlights.query must be a string."); return { ...parsePositiveInteger(obj.maxCharacters) ? { maxCharacters: parsePositiveInteger(obj.maxCharacters) } : {}, ...typeof obj.query === "string" ? { query: obj.query } : {}, ...parsePositiveInteger(obj.numSentences) ? { numSentences: parsePositiveInteger(obj.numSentences) } : {}, ...parsePositiveInteger(obj.highlightsPerUrl) ? { highlightsPerUrl: parsePositiveInteger(obj.highlightsPerUrl) } : {} }; }; const parseSummary = (value) => { if (typeof value === "boolean") return value; if (!value || typeof value !== "object" || Array.isArray(value)) return invalidContentsPayload("contents.summary must be a boolean or an object."); const obj = value; for (const key of Object.keys(obj)) if (key !== "query") return invalidContentsPayload(`contents.summary has unknown field "${key}". Only "query" is allowed.`); if ("query" in obj && typeof obj.query !== "string") return invalidContentsPayload("contents.summary.query must be a string."); return typeof obj.query === "string" ? { query: obj.query } : {}; }; if ("text" in raw) { const parsedText = parseText(raw.text); if (isErrorPayload(parsedText)) return parsedText; parsed.text = parsedText; } if ("highlights" in raw) { const parsedHighlights = parseHighlights(raw.highlights); if (isErrorPayload(parsedHighlights)) return parsedHighlights; parsed.highlights = parsedHighlights; } if ("summary" in raw) { const parsedSummary = parseSummary(raw.summary); if (isErrorPayload(parsedSummary)) return parsedSummary; parsed.summary = parsedSummary; } return { value: parsed }; } function normalizeExaResults(payload) { if (!payload || typeof payload !== "object") return []; const results = payload.results; if (!Array.isArray(results)) return []; return results.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))); } function resolveFreshnessStartDate(freshness) { const now = /* @__PURE__ */ new Date(); if (freshness === "day") { now.setUTCDate(now.getUTCDate() - 1); return now.toISOString(); } if (freshness === "week") { now.setUTCDate(now.getUTCDate() - 7); return now.toISOString(); } if (freshness === "month") { const currentDay = now.getUTCDate(); now.setUTCDate(1); now.setUTCMonth(now.getUTCMonth() - 1); const lastDayOfTargetMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate(); now.setUTCDate(Math.min(currentDay, lastDayOfTargetMonth)); return now.toISOString(); } now.setUTCFullYear(now.getUTCFullYear() - 1); return now.toISOString(); } async function runExaSearch(params) { const body = { query: params.query, numResults: params.count, type: params.type, contents: params.contents ?? { highlights: true } }; if (params.dateAfter) body.startPublishedDate = params.dateAfter; else if (params.freshness) body.startPublishedDate = resolveFreshnessStartDate(params.freshness); if (params.dateBefore) body.endPublishedDate = params.dateBefore; return withTrustedWebSearchEndpoint({ url: params.endpoint, timeoutSeconds: params.timeoutSeconds, init: { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", "x-api-key": params.apiKey, "x-exa-integration": "openclaw" }, body: JSON.stringify(body) } }, async (res) => { if (!res.ok) { const detail = await res.text(); throw new Error(`Exa API error (${res.status}): ${detail || res.statusText}`); } return readExaSearchResults(res); }); } function missingExaKeyPayload() { return { error: "missing_exa_api_key", message: "web_search (exa) needs an Exa API key. Set EXA_API_KEY in the Gateway environment, or configure tools.web.search.exa.apiKey.", docs: "https://docs.openclaw.ai/tools/web" }; } function buildExaCacheKey(params) { return buildSearchCacheKey([ "exa", params.endpoint, params.type, params.query, params.count, params.freshness, params.dateAfter, params.dateBefore, params.contents?.highlights ? JSON.stringify(params.contents.highlights) : void 0, params.contents?.text ? JSON.stringify(params.contents.text) : void 0, params.contents?.summary ? JSON.stringify(params.contents.summary) : void 0 ]); } async function executeExaWebSearchProviderTool(ctx, args) { const searchConfig = mergeScopedSearchConfig(ctx.searchConfig, "exa", resolveProviderWebSearchPluginConfig(ctx.config, "exa")); const params = args; const exaConfig = resolveExaConfig(searchConfig); const apiKey = resolveExaApiKey(exaConfig); if (!apiKey) return missingExaKeyPayload(); const endpointResult = resolveExaSearchEndpoint(exaConfig); if ("error" in endpointResult) return endpointResult; const endpoint = endpointResult.endpoint; const query = readStringParam(params, "query", { required: true }); const rawType = readStringParam(params, "type"); const type = EXA_SEARCH_TYPES.includes(rawType) ? rawType : "auto"; const count = readPositiveIntegerParam(params, "count", { max: EXA_MAX_SEARCH_COUNT, message: `count must be an integer from 1 to ${EXA_MAX_SEARCH_COUNT}.` }) ?? searchConfig?.maxResults ?? void 0; const rawFreshness = readStringParam(params, "freshness"); const freshness = normalizeExaFreshness(rawFreshness); if (rawFreshness && !freshness) return { error: "invalid_freshness", message: "freshness must be one of \"day\", \"week\", \"month\", or \"year\".", docs: "https://docs.openclaw.ai/tools/web" }; const rawDateAfter = readStringParam(params, "date_after"); const rawDateBefore = readStringParam(params, "date_before"); if (freshness && (rawDateAfter || rawDateBefore)) return { error: "conflicting_time_filters", message: "freshness cannot be combined with date_after or date_before. Use one time-filter mode.", docs: "https://docs.openclaw.ai/tools/web" }; const parsedDateRange = parseIsoDateRange({ rawDateAfter, rawDateBefore, invalidDateAfterMessage: "date_after must be YYYY-MM-DD format.", invalidDateBeforeMessage: "date_before must be YYYY-MM-DD format.", invalidDateRangeMessage: "date_after must be earlier than or equal to date_before." }); if ("error" in parsedDateRange) return parsedDateRange; const { dateAfter, dateBefore } = parsedDateRange; const parsedContents = parseExaContents(params.contents); if (isErrorPayload(parsedContents)) return parsedContents; const contents = parsedContents.value && Object.keys(parsedContents.value).length > 0 ? parsedContents.value : void 0; const resolvedCount = resolveExaSearchCount(count, 5); const cacheKey = buildExaCacheKey({ endpoint, type, query, count: resolvedCount, freshness, dateAfter, dateBefore, contents }); const cached = readCachedSearchPayload(cacheKey); if (cached) return cached; const start = Date.now(); const results = await runExaSearch({ apiKey, endpoint, query, count: resolvedCount, freshness, dateAfter, dateBefore, type, contents, timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig) }); const payload = { query, provider: "exa", count: results.length, tookMs: Date.now() - start, externalContent: { untrusted: true, source: "web_search", provider: "exa", wrapped: true }, results: results.map((entry) => { const title = typeof entry.title === "string" ? entry.title : ""; const url = typeof entry.url === "string" ? entry.url : ""; const description = resolveExaDescription(entry); const summary = normalizeOptionalString(entry.summary) ?? ""; const highlightScores = Array.isArray(entry.highlightScores) ? entry.highlightScores.filter((score) => typeof score === "number" && Number.isFinite(score)) : []; const published = typeof entry.publishedDate === "string" && entry.publishedDate ? entry.publishedDate : void 0; return Object.assign({ title: title ? wrapWebContent(title, `web_search`) : ``, url, description: description ? wrapWebContent(description, `web_search`) : ``, published, siteName: resolveSiteName(url) || void 0 }, summary ? { summary: wrapWebContent(summary, `web_search`) } : {}, highlightScores.length > 0 ? { highlightScores } : {}); }) }; writeCachedSearchPayload(cacheKey, payload, resolveSearchCacheTtlMs(searchConfig)); return payload; } const testing = { normalizeExaResults, normalizeExaFreshness, parseExaContents, buildExaCacheKey, resolveExaApiKey, resolveExaConfig, resolveExaDescription, resolveExaSearchCount, resolveExaSearchEndpoint, resolveFreshnessStartDate, readExaSearchResults }; //#endregion export { testing as __testing, testing, executeExaWebSearchProviderTool };