Skip to content

Commit dc63144

Browse files
authored
Merge pull request #18706 from Budibase/feat/handle-gemini-503
feat: handle gemini 503 better
2 parents b663738 + e99f8c3 commit dc63144

3 files changed

Lines changed: 140 additions & 8 deletions

File tree

packages/server/src/ai/tools/budibase/knowledgeFiles.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,33 @@ interface RankedMatch {
3030
file: KnowledgeBaseFile
3131
}
3232

33+
const GEMINI_RETRIEVAL_UNAVAILABLE_MESSAGE =
34+
"Gemini knowledge retrieval is temporarily unavailable (upstream 503). Budibase is operating normally; please retry shortly."
35+
const GEMINI_UPSTREAM_EVENT = "ai.gemini.upstream_unavailable"
36+
37+
const isGeminiRetrievalUnavailable = (error: unknown): boolean => {
38+
if (!error || typeof error !== "object") {
39+
return false
40+
}
41+
42+
const maybeStatus = (error as { status?: unknown }).status
43+
if (maybeStatus === 503) {
44+
return true
45+
}
46+
47+
const maybeMessage = String((error as { message?: unknown }).message || "")
48+
.toLowerCase()
49+
.trim()
50+
if (!maybeMessage) {
51+
return false
52+
}
53+
54+
return (
55+
maybeMessage.includes("upstream unavailable") ||
56+
maybeMessage.includes("service unavailable")
57+
)
58+
}
59+
3360
const toEpochMillis = (value?: string | number) => {
3461
if (value == null) {
3562
return 0
@@ -274,11 +301,31 @@ export const createKnowledgeSearchTool = (
274301
}
275302

276303
const agent = await sdk.ai.agents.getOrThrow(agentId)
277-
const result = await sdk.ai.rag.retrieveContextForAgent(agent, question)
278-
return {
279-
context: result.text,
280-
sources: result.sources,
281-
chunks: result.chunks,
304+
try {
305+
const result = await sdk.ai.rag.retrieveContextForAgent(agent, question)
306+
return {
307+
context: result.text,
308+
sources: result.sources,
309+
chunks: result.chunks,
310+
}
311+
} catch (error: any) {
312+
if (isGeminiRetrievalUnavailable(error)) {
313+
console.error("[AI_UPSTREAM] Gemini unavailable", {
314+
event: GEMINI_UPSTREAM_EVENT,
315+
provider: "gemini",
316+
path: "knowledge_retrieval",
317+
upstreamStatus: error?.status,
318+
agentId,
319+
errorMessage: error?.message,
320+
})
321+
throw new Error(GEMINI_RETRIEVAL_UNAVAILABLE_MESSAGE)
322+
}
323+
console.error("Failed to retrieve agent knowledge context", {
324+
agentId,
325+
status: error?.status,
326+
message: error?.message,
327+
})
328+
throw error
282329
}
283330
},
284331
}),

packages/server/src/ai/tools/budibase/tests/knowledgeFiles.spec.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,35 @@ import {
22
KnowledgeBaseFileStatus,
33
type KnowledgeBaseFile,
44
} from "@budibase/types"
5+
6+
import { features } from "@budibase/backend-core"
7+
import sdk from "../../../../sdk"
8+
import {
9+
createKnowledgeFilesTool,
10+
createKnowledgeSearchTool,
11+
} from "../knowledgeFiles"
12+
13+
jest.mock("@budibase/backend-core", () => ({
14+
features: {
15+
isEnabled: jest.fn(),
16+
},
17+
}))
18+
519
jest.mock("../../../../sdk", () => ({
620
__esModule: true,
721
default: {
822
ai: {
23+
agents: {
24+
getOrThrow: jest.fn(),
25+
},
926
rag: {
1027
listFilesForAgent: jest.fn(),
28+
retrieveContextForAgent: jest.fn(),
1129
},
1230
},
1331
},
1432
}))
1533

16-
import sdk from "../../../../sdk"
17-
import { createKnowledgeFilesTool } from "../knowledgeFiles"
18-
1934
const executeTool = async (
2035
agentId: string,
2136
input: {
@@ -35,6 +50,21 @@ const executeTool = async (
3550
})
3651
}
3752

53+
const executeSearchTool = async (
54+
agentId: string,
55+
input: { question: string }
56+
) => {
57+
const toolDef = createKnowledgeSearchTool(agentId)
58+
if (!toolDef.tool.execute) {
59+
throw new Error("tool.execute is not a function")
60+
}
61+
62+
return await toolDef.tool.execute(input, {
63+
toolCallId: "test-tool-call",
64+
messages: [],
65+
})
66+
}
67+
3868
describe("AI Tools - Knowledge files", () => {
3969
beforeEach(() => {
4070
jest.restoreAllMocks()
@@ -319,3 +349,38 @@ describe("AI Tools - Knowledge files", () => {
319349
expect(result.files[0].matchedBy).toBe("filename-contains")
320350
})
321351
})
352+
353+
describe("AI Tools - Knowledge search", () => {
354+
beforeEach(() => {
355+
jest.restoreAllMocks()
356+
jest.mocked(features.isEnabled).mockResolvedValue(true)
357+
})
358+
359+
it("hard-fails with a clear message when Gemini retrieval is unavailable", async () => {
360+
jest
361+
.spyOn(sdk.ai.agents, "getOrThrow")
362+
.mockResolvedValue({ _id: "agent_1" } as any)
363+
jest
364+
.spyOn(sdk.ai.rag, "retrieveContextForAgent")
365+
.mockRejectedValue({ status: 503, message: "upstream unavailable" })
366+
367+
await expect(
368+
executeSearchTool("agent_1", { question: "What is policy?" })
369+
).rejects.toThrow(
370+
"Gemini knowledge retrieval is temporarily unavailable (upstream 503). Budibase is operating normally; please retry shortly."
371+
)
372+
})
373+
374+
it("preserves non-provider retrieval errors", async () => {
375+
jest
376+
.spyOn(sdk.ai.agents, "getOrThrow")
377+
.mockResolvedValue({ _id: "agent_1" } as any)
378+
jest
379+
.spyOn(sdk.ai.rag, "retrieveContextForAgent")
380+
.mockRejectedValue(new Error("Failed to map source metadata"))
381+
382+
await expect(
383+
executeSearchTool("agent_1", { question: "What is policy?" })
384+
).rejects.toThrow("Failed to map source metadata")
385+
})
386+
})

packages/server/src/api/controllers/ai/files.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ import {
2222
RequiredKeys,
2323
} from "@budibase/types"
2424
import sdk from "../../../sdk"
25+
2526
import { fetchSharePointSitesByConnection } from "../../../sdk/workspace/ai/knowledgeSources/sharepointConnection"
2627
import { getSharePointSiteIds, getSharePointSources } from "./sharepoint"
2728

29+
const GEMINI_UPSTREAM_EVENT = "ai.gemini.upstream_unavailable"
30+
2831
const normalizeUpload = (fileInput: any) => {
2932
if (!fileInput) {
3033
return undefined
@@ -171,6 +174,23 @@ export async function uploadAgentFile(
171174
ctx.body = { file: updated }
172175
ctx.status = 201
173176
} catch (error: any) {
177+
const normalizedMessage = String(error?.message || "").toLowerCase()
178+
const isGeminiUpstreamUnavailable =
179+
error?.status === 503 ||
180+
error?.statusCode === 503 ||
181+
normalizedMessage.includes("upstream unavailable") ||
182+
normalizedMessage.includes("service unavailable")
183+
184+
if (isGeminiUpstreamUnavailable) {
185+
console.error("[AI_UPSTREAM] Gemini unavailable", {
186+
event: GEMINI_UPSTREAM_EVENT,
187+
provider: "gemini",
188+
path: "knowledge_ingest",
189+
upstreamStatus: error?.status,
190+
agentId,
191+
errorMessage: error?.message,
192+
})
193+
}
174194
console.error("Failed to upload agent file", error)
175195
throw new HTTPError(
176196
error?.message || "Failed to process uploaded file",

0 commit comments

Comments
 (0)