mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
feat: support freshness parameter for Perplexity web_search provider (#15343)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 01aba2bfba
Co-authored-by: echoVic <16428813+echoVic@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -83,3 +83,4 @@ USER.md
|
|||||||
.agent/*.json
|
.agent/*.json
|
||||||
!.agent/workflows/
|
!.agent/workflows/
|
||||||
local/
|
local/
|
||||||
|
package-lock.json
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Cron: add regression coverage for announce-mode isolated jobs so runs that already report `delivered: true` do not enqueue duplicate main-session relays, including delivery configs where `mode` is omitted and defaults to announce. (#15737) Thanks @brandonwise.
|
- Cron: add regression coverage for announce-mode isolated jobs so runs that already report `delivered: true` do not enqueue duplicate main-session relays, including delivery configs where `mode` is omitted and defaults to announce. (#15737) Thanks @brandonwise.
|
||||||
- Cron: honor `deleteAfterRun` in isolated announce delivery by mapping it to subagent announce cleanup mode, so cron run sessions configured for deletion are removed after completion. (#15368) Thanks @arosstale.
|
- Cron: honor `deleteAfterRun` in isolated announce delivery by mapping it to subagent announce cleanup mode, so cron run sessions configured for deletion are removed after completion. (#15368) Thanks @arosstale.
|
||||||
- Web tools/web_fetch: prefer `text/markdown` responses for Cloudflare Markdown for Agents, add `cf-markdown` extraction for markdown bodies, and redact fetched URLs in `x-markdown-tokens` debug logs to avoid leaking raw paths/query params. (#15376) Thanks @Yaxuan42.
|
- Web tools/web_fetch: prefer `text/markdown` responses for Cloudflare Markdown for Agents, add `cf-markdown` extraction for markdown bodies, and redact fetched URLs in `x-markdown-tokens` debug logs to avoid leaking raw paths/query params. (#15376) Thanks @Yaxuan42.
|
||||||
|
- Tools/web_search: support `freshness` for the Perplexity provider by mapping `pd`/`pw`/`pm`/`py` to Perplexity `search_recency_filter` values and including freshness in the Perplexity cache key. (#15343) Thanks @echoVic.
|
||||||
- Clawdock: avoid Zsh readonly variable collisions in helper scripts. (#15501) Thanks @nkelner.
|
- Clawdock: avoid Zsh readonly variable collisions in helper scripts. (#15501) Thanks @nkelner.
|
||||||
- Memory: switch default local embedding model to the QAT `embeddinggemma-300m-qat-Q8_0` variant for better quality at the same footprint. (#15429) Thanks @azade-c.
|
- Memory: switch default local embedding model to the QAT `embeddinggemma-300m-qat-Q8_0` variant for better quality at the same footprint. (#15429) Thanks @azade-c.
|
||||||
- Docs/Mermaid: remove hardcoded Mermaid init theme blocks from four docs diagrams so dark mode inherits readable theme defaults. (#15157) Thanks @heytulsiprasad.
|
- Docs/Mermaid: remove hardcoded Mermaid init theme blocks from four docs diagrams so dark mode inherits readable theme defaults. (#15157) Thanks @heytulsiprasad.
|
||||||
|
|||||||
@@ -175,7 +175,9 @@ Search the web using your configured provider.
|
|||||||
- `country` (optional): 2-letter country code for region-specific results (e.g., "DE", "US", "ALL"). If omitted, Brave chooses its default region.
|
- `country` (optional): 2-letter country code for region-specific results (e.g., "DE", "US", "ALL"). If omitted, Brave chooses its default region.
|
||||||
- `search_lang` (optional): ISO language code for search results (e.g., "de", "en", "fr")
|
- `search_lang` (optional): ISO language code for search results (e.g., "de", "en", "fr")
|
||||||
- `ui_lang` (optional): ISO language code for UI elements
|
- `ui_lang` (optional): ISO language code for UI elements
|
||||||
- `freshness` (optional, Brave only): filter by discovery time (`pd`, `pw`, `pm`, `py`, or `YYYY-MM-DDtoYYYY-MM-DD`)
|
- `freshness` (optional): filter by discovery time
|
||||||
|
- Brave: `pd`, `pw`, `pm`, `py`, or `YYYY-MM-DDtoYYYY-MM-DD`
|
||||||
|
- Perplexity: `pd`, `pw`, `pm`, `py`
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const {
|
|||||||
isDirectPerplexityBaseUrl,
|
isDirectPerplexityBaseUrl,
|
||||||
resolvePerplexityRequestModel,
|
resolvePerplexityRequestModel,
|
||||||
normalizeFreshness,
|
normalizeFreshness,
|
||||||
|
freshnessToPerplexityRecency,
|
||||||
resolveGrokApiKey,
|
resolveGrokApiKey,
|
||||||
resolveGrokModel,
|
resolveGrokModel,
|
||||||
resolveGrokInlineCitations,
|
resolveGrokInlineCitations,
|
||||||
@@ -128,6 +129,24 @@ describe("web_search freshness normalization", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("freshnessToPerplexityRecency", () => {
|
||||||
|
it("maps Brave shortcuts to Perplexity recency values", () => {
|
||||||
|
expect(freshnessToPerplexityRecency("pd")).toBe("day");
|
||||||
|
expect(freshnessToPerplexityRecency("pw")).toBe("week");
|
||||||
|
expect(freshnessToPerplexityRecency("pm")).toBe("month");
|
||||||
|
expect(freshnessToPerplexityRecency("py")).toBe("year");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for date ranges (not supported by Perplexity)", () => {
|
||||||
|
expect(freshnessToPerplexityRecency("2024-01-01to2024-01-31")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for undefined/empty input", () => {
|
||||||
|
expect(freshnessToPerplexityRecency(undefined)).toBeUndefined();
|
||||||
|
expect(freshnessToPerplexityRecency("")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("web_search grok config resolution", () => {
|
describe("web_search grok config resolution", () => {
|
||||||
it("uses config apiKey when provided", () => {
|
it("uses config apiKey when provided", () => {
|
||||||
expect(resolveGrokApiKey({ apiKey: "xai-test-key" })).toBe("xai-test-key");
|
expect(resolveGrokApiKey({ apiKey: "xai-test-key" })).toBe("xai-test-key");
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const WebSearchSchema = Type.Object({
|
|||||||
freshness: Type.Optional(
|
freshness: Type.Optional(
|
||||||
Type.String({
|
Type.String({
|
||||||
description:
|
description:
|
||||||
"Filter results by discovery time (Brave only). Values: 'pd' (past 24h), 'pw' (past week), 'pm' (past month), 'py' (past year), or date range 'YYYY-MM-DDtoYYYY-MM-DD'.",
|
"Filter results by discovery time. Brave supports 'pd', 'pw', 'pm', 'py', and date range 'YYYY-MM-DDtoYYYY-MM-DD'. Perplexity supports 'pd', 'pw', 'pm', and 'py'.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -403,6 +403,23 @@ function normalizeFreshness(value: string | undefined): string | undefined {
|
|||||||
return `${start}to${end}`;
|
return `${start}to${end}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map normalized freshness values (pd/pw/pm/py) to Perplexity's
|
||||||
|
* search_recency_filter values (day/week/month/year).
|
||||||
|
*/
|
||||||
|
function freshnessToPerplexityRecency(freshness: string | undefined): string | undefined {
|
||||||
|
if (!freshness) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
pd: "day",
|
||||||
|
pw: "week",
|
||||||
|
pm: "month",
|
||||||
|
py: "year",
|
||||||
|
};
|
||||||
|
return map[freshness] ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function isValidIsoDate(value: string): boolean {
|
function isValidIsoDate(value: string): boolean {
|
||||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -435,11 +452,27 @@ async function runPerplexitySearch(params: {
|
|||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
model: string;
|
model: string;
|
||||||
timeoutSeconds: number;
|
timeoutSeconds: number;
|
||||||
|
freshness?: string;
|
||||||
}): Promise<{ content: string; citations: string[] }> {
|
}): Promise<{ content: string; citations: string[] }> {
|
||||||
const baseUrl = params.baseUrl.trim().replace(/\/$/, "");
|
const baseUrl = params.baseUrl.trim().replace(/\/$/, "");
|
||||||
const endpoint = `${baseUrl}/chat/completions`;
|
const endpoint = `${baseUrl}/chat/completions`;
|
||||||
const model = resolvePerplexityRequestModel(baseUrl, params.model);
|
const model = resolvePerplexityRequestModel(baseUrl, params.model);
|
||||||
|
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: params.query,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const recencyFilter = freshnessToPerplexityRecency(params.freshness);
|
||||||
|
if (recencyFilter) {
|
||||||
|
body.search_recency_filter = recencyFilter;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(endpoint, {
|
const res = await fetch(endpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -448,15 +481,7 @@ async function runPerplexitySearch(params: {
|
|||||||
"HTTP-Referer": "https://openclaw.ai",
|
"HTTP-Referer": "https://openclaw.ai",
|
||||||
"X-Title": "OpenClaw Web Search",
|
"X-Title": "OpenClaw Web Search",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(body),
|
||||||
model,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: params.query,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
signal: withTimeout(undefined, params.timeoutSeconds * 1000),
|
signal: withTimeout(undefined, params.timeoutSeconds * 1000),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -544,7 +569,7 @@ async function runWebSearch(params: {
|
|||||||
params.provider === "brave"
|
params.provider === "brave"
|
||||||
? `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}:${params.freshness || "default"}`
|
? `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}:${params.freshness || "default"}`
|
||||||
: params.provider === "perplexity"
|
: params.provider === "perplexity"
|
||||||
? `${params.provider}:${params.query}:${params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL}:${params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL}`
|
? `${params.provider}:${params.query}:${params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL}:${params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL}:${params.freshness || "default"}`
|
||||||
: `${params.provider}:${params.query}:${params.grokModel ?? DEFAULT_GROK_MODEL}:${String(params.grokInlineCitations ?? false)}`,
|
: `${params.provider}:${params.query}:${params.grokModel ?? DEFAULT_GROK_MODEL}:${String(params.grokInlineCitations ?? false)}`,
|
||||||
);
|
);
|
||||||
const cached = readCache(SEARCH_CACHE, cacheKey);
|
const cached = readCache(SEARCH_CACHE, cacheKey);
|
||||||
@@ -561,6 +586,7 @@ async function runWebSearch(params: {
|
|||||||
baseUrl: params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL,
|
baseUrl: params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL,
|
||||||
model: params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL,
|
model: params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL,
|
||||||
timeoutSeconds: params.timeoutSeconds,
|
timeoutSeconds: params.timeoutSeconds,
|
||||||
|
freshness: params.freshness,
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -722,10 +748,10 @@ export function createWebSearchTool(options?: {
|
|||||||
const search_lang = readStringParam(params, "search_lang");
|
const search_lang = readStringParam(params, "search_lang");
|
||||||
const ui_lang = readStringParam(params, "ui_lang");
|
const ui_lang = readStringParam(params, "ui_lang");
|
||||||
const rawFreshness = readStringParam(params, "freshness");
|
const rawFreshness = readStringParam(params, "freshness");
|
||||||
if (rawFreshness && provider !== "brave") {
|
if (rawFreshness && provider !== "brave" && provider !== "perplexity") {
|
||||||
return jsonResult({
|
return jsonResult({
|
||||||
error: "unsupported_freshness",
|
error: "unsupported_freshness",
|
||||||
message: "freshness is only supported by the Brave web_search provider.",
|
message: "freshness is only supported by the Brave and Perplexity web_search providers.",
|
||||||
docs: "https://docs.openclaw.ai/tools/web",
|
docs: "https://docs.openclaw.ai/tools/web",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -769,6 +795,7 @@ export const __testing = {
|
|||||||
isDirectPerplexityBaseUrl,
|
isDirectPerplexityBaseUrl,
|
||||||
resolvePerplexityRequestModel,
|
resolvePerplexityRequestModel,
|
||||||
normalizeFreshness,
|
normalizeFreshness,
|
||||||
|
freshnessToPerplexityRecency,
|
||||||
resolveGrokApiKey,
|
resolveGrokApiKey,
|
||||||
resolveGrokModel,
|
resolveGrokModel,
|
||||||
resolveGrokInlineCitations,
|
resolveGrokInlineCitations,
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ describe("web_search perplexity baseUrl defaults", () => {
|
|||||||
expect(body.model).toBe("sonar-pro");
|
expect(body.model).toBe("sonar-pro");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects freshness for Perplexity provider", async () => {
|
it("passes freshness to Perplexity provider as search_recency_filter", async () => {
|
||||||
vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test");
|
vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test");
|
||||||
const mockFetch = vi.fn(() =>
|
const mockFetch = vi.fn(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
@@ -174,10 +174,11 @@ describe("web_search perplexity baseUrl defaults", () => {
|
|||||||
config: { tools: { web: { search: { provider: "perplexity" } } } },
|
config: { tools: { web: { search: { provider: "perplexity" } } } },
|
||||||
sandboxed: true,
|
sandboxed: true,
|
||||||
});
|
});
|
||||||
const result = await tool?.execute?.(1, { query: "test", freshness: "pw" });
|
await tool?.execute?.(1, { query: "perplexity-freshness-test", freshness: "pw" });
|
||||||
|
|
||||||
expect(mockFetch).not.toHaveBeenCalled();
|
expect(mockFetch).toHaveBeenCalledOnce();
|
||||||
expect(result?.details).toMatchObject({ error: "unsupported_freshness" });
|
const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
|
||||||
|
expect(body.search_recency_filter).toBe("week");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defaults to OpenRouter when OPENROUTER_API_KEY is set", async () => {
|
it("defaults to OpenRouter when OPENROUTER_API_KEY is set", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user