mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:00:44 +00:00
fix(gemini): pass search time filters
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
||||
buildSearchCacheKey,
|
||||
buildUnsupportedSearchFilterResponse,
|
||||
DEFAULT_SEARCH_COUNT,
|
||||
normalizeFreshness,
|
||||
parseIsoDateRange,
|
||||
readCachedSearchPayload,
|
||||
readConfiguredSecretString,
|
||||
readNumberParam,
|
||||
@@ -27,6 +29,13 @@ import {
|
||||
type GeminiConfig,
|
||||
} from "./gemini-web-search-provider.shared.js";
|
||||
|
||||
type GeminiFreshness = "day" | "week" | "month" | "year";
|
||||
|
||||
type GeminiTimeRangeFilter = {
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
};
|
||||
|
||||
type GeminiGroundingResponse = {
|
||||
candidates?: Array<{
|
||||
content?: {
|
||||
@@ -50,6 +59,99 @@ type GeminiGroundingResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
const GEMINI_FRESHNESS_DAYS: Record<GeminiFreshness, number> = {
|
||||
day: 1,
|
||||
week: 7,
|
||||
month: 30,
|
||||
year: 365,
|
||||
};
|
||||
|
||||
function isoDateStart(value: string): string {
|
||||
return `${value}T00:00:00Z`;
|
||||
}
|
||||
|
||||
function isoDateExclusiveEnd(value: string): string {
|
||||
const end = new Date(`${value}T00:00:00Z`);
|
||||
end.setUTCDate(end.getUTCDate() + 1);
|
||||
return end.toISOString();
|
||||
}
|
||||
|
||||
function freshnessStartTime(freshness: GeminiFreshness, now: Date): string {
|
||||
const start = new Date(now.getTime());
|
||||
start.setUTCDate(start.getUTCDate() - GEMINI_FRESHNESS_DAYS[freshness]);
|
||||
return start.toISOString();
|
||||
}
|
||||
|
||||
function resolveGeminiTimeRangeFilter(
|
||||
args: Record<string, unknown>,
|
||||
now = new Date(),
|
||||
):
|
||||
| { timeRangeFilter?: GeminiTimeRangeFilter }
|
||||
| {
|
||||
error:
|
||||
| "invalid_freshness"
|
||||
| "invalid_date"
|
||||
| "invalid_date_range"
|
||||
| "conflicting_time_filters";
|
||||
message: string;
|
||||
docs: string;
|
||||
} {
|
||||
const rawFreshness = readStringParam(args, "freshness");
|
||||
const freshness = rawFreshness
|
||||
? (normalizeFreshness(rawFreshness, "perplexity") as GeminiFreshness | undefined)
|
||||
: undefined;
|
||||
if (rawFreshness && !freshness) {
|
||||
return {
|
||||
error: "invalid_freshness",
|
||||
message: "freshness must be day, week, month, year, or the shortcuts pd, pw, pm, py.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
|
||||
const rawDateAfter = readStringParam(args, "date_after");
|
||||
const rawDateBefore = readStringParam(args, "date_before");
|
||||
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 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 before date_before.",
|
||||
});
|
||||
if ("error" in parsedDateRange) {
|
||||
return parsedDateRange;
|
||||
}
|
||||
|
||||
if (freshness) {
|
||||
return {
|
||||
timeRangeFilter: {
|
||||
startTime: freshnessStartTime(freshness, now),
|
||||
endTime: now.toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { dateAfter, dateBefore } = parsedDateRange;
|
||||
if (!dateAfter && !dateBefore) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
timeRangeFilter: {
|
||||
startTime: dateAfter ? isoDateStart(dateAfter) : "1970-01-01T00:00:00Z",
|
||||
endTime: dateBefore ? isoDateExclusiveEnd(dateBefore) : now.toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveGeminiRuntimeApiKey(gemini?: GeminiConfig): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(gemini?.apiKey, "tools.web.search.gemini.apiKey") ??
|
||||
@@ -63,8 +165,11 @@ async function runGeminiSearch(params: {
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
timeoutSeconds: number;
|
||||
timeRangeFilter?: GeminiTimeRangeFilter;
|
||||
}): Promise<{ content: string; citations: Array<{ url: string; title?: string }> }> {
|
||||
const endpoint = `${params.baseUrl}/models/${params.model}:generateContent`;
|
||||
const googleSearch =
|
||||
params.timeRangeFilter === undefined ? {} : { timeRangeFilter: params.timeRangeFilter };
|
||||
|
||||
return withTrustedWebSearchEndpoint(
|
||||
{
|
||||
@@ -78,7 +183,7 @@ async function runGeminiSearch(params: {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
contents: [{ parts: [{ text: params.query }] }],
|
||||
tools: [{ google_search: {} }],
|
||||
tools: [{ google_search: googleSearch }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -140,11 +245,22 @@ export async function executeGeminiSearch(
|
||||
args: Record<string, unknown>,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const unsupportedResponse = buildUnsupportedSearchFilterResponse(args, "gemini");
|
||||
const unsupportedResponse = buildUnsupportedSearchFilterResponse(
|
||||
{
|
||||
country: args.country,
|
||||
language: args.language,
|
||||
},
|
||||
"gemini",
|
||||
);
|
||||
if (unsupportedResponse) {
|
||||
return unsupportedResponse;
|
||||
}
|
||||
|
||||
const timeRange = resolveGeminiTimeRangeFilter(args);
|
||||
if ("error" in timeRange) {
|
||||
return timeRange;
|
||||
}
|
||||
|
||||
const geminiConfig = resolveGeminiConfig(searchConfig);
|
||||
const apiKey = resolveGeminiRuntimeApiKey(geminiConfig);
|
||||
if (!apiKey) {
|
||||
@@ -167,6 +283,8 @@ export async function executeGeminiSearch(
|
||||
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
baseUrl,
|
||||
model,
|
||||
timeRange.timeRangeFilter?.startTime,
|
||||
timeRange.timeRangeFilter?.endTime,
|
||||
]);
|
||||
const cached = readCachedSearchPayload(cacheKey);
|
||||
if (cached) {
|
||||
@@ -180,6 +298,7 @@ export async function executeGeminiSearch(
|
||||
baseUrl,
|
||||
model,
|
||||
timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig),
|
||||
timeRangeFilter: timeRange.timeRangeFilter,
|
||||
});
|
||||
const payload = {
|
||||
query,
|
||||
|
||||
@@ -34,9 +34,18 @@ const GEMINI_TOOL_PARAMETERS = {
|
||||
},
|
||||
country: { type: "string", description: "Not supported by Gemini." },
|
||||
language: { type: "string", description: "Not supported by Gemini." },
|
||||
freshness: { type: "string", description: "Not supported by Gemini." },
|
||||
date_after: { type: "string", description: "Not supported by Gemini." },
|
||||
date_before: { type: "string", description: "Not supported by Gemini." },
|
||||
freshness: {
|
||||
type: "string",
|
||||
description: "Limit Google Search grounding to recent results: day, week, month, or year.",
|
||||
},
|
||||
date_after: {
|
||||
type: "string",
|
||||
description: "Only ground with results published after this date (YYYY-MM-DD).",
|
||||
},
|
||||
date_before: {
|
||||
type: "string",
|
||||
description: "Only ground with results published before this date (YYYY-MM-DD).",
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
} satisfies Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user