mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 08:48:37 +00:00
fix(agents): fix Brave llm-context empty snippets (#41387)
Merged via squash.
Prepared head SHA: 1e6f1d9d51
Co-authored-by: zheliu2 <15888718+zheliu2@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
@@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Agents/fallback: treat HTTP 499 responses as transient in both raw-text and structured failover paths so Anthropic-style client-closed overload responses trigger model fallback reliably. (#41468) thanks @zeroasterisk.
|
- Agents/fallback: treat HTTP 499 responses as transient in both raw-text and structured failover paths so Anthropic-style client-closed overload responses trigger model fallback reliably. (#41468) thanks @zeroasterisk.
|
||||||
- Plugins/context-engine model auth: expose `runtime.modelAuth` and plugin-sdk auth helpers so plugins can resolve provider/model API keys through the normal auth pipeline. (#41090) thanks @xinhuagu.
|
- Plugins/context-engine model auth: expose `runtime.modelAuth` and plugin-sdk auth helpers so plugins can resolve provider/model API keys through the normal auth pipeline. (#41090) thanks @xinhuagu.
|
||||||
- CLI/memory teardown: close cached memory search/index managers in the one-shot CLI shutdown path so watcher-backed memory caches no longer keep completed CLI runs alive after output finishes. (#40389) thanks @Julbarth.
|
- CLI/memory teardown: close cached memory search/index managers in the one-shot CLI shutdown path so watcher-backed memory caches no longer keep completed CLI runs alive after output finishes. (#40389) thanks @Julbarth.
|
||||||
|
- Tools/web search: treat Brave `llm-context` grounding snippets as plain strings so `web_search` no longer returns empty snippet arrays in LLM Context mode. (#41387) thanks @zheliu2.
|
||||||
|
|
||||||
## 2026.3.8
|
## 2026.3.8
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const {
|
|||||||
resolveKimiBaseUrl,
|
resolveKimiBaseUrl,
|
||||||
extractKimiCitations,
|
extractKimiCitations,
|
||||||
resolveBraveMode,
|
resolveBraveMode,
|
||||||
|
mapBraveLlmContextResults,
|
||||||
} = __testing;
|
} = __testing;
|
||||||
|
|
||||||
const kimiApiKeyEnv = ["KIMI_API", "KEY"].join("_");
|
const kimiApiKeyEnv = ["KIMI_API", "KEY"].join("_");
|
||||||
@@ -393,3 +394,77 @@ describe("resolveBraveMode", () => {
|
|||||||
expect(resolveBraveMode({ mode: "invalid" })).toBe("web");
|
expect(resolveBraveMode({ mode: "invalid" })).toBe("web");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("mapBraveLlmContextResults", () => {
|
||||||
|
it("maps plain string snippets correctly", () => {
|
||||||
|
const results = mapBraveLlmContextResults({
|
||||||
|
grounding: {
|
||||||
|
generic: [
|
||||||
|
{
|
||||||
|
url: "https://example.com/page",
|
||||||
|
title: "Example Page",
|
||||||
|
snippets: ["first snippet", "second snippet"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(results).toEqual([
|
||||||
|
{
|
||||||
|
url: "https://example.com/page",
|
||||||
|
title: "Example Page",
|
||||||
|
snippets: ["first snippet", "second snippet"],
|
||||||
|
siteName: "example.com",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters out non-string and empty snippets", () => {
|
||||||
|
const results = mapBraveLlmContextResults({
|
||||||
|
grounding: {
|
||||||
|
generic: [
|
||||||
|
{
|
||||||
|
url: "https://example.com",
|
||||||
|
title: "Test",
|
||||||
|
snippets: ["valid", "", null, undefined, 42, { text: "object" }] as string[],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(results[0].snippets).toEqual(["valid"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles missing snippets array", () => {
|
||||||
|
const results = mapBraveLlmContextResults({
|
||||||
|
grounding: {
|
||||||
|
generic: [{ url: "https://example.com", title: "No Snippets" } as never],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(results[0].snippets).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles empty grounding.generic", () => {
|
||||||
|
expect(mapBraveLlmContextResults({ grounding: { generic: [] } })).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles missing grounding.generic", () => {
|
||||||
|
expect(mapBraveLlmContextResults({ grounding: {} } as never)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves siteName from URL hostname", () => {
|
||||||
|
const results = mapBraveLlmContextResults({
|
||||||
|
grounding: {
|
||||||
|
generic: [{ url: "https://docs.example.org/path", title: "Docs", snippets: ["text"] }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(results[0].siteName).toBe("docs.example.org");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets siteName to undefined for invalid URLs", () => {
|
||||||
|
const results = mapBraveLlmContextResults({
|
||||||
|
grounding: {
|
||||||
|
generic: [{ url: "not-a-url", title: "Bad URL", snippets: ["text"] }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(results[0].siteName).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -272,8 +272,7 @@ type BraveSearchResponse = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type BraveLlmContextSnippet = { text: string };
|
type BraveLlmContextResult = { url: string; title: string; snippets: string[] };
|
||||||
type BraveLlmContextResult = { url: string; title: string; snippets: BraveLlmContextSnippet[] };
|
|
||||||
type BraveLlmContextResponse = {
|
type BraveLlmContextResponse = {
|
||||||
grounding: { generic?: BraveLlmContextResult[] };
|
grounding: { generic?: BraveLlmContextResult[] };
|
||||||
sources?: { url?: string; hostname?: string; date?: string }[];
|
sources?: { url?: string; hostname?: string; date?: string }[];
|
||||||
@@ -1429,6 +1428,18 @@ async function runKimiSearch(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapBraveLlmContextResults(
|
||||||
|
data: BraveLlmContextResponse,
|
||||||
|
): { url: string; title: string; snippets: string[]; siteName?: string }[] {
|
||||||
|
const genericResults = Array.isArray(data.grounding?.generic) ? data.grounding.generic : [];
|
||||||
|
return genericResults.map((entry) => ({
|
||||||
|
url: entry.url ?? "",
|
||||||
|
title: entry.title ?? "",
|
||||||
|
snippets: (entry.snippets ?? []).filter((s) => typeof s === "string" && s.length > 0),
|
||||||
|
siteName: resolveSiteName(entry.url) || undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async function runBraveLlmContextSearch(params: {
|
async function runBraveLlmContextSearch(params: {
|
||||||
query: string;
|
query: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@@ -1477,13 +1488,7 @@ async function runBraveLlmContextSearch(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = (await res.json()) as BraveLlmContextResponse;
|
const data = (await res.json()) as BraveLlmContextResponse;
|
||||||
const genericResults = Array.isArray(data.grounding?.generic) ? data.grounding.generic : [];
|
const mapped = mapBraveLlmContextResults(data);
|
||||||
const mapped = genericResults.map((entry) => ({
|
|
||||||
url: entry.url ?? "",
|
|
||||||
title: entry.title ?? "",
|
|
||||||
snippets: (entry.snippets ?? []).map((s) => s.text ?? "").filter(Boolean),
|
|
||||||
siteName: resolveSiteName(entry.url) || undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { results: mapped, sources: data.sources };
|
return { results: mapped, sources: data.sources };
|
||||||
},
|
},
|
||||||
@@ -2122,4 +2127,5 @@ export const __testing = {
|
|||||||
extractKimiCitations,
|
extractKimiCitations,
|
||||||
resolveRedirectUrl: resolveCitationRedirectUrl,
|
resolveRedirectUrl: resolveCitationRedirectUrl,
|
||||||
resolveBraveMode,
|
resolveBraveMode,
|
||||||
|
mapBraveLlmContextResults,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -694,7 +694,7 @@ describe("web_search external content wrapping", () => {
|
|||||||
const mockFetch = installBraveLlmContextFetch({
|
const mockFetch = installBraveLlmContextFetch({
|
||||||
title: "Context title",
|
title: "Context title",
|
||||||
url: "https://example.com/ctx",
|
url: "https://example.com/ctx",
|
||||||
snippets: [{ text: "Context chunk one" }, { text: "Context chunk two" }],
|
snippets: ["Context chunk one", "Context chunk two"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const tool = createWebSearchTool({
|
const tool = createWebSearchTool({
|
||||||
|
|||||||
Reference in New Issue
Block a user