feat(search): retry failed nodes
This commit is contained in:
@ -12,6 +12,7 @@
|
||||
} from '~/constants/injection-keys'
|
||||
import Flow from './SearchFlow.vue'
|
||||
import SearchFlow from './SearchFlow.vue'
|
||||
import { isChildNode, isParentNode, isRootNode } from '~/utils/tree-node'
|
||||
|
||||
export type DeepResearchNodeStatus = Exclude<ResearchStep['type'], 'complete'>
|
||||
|
||||
@ -115,7 +116,7 @@
|
||||
}
|
||||
}
|
||||
// Update the node
|
||||
if (!isRootNode(node)) {
|
||||
if (!isRootNode(node.id)) {
|
||||
node.label = step.result.query ?? ''
|
||||
node.researchGoal = step.result.researchGoal
|
||||
}
|
||||
@ -196,10 +197,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isRootNode(node: DeepResearchNode) {
|
||||
return node.id === '0'
|
||||
}
|
||||
|
||||
function selectNode(nodeId: string) {
|
||||
if (selectedNodeId.value === nodeId) {
|
||||
selectedNodeId.value = undefined
|
||||
@ -208,37 +205,103 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function startResearch() {
|
||||
async function startResearch(retryNode?: DeepResearchNode) {
|
||||
if (!form.value.query || !form.value.breadth || !form.value.depth) return
|
||||
|
||||
nodes.value = [{ ...rootNode }]
|
||||
selectedNodeId.value = undefined
|
||||
searchResults.value = {}
|
||||
isLoading.value = true
|
||||
flowRef.value?.clearNodes()
|
||||
// 如果不是重试,清空所有节点
|
||||
if (!retryNode) {
|
||||
nodes.value = [{ ...rootNode }]
|
||||
selectedNodeId.value = undefined
|
||||
searchResults.value = {}
|
||||
flowRef.value?.clearNodes()
|
||||
isLoading.value = true
|
||||
}
|
||||
|
||||
// Wait after the flow is cleared
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
|
||||
try {
|
||||
const searchLanguage = config.value.webSearch.searchLanguage
|
||||
? t('language', {}, { locale: config.value.webSearch.searchLanguage })
|
||||
: undefined
|
||||
let query = getCombinedQuery(form.value, feedback.value)
|
||||
let existingLearnings: string[] = []
|
||||
let existingVisitedUrls: string[] = []
|
||||
let currentDepth = 1
|
||||
let breadth = form.value.breadth
|
||||
|
||||
if (retryNode) {
|
||||
query = retryNode.label
|
||||
// Set the search depth and breadth to its parent's
|
||||
if (!isRootNode(retryNode.id)) {
|
||||
const parentId = parentNodeId(retryNode.id)!
|
||||
currentDepth = nodeDepth(parentId)
|
||||
breadth = searchBreadth(breadth, parentId)
|
||||
}
|
||||
// Collect all parent nodes' learnings and visitedUrls
|
||||
const parentNodes = nodes.value.filter((n) =>
|
||||
isParentNode(n.id, retryNode.id),
|
||||
)
|
||||
existingLearnings = parentNodes
|
||||
.flatMap((n) => n.learnings || [])
|
||||
.filter(Boolean)
|
||||
existingVisitedUrls = parentNodes
|
||||
.flatMap((n) => n.searchResults || [])
|
||||
.map((r) => r.url)
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
await deepResearch({
|
||||
query: getCombinedQuery(form.value, feedback.value),
|
||||
query,
|
||||
retryNode,
|
||||
currentDepth,
|
||||
breadth,
|
||||
maxDepth: form.value.depth,
|
||||
breadth: form.value.breadth,
|
||||
languageCode: locale.value,
|
||||
searchLanguage,
|
||||
learnings: existingLearnings,
|
||||
visitedUrls: existingVisitedUrls,
|
||||
onProgress: handleResearchProgress,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Research failed:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
if (!retryNode) {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function retryNode(nodeId: string) {
|
||||
console.log('[DeepResearch] retryNode', nodeId, isLoading.value)
|
||||
if (!nodeId || isLoading.value) return
|
||||
|
||||
// Remove all child nodes first
|
||||
nodes.value = nodes.value.filter((n) => !isChildNode(nodeId, n.id))
|
||||
flowRef.value?.removeChildNodes(nodeId)
|
||||
|
||||
const node = nodes.value.find((n) => n.id === nodeId)
|
||||
// Take a clone of the node
|
||||
// Used in `deepResearch()` to access the node's original query and searchGoal
|
||||
let nodeCurrentData: DeepResearchNode | undefined
|
||||
|
||||
if (node) {
|
||||
nodeCurrentData = { ...node }
|
||||
node.status = undefined
|
||||
node.error = undefined
|
||||
node.searchResults = undefined
|
||||
node.learnings = undefined
|
||||
node.generateLearningsReasoning = undefined
|
||||
node.generateQueriesReasoning = undefined
|
||||
|
||||
// Remove related search results
|
||||
delete searchResults.value[nodeId]
|
||||
Object.keys(searchResults.value).forEach((key) => {
|
||||
if (isChildNode(nodeId, key)) {
|
||||
delete searchResults.value[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await startResearch(nodeCurrentData)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
startResearch,
|
||||
isLoading,
|
||||
@ -273,6 +336,11 @@
|
||||
color="error"
|
||||
variant="soft"
|
||||
:duration="8000"
|
||||
:actions="[{
|
||||
label: $t('webBrowsing.retry'),
|
||||
color: 'secondary',
|
||||
onClick: () => retryNode(selectedNode!.id),
|
||||
}]"
|
||||
/>
|
||||
<h2 class="text-xl font-bold my-2">
|
||||
{{ selectedNode.label ?? $t('webBrowsing.generating') }}
|
||||
@ -283,7 +351,7 @@
|
||||
{{ t('webBrowsing.researchGoal') }}
|
||||
</h3>
|
||||
<!-- Root node has no additional information -->
|
||||
<p v-if="isRootNode(selectedNode)">
|
||||
<p v-if="isRootNode(selectedNode.id)">
|
||||
{{ t('webBrowsing.startNode.description') }}
|
||||
</p>
|
||||
<p
|
||||
|
@ -96,6 +96,21 @@
|
||||
hasUserInteraction = false
|
||||
}
|
||||
|
||||
function isChildNode(parentId: string, childId: string) {
|
||||
return childId.length > parentId.length && childId.startsWith(parentId)
|
||||
}
|
||||
|
||||
function removeChildNodes(parentId: string) {
|
||||
const childNodes = nodes.value.filter((n) => isChildNode(parentId, n.id))
|
||||
childNodes.forEach((node) => {
|
||||
// 移除节点和相关的边
|
||||
nodes.value = nodes.value.filter((n) => n.id !== node.id)
|
||||
edges.value = edges.value.filter(
|
||||
(e) => e.source !== node.id && e.target !== node.id,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function handleDrag(e: PointerEvent | FlowEvents['move']) {
|
||||
// Triggered by VueFlow internal logic
|
||||
if ('event' in e && !e.event.sourceEvent) {
|
||||
@ -109,6 +124,7 @@
|
||||
addNode,
|
||||
updateNode,
|
||||
clearNodes,
|
||||
removeChildNodes,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
Reference in New Issue
Block a user