feat: support reasoning models like DeepSeek R1

This commit is contained in:
AnotiaWang
2025-02-14 15:20:02 +08:00
parent 93527597b7
commit e7296df78f
17 changed files with 549 additions and 171 deletions

View File

@ -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"

View 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>

View File

@ -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"

View File

@ -102,7 +102,7 @@
block
@click="handleSubmit"
>
{{ isLoadingFeedback ? 'Researching...' : $t('researchTopic.start') }}
{{ isLoadingFeedback ? $t('researchTopic.researching') : $t('researchTopic.start') }}
</UButton>
</template>
</UCard>

View File

@ -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>

View File

@ -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[]