feat: support reasoning models like DeepSeek R1
This commit is contained in:
@ -38,6 +38,14 @@
|
||||
}
|
||||
|
||||
switch (step.type) {
|
||||
case 'generating_query_reasoning': {
|
||||
if (node) {
|
||||
node.generateQueriesReasoning =
|
||||
(node.generateQueriesReasoning ?? '') + step.delta
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'generating_query': {
|
||||
if (!node) {
|
||||
// 创建新节点
|
||||
@ -86,10 +94,17 @@
|
||||
break
|
||||
}
|
||||
|
||||
case 'processing_serach_result_reasoning': {
|
||||
if (node) {
|
||||
node.generateLearningsReasoning =
|
||||
(node.generateLearningsReasoning ?? '') + step.delta
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'processing_serach_result': {
|
||||
if (node) {
|
||||
node.learnings = step.result.learnings || []
|
||||
node.followUpQuestions = step.result.followUpQuestions || []
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -157,6 +172,8 @@
|
||||
selectedNode.value = undefined
|
||||
searchResults.value = {}
|
||||
isLoading.value = true
|
||||
// Clear the root node's reasoning content
|
||||
tree.value.generateQueriesReasoning = ''
|
||||
try {
|
||||
const searchLanguage = config.value.webSearch.searchLanguage
|
||||
? t('language', {}, { locale: config.value.webSearch.searchLanguage })
|
||||
@ -211,20 +228,28 @@
|
||||
{{ selectedNode.label ?? $t('webBrowsing.generating') }}
|
||||
</h2>
|
||||
|
||||
<!-- Set loading default to true, because currently don't know how to handle it otherwise -->
|
||||
<ReasoningAccordion
|
||||
v-model="selectedNode.generateQueriesReasoning"
|
||||
loading
|
||||
/>
|
||||
|
||||
<!-- Research goal -->
|
||||
<h3 class="text-lg font-semibold mt-2">
|
||||
{{ t('webBrowsing.researchGoal') }}
|
||||
</h3>
|
||||
<!-- Root node has no additional information -->
|
||||
<p v-if="selectedNode.id === '0'">
|
||||
{{ t('webBrowsing.startNode.description') }}
|
||||
</p>
|
||||
<template v-else>
|
||||
<h3 class="text-lg font-semibold mt-2">
|
||||
{{ t('webBrowsing.researchGoal') }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="selectedNode.researchGoal"
|
||||
class="prose max-w-none"
|
||||
v-html="marked(selectedNode.researchGoal, { gfm: true })"
|
||||
/>
|
||||
|
||||
<!-- Visited URLs -->
|
||||
<h3 class="text-lg font-semibold mt-2">
|
||||
{{ t('webBrowsing.visitedUrls') }}
|
||||
</h3>
|
||||
@ -245,9 +270,15 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Learnings -->
|
||||
<h3 class="text-lg font-semibold mt-2">
|
||||
{{ t('webBrowsing.learnings') }}
|
||||
</h3>
|
||||
|
||||
<ReasoningAccordion
|
||||
v-model="selectedNode.generateQueriesReasoning"
|
||||
loading
|
||||
/>
|
||||
<p
|
||||
v-for="(learning, index) in selectedNode.learnings"
|
||||
class="prose max-w-none"
|
||||
|
52
components/ReasoningAccordion.vue
Normal file
52
components/ReasoningAccordion.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<!-- Shows an accordion for reasoning (CoT) content. The accordion is default invisible,
|
||||
until modelValue's length > 0 -->
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string>()
|
||||
const items = computed(() => [
|
||||
{
|
||||
icon: 'i-lucide-brain',
|
||||
content: modelValue.value,
|
||||
},
|
||||
])
|
||||
const currentOpen = ref('0')
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.loading) {
|
||||
currentOpen.value = '0'
|
||||
} else {
|
||||
currentOpen.value = '-1'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UAccordion
|
||||
v-if="modelValue"
|
||||
v-model="currentOpen"
|
||||
class="border border-gray-200 dark:border-gray-800 rounded-lg px-3 sm:px-4"
|
||||
:items="items"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #leading="{ item }">
|
||||
<div
|
||||
:class="[
|
||||
loading && 'animate-pulse',
|
||||
'flex items-center gap-2 text-(--ui-primary)',
|
||||
]"
|
||||
>
|
||||
<UIcon :name="item.icon" size="20" />
|
||||
{{ loading ? $t('modelThinking') : $t('modelThinkingComplete') }}
|
||||
</div>
|
||||
</template>
|
||||
<template #content="{ item }">
|
||||
<p class="text-sm text-gray-500 whitespace-pre-wrap mb-4">
|
||||
{{ item.content }}
|
||||
</p>
|
||||
</template>
|
||||
</UAccordion>
|
||||
</template>
|
@ -15,6 +15,7 @@
|
||||
}>()
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const reasoningContent = ref('')
|
||||
const feedback = ref<ResearchFeedbackResult[]>([])
|
||||
|
||||
const isLoading = ref(false)
|
||||
@ -39,17 +40,27 @@
|
||||
numQuestions,
|
||||
language: t('language', {}, { locale: locale.value }),
|
||||
})) {
|
||||
const questions = f.questions!.filter((s) => typeof s === 'string')
|
||||
// Incrementally update modelValue
|
||||
for (let i = 0; i < questions.length; i += 1) {
|
||||
if (feedback.value[i]) {
|
||||
feedback.value[i].assistantQuestion = questions[i]
|
||||
} else {
|
||||
feedback.value.push({
|
||||
assistantQuestion: questions[i],
|
||||
userAnswer: '',
|
||||
})
|
||||
if (f.type === 'reasoning') {
|
||||
reasoningContent.value += f.delta
|
||||
} else if (f.type === 'error') {
|
||||
error.value = f.message
|
||||
} else if (f.type === 'object') {
|
||||
const questions = f.value.questions!.filter(
|
||||
(s) => typeof s === 'string',
|
||||
)
|
||||
// Incrementally update modelValue
|
||||
for (let i = 0; i < questions.length; i += 1) {
|
||||
if (feedback.value[i]) {
|
||||
feedback.value[i].assistantQuestion = questions[i]
|
||||
} else {
|
||||
feedback.value.push({
|
||||
assistantQuestion: questions[i],
|
||||
userAnswer: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (f.type === 'bad-end') {
|
||||
error.value = t('invalidStructuredOutput')
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
@ -66,6 +77,7 @@
|
||||
function clear() {
|
||||
feedback.value = []
|
||||
error.value = ''
|
||||
reasoningContent.value = ''
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@ -85,13 +97,16 @@
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div v-if="!feedback.length && !error">
|
||||
<div v-if="!feedback.length && !reasoningContent && !error">
|
||||
{{ $t('modelFeedback.waiting') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="error" class="text-red-500 whitespace-pre-wrap">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<ReasoningAccordion v-model="reasoningContent" :loading="isLoading" />
|
||||
|
||||
<div
|
||||
v-for="(feedback, index) in feedback"
|
||||
class="flex flex-col gap-2"
|
||||
|
@ -102,7 +102,7 @@
|
||||
block
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ isLoadingFeedback ? 'Researching...' : $t('researchTopic.start') }}
|
||||
{{ isLoadingFeedback ? $t('researchTopic.researching') : $t('researchTopic.start') }}
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
|
@ -16,6 +16,7 @@
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
const loadingExportPdf = ref(false)
|
||||
const reasoningContent = ref('')
|
||||
const reportContent = ref('')
|
||||
const reportHtml = computed(() =>
|
||||
marked(reportContent.value, { silent: true, gfm: true, breaks: true }),
|
||||
@ -29,9 +30,20 @@
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
reportContent.value = ''
|
||||
reasoningContent.value = ''
|
||||
try {
|
||||
for await (const chunk of writeFinalReport(params).textStream) {
|
||||
reportContent.value += chunk
|
||||
for await (const chunk of writeFinalReport(params).fullStream) {
|
||||
if (chunk.type === 'reasoning') {
|
||||
reasoningContent.value += chunk.textDelta
|
||||
} else if (chunk.type === 'text-delta') {
|
||||
reportContent.value += chunk.textDelta
|
||||
} else if (chunk.type === 'error') {
|
||||
error.value = t('researchReport.error', [
|
||||
chunk.error instanceof Error
|
||||
? chunk.error.message
|
||||
: String(chunk.error),
|
||||
])
|
||||
}
|
||||
}
|
||||
reportContent.value += `\n\n## ${t(
|
||||
'researchReport.sources',
|
||||
@ -158,21 +170,25 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="error" class="text-red-500">{{ error }}</div>
|
||||
|
||||
<ReasoningAccordion
|
||||
v-if="reasoningContent"
|
||||
v-model="reasoningContent"
|
||||
class="mb-4"
|
||||
:loading="loading"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="reportContent"
|
||||
id="report-content"
|
||||
class="prose prose-sm max-w-none p-6 bg-gray-50 dark:bg-gray-800 dark:text-white rounded-lg shadow"
|
||||
v-html="reportHtml"
|
||||
/>
|
||||
<template v-else>
|
||||
<div v-if="error" class="text-red-500">{{ error }}</div>
|
||||
<div v-else>
|
||||
{{
|
||||
loading
|
||||
? $t('researchReport.generating')
|
||||
: $t('researchReport.waiting')
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
{{
|
||||
loading ? $t('researchReport.generating') : $t('researchReport.waiting')
|
||||
}}
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
@ -6,11 +6,15 @@
|
||||
|
||||
export type TreeNode = {
|
||||
id: string
|
||||
/** Label, represents the search query */
|
||||
/** Label, represents the search query. Generated from parent node. */
|
||||
label: string
|
||||
/** The research goal of this node. Generated from parent node. */
|
||||
researchGoal?: string
|
||||
/** Reasoning content when generating queries for the next iteration. */
|
||||
generateQueriesReasoning?: string
|
||||
/** Reasoning content when generating learnings for this iteration. */
|
||||
generateLearningsReasoning?: string
|
||||
learnings?: string[]
|
||||
followUpQuestions?: string[]
|
||||
visitedUrls?: string[]
|
||||
status?: TreeNodeStatus
|
||||
children: TreeNode[]
|
||||
|
Reference in New Issue
Block a user