mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-22 02:15:26 -06:00
fix(perplexity): reject empty grounded search answers (#126780)
This commit is contained in:
committed by
GitHub
parent
8fdc6d7c36
commit
ff0113cdb2
@@ -298,8 +298,14 @@ async function runPerplexitySearch(params: {
|
||||
return await throwWebSearchApiError(res, "Perplexity");
|
||||
}
|
||||
const data = await readProviderJsonResponse<PerplexitySearchResponse>(res, "Perplexity");
|
||||
const content = data.choices?.[0]?.message?.content;
|
||||
if (typeof content !== "string" || !content.trim()) {
|
||||
throw new Error(
|
||||
"Perplexity search returned no final answer. Retry the query or choose another search provider.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
content: data.choices?.[0]?.message?.content ?? "No response",
|
||||
content,
|
||||
citations: extractPerplexityCitations(data),
|
||||
};
|
||||
},
|
||||
@@ -461,7 +467,7 @@ export async function executePerplexitySearch(
|
||||
runtime.baseUrl,
|
||||
runtime.model,
|
||||
query,
|
||||
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
structured ? resolveSearchCount(count, DEFAULT_SEARCH_COUNT) : undefined,
|
||||
country,
|
||||
language,
|
||||
freshness,
|
||||
|
||||
@@ -21,6 +21,33 @@ const openRouterPerplexityApiKey = ["sk", "or", "v1", "test"].join("-");
|
||||
const directPerplexityApiKey = ["pplx", "test"].join("-");
|
||||
const enterprisePerplexityApiKey = ["enterprise", "perplexity", "test"].join("-");
|
||||
|
||||
function mockPerplexityResponseOnce(body: unknown): void {
|
||||
withTrustedWebSearchEndpointMock.mockImplementationOnce(
|
||||
async (_params: { init: RequestInit }, run: (response: Response) => Promise<unknown>) =>
|
||||
await run(
|
||||
new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function createConfiguredPerplexityTool(structured: boolean) {
|
||||
const webSearch = {
|
||||
apiKey: directPerplexityApiKey,
|
||||
...(structured ? {} : { baseUrl: "https://api.perplexity.ai" }),
|
||||
};
|
||||
const tool = createPerplexityWebSearchProvider().createTool({
|
||||
config: { plugins: { entries: { perplexity: { config: { webSearch } } } } },
|
||||
searchConfig: {},
|
||||
});
|
||||
if (!tool) {
|
||||
throw new Error("Expected tool definition");
|
||||
}
|
||||
return tool;
|
||||
}
|
||||
|
||||
describe("perplexity web search provider", () => {
|
||||
it("points missing-key users to fetch/browser alternatives", async () => {
|
||||
await withEnvAsync(
|
||||
@@ -67,6 +94,100 @@ describe("perplexity web search provider", () => {
|
||||
expect(withTrustedWebSearchEndpointMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "missing choices", response: {} },
|
||||
{ name: "empty choices", response: { choices: [] } },
|
||||
{ name: "missing message", response: { choices: [{}] } },
|
||||
{ name: "missing content", response: { choices: [{ message: {} }] } },
|
||||
{ name: "null content", response: { choices: [{ message: { content: null } }] } },
|
||||
{ name: "empty content", response: { choices: [{ message: { content: "" } }] } },
|
||||
{ name: "whitespace content", response: { choices: [{ message: { content: " \n " } }] } },
|
||||
{
|
||||
name: "citations without an answer",
|
||||
response: {
|
||||
choices: [{ message: { content: null } }],
|
||||
citations: ["https://example.test/source"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tool calls without an answer",
|
||||
response: {
|
||||
choices: [{ finish_reason: "tool_calls", message: { content: null, tool_calls: [] } }],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "audio without an answer",
|
||||
response: { choices: [{ message: { content: null, audio: { id: "audio-response" } } }] },
|
||||
},
|
||||
])("rejects and does not cache chat-completions $name", async ({ name, response }) => {
|
||||
withTrustedWebSearchEndpointMock.mockReset();
|
||||
mockPerplexityResponseOnce(response);
|
||||
mockPerplexityResponseOnce({
|
||||
choices: [{ message: { content: " Recovered grounded answer " } }],
|
||||
citations: ["https://example.test/recovered"],
|
||||
});
|
||||
|
||||
const tool = createConfiguredPerplexityTool(false);
|
||||
const args = { query: `perplexity empty answer ${name}` };
|
||||
await expect(tool.execute(args)).rejects.toThrow(
|
||||
"Perplexity search returned no final answer. Retry the query or choose another search provider.",
|
||||
);
|
||||
|
||||
const recovered = await tool.execute(args);
|
||||
expect(recovered.content).toContain(" Recovered grounded answer ");
|
||||
expect(recovered.citations).toEqual(["https://example.test/recovered"]);
|
||||
expect(withTrustedWebSearchEndpointMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "chat completions", structured: false, expectedRequests: 1 },
|
||||
{ name: "native Search API", structured: true, expectedRequests: 2 },
|
||||
])(
|
||||
"uses count as a cache dimension only when $name sends it upstream",
|
||||
async ({ name, structured, expectedRequests }) => {
|
||||
withTrustedWebSearchEndpointMock.mockReset();
|
||||
const response = structured
|
||||
? { results: [] }
|
||||
: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: "Grounded answer",
|
||||
annotations: [
|
||||
{
|
||||
type: "url_citation",
|
||||
url_citation: { url: "https://example.test/citation" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
mockPerplexityResponseOnce(response);
|
||||
if (structured) {
|
||||
mockPerplexityResponseOnce(response);
|
||||
}
|
||||
|
||||
const tool = createConfiguredPerplexityTool(structured);
|
||||
const query = `perplexity cache count ${name}`;
|
||||
const first = await tool.execute({ query, count: 1 });
|
||||
const second = await tool.execute({ query, count: 7 });
|
||||
const third = await tool.execute({ query, count: 1 });
|
||||
|
||||
expect(first.cached).toBeUndefined();
|
||||
expect(second.cached).toBe(structured ? undefined : true);
|
||||
expect(third.cached).toBe(true);
|
||||
expect(withTrustedWebSearchEndpointMock).toHaveBeenCalledTimes(expectedRequests);
|
||||
if (structured) {
|
||||
expect(first.results).toEqual([]);
|
||||
expect(first.count).toBe(0);
|
||||
} else {
|
||||
expect(first.content).toContain("Grounded answer");
|
||||
expect(first.citations).toEqual(["https://example.test/citation"]);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ name: "native Search API", webSearch: { apiKey: "pplx-test" } },
|
||||
{
|
||||
@@ -216,15 +337,7 @@ describe("perplexity web search provider", () => {
|
||||
});
|
||||
|
||||
it("sends official date filter fields in the Search API request body", async () => {
|
||||
withTrustedWebSearchEndpointMock.mockImplementationOnce(
|
||||
async (_params: { init: RequestInit }, run: (response: Response) => Promise<unknown>) =>
|
||||
await run(
|
||||
new Response(JSON.stringify({ results: [] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
mockPerplexityResponseOnce({ results: [] });
|
||||
|
||||
await withEnvAsync(
|
||||
{ [perplexityApiKeyEnv]: directPerplexityApiKey, [openRouterApiKeyEnv]: undefined },
|
||||
|
||||
Reference in New Issue
Block a user