UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

340 lines (339 loc) 14.7 kB
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js"; import { b as readStringParam, g as readPositiveIntegerParam, v as readStringArrayParam } from "./common-C4yy9V-D.js"; import { a as wrapWebContent } from "./external-content-pX-Pk1Iu.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import { S as writeCachedSearchPayload, _ as resolveSearchTimeoutSeconds, c as normalizeToIsoDate, f as readCachedSearchPayload, g as resolveSearchCount, h as resolveSearchCacheTtlMs, i as buildSearchCacheKey, m as readProviderEnvValue, o as isoToPerplexityDate, p as readConfiguredSecretString, s as normalizeFreshness, v as resolveSiteName, x as withTrustedWebSearchEndpoint, y as throwWebSearchApiError } from "./web-search-provider-common-DisZTXTb.js"; import "./provider-web-search-9G87vZMY.js"; import { a as isDirectPerplexityBaseUrl, i as inferPerplexityBaseUrlFromApiKey, n as PERPLEXITY_DIRECT_BASE_URL, t as DEFAULT_PERPLEXITY_BASE_URL } from "./perplexity-web-search-provider.shared-NjcTiVAb.js"; //#region extensions/perplexity/src/perplexity-web-search-provider.runtime.ts const PERPLEXITY_SEARCH_ENDPOINT = "https://api.perplexity.ai/search"; const DEFAULT_PERPLEXITY_MODEL = "perplexity/sonar-pro"; function resolvePerplexityConfig(searchConfig) { const perplexity = searchConfig?.perplexity; return perplexity && typeof perplexity === "object" && !Array.isArray(perplexity) ? perplexity : {}; } function resolvePerplexityApiKey(perplexity) { const fromConfig = readConfiguredSecretString(perplexity?.apiKey, "tools.web.search.perplexity.apiKey"); if (fromConfig) return { apiKey: fromConfig, source: "config" }; const fromPerplexityEnv = readProviderEnvValue(["PERPLEXITY_API_KEY"]); if (fromPerplexityEnv) return { apiKey: fromPerplexityEnv, source: "perplexity_env" }; const fromOpenRouterEnv = readProviderEnvValue(["OPENROUTER_API_KEY"]); if (fromOpenRouterEnv) return { apiKey: fromOpenRouterEnv, source: "openrouter_env" }; return { apiKey: void 0, source: "none" }; } function resolvePerplexityBaseUrl(perplexity, authSource = "none", configuredKey) { const fromConfig = normalizeOptionalString(perplexity?.baseUrl) ?? ""; if (fromConfig) return fromConfig; if (authSource === "perplexity_env") return PERPLEXITY_DIRECT_BASE_URL; if (authSource === "openrouter_env") return DEFAULT_PERPLEXITY_BASE_URL; if (authSource === "config") return inferPerplexityBaseUrlFromApiKey(configuredKey) === "openrouter" ? DEFAULT_PERPLEXITY_BASE_URL : PERPLEXITY_DIRECT_BASE_URL; return DEFAULT_PERPLEXITY_BASE_URL; } function resolvePerplexityModel(perplexity) { return (normalizeOptionalString(perplexity?.model) ?? "") || DEFAULT_PERPLEXITY_MODEL; } function resolvePerplexityRequestModel(baseUrl, model) { if (!isDirectPerplexityBaseUrl(baseUrl)) return model; return model.startsWith("perplexity/") ? model.slice(11) : model; } function buildPerplexityRequestHeaders(apiKey, acceptJson = false) { return { "Content-Type": "application/json", ...acceptJson ? { Accept: "application/json" } : {}, Authorization: `Bearer ${apiKey}`, "HTTP-Referer": "https://openclaw.ai", "X-Title": "OpenClaw Web Search" }; } async function readPerplexityJsonResponse(response, label) { try { return await response.json(); } catch (cause) { throw new Error(`${label}: malformed JSON response`, { cause }); } } function resolvePerplexityTransport(perplexity) { const auth = resolvePerplexityApiKey(perplexity); const baseUrl = resolvePerplexityBaseUrl(perplexity, auth.source, auth.apiKey); const model = resolvePerplexityModel(perplexity); const hasLegacyOverride = Boolean(normalizeOptionalString(perplexity?.baseUrl) || normalizeOptionalString(perplexity?.model)); return { ...auth, baseUrl, model, transport: hasLegacyOverride || !isDirectPerplexityBaseUrl(baseUrl) ? "chat_completions" : "search_api" }; } function extractPerplexityCitations(data) { const topLevel = (data.citations ?? []).filter((url) => Boolean(normalizeOptionalString(url))); if (topLevel.length > 0) return uniqueStrings(topLevel); const citations = []; for (const choice of data.choices ?? []) for (const annotation of choice.message?.annotations ?? []) { if (annotation.type !== "url_citation") continue; const normalizedUrl = normalizeOptionalString(typeof annotation.url_citation?.url === "string" ? annotation.url_citation.url : typeof annotation.url === "string" ? annotation.url : void 0); if (normalizedUrl) citations.push(normalizedUrl); } return uniqueStrings(citations); } async function runPerplexitySearchApi(params) { const body = { query: params.query, max_results: params.count }; if (params.country) body.country = params.country; if (params.searchDomainFilter?.length) body.search_domain_filter = params.searchDomainFilter; if (params.searchRecencyFilter) body.search_recency_filter = params.searchRecencyFilter; if (params.searchLanguageFilter?.length) body.search_language_filter = params.searchLanguageFilter; if (params.searchAfterDate) body.search_after_date = params.searchAfterDate; if (params.searchBeforeDate) body.search_before_date = params.searchBeforeDate; if (params.maxTokens !== void 0) body.max_tokens = params.maxTokens; if (params.maxTokensPerPage !== void 0) body.max_tokens_per_page = params.maxTokensPerPage; return withTrustedWebSearchEndpoint({ url: PERPLEXITY_SEARCH_ENDPOINT, timeoutSeconds: params.timeoutSeconds, init: { method: "POST", headers: buildPerplexityRequestHeaders(params.apiKey, true), body: JSON.stringify(body) } }, async (res) => { if (!res.ok) return await throwWebSearchApiError(res, "Perplexity Search"); return ((await readPerplexityJsonResponse(res, "Perplexity Search")).results ?? []).map((entry) => ({ title: entry.title ? wrapWebContent(entry.title, "web_search") : "", url: entry.url ?? "", description: entry.snippet ? wrapWebContent(entry.snippet, "web_search") : "", published: entry.date ?? void 0, siteName: resolveSiteName(entry.url) || void 0 })); }); } async function runPerplexitySearch(params) { const endpoint = `${params.baseUrl.trim().replace(/\/$/, "")}/chat/completions`; const body = { model: resolvePerplexityRequestModel(params.baseUrl, params.model), messages: [{ role: "user", content: params.query }] }; if (params.freshness) body.search_recency_filter = params.freshness; return withTrustedWebSearchEndpoint({ url: endpoint, timeoutSeconds: params.timeoutSeconds, init: { method: "POST", headers: buildPerplexityRequestHeaders(params.apiKey), body: JSON.stringify(body) } }, async (res) => { if (!res.ok) return await throwWebSearchApiError(res, "Perplexity"); const data = await readPerplexityJsonResponse(res, "Perplexity"); return { content: data.choices?.[0]?.message?.content ?? "No response", citations: extractPerplexityCitations(data) }; }); } async function executePerplexitySearch(args, searchConfig) { const runtime = resolvePerplexityTransport(resolvePerplexityConfig(searchConfig)); if (!runtime.apiKey) return { error: "missing_perplexity_api_key", message: "web_search (perplexity) needs an API key. Set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment, or configure tools.web.search.perplexity.apiKey. If you do not want to configure a search API key, use web_fetch for a specific URL or the browser tool for interactive pages.", docs: "https://docs.openclaw.ai/tools/web" }; const query = readStringParam(args, "query", { required: true }); const count = readPositiveIntegerParam(args, "count", { max: 10, message: `count must be an integer from 1 to 10.` }) ?? searchConfig?.maxResults ?? void 0; const rawFreshness = readStringParam(args, "freshness"); const freshness = rawFreshness ? normalizeFreshness(rawFreshness, "perplexity") : void 0; if (rawFreshness && !freshness) return { error: "invalid_freshness", message: "freshness must be day, week, month, or year.", docs: "https://docs.openclaw.ai/tools/web" }; const structured = runtime.transport === "search_api"; const country = readStringParam(args, "country"); const language = readStringParam(args, "language"); const rawDateAfter = readStringParam(args, "date_after"); const rawDateBefore = readStringParam(args, "date_before"); const domainFilter = readStringArrayParam(args, "domain_filter"); const maxTokens = readPositiveIntegerParam(args, "max_tokens", { max: 1e6, message: "max_tokens must be a positive integer." }); const maxTokensPerPage = readPositiveIntegerParam(args, "max_tokens_per_page", { message: "max_tokens_per_page must be a positive integer." }); if (!structured) { if (country) return { error: "unsupported_country", message: "country filtering is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it.", docs: "https://docs.openclaw.ai/tools/web" }; if (language) return { error: "unsupported_language", message: "language filtering is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it.", docs: "https://docs.openclaw.ai/tools/web" }; if (rawDateAfter || rawDateBefore) return { error: "unsupported_date_filter", message: "date_after/date_before are only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable them.", docs: "https://docs.openclaw.ai/tools/web" }; if (domainFilter?.length) return { error: "unsupported_domain_filter", message: "domain_filter is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it.", docs: "https://docs.openclaw.ai/tools/web" }; if (maxTokens !== void 0 || maxTokensPerPage !== void 0) return { error: "unsupported_content_budget", message: "max_tokens and max_tokens_per_page are only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable them.", docs: "https://docs.openclaw.ai/tools/web" }; } if (language && !/^[a-z]{2}$/iu.test(language)) return { error: "invalid_language", message: "language must be a 2-letter ISO 639-1 code like 'en', 'de', or 'fr'.", docs: "https://docs.openclaw.ai/tools/web" }; if (rawFreshness && (rawDateAfter || rawDateBefore)) return { error: "conflicting_time_filters", message: "freshness and date_after/date_before cannot be used together. Use either freshness (day/week/month/year) or a date range (date_after/date_before), not both.", docs: "https://docs.openclaw.ai/tools/web" }; const dateAfter = rawDateAfter ? normalizeToIsoDate(rawDateAfter) : void 0; const dateBefore = rawDateBefore ? normalizeToIsoDate(rawDateBefore) : void 0; if (rawDateAfter && !dateAfter) return { error: "invalid_date", message: "date_after must be YYYY-MM-DD format.", docs: "https://docs.openclaw.ai/tools/web" }; if (rawDateBefore && !dateBefore) return { error: "invalid_date", message: "date_before must be YYYY-MM-DD format.", docs: "https://docs.openclaw.ai/tools/web" }; if (dateAfter && dateBefore && dateAfter > dateBefore) return { error: "invalid_date_range", message: "date_after must be before date_before.", docs: "https://docs.openclaw.ai/tools/web" }; if (domainFilter?.length) { const hasDeny = domainFilter.some((entry) => entry.startsWith("-")); const hasAllow = domainFilter.some((entry) => !entry.startsWith("-")); if (hasDeny && hasAllow) return { error: "invalid_domain_filter", message: "domain_filter cannot mix allowlist and denylist entries. Use either all positive entries (allowlist) or all entries prefixed with '-' (denylist).", docs: "https://docs.openclaw.ai/tools/web" }; if (domainFilter.length > 20) return { error: "invalid_domain_filter", message: "domain_filter supports a maximum of 20 domains.", docs: "https://docs.openclaw.ai/tools/web" }; } const cacheKey = buildSearchCacheKey([ "perplexity", runtime.transport, runtime.baseUrl, runtime.model, query, resolveSearchCount(count, 5), country, language, freshness, dateAfter, dateBefore, domainFilter?.join(","), maxTokens, maxTokensPerPage ]); const cached = readCachedSearchPayload(cacheKey); if (cached) return cached; const start = Date.now(); const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig); const payload = runtime.transport === "chat_completions" ? { query, provider: "perplexity", model: runtime.model, tookMs: Date.now() - start, externalContent: { untrusted: true, source: "web_search", provider: "perplexity", wrapped: true }, ...await (async () => { const result = await runPerplexitySearch({ query, apiKey: runtime.apiKey, baseUrl: runtime.baseUrl, model: runtime.model, timeoutSeconds, freshness }); return { content: wrapWebContent(result.content, "web_search"), citations: result.citations }; })() } : { query, provider: "perplexity", count: 0, tookMs: Date.now() - start, externalContent: { untrusted: true, source: "web_search", provider: "perplexity", wrapped: true }, results: await runPerplexitySearchApi({ query, apiKey: runtime.apiKey, count: resolveSearchCount(count, 5), timeoutSeconds, country: country ?? void 0, searchDomainFilter: domainFilter, searchRecencyFilter: freshness, searchLanguageFilter: language ? [language] : void 0, searchAfterDate: dateAfter ? isoToPerplexityDate(dateAfter) : void 0, searchBeforeDate: dateBefore ? isoToPerplexityDate(dateBefore) : void 0, maxTokens: maxTokens ?? void 0, maxTokensPerPage: maxTokensPerPage ?? void 0 }) }; if (Array.isArray(payload.results)) { payload.count = payload.results.length; payload.tookMs = Date.now() - start; } else payload.tookMs = Date.now() - start; writeCachedSearchPayload(cacheKey, payload, resolveSearchCacheTtlMs(searchConfig)); return payload; } const testing = { inferPerplexityBaseUrlFromApiKey, resolvePerplexityBaseUrl, resolvePerplexityModel, resolvePerplexityTransport, isDirectPerplexityBaseUrl, resolvePerplexityRequestModel, resolvePerplexityApiKey, readPerplexityJsonResponse, normalizeToIsoDate, isoToPerplexityDate }; //#endregion export { testing as __testing, testing, executePerplexitySearch };