ai: shake-out fixes from first end-to-end search_kb run
Three problems surfaced when driving search_kb / read_chunk through a
real DeepSeek chat for the first time:
1. vite.config.ts: minisearch wasn't in `aliasedDeps`, so when
@crema/lexical-rag-ui imports it, Vite couldn't resolve the bare
specifier from the sibling lib's location. Added it next to the
other shared deps. tsconfig paths alone is not enough — the Vite
alias is what propagates resolution to sibling-lib code.
2. ai.tsx reindexKB: was using `toast.show?.()` which doesn't exist on
useToast()'s API. Optional chaining silently no-op'd, so the button
click ran the fetch but produced zero UI feedback (success or
failure). Switched to the actual API: toast.info / toast.success /
toast.error. Added a console.error in the catch arm so the
underlying exception is visible in DevTools when something does go
wrong.
3. ai.tsx MAX_TOOL_ITERATIONS: cap was 3, which is too tight for
agentic search→read→search loops on real questions. Bumped to 6.
More importantly, when the cap IS reached, the runner now
synthesises tool-error messages for each pending tool_call and
continues the chat — instead of silently dropping them, which left
the conversation with an assistant.tool_calls turn but no matching
tool messages. DeepSeek (and the OpenAI spec) reject that
conversation with 400 ("insufficient tool messages following
tool_calls"), poisoning the thread. Now the model gets a clean
"max iterations reached" signal and produces a final answer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -127,9 +127,10 @@ async function reindexKB(
|
|||||||
window.sessionStorage.getItem("arcadia_access_token")) ||
|
window.sessionStorage.getItem("arcadia_access_token")) ||
|
||||||
"dev"
|
"dev"
|
||||||
const url = `${baseUrl}/index/${encodeURIComponent(corpus)}/build`
|
const url = `${baseUrl}/index/${encodeURIComponent(corpus)}/build`
|
||||||
toast.show?.({
|
// Use useToast's actual API (see lib-notification-ui): tone helpers
|
||||||
title: "Reindexing…",
|
// success/error/info/warning, plus generic toast({ title, description, tone }).
|
||||||
description: `Rebuilding corpus '${corpus}'.`,
|
toast.info(`Reindexing '${corpus}'…`, {
|
||||||
|
description: `POST ${url}`,
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
@@ -140,16 +141,14 @@ async function reindexKB(
|
|||||||
throw new Error(`HTTP ${res.status}: ${await res.text()}`)
|
throw new Error(`HTTP ${res.status}: ${await res.text()}`)
|
||||||
}
|
}
|
||||||
const out = (await res.json()) as { chunk_count: number; built_at: string }
|
const out = (await res.json()) as { chunk_count: number; built_at: string }
|
||||||
toast.show?.({
|
toast.success("Reindex complete", {
|
||||||
title: "Reindex complete",
|
|
||||||
description: `${out.chunk_count} chunks indexed for '${corpus}'.`,
|
description: `${out.chunk_count} chunks indexed for '${corpus}'.`,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.show?.({
|
const msg = err instanceof Error ? err.message : String(err)
|
||||||
title: "Reindex failed",
|
// eslint-disable-next-line no-console
|
||||||
description: err instanceof Error ? err.message : String(err),
|
console.error("[reindexKB]", err)
|
||||||
tone: "error",
|
toast.error("Reindex failed", { description: msg })
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,7 +817,7 @@ function ChatSurface({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [activeAgent, messages, isStreaming, agentHistory])
|
}, [activeAgent, messages, isStreaming, agentHistory])
|
||||||
const MAX_TOOL_ITERATIONS = 3
|
const MAX_TOOL_ITERATIONS = 6
|
||||||
const [pendingConfirm, setPendingConfirm] = useState<{
|
const [pendingConfirm, setPendingConfirm] = useState<{
|
||||||
/** Message index that emitted the write calls. */
|
/** Message index that emitted the write calls. */
|
||||||
afterIndex: number
|
afterIndex: number
|
||||||
@@ -856,7 +855,27 @@ function ChatSurface({
|
|||||||
toolIterationsRef.current = 0
|
toolIterationsRef.current = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (toolIterationsRef.current >= MAX_TOOL_ITERATIONS) return
|
if (toolIterationsRef.current >= MAX_TOOL_ITERATIONS) {
|
||||||
|
// Cap reached. Synthesize a tool-error response for each pending
|
||||||
|
// call so the conversation stays valid (every assistant tool_calls
|
||||||
|
// turn must be followed by tool messages — otherwise the next
|
||||||
|
// request to DeepSeek 400s on "insufficient tool messages"). Then
|
||||||
|
// continue the chat so the model writes a final answer with what
|
||||||
|
// it has, or apologizes for not finishing.
|
||||||
|
const cappedMessages = calls.map((c) => ({
|
||||||
|
role: "tool" as const,
|
||||||
|
content: JSON.stringify({
|
||||||
|
error: `MAX_TOOL_ITERATIONS (${MAX_TOOL_ITERATIONS}) reached — please answer with what you have so far.`,
|
||||||
|
}),
|
||||||
|
toolCallId: c.id,
|
||||||
|
name: c.name,
|
||||||
|
}))
|
||||||
|
void continueChat(cappedMessages, {
|
||||||
|
system: systemPrompt,
|
||||||
|
tools: getOpenAITools(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
toolIterationsRef.current += 1
|
toolIterationsRef.current += 1
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const { reads, writes } = classifyCalls(calls)
|
const { reads, writes } = classifyCalls(calls)
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ const aliasedDeps = [
|
|||||||
"@tiptap/extension-link",
|
"@tiptap/extension-link",
|
||||||
"@tiptap/extension-placeholder",
|
"@tiptap/extension-placeholder",
|
||||||
"@tiptap/extension-image",
|
"@tiptap/extension-image",
|
||||||
|
"minisearch",
|
||||||
]
|
]
|
||||||
const sharedDepAliases = Object.fromEntries(
|
const sharedDepAliases = Object.fromEntries(
|
||||||
aliasedDeps.map((name) => [name, resolvePath(nodeModules, name)]),
|
aliasedDeps.map((name) => [name, resolvePath(nodeModules, name)]),
|
||||||
|
|||||||
Reference in New Issue
Block a user